1/*	$NetBSD: send_to_kdc.c,v 1.9 2023/06/19 21:41:44 christos Exp $	*/
2
3/*
4 * Copyright (c) 1997 - 2002 Kungliga Tekniska H��gskolan
5 * (Royal Institute of Technology, Stockholm, Sweden).
6 * All rights reserved.
7 *
8 * Portions Copyright (c) 2010 - 2013 Apple Inc. All rights reserved.
9 *
10 * Redistribution and use in source and binary forms, with or without
11 * modification, are permitted provided that the following conditions
12 * are met:
13 *
14 * 1. Redistributions of source code must retain the above copyright
15 *    notice, this list of conditions and the following disclaimer.
16 *
17 * 2. Redistributions in binary form must reproduce the above copyright
18 *    notice, this list of conditions and the following disclaimer in the
19 *    documentation and/or other materials provided with the distribution.
20 *
21 * 3. Neither the name of the Institute nor the names of its contributors
22 *    may be used to endorse or promote products derived from this software
23 *    without specific prior written permission.
24 *
25 * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
26 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
27 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
28 * ARE DISCLAIMED.  IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
29 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
30 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
31 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
32 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
33 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
34 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
35 * SUCH DAMAGE.
36 */
37
38#include "krb5_locl.h"
39#include "send_to_kdc_plugin.h"
40
41/**
42 * @section send_to_kdc Locating and sending packets to the KDC
43 *
44 * The send to kdc code is responsible to request the list of KDC from
45 * the locate-kdc subsystem and then send requests to each of them.
46 *
47 * - Each second a new hostname is tried.
48 * - If the hostname have several addresses, the first will be tried
49 *   directly then in turn the other will be tried every 3 seconds
50 *   (host_timeout).
51 * - UDP requests are tried 3 times, and it tried with a individual timeout of kdc_timeout / 3.
52 * - TCP and HTTP requests are tried 1 time.
53 *
54 *  Total wait time shorter then (number of addresses * 3) + kdc_timeout seconds.
55 *
56 */
57
58static int
59init_port(const char *s, int fallback)
60{
61    int tmp;
62
63    if (s && sscanf(s, "%d", &tmp) == 1)
64        return htons(tmp);
65    return fallback;
66}
67
68struct send_via_plugin_s {
69    krb5_const_realm realm;
70    krb5_krbhst_info *hi;
71    time_t timeout;
72    const krb5_data *send_data;
73    krb5_data *receive;
74};
75
76
77static krb5_error_code KRB5_LIB_CALL
78kdccallback(krb5_context context, const void *plug, void *plugctx, void *userctx)
79{
80    const krb5plugin_send_to_kdc_ftable *service = (const krb5plugin_send_to_kdc_ftable *)plug;
81    struct send_via_plugin_s *ctx = userctx;
82
83    if (service->send_to_kdc == NULL)
84	return KRB5_PLUGIN_NO_HANDLE;
85    return service->send_to_kdc(context, plugctx, ctx->hi, ctx->timeout,
86				ctx->send_data, ctx->receive);
87}
88
89static krb5_error_code KRB5_LIB_CALL
90realmcallback(krb5_context context, const void *plug, void *plugctx, void *userctx)
91{
92    const krb5plugin_send_to_kdc_ftable *service = (const krb5plugin_send_to_kdc_ftable *)plug;
93    struct send_via_plugin_s *ctx = userctx;
94
95    if (service->send_to_realm == NULL)
96	return KRB5_PLUGIN_NO_HANDLE;
97    return service->send_to_realm(context, plugctx, ctx->realm, ctx->timeout,
98				  ctx->send_data, ctx->receive);
99}
100
101static krb5_error_code
102kdc_via_plugin(krb5_context context,
103	       krb5_krbhst_info *hi,
104	       time_t timeout,
105	       const krb5_data *send_data,
106	       krb5_data *receive)
107{
108    struct send_via_plugin_s userctx;
109
110    userctx.realm = NULL;
111    userctx.hi = hi;
112    userctx.timeout = timeout;
113    userctx.send_data = send_data;
114    userctx.receive = receive;
115
116    return _krb5_plugin_run_f(context, "krb5", KRB5_PLUGIN_SEND_TO_KDC,
117			      KRB5_PLUGIN_SEND_TO_KDC_VERSION_0, 0,
118			      &userctx, kdccallback);
119}
120
121static krb5_error_code
122realm_via_plugin(krb5_context context,
123		 krb5_const_realm realm,
124		 time_t timeout,
125		 const krb5_data *send_data,
126		 krb5_data *receive)
127{
128    struct send_via_plugin_s userctx;
129
130    userctx.realm = realm;
131    userctx.hi = NULL;
132    userctx.timeout = timeout;
133    userctx.send_data = send_data;
134    userctx.receive = receive;
135
136    return _krb5_plugin_run_f(context, "krb5", KRB5_PLUGIN_SEND_TO_KDC,
137			      KRB5_PLUGIN_SEND_TO_KDC_VERSION_2, 0,
138			      &userctx, realmcallback);
139}
140
141struct krb5_sendto_ctx_data {
142    int flags;
143    int type;
144    krb5_sendto_ctx_func func;
145    void *data;
146    char *hostname;
147    krb5_krbhst_handle krbhst;
148
149    /* context2 */
150    const krb5_data *send_data;
151    krb5_data response;
152    heim_array_t hosts;
153    int stateflags;
154#define KRBHST_COMPLETED	1
155
156    /* prexmit */
157    krb5_sendto_prexmit prexmit_func;
158    void *prexmit_ctx;
159
160    /* stats */
161    struct {
162	struct timeval start_time;
163	struct timeval name_resolution;
164	struct timeval krbhst;
165	unsigned long sent_packets;
166	unsigned long num_hosts;
167    } stats;
168    unsigned int stid;
169};
170
171static void
172dealloc_sendto_ctx(void *ptr)
173{
174    krb5_sendto_ctx ctx = (krb5_sendto_ctx)ptr;
175    if (ctx->hostname)
176	free(ctx->hostname);
177    heim_release(ctx->hosts);
178    heim_release(ctx->krbhst);
179}
180
181KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL
182krb5_sendto_ctx_alloc(krb5_context context, krb5_sendto_ctx *ctx)
183{
184    *ctx = heim_alloc(sizeof(**ctx), "sendto-context", dealloc_sendto_ctx);
185    if (*ctx == NULL)
186	return krb5_enomem(context);
187    (*ctx)->hosts = heim_array_create();
188
189    return 0;
190}
191
192KRB5_LIB_FUNCTION void KRB5_LIB_CALL
193krb5_sendto_ctx_add_flags(krb5_sendto_ctx ctx, int flags)
194{
195    ctx->flags |= flags;
196}
197
198KRB5_LIB_FUNCTION int KRB5_LIB_CALL
199krb5_sendto_ctx_get_flags(krb5_sendto_ctx ctx)
200{
201    return ctx->flags;
202}
203
204KRB5_LIB_FUNCTION void KRB5_LIB_CALL
205krb5_sendto_ctx_set_type(krb5_sendto_ctx ctx, int type)
206{
207    ctx->type = type;
208}
209
210KRB5_LIB_FUNCTION void KRB5_LIB_CALL
211krb5_sendto_ctx_set_func(krb5_sendto_ctx ctx,
212			 krb5_sendto_ctx_func func,
213			 void *data)
214{
215    ctx->func = func;
216    ctx->data = data;
217}
218
219KRB5_LIB_FUNCTION void KRB5_LIB_CALL
220_krb5_sendto_ctx_set_prexmit(krb5_sendto_ctx ctx,
221			     krb5_sendto_prexmit prexmit,
222			     void *data)
223{
224    ctx->prexmit_func = prexmit;
225    ctx->prexmit_ctx = data;
226}
227
228KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL
229krb5_sendto_set_hostname(krb5_context context,
230			 krb5_sendto_ctx ctx,
231			 const char *hostname)
232{
233    if (ctx->hostname == NULL)
234	free(ctx->hostname);
235    ctx->hostname = strdup(hostname);
236    if (ctx->hostname == NULL) {
237	krb5_set_error_message(context, ENOMEM, N_("malloc: out of memory", ""));
238	return ENOMEM;
239    }
240    return 0;
241}
242
243KRB5_LIB_FUNCTION void KRB5_LIB_CALL
244_krb5_sendto_ctx_set_krb5hst(krb5_context context,
245			     krb5_sendto_ctx ctx,
246			     krb5_krbhst_handle handle)
247{
248    heim_release(ctx->krbhst);
249    ctx->krbhst = heim_retain(handle);
250}
251
252KRB5_LIB_FUNCTION void KRB5_LIB_CALL
253krb5_sendto_ctx_free(krb5_context context, krb5_sendto_ctx ctx)
254{
255    heim_release(ctx);
256}
257
258KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL
259_krb5_kdc_retry(krb5_context context, krb5_sendto_ctx ctx, void *data,
260		const krb5_data *reply, int *action)
261{
262    krb5_error_code ret;
263    KRB_ERROR error;
264
265    if(krb5_rd_error(context, reply, &error))
266	return 0;
267
268    ret = krb5_error_from_rd_error(context, &error, NULL);
269    krb5_free_error_contents(context, &error);
270
271    switch(ret) {
272    case KRB5KRB_ERR_RESPONSE_TOO_BIG: {
273	if (krb5_sendto_ctx_get_flags(ctx) & KRB5_KRBHST_FLAGS_LARGE_MSG)
274	    break;
275	krb5_sendto_ctx_add_flags(ctx, KRB5_KRBHST_FLAGS_LARGE_MSG);
276	*action = KRB5_SENDTO_RESET;
277	break;
278    }
279    case KRB5KDC_ERR_SVC_UNAVAILABLE:
280	*action = KRB5_SENDTO_CONTINUE;
281	break;
282    }
283    return 0;
284}
285
286/*
287 *
288 */
289
290struct host;
291
292struct host_fun {
293    krb5_error_code (*prepare)(krb5_context, struct host *, const krb5_data *);
294    krb5_error_code (*send_fn)(krb5_context, struct host *);
295    krb5_error_code (*recv_fn)(krb5_context, struct host *, krb5_data *);
296    int ntries;
297};
298
299struct host {
300    enum host_state { CONNECT, CONNECTING, CONNECTED, WAITING_REPLY, DEAD } state;
301    krb5_krbhst_info *hi;
302    struct addrinfo *ai;
303    rk_socket_t fd;
304    struct host_fun *fun;
305    unsigned int tries;
306    time_t timeout;
307    krb5_data data;
308    unsigned int tid;
309};
310
311static void
312debug_host(krb5_context context, int level, struct host *host, const char *fmt, ...)
313	__attribute__ ((__format__ (__printf__, 4, 5)));
314
315static void
316debug_host(krb5_context context, int level, struct host *host, const char *fmt, ...)
317{
318    const char *proto = "unknown";
319    const char *state;
320    char name[NI_MAXHOST], port[NI_MAXSERV];
321    char *text = NULL;
322    va_list ap;
323    int ret;
324
325    if (!_krb5_have_debug(context, 5))
326	return;
327
328    va_start(ap, fmt);
329    ret = vasprintf(&text, fmt, ap);
330    va_end(ap);
331    if (ret == -1 || text == NULL)
332	return;
333
334    if (host->hi->proto == KRB5_KRBHST_HTTP)
335	proto = "http";
336    else if (host->hi->proto == KRB5_KRBHST_TCP)
337	proto = "tcp";
338    else if (host->hi->proto == KRB5_KRBHST_UDP)
339	proto = "udp";
340
341    if (getnameinfo(host->ai->ai_addr, host->ai->ai_addrlen,
342		    name, sizeof(name), port, sizeof(port), NI_NUMERICHOST) != 0)
343	name[0] = '\0';
344
345    switch (host->state) {
346    case CONNECT:	state = "CONNECT";		break;
347    case CONNECTING:	state = "CONNECTING";		break;
348    case CONNECTED:	state = "CONNECTED";		break;
349    case WAITING_REPLY:	state = "WAITING_REPLY";	break;
350    case DEAD:		state = "DEAD";			break;
351    default:		state = "unknown";		break;
352    }
353
354    _krb5_debug(context, level, "%s: %s %s:%s (%s) state=%s tid: %08x", text,
355		proto, name, port, host->hi->hostname, state, host->tid);
356    free(text);
357}
358
359
360static void
361deallocate_host(void *ptr)
362{
363    struct host *host = ptr;
364    if (!rk_IS_BAD_SOCKET(host->fd))
365	rk_closesocket(host->fd);
366    krb5_data_free(&host->data);
367    host->ai = NULL;
368}
369
370static void
371host_dead(krb5_context context, struct host *host, const char *msg)
372{
373    debug_host(context, 5, host, "%s", msg);
374    rk_closesocket(host->fd);
375    host->fd = rk_INVALID_SOCKET;
376    host->state = DEAD;
377}
378
379static krb5_error_code
380send_stream(krb5_context context, struct host *host)
381{
382    ssize_t len;
383
384    len = krb5_net_write(context, &host->fd, host->data.data, host->data.length);
385
386    if (len < 0)
387	return errno;
388    else if (len < host->data.length) {
389	host->data.length -= len;
390	memmove(host->data.data, ((uint8_t *)host->data.data) + len, host->data.length - len);
391	return -1;
392    } else {
393	krb5_data_free(&host->data);
394	return 0;
395    }
396}
397
398static krb5_error_code
399recv_stream(krb5_context context, struct host *host)
400{
401    krb5_error_code ret;
402    size_t oldlen;
403    ssize_t sret;
404    int nbytes;
405
406    if (rk_SOCK_IOCTL(host->fd, FIONREAD, &nbytes) != 0 || nbytes <= 0)
407	return HEIM_NET_CONN_REFUSED;
408
409    if (context->max_msg_size - host->data.length < nbytes) {
410	krb5_set_error_message(context, KRB5KRB_ERR_FIELD_TOOLONG,
411			       N_("TCP message from KDC too large %d", ""),
412			       (int)(host->data.length + nbytes));
413	return KRB5KRB_ERR_FIELD_TOOLONG;
414    }
415
416    oldlen = host->data.length;
417
418    ret = krb5_data_realloc(&host->data, oldlen + nbytes + 1 /* NUL */);
419    if (ret)
420	return ret;
421
422    sret = krb5_net_read(context, &host->fd, ((uint8_t *)host->data.data) + oldlen, nbytes);
423    if (sret <= 0) {
424	ret = errno;
425	return ret;
426    }
427    host->data.length = oldlen + sret;
428    /* zero terminate for http transport */
429    ((uint8_t *)host->data.data)[host->data.length] = '\0';
430
431    return 0;
432}
433
434/*
435 *
436 */
437
438static void
439host_next_timeout(krb5_context context, struct host *host)
440{
441    host->timeout = context->kdc_timeout / host->fun->ntries;
442    if (host->timeout == 0)
443	host->timeout = 1;
444
445    host->timeout += time(NULL);
446}
447
448/*
449 * connected host
450 */
451
452static void
453host_connected(krb5_context context, krb5_sendto_ctx ctx, struct host *host)
454{
455    krb5_error_code ret;
456
457    host->state = CONNECTED;
458    /*
459     * Now prepare data to send to host
460     */
461    if (ctx->prexmit_func) {
462	krb5_data data;
463
464	krb5_data_zero(&data);
465
466	ret = ctx->prexmit_func(context, host->hi->proto,
467				ctx->prexmit_ctx, host->fd, &data);
468	if (ret == 0) {
469	    if (data.length == 0) {
470		host_dead(context, host, "prexmit function didn't send data");
471		return;
472	    }
473	    ret = host->fun->prepare(context, host, &data);
474	    krb5_data_free(&data);
475	}
476
477    } else {
478	ret = host->fun->prepare(context, host, ctx->send_data);
479    }
480    if (ret)
481	debug_host(context, 5, host, "failed to prexmit/prepare");
482}
483
484/*
485 * connect host
486 */
487
488static void
489host_connect(krb5_context context, krb5_sendto_ctx ctx, struct host *host)
490{
491    krb5_krbhst_info *hi = host->hi;
492    struct addrinfo *ai = host->ai;
493
494    debug_host(context, 5, host, "connecting to host");
495
496    if (connect(host->fd, ai->ai_addr, ai->ai_addrlen) < 0) {
497#ifdef HAVE_WINSOCK
498	if (WSAGetLastError() == WSAEWOULDBLOCK)
499	    errno = EINPROGRESS;
500#endif /* HAVE_WINSOCK */
501	if (errno == EINPROGRESS && (hi->proto == KRB5_KRBHST_HTTP || hi->proto == KRB5_KRBHST_TCP)) {
502	    debug_host(context, 5, host, "connecting to %d", host->fd);
503	    host->state = CONNECTING;
504	} else {
505	    host_dead(context, host, "failed to connect");
506	}
507    } else {
508	host_connected(context, ctx, host);
509    }
510
511    host_next_timeout(context, host);
512}
513
514/*
515 * HTTP transport
516 */
517
518static krb5_error_code
519prepare_http(krb5_context context, struct host *host, const krb5_data *data)
520{
521    char *str = NULL, *request = NULL;
522    krb5_error_code ret;
523    int len;
524
525    heim_assert(host->data.length == 0, "prepare_http called twice");
526
527    len = rk_base64_encode(data->data, data->length, &str);
528    if(len < 0)
529	return ENOMEM;
530
531    if (context->http_proxy)
532	ret = asprintf(&request, "GET http://%s/%s HTTP/1.0\r\n\r\n", host->hi->hostname, str);
533    else
534	ret = asprintf(&request, "GET /%s HTTP/1.0\r\n\r\n", str);
535    free(str);
536    if(ret < 0 || request == NULL)
537	return ENOMEM;
538
539    host->data.data = request;
540    host->data.length = strlen(request);
541
542    return 0;
543}
544
545static krb5_error_code
546recv_http(krb5_context context, struct host *host, krb5_data *data)
547{
548    krb5_error_code ret;
549    unsigned long rep_len;
550    size_t len;
551    char *p;
552
553    /*
554     * recv_stream returns a NUL terminated stream
555     */
556
557    ret = recv_stream(context, host);
558    if (ret)
559	return ret;
560
561    p = strstr(host->data.data, "\r\n\r\n");
562    if (p == NULL)
563	return -1;
564    p += 4;
565
566    len = host->data.length - (p - (char *)host->data.data);
567    if (len < 4)
568	return -1;
569
570    _krb5_get_int(p, &rep_len, 4);
571    if (len < rep_len)
572	return -1;
573
574    p += 4;
575
576    memmove(host->data.data, p, rep_len);
577    host->data.length = rep_len;
578
579    *data = host->data;
580    krb5_data_zero(&host->data);
581
582    return 0;
583}
584
585/*
586 * TCP transport
587 */
588
589static krb5_error_code
590prepare_tcp(krb5_context context, struct host *host, const krb5_data *data)
591{
592    krb5_error_code ret;
593    krb5_storage *sp;
594
595    heim_assert(host->data.length == 0, "prepare_tcp called twice");
596
597    sp = krb5_storage_emem();
598    if (sp == NULL)
599	return ENOMEM;
600
601    ret = krb5_store_data(sp, *data);
602    if (ret) {
603	krb5_storage_free(sp);
604	return ret;
605    }
606    ret = krb5_storage_to_data(sp, &host->data);
607    krb5_storage_free(sp);
608
609    return ret;
610}
611
612static krb5_error_code
613recv_tcp(krb5_context context, struct host *host, krb5_data *data)
614{
615    krb5_error_code ret;
616    unsigned long pktlen;
617
618    ret = recv_stream(context, host);
619    if (ret)
620	return ret;
621
622    if (host->data.length < 4)
623	return -1;
624
625    _krb5_get_int(host->data.data, &pktlen, 4);
626
627    if (pktlen > host->data.length - 4)
628	return -1;
629
630    memmove(host->data.data, ((uint8_t *)host->data.data) + 4, host->data.length - 4);
631    host->data.length -= 4;
632
633    *data = host->data;
634    krb5_data_zero(&host->data);
635
636    return 0;
637}
638
639/*
640 * UDP transport
641 */
642
643static krb5_error_code
644prepare_udp(krb5_context context, struct host *host, const krb5_data *data)
645{
646    return krb5_data_copy(&host->data, data->data, data->length);
647}
648
649static krb5_error_code
650send_udp(krb5_context context, struct host *host)
651{
652    if (send(host->fd, host->data.data, host->data.length, 0) < 0)
653	return errno;
654    return 0;
655}
656
657static krb5_error_code
658recv_udp(krb5_context context, struct host *host, krb5_data *data)
659{
660    krb5_error_code ret;
661    int nbytes;
662
663
664    if (rk_SOCK_IOCTL(host->fd, FIONREAD, &nbytes) != 0 || nbytes <= 0)
665	return HEIM_NET_CONN_REFUSED;
666
667    if (context->max_msg_size < nbytes) {
668	krb5_set_error_message(context, KRB5KRB_ERR_FIELD_TOOLONG,
669			       N_("UDP message from KDC too large %d", ""),
670			       (int)nbytes);
671	return KRB5KRB_ERR_FIELD_TOOLONG;
672    }
673
674    ret = krb5_data_alloc(data, nbytes);
675    if (ret)
676	return ret;
677
678    ret = recv(host->fd, data->data, data->length, 0);
679    if (ret < 0) {
680	ret = errno;
681	krb5_data_free(data);
682	return ret;
683    }
684    data->length = ret;
685
686    return 0;
687}
688
689static struct host_fun http_fun = {
690    prepare_http,
691    send_stream,
692    recv_http,
693    1
694};
695static struct host_fun tcp_fun = {
696    prepare_tcp,
697    send_stream,
698    recv_tcp,
699    1
700};
701static struct host_fun udp_fun = {
702    prepare_udp,
703    send_udp,
704    recv_udp,
705    3
706};
707
708
709/*
710 * Host state machine
711 */
712
713static int
714eval_host_state(krb5_context context,
715		krb5_sendto_ctx ctx,
716		struct host *host,
717		int readable, int writeable)
718{
719    krb5_error_code ret;
720
721    if (host->state == CONNECT) {
722	/* check if its this host time to connect */
723	if (host->timeout < time(NULL))
724	    host_connect(context, ctx, host);
725	return 0;
726    }
727
728    if (host->state == CONNECTING && writeable)
729	host_connected(context, ctx, host);
730
731    if (readable) {
732
733	debug_host(context, 5, host, "reading packet");
734
735	ret = host->fun->recv_fn(context, host, &ctx->response);
736	if (ret == -1) {
737	    /* not done yet */
738	} else if (ret == 0) {
739	    /* if recv_foo function returns 0, we have a complete reply */
740	    debug_host(context, 5, host, "host completed");
741	    return 1;
742	} else {
743	    host_dead(context, host, "host disconnected");
744	}
745    }
746
747    /* check if there is anything to send, state might DEAD after read */
748    if (writeable && host->state == CONNECTED) {
749
750	ctx->stats.sent_packets++;
751
752	debug_host(context, 5, host, "writing packet");
753
754	ret = host->fun->send_fn(context, host);
755	if (ret == -1) {
756	    /* not done yet */
757	} else if (ret) {
758	    host_dead(context, host, "host dead, write failed");
759	} else
760	    host->state = WAITING_REPLY;
761    }
762
763    return 0;
764}
765
766/*
767 *
768 */
769
770static krb5_error_code
771submit_request(krb5_context context, krb5_sendto_ctx ctx, krb5_krbhst_info *hi)
772{
773    unsigned long submitted_host = 0;
774    krb5_boolean freeai = FALSE;
775    struct timeval nrstart, nrstop;
776    krb5_error_code ret;
777    struct addrinfo *ai = NULL, *a;
778    struct host *host;
779
780    ret = kdc_via_plugin(context, hi, context->kdc_timeout,
781			 ctx->send_data, &ctx->response);
782    if (ret == 0) {
783	return 0;
784    } else if (ret != KRB5_PLUGIN_NO_HANDLE) {
785	_krb5_debug(context, 5, "send via plugin failed %s: %d",
786		    hi->hostname, ret);
787	return ret;
788    }
789
790    /*
791     * If we have a proxy, let use the address of the proxy instead of
792     * the KDC and let the proxy deal with the resolving of the KDC.
793     */
794
795    gettimeofday(&nrstart, NULL);
796
797    if (hi->proto == KRB5_KRBHST_HTTP && context->http_proxy) {
798	char *proxy2 = strdup(context->http_proxy);
799	char *el, *proxy  = proxy2;
800	struct addrinfo hints;
801	char portstr[NI_MAXSERV];
802	unsigned short nport;
803
804	if (proxy == NULL)
805	    return ENOMEM;
806	if (strncmp(proxy, "http://", 7) == 0)
807	    proxy += 7;
808
809	/* check for url terminating slash */
810	el = strchr(proxy, '/');
811	if (el != NULL)
812	    *el = '\0';
813
814	/* check for port in hostname, used below as port */
815	el = strchr(proxy, ':');
816	if(el != NULL)
817	    *el++ = '\0';
818
819	memset(&hints, 0, sizeof(hints));
820	hints.ai_family   = PF_UNSPEC;
821	hints.ai_socktype = SOCK_STREAM;
822
823	/* On some systems ntohs(foo(..., htons(...))) causes shadowing */
824	nport = init_port(el, htons(80));
825	snprintf(portstr, sizeof(portstr), "%d", ntohs(nport));
826
827	ret = getaddrinfo(proxy, portstr, &hints, &ai);
828	free(proxy2);
829	if (ret)
830	    return krb5_eai_to_heim_errno(ret, errno);
831
832	freeai = TRUE;
833
834    } else {
835	ret = krb5_krbhst_get_addrinfo(context, hi, &ai);
836	if (ret)
837	    return ret;
838    }
839
840    /* add up times */
841    gettimeofday(&nrstop, NULL);
842    timevalsub(&nrstop, &nrstart);
843    timevaladd(&ctx->stats.name_resolution, &nrstop);
844
845    ctx->stats.num_hosts++;
846
847    for (a = ai; a != NULL; a = a->ai_next) {
848	rk_socket_t fd;
849
850	fd = socket(a->ai_family, a->ai_socktype | SOCK_CLOEXEC, a->ai_protocol);
851	if (rk_IS_BAD_SOCKET(fd))
852	    continue;
853	rk_cloexec(fd);
854
855#ifndef NO_LIMIT_FD_SETSIZE
856	if (fd >= FD_SETSIZE) {
857	    _krb5_debug(context, 0, "fd too large for select");
858	    rk_closesocket(fd);
859	    continue;
860	}
861#endif
862	socket_set_nonblocking(fd, 1);
863
864	host = heim_alloc(sizeof(*host), "sendto-host", deallocate_host);
865	if (host == NULL) {
866            if (freeai)
867                freeaddrinfo(ai);
868	    rk_closesocket(fd);
869	    return ENOMEM;
870	}
871	host->hi = hi;
872	host->fd = fd;
873	host->ai = a;
874	/* next version of stid */
875	host->tid = ctx->stid = (ctx->stid & 0xffff0000) | ((ctx->stid & 0xffff) + 1);
876
877	host->state = CONNECT;
878
879	switch (host->hi->proto) {
880	case KRB5_KRBHST_HTTP :
881	    host->fun = &http_fun;
882	    break;
883	case KRB5_KRBHST_TCP :
884	    host->fun = &tcp_fun;
885	    break;
886	case KRB5_KRBHST_UDP :
887	    host->fun = &udp_fun;
888	    break;
889	default:
890	    heim_abort("undefined http transport protocol: %d", (int)host->hi->proto);
891	}
892
893	host->tries = host->fun->ntries;
894
895	/*
896	 * Connect directly next host, wait a host_timeout for each next address.
897	 * We try host_connect() here, checking the return code because as we do
898	 * non-blocking connects, any error here indicates that the address is just
899	 * offline.  That is, it's something like "No route to host" which is not
900	 * worth retrying.  And so, we fail directly and immediately to the next
901	 * address for this host without enqueueing the address for retries.
902	 */
903	if (submitted_host == 0) {
904	    host_connect(context, ctx, host);
905	    if (host->state == DEAD)
906		continue;
907	} else {
908	    debug_host(context, 5, host,
909		       "Queuing host in future (in %ds), its the %lu address on the same name",
910		       (int)(context->host_timeout * submitted_host), submitted_host + 1);
911	    host->timeout = time(NULL) + (submitted_host * context->host_timeout);
912	}
913
914	heim_array_append_value(ctx->hosts, host);
915	heim_release(host);
916	submitted_host++;
917    }
918
919    if (freeai)
920	freeaddrinfo(ai);
921
922    if (submitted_host == 0)
923	return KRB5_KDC_UNREACH;
924
925    return 0;
926}
927
928struct wait_ctx {
929    krb5_context context;
930    krb5_sendto_ctx ctx;
931    fd_set rfds;
932    fd_set wfds;
933    rk_socket_t max_fd;
934    int got_reply;
935    time_t timenow;
936};
937
938static void
939wait_setup(heim_object_t obj, void *iter_ctx, int *stop)
940{
941    struct wait_ctx *wait_ctx = iter_ctx;
942    struct host *h = (struct host *)obj;
943
944    if (h->state == CONNECT) {
945	if (h->timeout >= wait_ctx->timenow)
946	    return;
947	host_connect(wait_ctx->context, wait_ctx->ctx, h);
948    }
949
950    /* skip dead hosts */
951    if (h->state == DEAD)
952	return;
953
954    /* if host timed out, dec tries and (retry or kill host) */
955    if (h->timeout < wait_ctx->timenow) {
956	heim_assert(h->tries != 0, "tries should not reach 0");
957	h->tries--;
958	if (h->tries == 0) {
959	    host_dead(wait_ctx->context, h, "host timed out");
960	    return;
961	} else {
962	    debug_host(wait_ctx->context, 5, h, "retrying sending to");
963	    host_next_timeout(wait_ctx->context, h);
964	    host_connected(wait_ctx->context, wait_ctx->ctx, h);
965	}
966    }
967
968#ifndef NO_LIMIT_FD_SETSIZE
969    heim_assert(h->fd < FD_SETSIZE, "fd too large");
970#endif
971    switch (h->state) {
972    case WAITING_REPLY:
973	FD_SET(h->fd, &wait_ctx->rfds);
974	break;
975    case CONNECTING:
976    case CONNECTED:
977	FD_SET(h->fd, &wait_ctx->rfds);
978	FD_SET(h->fd, &wait_ctx->wfds);
979	break;
980    default:
981	debug_host(wait_ctx->context, 5, h, "invalid sendto host state");
982	heim_abort("invalid sendto host state");
983    }
984    if (h->fd > wait_ctx->max_fd || wait_ctx->max_fd == rk_INVALID_SOCKET)
985	wait_ctx->max_fd = h->fd;
986}
987
988static int
989wait_filter_dead(heim_object_t obj, void *ctx)
990{
991    struct host *h = (struct host *)obj;
992    return (int)((h->state == DEAD) ? true : false);
993}
994
995static void
996wait_accelerate(heim_object_t obj, void *ctx, int *stop)
997{
998    struct host *h = (struct host *)obj;
999
1000    if (h->state == CONNECT && h->timeout > 0)
1001	h->timeout--;
1002}
1003
1004static void
1005wait_process(heim_object_t obj, void *ctx, int *stop)
1006{
1007    struct wait_ctx *wait_ctx = ctx;
1008    struct host *h = (struct host *)obj;
1009    int readable, writeable;
1010    heim_assert(h->state != DEAD, "dead host resurected");
1011
1012#ifndef NO_LIMIT_FD_SETSIZE
1013    heim_assert(h->fd < FD_SETSIZE, "fd too large");
1014#endif
1015    readable = FD_ISSET(h->fd, &wait_ctx->rfds);
1016    writeable = FD_ISSET(h->fd, &wait_ctx->wfds);
1017
1018    if (readable || writeable || h->state == CONNECT)
1019	wait_ctx->got_reply |= eval_host_state(wait_ctx->context, wait_ctx->ctx, h, readable, writeable);
1020
1021    /* if there is already a reply, just fall though the array */
1022    if (wait_ctx->got_reply)
1023	*stop = 1;
1024}
1025
1026static krb5_error_code
1027wait_response(krb5_context context, int *action, krb5_sendto_ctx ctx)
1028{
1029    struct wait_ctx wait_ctx;
1030    struct timeval tv;
1031    int ret;
1032
1033    wait_ctx.context = context;
1034    wait_ctx.ctx = ctx;
1035    FD_ZERO(&wait_ctx.rfds);
1036    FD_ZERO(&wait_ctx.wfds);
1037    wait_ctx.max_fd = rk_INVALID_SOCKET;
1038
1039    /* oh, we have a reply, it must be a plugin that got it for us */
1040    if (ctx->response.length) {
1041	*action = KRB5_SENDTO_FILTER;
1042	return 0;
1043    }
1044
1045    wait_ctx.timenow = time(NULL);
1046
1047    heim_array_iterate_f(ctx->hosts, &wait_ctx, wait_setup);
1048    heim_array_filter_f(ctx->hosts, &wait_ctx, wait_filter_dead);
1049
1050    if (heim_array_get_length(ctx->hosts) == 0) {
1051	if (ctx->stateflags & KRBHST_COMPLETED) {
1052	    _krb5_debug(context, 5, "no more hosts to send/recv packets to/from "
1053			 "trying to pulling more hosts");
1054	    *action = KRB5_SENDTO_FAILED;
1055	} else {
1056	    _krb5_debug(context, 5, "no more hosts to send/recv packets to/from "
1057			 "and no more hosts -> failure");
1058	    *action = KRB5_SENDTO_TIMEOUT;
1059	}
1060	return 0;
1061    }
1062
1063    if (wait_ctx.max_fd == rk_INVALID_SOCKET) {
1064	/*
1065	 * If we don't find a host which can make progress, then
1066	 * we accelerate the process by moving all of the contestants
1067	 * up by 1s.
1068	 */
1069	_krb5_debug(context, 5, "wait_response: moving the contestants forward");
1070	heim_array_iterate_f(ctx->hosts, &wait_ctx, wait_accelerate);
1071	return 0;
1072    }
1073
1074    tv.tv_sec = 1;
1075    tv.tv_usec = 0;
1076
1077    ret = select(wait_ctx.max_fd + 1, &wait_ctx.rfds, &wait_ctx.wfds, NULL, &tv);
1078    if (ret < 0)
1079	return errno;
1080    if (ret == 0) {
1081	*action = KRB5_SENDTO_TIMEOUT;
1082	return 0;
1083    }
1084
1085    wait_ctx.got_reply = 0;
1086    heim_array_iterate_f(ctx->hosts, &wait_ctx, wait_process);
1087    if (wait_ctx.got_reply)
1088	*action = KRB5_SENDTO_FILTER;
1089    else
1090	*action = KRB5_SENDTO_CONTINUE;
1091
1092    return 0;
1093}
1094
1095static void
1096reset_context(krb5_context context, krb5_sendto_ctx ctx)
1097{
1098    krb5_data_free(&ctx->response);
1099    heim_release(ctx->hosts);
1100    ctx->hosts = heim_array_create();
1101    ctx->stateflags = 0;
1102}
1103
1104
1105/*
1106 *
1107 */
1108
1109KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL
1110krb5_sendto_context(krb5_context context,
1111		    krb5_sendto_ctx ctx,
1112		    const krb5_data *send_data,
1113		    krb5_const_realm realm,
1114		    krb5_data *receive)
1115{
1116    krb5_error_code ret = 0;
1117    krb5_krbhst_handle handle = NULL;
1118    struct timeval nrstart, nrstop, stop_time;
1119    int type, freectx = 0;
1120    int action;
1121    int numreset = 0;
1122
1123    krb5_data_zero(receive);
1124
1125    if (ctx == NULL) {
1126	ret = krb5_sendto_ctx_alloc(context, &ctx);
1127	if (ret)
1128	    goto out;
1129	freectx = 1;
1130    }
1131
1132    ctx->stid = (context->num_kdc_requests++) << 16;
1133
1134    memset(&ctx->stats, 0, sizeof(ctx->stats));
1135    gettimeofday(&ctx->stats.start_time, NULL);
1136
1137    type = ctx->type;
1138    if (type == 0) {
1139	if ((ctx->flags & KRB5_KRBHST_FLAGS_MASTER) || context->use_admin_kdc)
1140	    type = KRB5_KRBHST_ADMIN;
1141	else
1142	    type = KRB5_KRBHST_KDC;
1143    }
1144
1145    ctx->send_data = send_data;
1146
1147    if ((int)send_data->length > context->large_msg_size)
1148	ctx->flags |= KRB5_KRBHST_FLAGS_LARGE_MSG;
1149
1150    /* loop until we get back a appropriate response */
1151
1152    action = KRB5_SENDTO_INITIAL;
1153
1154    while (action != KRB5_SENDTO_DONE && action != KRB5_SENDTO_FAILED) {
1155	krb5_krbhst_info *hi;
1156
1157	switch (action) {
1158	case KRB5_SENDTO_INITIAL:
1159	    ret = realm_via_plugin(context, realm, context->kdc_timeout,
1160				   send_data, &ctx->response);
1161	    if (ret == 0 || ret != KRB5_PLUGIN_NO_HANDLE) {
1162		action = KRB5_SENDTO_DONE;
1163		break;
1164	    }
1165	    action = KRB5_SENDTO_KRBHST;
1166	    /* FALLTHROUGH */
1167	case KRB5_SENDTO_KRBHST:
1168	    if (ctx->krbhst == NULL) {
1169		ret = krb5_krbhst_init_flags(context, realm, type,
1170					     ctx->flags, &handle);
1171		if (ret)
1172		    goto out;
1173
1174		if (ctx->hostname) {
1175		    ret = krb5_krbhst_set_hostname(context, handle, ctx->hostname);
1176		    if (ret)
1177			goto out;
1178		}
1179
1180	    } else {
1181		handle = heim_retain(ctx->krbhst);
1182	    }
1183	    action = KRB5_SENDTO_TIMEOUT;
1184	    /* FALLTHROUGH */
1185	case KRB5_SENDTO_TIMEOUT:
1186
1187	    /*
1188	     * If we completed, just got to next step
1189	     */
1190
1191	    if (ctx->stateflags & KRBHST_COMPLETED) {
1192		action = KRB5_SENDTO_CONTINUE;
1193		break;
1194	    }
1195
1196	    /*
1197	     * Pull out next host, if there is no more, close the
1198	     * handle and mark as completed.
1199	     *
1200	     * Collect time spent in krbhst (dns, plugin, etc)
1201	     */
1202
1203
1204	    gettimeofday(&nrstart, NULL);
1205
1206	    ret = krb5_krbhst_next(context, handle, &hi);
1207
1208	    gettimeofday(&nrstop, NULL);
1209	    timevalsub(&nrstop, &nrstart);
1210	    timevaladd(&ctx->stats.krbhst, &nrstop);
1211
1212	    action = KRB5_SENDTO_CONTINUE;
1213	    if (ret == 0) {
1214		_krb5_debug(context, 5, "submitting new requests to new host");
1215		if (submit_request(context, ctx, hi) != 0)
1216		    action = KRB5_SENDTO_TIMEOUT;
1217	    } else {
1218		_krb5_debug(context, 5, "out of hosts, waiting for replies");
1219		ctx->stateflags |= KRBHST_COMPLETED;
1220	    }
1221
1222	    break;
1223	case KRB5_SENDTO_CONTINUE:
1224
1225	    ret = wait_response(context, &action, ctx);
1226	    if (ret)
1227		goto out;
1228
1229	    break;
1230	case KRB5_SENDTO_RESET:
1231	    /* start over */
1232	    _krb5_debug(context, 5,
1233			"krb5_sendto trying over again (reset): %d",
1234			numreset);
1235	    reset_context(context, ctx);
1236	    if (handle) {
1237		krb5_krbhst_free(context, handle);
1238		handle = NULL;
1239	    }
1240	    numreset++;
1241	    if (numreset >= 3)
1242		action = KRB5_SENDTO_FAILED;
1243	    else
1244		action = KRB5_SENDTO_KRBHST;
1245
1246	    break;
1247	case KRB5_SENDTO_FILTER:
1248	    /* default to next state, the filter function might modify this */
1249	    action = KRB5_SENDTO_DONE;
1250
1251	    if (ctx->func) {
1252		ret = (*ctx->func)(context, ctx, ctx->data,
1253				   &ctx->response, &action);
1254		if (ret)
1255		    goto out;
1256	    }
1257	    break;
1258	case KRB5_SENDTO_FAILED:
1259	    ret = KRB5_KDC_UNREACH;
1260	    break;
1261	case KRB5_SENDTO_DONE:
1262	    ret = 0;
1263	    break;
1264	default:
1265	    heim_abort("invalid krb5_sendto_context state");
1266	}
1267    }
1268
1269out:
1270    gettimeofday(&stop_time, NULL);
1271    timevalsub(&stop_time, &ctx->stats.start_time);
1272    if (ret == 0 && ctx->response.length) {
1273	*receive = ctx->response;
1274	krb5_data_zero(&ctx->response);
1275    } else {
1276	krb5_data_free(&ctx->response);
1277	krb5_clear_error_message (context);
1278	ret = KRB5_KDC_UNREACH;
1279	krb5_set_error_message(context, ret,
1280			       N_("unable to reach any KDC in realm %s", ""),
1281			       realm);
1282    }
1283
1284    _krb5_debug(context, 1,
1285		"%s %s done: %d hosts %lu packets %lu:"
1286		" wc: %jd.%06lu nr: %jd.%06lu kh: %jd.%06lu tid: %08x",
1287		__func__, realm, ret,
1288		ctx->stats.num_hosts, ctx->stats.sent_packets,
1289		(intmax_t)stop_time.tv_sec,
1290		(unsigned long)stop_time.tv_usec,
1291		(intmax_t)ctx->stats.name_resolution.tv_sec,
1292		(unsigned long)ctx->stats.name_resolution.tv_usec,
1293		(intmax_t)ctx->stats.krbhst.tv_sec,
1294		(unsigned long)ctx->stats.krbhst.tv_usec, ctx->stid);
1295
1296    if (freectx)
1297	krb5_sendto_ctx_free(context, ctx);
1298    else
1299	reset_context(context, ctx);
1300
1301    if (handle)
1302	krb5_krbhst_free(context, handle);
1303
1304    return ret;
1305}
1306