1 /* MODULE: auth_httpform */
2
3 /* COPYRIGHT
4 * Copyright (c) 2005 Pyx Engineering AG
5 * Copyright (c) 1998 Messaging Direct Ltd.
6 * All rights reserved.
7 *
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions
10 * are met:
11 * 1. Redistributions of source code must retain the above copyright
12 * notice, this list of conditions and the following disclaimer.
13 * 2. Redistributions in binary form must reproduce the above copyright
14 * notice, this list of conditions and the following disclaimer in the
15 * documentation and/or other materials provided with the distribution.
16 *
17 * THIS SOFTWARE IS PROVIDED BY MESSAGING DIRECT LTD. ``AS IS'' AND ANY
18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL MESSAGING DIRECT LTD. OR
21 * ITS EMPLOYEES OR AGENTS BE LIABLE FOR ANY DIRECT, INDIRECT,
22 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
23 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
24 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
26 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
27 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
28 * DAMAGE.
29 *
30 * Copyright 1998, 1999 Carnegie Mellon University
31 *
32 * All Rights Reserved
33 *
34 * Permission to use, copy, modify, and distribute this software and its
35 * documentation for any purpose and without fee is hereby granted,
36 * provided that the above copyright notice appear in all copies and that
37 * both that copyright notice and this permission notice appear in
38 * supporting documentation, and that the name of Carnegie Mellon
39 * University not be used in advertising or publicity pertaining to
40 * distribution of the software without specific, written prior
41 * permission.
42 *
43 * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
44 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
45 * FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE FOR
46 * ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
47 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
48 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
49 * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
50 * END COPYRIGHT */
51
52 /* SYNOPSIS
53 * Proxy authentication to a remote HTTP server.
54 * END SYNOPSIS */
55
56 #include <config.h>
57
58 /* PUBLIC DEPENDENCIES */
59 #include <unistd.h>
60 #include <stdlib.h>
61 #include <assert.h>
62 #include <errno.h>
63 #include <string.h>
64 #ifdef _AIX
65 # include <strings.h>
66 #endif /* _AIX */
67 #include <syslog.h>
68 #include <sys/types.h>
69 #include <sys/socket.h>
70 #include <netinet/in.h>
71 #include <arpa/inet.h>
72 #include <signal.h>
73 #include <netdb.h>
74 #include <stdio.h>
75
76 #include "mechanisms.h"
77 #include "utils.h"
78 #include "cfile.h"
79 #include "globals.h"
80 #include "auth_httpform.h"
81 /* END PUBLIC DEPENDENCIES */
82
83 #ifndef MAX
84 #define MAX(p,q) ((p >= q) ? p : q)
85 #endif
86
87 #ifndef MIN
88 #define MIN(p,q) ((p <= q) ? p : q)
89 #endif
90
91 /* PRIVATE DEPENDENCIES */
92 static cfile config = NULL;
93 static const char *r_host = "localhost"; /* remote host (mech_option) */
94 static const char *r_port = "80"; /* remote port (mech_option) */
95 static const char *r_uri = NULL; /* URI to call (mech_option) */
96 static const char *formdata = NULL; /* HTML form data (mech_option) */
97 static struct addrinfo *ai = NULL; /* remote host, as looked up */
98 /* END PRIVATE DEPENDENCIES */
99
100 #define NETWORK_IO_TIMEOUT 30 /* network I/O timeout (seconds) */
101 #define RESP_LEN 1000 /* size of read response buffer */
102
103 #define TWO_CRLF "\r\n\r\n"
104 #define CRLF "\r\n"
105 #define SPACE " "
106
107 #define HTTP_STATUS_SUCCESS "200"
108 #define HTTP_STATUS_NOCONTENT "204"
109 #define HTTP_STATUS_REFUSE "403"
110
111 /* Common failure response strings for auth_httpform() */
112
113 #define RESP_IERROR "NO [ALERT] saslauthd internal error"
114 #define RESP_UNAVAILABLE "NO [ALERT] The remote authentication server is currently unavailable"
115 #define RESP_UNEXPECTED "NO [ALERT] Unexpected response from remote authentication server"
116
117 /* FUNCTION: sig_null */
118
119 /* SYNOPSIS
120 * Catch and ignore a signal.
121 * END SYNOPSIS */
122
123 static RETSIGTYPE /* R: OS dependent */
sig_null(int sig)124 sig_null (
125 /* PARAMETERS */
126 int sig /* I: signal being caught */
127 /* END PARAMETERS */
128 )
129 {
130 switch (sig) {
131
132 case SIGALRM:
133 signal(SIGALRM, sig_null);
134 break;
135
136 case SIGPIPE:
137 signal(SIGPIPE, sig_null);
138 break;
139
140 default:
141 logger(L_INFO, "auth_httpform", "unexpected signal %d", sig);
142 break;
143 }
144 #ifdef __APPLE__
145 return;
146 #else /* __APPLE__ */
147 # if RETSIGTYPE == void
148 return;
149 # else /* RETSIGTYPE */
150 return 0;
151 # endif /* RETSIGTYPE */
152 #endif /* __APPLE__ */
153 }
154
155 /* END FUNCTION: sig_null */
156
157 /* FUNCTION: url_escape */
158
159 /* SYNOPSIS
160 * URL-escapes the given string
161 *
162 * Note: calling function must free memory.
163 *
164 * END SYNOPSIS */
url_escape(const char * string)165 static char *url_escape(
166 /* PARAMETERS */
167 const char *string
168 /* END PARAMETERS */
169 )
170 {
171 /* VARIABLES */
172 size_t length = strlen(string);
173 size_t alloc = length+50; /* add some reserve */
174 char *out;
175 size_t outidx=0, inidx=0;
176 /* END VARIABLES */
177
178 out = malloc(alloc);
179 if (!out)
180 return NULL;
181
182 while (inidx < length) {
183 unsigned char in = (unsigned char)string[inidx];
184 if (!(in >= 'a' && in <= 'z') &&
185 !(in >= 'A' && in <= 'Z') &&
186 !(in >= '0' && in <= '9')) {
187
188 /* encode it */
189 if (outidx+3 > alloc) {
190 /* the size grows with two, since this'll become a %XX */
191 char *tmp = NULL;
192 alloc *= 2;
193 tmp = realloc(out, alloc);
194 if (!tmp) {
195 free(out);
196 return NULL;
197 } else {
198 out = tmp;
199 }
200 }
201
202 snprintf(&out[outidx], 4, "%%%02X", in);
203 outidx += 3;
204 } else {
205 /* just copy this */
206 out[outidx++] = in;
207 }
208
209 inidx++;
210 }
211 out[outidx] = 0; /* terminate it */
212 return out;
213 }
214
215 /* END FUNCTION: url_escape */
216
217 /* FUNCTION: create_post_data */
218
219 /* SYNOPSIS
220 * Replace %u, %p and %r in the form data read from the config file
221 * with the actual username and password.
222 *
223 * Large parts of this functions have been shamelessly copied from
224 * the sql_create_statement() in the sql.c plugin code
225 *
226 * Note: calling function must free memory.
227 *
228 * END SYNOPSIS */
229
create_post_data(const char * formdata,const char * user,const char * password,const char * realm)230 static char *create_post_data(
231 /* PARAMETERS */
232 const char *formdata,
233 const char *user,
234 const char *password,
235 const char *realm
236 /* END PARAMETERS */
237 )
238 {
239 /* VARIABLES */
240 const char *ptr, *line_ptr;
241 char *esc_user = NULL, *esc_password = NULL, *esc_realm = NULL;
242 char *buf = NULL, *buf_ptr;
243 int filtersize;
244 int ulen, plen, rlen;
245 int numpercents=0;
246 int biggest;
247 size_t i;
248 /* END VARIABLES */
249
250 user = esc_user = url_escape(user);
251 password = esc_password = url_escape(password);
252 realm = esc_realm = url_escape(realm);
253 if (!user || !password || !realm) {
254 logger(LOG_ERR, "auth_httpform:create_post_data", "failed to allocate memory");
255 goto CLEANUP;
256 }
257
258 /* calculate memory needed for creating the complete query string. */
259 ulen = strlen(user);
260 plen = strlen(password);
261 rlen = strlen(realm);
262
263 /* what if we have multiple %foo occurrences in the input query? */
264 for (i = 0; i < strlen(formdata); i++) {
265 if (formdata[i] == '%') {
266 numpercents++;
267 }
268 }
269
270 /* find the biggest of ulen, plen */
271 biggest = MAX(MAX(ulen, plen), rlen);
272
273 /* don't forget the trailing 0x0 */
274 filtersize = strlen(formdata) + 1 + (numpercents*biggest)+1;
275
276 /* ok, now try to allocate a chunk of that size */
277 buf = (char *) malloc(filtersize);
278
279 if (!buf) {
280 logger(LOG_ERR, "auth_httpform:create_post_data", "failed to allocate memory");
281 goto CLEANUP;
282 }
283
284 buf_ptr = buf;
285 line_ptr = formdata;
286
287 /* replace the strings */
288 while ( (ptr = strchr(line_ptr, '%')) ) {
289 /* copy up to but not including the next % */
290 memcpy(buf_ptr, line_ptr, ptr - line_ptr);
291 buf_ptr += ptr - line_ptr;
292 ptr++;
293 switch (ptr[0]) {
294 case '%':
295 buf_ptr[0] = '%';
296 buf_ptr++;
297 break;
298 case 'u':
299 memcpy(buf_ptr, user, ulen);
300 buf_ptr += ulen;
301 break;
302 case 'p':
303 memcpy(buf_ptr, password, plen);
304 buf_ptr += plen;
305 break;
306 case 'r':
307 memcpy(buf_ptr, realm, rlen);
308 buf_ptr += rlen;
309 break;
310 default:
311 buf_ptr[0] = '%';
312 buf_ptr[1] = ptr[0];
313 buf_ptr += 2;
314 break;
315 }
316 ptr++;
317 line_ptr = ptr;
318 }
319
320 /* don't forget the rest */
321 memcpy(buf_ptr, line_ptr, strlen(line_ptr)+1);
322
323 CLEANUP:
324 if (esc_user) {
325 memset(esc_user, 0, strlen(esc_user));
326 free(esc_user);
327 }
328 if (esc_password) {
329 memset(esc_password, 0, strlen(esc_password));
330 free(esc_password);
331 }
332 if (esc_realm) {
333 memset(esc_realm, 0, strlen(realm));
334 free(esc_realm);
335 }
336
337 return buf;
338 }
339
340 /* END FUNCTION: create_post_data */
341
342 /* FUNCTION: build_sasl_response */
343
344 /* SYNOPSIS
345 * Build a SASL response out of the HTTP response
346 *
347 * Note: The returned string is malloced and will be free'd by the
348 * saslauthd core
349 *
350 * END SYNOPSIS */
build_sasl_response(const char * http_response)351 static char *build_sasl_response(
352 /* PARAMETERS */
353 const char *http_response
354 /* END PARAMETERS */
355 )
356 {
357 /* VARIABLES */
358 size_t length = 0;
359 char *c, *http_response_code, *http_response_string;
360 char *sasl_response;
361 /* END VARIABLES */
362
363 /* parse the response, just the first line */
364 /* e.g. HTTP/1.1 200 OK */
365 /* e.g. HTTP/1.1 204 No Content */
366 /* e.g. HTTP/1.1 403 User unknown */
367 c = strpbrk(http_response, CRLF);
368 if (c != NULL) {
369 *c = '\0'; /* tie off line termination */
370 }
371
372 /* isolate the HTTP response code and string */
373 http_response_code = strpbrk(http_response, SPACE) + 1;
374 http_response_string = strpbrk(http_response_code, SPACE) + 1;
375 *(http_response_string-1) = '\0'; /* replace space after code with 0 */
376
377 if (!strcmp(http_response_code, HTTP_STATUS_SUCCESS) ||
378 !strcmp(http_response_code, HTTP_STATUS_NOCONTENT)) {
379 return strdup("OK remote authentication successful");
380 }
381 if (!strcmp(http_response_code, HTTP_STATUS_REFUSE)) {
382 /* return the HTTP response string as the SASL response */
383 length = strlen(http_response_string) + 3 + 1;
384 sasl_response = malloc(length);
385 if (sasl_response == NULL)
386 return NULL;
387
388 snprintf(sasl_response, length, "NO %s", http_response_string);
389 return sasl_response;
390 }
391
392 logger(L_INFO, "auth_httpform", "unexpected response to auth request: %s %s",
393 http_response_code, http_response_string);
394
395 return strdup(RESP_UNEXPECTED);
396 }
397
398 /* END FUNCTION: build_sasl_response */
399
400 /* FUNCTION: auth_httpform_init */
401
402 /* SYNOPSIS
403 * Validate the host and service names for the remote server.
404 * END SYNOPSIS */
405
406 int
auth_httpform_init(void)407 auth_httpform_init (
408 /* PARAMETERS */
409 void /* no parameters */
410 /* END PARAMETERS */
411 )
412 {
413 /* VARIABLES */
414 int rc;
415 char *configname = NULL;
416 struct addrinfo hints;
417 /* END VARIABLES */
418
419 /* name of config file may be given with -O option */
420 if (mech_option)
421 configname = mech_option;
422 else if (access(SASLAUTHD_CONF_FILE_DEFAULT, F_OK) == 0)
423 configname = SASLAUTHD_CONF_FILE_DEFAULT;
424
425 /* open and read config file */
426 if (configname) {
427 char complaint[1024];
428
429 if (!(config = cfile_read(configname, complaint, sizeof (complaint)))) {
430 syslog(LOG_ERR, "auth_httpform_init %s", complaint);
431 return -1;
432 }
433 }
434
435 if (config) {
436 r_host = cfile_getstring(config, "httpform_host", r_host);
437 r_port = cfile_getstring(config, "httpform_port", r_port);
438 r_uri = cfile_getstring(config, "httpform_uri", r_uri);
439 formdata = cfile_getstring(config, "httpform_data", formdata);
440 }
441
442 if (formdata == NULL || r_uri == NULL) {
443 syslog(LOG_ERR, "auth_httpform_init formdata and uri must be specified");
444 return -1;
445 }
446
447 /* lookup the host/port - taken from auth_rimap */
448 if (ai)
449 freeaddrinfo(ai);
450 memset(&hints, 0, sizeof(hints));
451 hints.ai_family = PF_UNSPEC;
452 hints.ai_socktype = SOCK_STREAM;
453 hints.ai_flags = AI_CANONNAME;
454 if ((rc = getaddrinfo(r_host, r_port, &hints, &ai)) != 0) {
455 syslog(LOG_ERR, "auth_httpform_init: getaddrinfo %s/%s: %s",
456 r_host, r_port, gai_strerror(rc));
457 return -1;
458 }
459
460 /* Make sure we have AF_INET or AF_INET6 addresses. */
461 if (ai->ai_family != AF_INET && ai->ai_family != AF_INET6) {
462 syslog(LOG_ERR, "auth_httpform_init: no IP address info for %s",
463 ai->ai_canonname ? ai->ai_canonname : r_host);
464 freeaddrinfo(ai);
465 ai = NULL;
466 return -1;
467 }
468
469 return 0;
470 }
471
472 /* END FUNCTION: auth_httpform_init */
473
474 /* FUNCTION: auth_httpform */
475
476 /* SYNOPSIS
477 * Proxy authenticate to a remote HTTP server with a form POST.
478 *
479 * This mechanism takes the plaintext authenticator and password, forms
480 * them into an HTTP POST request. If the HTTP server responds with a 200/204
481 * status code, the credentials are considered valid. If it responds with
482 * a 403 HTTP status code, the credentials are considered wrong. Any other
483 * HTTP status code is treated like a network error.
484 */
485
486 /* XXX This should be extended to support SASL PLAIN authentication */
487
488 char * /* R: Allocated response string */
auth_httpform(const char * user,const char * password,const char * service,const char * realm)489 auth_httpform (
490 /* PARAMETERS */
491 const char *user, /* I: plaintext authenticator */
492 const char *password, /* I: plaintext password */
493 const char *service __attribute__((unused)),
494 const char *realm /* I: user's realm */
495 /* END PARAMETERS */
496 )
497 {
498 /* VARIABLES */
499 int s=-1; /* socket to remote auth host */
500 struct addrinfo *r; /* remote socket address info */
501 char *req; /* request, with user and pw */
502 int rc; /* return code scratch area */
503 char postbuf[RESP_LEN]; /* request buffer */
504 int postlen; /* length of post request */
505 char rbuf[RESP_LEN]; /* response read buffer */
506 char hbuf[NI_MAXHOST], pbuf[NI_MAXSERV];
507 int saved_errno;
508 int niflags;
509 /* END VARIABLES */
510
511 /* sanity checks */
512 assert(user != NULL);
513 assert(password != NULL);
514
515 /*establish connection to remote */
516 for (r = ai; r; r = r->ai_next) {
517 s = socket(r->ai_family, r->ai_socktype, r->ai_protocol);
518 if (s < 0)
519 continue;
520 if (connect(s, r->ai_addr, r->ai_addrlen) >= 0)
521 break;
522 close(s);
523 s = -1;
524 saved_errno = errno;
525 niflags = (NI_NUMERICHOST | NI_NUMERICSERV);
526 #ifdef NI_WITHSCOPEID
527 if (r->ai_family == AF_INET6)
528 niflags |= NI_WITHSCOPEID;
529 #endif
530 if (getnameinfo(r->ai_addr, r->ai_addrlen, hbuf, sizeof(hbuf),
531 pbuf, sizeof(pbuf), niflags) != 0) {
532 strlcpy(hbuf, "unknown", sizeof(hbuf));
533 strlcpy(pbuf, "unknown", sizeof(pbuf));
534 }
535 errno = saved_errno;
536 syslog(LOG_WARNING, "auth_httpform: connect %s[%s]/%s: %m",
537 ai->ai_canonname ? ai->ai_canonname : r_host, hbuf, pbuf);
538 }
539 if (s < 0) {
540 if (!ai) {
541 syslog(LOG_WARNING, "auth_httpform: no address given");
542 return strdup("NO [ALERT] No address given");
543 }
544
545 if (getnameinfo(ai->ai_addr, ai->ai_addrlen, NULL, 0,
546 pbuf, sizeof(pbuf), NI_NUMERICSERV) != 0)
547 strlcpy(pbuf, "unknown", sizeof(pbuf));
548 syslog(LOG_WARNING, "auth_httpform: couldn't connect to %s/%s",
549 ai->ai_canonname ? ai->ai_canonname : r_host, pbuf);
550 return strdup("NO [ALERT] Couldn't contact remote authentication server");
551 }
552
553 /* CLAIM: we now have a TCP connection to the remote HTTP server */
554
555 /*
556 * Install noop signal handlers. These just reinstall the handler
557 * and return so that we take an EINTR during network I/O.
558 */
559 (void) signal(SIGALRM, sig_null);
560 (void) signal(SIGPIPE, sig_null);
561
562 /* build the HTTP request */
563 req = create_post_data(formdata, user, password, realm);
564 if (req == NULL) {
565 close(s);
566 syslog(LOG_WARNING, "auth_httpform: create_post_data == NULL");
567 return strdup(RESP_IERROR);
568 }
569
570 postlen = snprintf(postbuf, RESP_LEN-1,
571 "POST %s HTTP/1.1" CRLF
572 "Host: %s:%s" CRLF
573 "Connection: close" CRLF
574 "User-Agent: saslauthd" CRLF
575 "Accept: */*" CRLF
576 "Content-Type: application/x-www-form-urlencoded" CRLF
577 "Content-Length: %d" TWO_CRLF
578 "%s",
579 r_uri, r_host, r_port, (int)strlen(req), req);
580
581 if (flags & VERBOSE) {
582 syslog(LOG_DEBUG, "auth_httpform: sending %s %s %s",
583 r_host, r_uri, req);
584 }
585
586 /* send it */
587 alarm(NETWORK_IO_TIMEOUT);
588 rc = tx_rec(s, postbuf, postlen);
589 alarm(0);
590
591 if (rc < postlen) {
592 syslog(LOG_WARNING, "auth_httpform: failed to send request");
593 memset(req, 0, strlen(req));
594 free(req);
595 memset(postbuf, 0, postlen);
596 close(s);
597 return strdup(RESP_IERROR);
598 }
599
600 /* don't need these any longer */
601 memset(req, 0, strlen(req));
602 free(req);
603 memset(postbuf, 0, postlen);
604
605 /* read and parse the response */
606 alarm(NETWORK_IO_TIMEOUT);
607 rc = read(s, rbuf, RESP_LEN-1);
608 alarm(0);
609
610 close(s); /* we're done with the remote */
611
612 if (rc == -1) {
613 syslog(LOG_WARNING, "auth_httpform: read (response): %m");
614 return strdup(RESP_IERROR);
615 }
616
617 rc = MIN(rc, RESP_LEN - 1); /* don't write past rbuf */
618 rbuf[rc] = '\0'; /* make sure str-funcs find null */
619
620 if (flags & VERBOSE) {
621 syslog(LOG_DEBUG, "auth_httpform: [%s] %s", user, rbuf);
622 }
623
624 return build_sasl_response(rbuf);
625 }
626
627 /* END FUNCTION: auth_httpform */
628
629 /* END MODULE: auth_httpform */
630