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