GSS_S_PROMPTING_NEEDED is a bit
[cyrus-sasl.git] / saslauthd / auth_httpform.c
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 #ifdef __GNUC__
57 #ident "$Id: auth_httpform.c,v 1.2 2006/04/19 19:51:04 murch Exp $"
58 #endif
59
60 /* PUBLIC DEPENDENCIES */
61 #include <unistd.h>
62 #include <stdlib.h>
63 #include <assert.h>
64 #include <errno.h>
65 #include <string.h>
66 #ifdef _AIX
67 # include <strings.h>
68 #endif /* _AIX */
69 #include <syslog.h>
70 #include <sys/types.h>
71 #include <sys/socket.h>
72 #include <netinet/in.h>
73 #include <arpa/inet.h>
74 #include <signal.h>
75 #include <netdb.h>
76
77 #include "mechanisms.h"
78 #include "utils.h"
79 #include "cfile.h"
80 #include "globals.h"
81 #include "auth_httpform.h"
82 /* END PUBLIC DEPENDENCIES */
83
84 #ifndef MAX
85 #define MAX(p,q) ((p >= q) ? p : q)
86 #endif
87
88 /* PRIVATE DEPENDENCIES */
89 static cfile config = NULL;
90 static const char *r_host = "localhost";  /* remote host (mech_option) */
91 static const char *r_port = "80";       /* remote port (mech_option) */
92 static const char *r_uri = NULL;        /* URI to call (mech_option) */
93 static const char *formdata = NULL;     /* HTML form data (mech_option) */
94 static struct addrinfo *ai = NULL;      /* remote host, as looked up    */
95 /* END PRIVATE DEPENDENCIES */
96
97 #define NETWORK_IO_TIMEOUT 30           /* network I/O timeout (seconds) */
98 #define RESP_LEN 1000                   /* size of read response buffer  */
99
100 #define TWO_CRLF "\r\n\r\n"
101 #define CRLF "\r\n"
102 #define SPACE " "
103
104 #define HTTP_STATUS_SUCCESS "200"
105 #define HTTP_STATUS_REFUSE "403"
106
107 /* Common failure response strings for auth_httpform() */
108
109 #define RESP_IERROR     "NO [ALERT] saslauthd internal error"
110 #define RESP_UNAVAILABLE "NO [ALERT] The remote authentication server is currently unavailable"
111 #define RESP_UNEXPECTED "NO [ALERT] Unexpected response from remote authentication server"
112 \f
113 /* FUNCTION: sig_null */
114
115 /* SYNOPSIS
116  * Catch and ignore a signal.
117  * END SYNOPSIS */
118
119 static RETSIGTYPE                               /* R: OS dependent */
120 sig_null (
121   /* PARAMETERS */
122   int sig                                       /* I: signal being caught */
123   /* END PARAMETERS */
124   )
125 {
126     switch (sig) {
127         
128       case SIGALRM:
129         signal(SIGALRM, sig_null);
130         break;
131
132       case SIGPIPE:
133         signal(SIGPIPE, sig_null);
134         break;
135
136       default:
137         logger(L_INFO, "auth_httpform", "unexpected signal %d", sig);
138         break;
139     }
140 #ifdef __APPLE__
141     return;
142 #else /* __APPLE__ */
143 # if RETSIGTYPE == void
144     return;
145 # else /* RETSIGTYPE */
146     return 0;
147 # endif /* RETSIGTYPE */
148 #endif /* __APPLE__ */
149 }
150
151 /* END FUNCTION: sig_null */
152 \f
153 /* FUNCTION: url_escape */
154
155 /* SYNOPSIS
156  * URL-escapes the given string 
157  * 
158  * Note: calling function must free memory.
159  * 
160  * END SYNOPSIS */
161 static char *url_escape(
162   /* PARAMETERS */
163   const char *string
164   /* END PARAMETERS */
165   )
166 {
167     /* VARIABLES */
168     size_t length = strlen(string);
169     size_t alloc = length+50;   /* add some reserve */
170     char *out;
171     int outidx=0, inidx=0;
172     /* END VARIABLES */
173
174     out = malloc(alloc);
175     if (!out)
176         return NULL;
177
178     while (inidx < length) {
179         char in = string[inidx];
180         if (!(in >= 'a' && in <= 'z') &&
181             !(in >= 'A' && in <= 'Z') &&
182             !(in >= '0' && in <= '9') &&
183             in != '&' && in != '=' && in != '-' && in != '_') {
184
185             /* encode it */
186             if (outidx+3 > alloc) {
187                 /* the size grows with two, since this'll become a %XX */
188                 char *tmp = NULL;
189                 alloc *= 2;
190                 tmp = realloc(out, alloc);
191                 if (!tmp) {
192                     free(out);
193                     return NULL;
194                 } else {
195                     out = tmp;
196                 }
197             }
198             
199             snprintf(&out[outidx], 4, "%%%02X", in);
200             outidx += 3;
201         } else {
202             /* just copy this */
203             out[outidx++] = in;
204         }
205
206         inidx++;
207     }
208     out[outidx] = 0; /* terminate it */
209     return out;
210 }
211
212 /* END FUNCTION: url_escape */
213 \f
214 /* FUNCTION: create_post_data */
215
216 /* SYNOPSIS
217  * Replace %u, %p and %r in the form data read from the config file
218  * with the actual username and password.
219  * 
220  * Large parts of this functions have been shamelessly copied from
221  * the sql_create_statement() in the sql.c plugin code
222  * 
223  * Note: calling function must free memory.
224  * 
225  * END SYNOPSIS */
226
227 static char *create_post_data(
228   /* PARAMETERS */
229   const char *formdata, 
230   const char *user,
231   const char *password,
232   const char *realm
233   /* END PARAMETERS */
234   )
235 {
236     /* VARIABLES */
237     const char *ptr, *line_ptr;
238     char *buf, *buf_ptr;
239     int filtersize;
240     int ulen, plen, rlen;
241     int numpercents=0;
242     int biggest;
243     size_t i;
244     /* END VARIABLES */
245     
246     /* calculate memory needed for creating the complete query string. */
247     ulen = strlen(user);
248     plen = strlen(password);
249     rlen = strlen(realm);
250     
251     /* what if we have multiple %foo occurrences in the input query? */
252     for (i = 0; i < strlen(formdata); i++) {
253         if (formdata[i] == '%') {
254             numpercents++;
255         }
256     }
257     
258     /* find the biggest of ulen, plen */
259     biggest = MAX(MAX(ulen, plen), rlen);
260     
261     /* don't forget the trailing 0x0 */
262     filtersize = strlen(formdata) + 1 + (numpercents*biggest)+1;
263     
264     /* ok, now try to allocate a chunk of that size */
265     buf = (char *) malloc(filtersize);
266     
267     if (!buf) {
268         logger(LOG_ERR, "auth_httpform:create_post_data", "failed to allocate memory");
269         return NULL;
270     }
271     
272     buf_ptr = buf;
273     line_ptr = formdata;
274     
275     /* replace the strings */
276     while ( (ptr = strchr(line_ptr, '%')) ) {
277         /* copy up to but not including the next % */
278         memcpy(buf_ptr, line_ptr, ptr - line_ptr); 
279         buf_ptr += ptr - line_ptr;
280         ptr++;
281         switch (ptr[0]) {
282         case '%':
283             buf_ptr[0] = '%';
284             buf_ptr++;
285             break;
286         case 'u':
287             memcpy(buf_ptr, user, ulen);
288             buf_ptr += ulen;
289             break;
290         case 'p':
291             memcpy(buf_ptr, password, plen);
292             buf_ptr += plen;
293             break;
294         case 'r':
295             memcpy(buf_ptr, realm, rlen);
296             buf_ptr += rlen;
297             break;
298         default:
299             buf_ptr[0] = '%';
300             buf_ptr[1] = ptr[0];
301             buf_ptr += 2;
302             break;
303         }
304         ptr++;
305         line_ptr = ptr;
306     }
307
308     /* don't forget the rest */    
309     memcpy(buf_ptr, line_ptr, strlen(line_ptr)+1);
310
311     return buf;
312 }
313
314 /* END FUNCTION: create_post_data */
315 \f
316 /* FUNCTION: build_sasl_response */
317
318 /* SYNOPSIS
319  * Build a SASL response out of the HTTP response
320  * 
321  * Note: The returned string is malloced and will be free'd by the 
322  * saslauthd core
323  * 
324  * END SYNOPSIS */
325 static char *build_sasl_response(
326   /* PARAMETERS */
327   const char *http_response
328   /* END PARAMETERS */
329   )
330 {
331     /* VARIABLES */
332     size_t length = 0;
333     char *c, *http_response_code, *http_response_string;
334     char *sasl_response;
335     /* END VARIABLES */
336     
337     /* parse the response, just the first line */
338     /* e.g. HTTP/1.1 200 OK */
339     /* e.g. HTTP/1.1 403 User unknown */
340     c = strpbrk(http_response, CRLF);
341     if (c != NULL) {
342         *c = '\0';                      /* tie off line termination */
343     }
344
345     /* isolate the HTTP response code and string */
346     http_response_code = strpbrk(http_response, SPACE) + 1;
347     http_response_string = strpbrk(http_response_code, SPACE) + 1;
348     *(http_response_string-1) = '\0';  /* replace space after code with 0 */
349
350     if (!strcmp(http_response_code, HTTP_STATUS_SUCCESS)) {
351         return strdup("OK remote authentication successful");
352     }
353     if (!strcmp(http_response_code, HTTP_STATUS_REFUSE)) {
354         /* return the HTTP response string as the SASL response */
355         length = strlen(http_response_string) + 3 + 1;
356         sasl_response = malloc(length);
357         if (sasl_response == NULL)
358             return NULL;
359             
360         snprintf(sasl_response, length, "NO %s", http_response_string);
361         return sasl_response;
362     }
363     
364     logger(L_INFO, "auth_httpform", "unexpected response to auth request: %s %s",
365            http_response_code, http_response_string);
366
367     return strdup(RESP_UNEXPECTED);
368 }
369
370 /* END FUNCTION: build_sasl_response */
371 \f
372 /* FUNCTION: auth_httpform_init */
373
374 /* SYNOPSIS
375  * Validate the host and service names for the remote server.
376  * END SYNOPSIS */
377
378 int
379 auth_httpform_init (
380   /* PARAMETERS */
381   void                                  /* no parameters */
382   /* END PARAMETERS */
383   )
384 {
385     /* VARIABLES */
386     int rc;
387     char *configname = NULL;
388     struct addrinfo hints;
389     /* END VARIABLES */
390
391     /* name of config file may be given with -O option */
392     if (mech_option)
393         configname = mech_option;
394     else if (access(SASLAUTHD_CONF_FILE_DEFAULT, F_OK) == 0)
395         configname = SASLAUTHD_CONF_FILE_DEFAULT;
396  
397     /* open and read config file */
398     if (configname) {
399         char complaint[1024];
400
401         if (!(config = cfile_read(configname, complaint, sizeof (complaint)))) {
402             syslog(LOG_ERR, "auth_httpform_init %s", complaint);
403             return -1;
404         }
405     }
406
407     if (config) {
408         r_host = cfile_getstring(config, "httpform_host", r_host);
409         r_port = cfile_getstring(config, "httpform_port", r_port);
410         r_uri = cfile_getstring(config, "httpform_uri", r_uri);
411         formdata = cfile_getstring(config, "httpform_data", formdata);
412     }
413     
414     if (formdata == NULL || r_uri == NULL) {
415         syslog(LOG_ERR, "auth_httpform_init formdata and uri must be specified");
416         return -1;
417     }
418
419     /* lookup the host/port - taken from auth_rimap */
420     if (ai)
421         freeaddrinfo(ai);
422     memset(&hints, 0, sizeof(hints));
423     hints.ai_family = PF_UNSPEC;
424     hints.ai_socktype = SOCK_STREAM;
425     hints.ai_flags = AI_CANONNAME;
426     if ((rc = getaddrinfo(r_host, r_port, &hints, &ai)) != 0) {
427         syslog(LOG_ERR, "auth_httpform_init: getaddrinfo %s/%s: %s",
428                r_host, r_port, gai_strerror(rc));
429         return -1;
430      }
431      
432     /* Make sure we have AF_INET or AF_INET6 addresses. */
433     if (ai->ai_family != AF_INET && ai->ai_family != AF_INET6) {
434         syslog(LOG_ERR, "auth_httpform_init: no IP address info for %s",
435                ai->ai_canonname ? ai->ai_canonname : r_host);
436         freeaddrinfo(ai);
437         ai = NULL;
438         return -1;
439     }
440
441     return 0;
442 }
443
444 /* END FUNCTION: auth_httpform_init */
445 \f
446 /* FUNCTION: auth_httpform */
447
448 /* SYNOPSIS
449  * Proxy authenticate to a remote HTTP server with a form POST.
450  *
451  * This mechanism takes the plaintext authenticator and password, forms
452  * them into an HTTP POST request. If the HTTP server responds with a 200
453  * status code, the credentials are considered valid. If it responds with
454  * a 403 HTTP status code, the credentials are considered wrong. Any other
455  * HTTP status code is treated like a network error.
456  */
457
458 /* XXX This should be extended to support SASL PLAIN authentication */
459
460 char *                                  /* R: Allocated response string */
461 auth_httpform (
462   /* PARAMETERS */
463   const char *user,                     /* I: plaintext authenticator */
464   const char *password,                 /* I: plaintext password */
465   const char *service,
466   const char *realm
467   /* END PARAMETERS */
468   )
469 {
470     /* VARIABLES */
471     int s=-1;                           /* socket to remote auth host   */
472     struct addrinfo *r;                 /* remote socket address info   */
473     char *req;                          /* request, with user and pw    */
474     char *escreq;                       /* URL-escaped request          */
475     char *c;                            /* scratch pointer              */
476     int rc;                             /* return code scratch area     */
477     char postbuf[RESP_LEN];             /* request buffer               */
478     int postlen;                        /* length of post request       */
479     char rbuf[RESP_LEN];                /* response read buffer         */
480     char hbuf[NI_MAXHOST], pbuf[NI_MAXSERV];
481     int saved_errno;
482     int niflags;
483     /* END VARIABLES */
484
485     /* sanity checks */
486     assert(user != NULL);
487     assert(password != NULL);
488
489     /*establish connection to remote */
490     for (r = ai; r; r = r->ai_next) {
491         s = socket(r->ai_family, r->ai_socktype, r->ai_protocol);
492         if (s < 0)
493             continue;
494         if (connect(s, r->ai_addr, r->ai_addrlen) >= 0)
495             break;
496         close(s);
497         s = -1;
498         saved_errno = errno;
499         niflags = (NI_NUMERICHOST | NI_NUMERICSERV);
500 #ifdef NI_WITHSCOPEID
501         if (r->ai_family == AF_INET6)
502             niflags |= NI_WITHSCOPEID;
503 #endif
504         if (getnameinfo(r->ai_addr, r->ai_addrlen, hbuf, sizeof(hbuf),
505                         pbuf, sizeof(pbuf), niflags) != 0) {
506             strlcpy(hbuf, "unknown", sizeof(hbuf));
507             strlcpy(pbuf, "unknown", sizeof(pbuf));
508         }
509         errno = saved_errno;
510         syslog(LOG_WARNING, "auth_httpform: connect %s[%s]/%s: %m",
511                ai->ai_canonname ? ai->ai_canonname : r_host, hbuf, pbuf);
512     }
513     if (s < 0) {
514         if (getnameinfo(ai->ai_addr, ai->ai_addrlen, NULL, 0,
515                         pbuf, sizeof(pbuf), NI_NUMERICSERV) != 0)
516             strlcpy(pbuf, "unknown", sizeof(pbuf));
517         syslog(LOG_WARNING, "auth_httpform: couldn't connect to %s/%s",
518                ai->ai_canonname ? ai->ai_canonname : r_host, pbuf);
519         return strdup("NO [ALERT] Couldn't contact remote authentication server");
520     }
521
522     /* CLAIM: we now have a TCP connection to the remote HTTP server */
523
524     /*
525      * Install noop signal handlers. These just reinstall the handler
526      * and return so that we take an EINTR during network I/O.
527      */
528     (void) signal(SIGALRM, sig_null);
529     (void) signal(SIGPIPE, sig_null);
530     
531     /* build the HTTP request */
532     req = create_post_data(formdata, user, password, realm);
533     if (req == NULL) {
534         close(s);
535         syslog(LOG_WARNING, "auth_httpform: create_post_data == NULL");
536         return strdup(RESP_IERROR);
537     }
538     escreq = url_escape(req);
539     if (escreq == NULL) {
540         memset(req, 0, strlen(req));
541         free(req); 
542         close(s);
543         syslog(LOG_WARNING, "auth_httpform: url_escape == NULL");
544         return strdup(RESP_IERROR);
545     }
546
547     postlen = snprintf(postbuf, RESP_LEN-1,
548               "POST %s HTTP/1.1" CRLF
549               "Host: %s:%s" CRLF
550               "User-Agent: saslauthd" CRLF
551               "Accept: */*" CRLF
552               "Content-Type: application/x-www-form-urlencoded" CRLF
553               "Content-Length: %d" TWO_CRLF
554               "%s",
555               r_uri, r_host, r_port, strlen(escreq), escreq);
556
557     if (flags & VERBOSE) {
558         syslog(LOG_DEBUG, "auth_httpform: sending %s %s %s",
559                r_host, r_uri, escreq);
560     }
561     
562     /* send it */
563     alarm(NETWORK_IO_TIMEOUT);
564     rc = tx_rec(s, postbuf, postlen);
565     alarm(0);
566     
567     if (rc < postlen) {
568         syslog(LOG_WARNING, "auth_httpform: failed to send request");
569         memset(req, 0, strlen(req));
570         free(req); 
571         memset(escreq, 0, strlen(escreq));
572         free(escreq);
573         memset(postbuf, 0, postlen);
574         close(s);
575         return strdup(RESP_IERROR);
576     }
577
578     /* don't need these any longer */
579     memset(req, 0, strlen(req));
580     free(req); 
581     memset(escreq, 0, strlen(escreq));
582     free(escreq);
583     memset(postbuf, 0, postlen);
584
585     /* read and parse the response */
586     alarm(NETWORK_IO_TIMEOUT);
587     rc = read(s, rbuf, sizeof(rbuf));
588     alarm(0);
589     
590     close(s);                    /* we're done with the remote */
591
592     if (rc == -1) {
593         syslog(LOG_WARNING, "auth_httpform: read (response): %m");
594         return strdup(RESP_IERROR);
595     }
596
597     if (flags & VERBOSE) {
598         syslog(LOG_DEBUG, "auth_httpform: [%s] %s", user, rbuf);
599     }
600
601     rbuf[rc] = '\0';             /* make sure str-funcs find null */
602     return build_sasl_response(rbuf);
603 }
604
605 /* END FUNCTION: auth_httpform */
606
607 /* END MODULE: auth_httpform */