add a new http.conf option, sslbindaddr.
[asterisk/asterisk.git] / main / http.c
index d5d33f8..96cb8d1 100644 (file)
@@ -57,21 +57,86 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
 #define MAX_PREFIX 80
 #define DEFAULT_PREFIX "/asterisk"
 
-struct ast_http_server_instance {
-       FILE *f;
-       int fd;
+/*!
+ * In order to have SSL support, we need the openssl libraries.
+ * Still we can decide whether or not to use them by commenting
+ * in or out the DO_SSL macro.
+ * We declare most of ssl support variables unconditionally,
+ * because their number is small and this simplifies the code.
+ * XXX this eventually goes into a generic header file.
+ */
+#if defined(HAVE_OPENSSL) && (defined(HAVE_FUNOPEN) || defined(HAVE_FOPENCOOKIE))
+#define        DO_SSL  /* comment in/out if you want to support ssl */
+#endif
+
+#ifdef DO_SSL
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+static SSL_CTX* ssl_ctx;
+#endif /* DO_SSL */
+
+/* SSL support */
+#define AST_CERTFILE "asterisk.pem"
+static int do_ssl;
+static char *certfile;
+static char *cipher;
+
+/*!
+ * describes a server instance
+ */
+struct server_instance {
+       FILE *f;        /* fopen/funopen result */
+       int fd;         /* the socket returned by accept() */
+       int is_ssl;     /* is this an ssl session ? */
+#ifdef DO_SSL
+       SSL *ssl;       /* ssl state */
+#endif
        struct sockaddr_in requestor;
-       ast_http_callback callback;
+       struct server_args *parent;
+};
+
+/*!
+ * arguments for the accepting thread
+ */
+struct server_args {
+       struct sockaddr_in sin;
+       struct sockaddr_in oldsin;
+       int is_ssl;             /* is this an SSL accept ? */
+       int accept_fd;
+       pthread_t master;
+       void *(*accept_fn)(void *);     /* the function in charge of doing the accept */
+       void *(*worker_fn)(void *);     /* the function in charge of doing the actual work */
+       const char *name;
+};
+
+static void *http_root(void *arg);
+static void *httpd_helper_thread(void *arg);
+
+/*!
+ * we have up to two accepting threads, one for http, one for https
+ */
+static struct server_args http_desc = {
+       .accept_fd = -1,
+       .master = AST_PTHREADT_NULL,
+       .is_ssl = 0,
+       .name = "http server",
+       .accept_fn = http_root,
+       .worker_fn = httpd_helper_thread,
 };
 
-static struct ast_http_uri *uris;
+static struct server_args https_desc = {
+       .accept_fd = -1,
+       .master = AST_PTHREADT_NULL,
+       .is_ssl = 1,
+       .name = "https server",
+       .accept_fn = http_root,
+       .worker_fn = httpd_helper_thread,
+};
 
-static int httpfd = -1;
-static pthread_t master = AST_PTHREADT_NULL;
+static struct ast_http_uri *uris;      /*!< list of supported handlers */
 
 /* all valid URIs must be prepended by the string in prefix. */
 static char prefix[MAX_PREFIX];
-static struct sockaddr_in oldsin;
 static int enablestatic=0;
 
 /*! \brief Limit the kinds of files we're willing to serve up */
@@ -99,6 +164,17 @@ static char *ftype2mtype(const char *ftype, char *wkspace, int wkspacelen)
        return wkspace;
 }
 
+/* like ast_uri_decode, but replace '+' with ' ' */
+static char *uri_decode(char *buf)
+{
+       char *c;
+       ast_uri_decode(buf);
+       for (c = buf; *c; c++) {
+               if (*c == '+')
+                       *c = ' ';
+       }
+       return buf;
+}
 static char *static_callback(struct sockaddr_in *req, const char *uri, struct ast_variable *vars, int *status, char **title, int *contentlength)
 {
        char result[4096];
@@ -185,9 +261,12 @@ static char *httpstatus_callback(struct sockaddr_in *req, const char *uri, struc
 
        ast_build_string(&c, &reslen, "<tr><td><i>Prefix</i></td><td><b>%s</b></td></tr>\r\n", prefix);
        ast_build_string(&c, &reslen, "<tr><td><i>Bind Address</i></td><td><b>%s</b></td></tr>\r\n",
-                       ast_inet_ntoa(oldsin.sin_addr));
+                       ast_inet_ntoa(http_desc.oldsin.sin_addr));
        ast_build_string(&c, &reslen, "<tr><td><i>Bind Port</i></td><td><b>%d</b></td></tr>\r\n",
-                       ntohs(oldsin.sin_port));
+                       ntohs(http_desc.oldsin.sin_port));
+       if (do_ssl)
+               ast_build_string(&c, &reslen, "<tr><td><i>SSL Bind Port</i></td><td><b>%d</b></td></tr>\r\n",
+                       ntohs(https_desc.oldsin.sin_port));
        ast_build_string(&c, &reslen, "<tr><td colspan=\"2\"><hr></td></tr>\r\n");
        v = vars;
        while(v) {
@@ -298,7 +377,7 @@ static char *handle_uri(struct sockaddr_in *sin, char *uri, int *status, char **
                while ((val = strsep(&params, "&"))) {
                        var = strsep(&val, "=");
                        if (val)
-                               ast_uri_decode(val);
+                               uri_decode(val);
                        else 
                                val = "";
                        ast_uri_decode(var);
@@ -361,11 +440,101 @@ static char *handle_uri(struct sockaddr_in *sin, char *uri, int *status, char **
        return c;
 }
 
-static void *ast_httpd_helper_thread(void *data)
+#ifdef DO_SSL
+#if defined(HAVE_FUNOPEN)
+#define HOOK_T int
+#define LEN_T int
+#else
+#define HOOK_T ssize_t
+#define LEN_T size_t
+#endif
+/*!
+ * replacement read/write functions for SSL support.
+ * We use wrappers rather than SSL_read/SSL_write directly so
+ * we can put in some debugging.
+ */
+static HOOK_T ssl_read(void *cookie, char *buf, LEN_T len)
+{
+       int i = SSL_read(cookie, buf, len-1);
+#if 0
+       if (i >= 0)
+               buf[i] = '\0';
+       ast_verbose("ssl read size %d returns %d <%s>\n", (int)len, i, buf);
+#endif
+       return i;
+}
+
+static HOOK_T ssl_write(void *cookie, const char *buf, LEN_T len)
+{
+#if 0
+       char *s = alloca(len+1);
+       strncpy(s, buf, len);
+       s[len] = '\0';
+       ast_verbose("ssl write size %d <%s>\n", (int)len, s);
+#endif
+       return SSL_write(cookie, buf, len);
+}
+
+static int ssl_close(void *cookie)
+{
+       close(SSL_get_fd(cookie));
+       SSL_shutdown(cookie);
+       SSL_free(cookie);
+       return 0;
+}
+#endif /* DO_SSL */
+
+/*!
+ * creates a FILE * from the fd passed by the accept thread.
+ * This operation is potentially expensive (certificate verification),
+ * so we do it in the child thread context.
+ */
+static void *make_file_from_fd(void *data)
+{
+       struct server_instance *ser = data;
+
+       /*
+        * open a FILE * as appropriate.
+        */
+       if (!ser->is_ssl)
+               ser->f = fdopen(ser->fd, "w+");
+#ifdef DO_SSL
+       else if ( (ser->ssl = SSL_new(ssl_ctx)) ) {
+               SSL_set_fd(ser->ssl, ser->fd);
+               if (SSL_accept(ser->ssl) == 0)
+                       ast_verbose(" error setting up ssl connection");
+               else {
+#if defined(HAVE_FUNOPEN)      /* the BSD interface */
+                       ser->f = funopen(ser->ssl, ssl_read, ssl_write, NULL, ssl_close);
+
+#elif defined(HAVE_FOPENCOOKIE)        /* the glibc/linux interface */
+                       static const cookie_io_functions_t cookie_funcs = {
+                               ssl_read, ssl_write, NULL, ssl_close
+                       };
+                       ser->f = fopencookie(ser->ssl, "w+", cookie_funcs);
+#else
+                       /* could add other methods here */
+#endif
+               }
+               if (!ser->f)    /* no success opening descriptor stacking */
+                       SSL_free(ser->ssl);
+       }
+#endif /* DO_SSL */
+
+       if (!ser->f) {
+               close(ser->fd);
+               ast_log(LOG_WARNING, "FILE * open failed!\n");
+               free(ser);
+               return NULL;
+       }
+       return ser->parent->worker_fn(ser);
+}
+
+static void *httpd_helper_thread(void *data)
 {
        char buf[4096];
        char cookie[4096];
-       struct ast_http_server_instance *ser = data;
+       struct server_instance *ser = data;
        struct ast_variable *var, *prev=NULL, *vars=NULL;
        char *uri, *c, *title=NULL;
        int status = 200, contentlength = 0;
@@ -458,47 +627,51 @@ static void *ast_httpd_helper_thread(void *data)
                char timebuf[256];
 
                strftime(timebuf, sizeof(timebuf), "%a, %d %b %Y %H:%M:%S GMT", gmtime(&t));
-               ast_cli(ser->fd, "HTTP/1.1 %d %s\r\n", status, title ? title : "OK");
-               ast_cli(ser->fd, "Server: Asterisk\r\n");
-               ast_cli(ser->fd, "Date: %s\r\n", timebuf);
-               ast_cli(ser->fd, "Connection: close\r\n");
-               if (contentlength) {
+               fprintf(ser->f, "HTTP/1.1 %d %s\r\n"
+                               "Server: Asterisk\r\n"
+                               "Date: %s\r\n"
+                               "Connection: close\r\n",
+                       status, title ? title : "OK", timebuf);
+               if (!contentlength) {   /* opaque body ? just dump it hoping it is properly formatted */
+                       fprintf(ser->f, "%s", c);
+               } else {
                        char *tmp = strstr(c, "\r\n\r\n");
 
                        if (tmp) {
-                               ast_cli(ser->fd, "Content-length: %d\r\n", contentlength);
+                               fprintf(ser->f, "Content-length: %d\r\n", contentlength);
                                /* first write the header, then the body */
-                               write(ser->fd, c, (tmp + 4 - c));
-                               write(ser->fd, tmp + 4, contentlength);
+                               fwrite(c, 1, (tmp + 4 - c), ser->f);
+                               fwrite(tmp + 4, 1, contentlength, ser->f);
                        }
-               } else
-                       ast_cli(ser->fd, "%s", c);
+               }
                free(c);
        }
        if (title)
                free(title);
 
 done:
-       fclose(ser->f);
+       if (ser->f)
+               fclose(ser->f);
        free(ser);
        return NULL;
 }
 
 static void *http_root(void *data)
 {
+       struct server_args *desc = data;
        int fd;
        struct sockaddr_in sin;
        socklen_t sinlen;
-       struct ast_http_server_instance *ser;
+       struct server_instance *ser;
        pthread_t launched;
        pthread_attr_t attr;
        
        for (;;) {
                int flags;
 
-               ast_wait_for_input(httpfd, -1);
+               ast_wait_for_input(desc->accept_fd, -1);
                sinlen = sizeof(sin);
-               fd = accept(httpfd, (struct sockaddr *)&sin, &sinlen);
+               fd = accept(desc->accept_fd, (struct sockaddr *)&sin, &sinlen);
                if (fd < 0) {
                        if ((errno != EAGAIN) && (errno != EINTR))
                                ast_log(LOG_WARNING, "Accept failed: %s\n", strerror(errno));
@@ -513,18 +686,15 @@ static void *http_root(void *data)
                flags = fcntl(fd, F_GETFL);
                fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
                ser->fd = fd;
+               ser->is_ssl = desc->is_ssl;
+               ser->parent = desc;
                memcpy(&ser->requestor, &sin, sizeof(ser->requestor));
-               if ((ser->f = fdopen(ser->fd, "w+"))) {
-                       pthread_attr_init(&attr);
-                       pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
+
+               pthread_attr_init(&attr);
+               pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
                        
-                       if (ast_pthread_create_background(&launched, &attr, ast_httpd_helper_thread, ser)) {
-                               ast_log(LOG_WARNING, "Unable to launch helper thread: %s\n", strerror(errno));
-                               fclose(ser->f);
-                               free(ser);
-                       }
-               } else {
-                       ast_log(LOG_WARNING, "fdopen failed!\n");
+               if (ast_pthread_create_background(&launched, &attr, make_file_from_fd, ser)) {
+                       ast_log(LOG_WARNING, "Unable to launch helper thread: %s\n", strerror(errno));
                        close(ser->fd);
                        free(ser);
                }
@@ -543,66 +713,106 @@ char *ast_http_setcookie(const char *var, const char *val, int expires, char *bu
        return buf;
 }
 
+static int ssl_setup(void)
+{
+#ifndef DO_SSL
+       do_ssl = 0;
+       return 0;
+#else
+       if (!do_ssl)
+               return 0;
+       SSL_load_error_strings();
+       SSLeay_add_ssl_algorithms();
+       ssl_ctx = SSL_CTX_new( SSLv23_server_method() );
+       if (!ast_strlen_zero(certfile)) {
+               if (SSL_CTX_use_certificate_file(ssl_ctx, certfile, SSL_FILETYPE_PEM) == 0 ||
+                   SSL_CTX_use_PrivateKey_file(ssl_ctx, certfile, SSL_FILETYPE_PEM) == 0 ||
+                   SSL_CTX_check_private_key(ssl_ctx) == 0 ) {
+                       ast_verbose("ssl cert error <%s>", certfile);
+                       sleep(2);
+                       do_ssl = 0;
+                       return 0;
+               }
+       }
+       if (!ast_strlen_zero(cipher)) {
+               if (SSL_CTX_set_cipher_list(ssl_ctx, cipher) == 0 ) {
+                       ast_verbose("ssl cipher error <%s>", cipher);
+                       sleep(2);
+                       do_ssl = 0;
+                       return 0;
+               }
+       }
+       ast_verbose("ssl cert ok");
+       return 1;
+#endif
+}
 
-static void http_server_start(struct sockaddr_in *sin)
+/*!
+ * This is a generic (re)start routine for a TCP server,
+ * which does the socket/bind/listen and starts a thread for handling
+ * accept().
+ */
+static void server_start(struct server_args *desc)
 {
        int flags;
        int x = 1;
        
        /* Do nothing if nothing has changed */
-       if (!memcmp(&oldsin, sin, sizeof(oldsin))) {
+       if (!memcmp(&desc->oldsin, &desc->sin, sizeof(desc->oldsin))) {
                if (option_debug)
-                       ast_log(LOG_DEBUG, "Nothing changed in http\n");
+                       ast_log(LOG_DEBUG, "Nothing changed in %s\n", desc->name);
                return;
        }
        
-       memcpy(&oldsin, sin, sizeof(oldsin));
+       desc->oldsin = desc->sin;
        
        /* Shutdown a running server if there is one */
-       if (master != AST_PTHREADT_NULL) {
-               pthread_cancel(master);
-               pthread_kill(master, SIGURG);
-               pthread_join(master, NULL);
+       if (desc->master != AST_PTHREADT_NULL) {
+               pthread_cancel(desc->master);
+               pthread_kill(desc->master, SIGURG);
+               pthread_join(desc->master, NULL);
        }
        
-       if (httpfd != -1)
-               close(httpfd);
+       if (desc->accept_fd != -1)
+               close(desc->accept_fd);
 
        /* If there's no new server, stop here */
-       if (!sin->sin_family)
+       if (desc->sin.sin_family == 0)
                return;
-       
-       
-       httpfd = socket(AF_INET, SOCK_STREAM, 0);
-       if (httpfd < 0) {
-               ast_log(LOG_WARNING, "Unable to allocate socket: %s\n", strerror(errno));
+
+       desc->accept_fd = socket(AF_INET, SOCK_STREAM, 0);
+       if (desc->accept_fd < 0) {
+               ast_log(LOG_WARNING, "Unable to allocate socket for %s: %s\n",
+                       desc->name, strerror(errno));
                return;
        }
        
-       setsockopt(httpfd, SOL_SOCKET, SO_REUSEADDR, &x, sizeof(x));
-       if (bind(httpfd, (struct sockaddr *)sin, sizeof(*sin))) {
-               ast_log(LOG_NOTICE, "Unable to bind http server to %s:%d: %s\n",
-                       ast_inet_ntoa(sin->sin_addr), ntohs(sin->sin_port),
+       setsockopt(desc->accept_fd, SOL_SOCKET, SO_REUSEADDR, &x, sizeof(x));
+       if (bind(desc->accept_fd, (struct sockaddr *)&desc->sin, sizeof(desc->sin))) {
+               ast_log(LOG_NOTICE, "Unable to bind %s to %s:%d: %s\n",
+                       desc->name,
+                       ast_inet_ntoa(desc->sin.sin_addr), ntohs(desc->sin.sin_port),
                        strerror(errno));
-               close(httpfd);
-               httpfd = -1;
-               return;
+               goto error;
        }
-       if (listen(httpfd, 10)) {
-               ast_log(LOG_NOTICE, "Unable to listen!\n");
-               close(httpfd);
-               httpfd = -1;
-               return;
+       if (listen(desc->accept_fd, 10)) {
+               ast_log(LOG_NOTICE, "Unable to listen for %s!\n", desc->name);
+               goto error;
        }
-       flags = fcntl(httpfd, F_GETFL);
-       fcntl(httpfd, F_SETFL, flags | O_NONBLOCK);
-       if (ast_pthread_create_background(&master, NULL, http_root, NULL)) {
-               ast_log(LOG_NOTICE, "Unable to launch http server on %s:%d: %s\n",
-                               ast_inet_ntoa(sin->sin_addr), ntohs(sin->sin_port),
-                               strerror(errno));
-               close(httpfd);
-               httpfd = -1;
+       flags = fcntl(desc->accept_fd, F_GETFL);
+       fcntl(desc->accept_fd, F_SETFL, flags | O_NONBLOCK);
+       if (ast_pthread_create_background(&desc->master, NULL, desc->accept_fn, desc)) {
+               ast_log(LOG_NOTICE, "Unable to launch %s on %s:%d: %s\n",
+                       desc->name,
+                       ast_inet_ntoa(desc->sin.sin_addr), ntohs(desc->sin.sin_port),
+                       strerror(errno));
+               goto error;
        }
+       return;
+
+error:
+       close(desc->accept_fd);
+       desc->accept_fd = -1;
 }
 
 static int __ast_http_load(int reload)
@@ -611,27 +821,58 @@ static int __ast_http_load(int reload)
        struct ast_variable *v;
        int enabled=0;
        int newenablestatic=0;
-       struct sockaddr_in sin;
        struct hostent *hp;
        struct ast_hostent ahp;
        char newprefix[MAX_PREFIX];
+       int have_sslbindaddr = 0;
 
-       memset(&sin, 0, sizeof(sin));
-       sin.sin_port = 8088;
+       /* default values */
+       memset(&http_desc.sin, 0, sizeof(http_desc.sin));
+       http_desc.sin.sin_port = htons(8088);
+
+       memset(&https_desc.sin, 0, sizeof(https_desc.sin));
+       https_desc.sin.sin_port = htons(8089);
        strcpy(newprefix, DEFAULT_PREFIX);
        cfg = ast_config_load("http.conf");
+
+       do_ssl = 0;
+       if (certfile)
+               free(certfile);
+       certfile = ast_strdup(AST_CERTFILE);
+       if (cipher)
+               free(cipher);
+       cipher = ast_strdup("");
+
        if (cfg) {
                v = ast_variable_browse(cfg, "general");
                while(v) {
                        if (!strcasecmp(v->name, "enabled"))
                                enabled = ast_true(v->value);
+                       else if (!strcasecmp(v->name, "sslenable"))
+                               do_ssl = ast_true(v->value);
+                       else if (!strcasecmp(v->name, "sslbindport"))
+                               https_desc.sin.sin_port = htons(atoi(v->value));
+                       else if (!strcasecmp(v->name, "sslcert")) {
+                               free(certfile);
+                               certfile = ast_strdup(v->value);
+                       } else if (!strcasecmp(v->name, "sslcipher")) {
+                               free(cipher);
+                               cipher = ast_strdup(v->value);
+                       }
                        else if (!strcasecmp(v->name, "enablestatic"))
                                newenablestatic = ast_true(v->value);
                        else if (!strcasecmp(v->name, "bindport"))
-                               sin.sin_port = ntohs(atoi(v->value));
-                       else if (!strcasecmp(v->name, "bindaddr")) {
+                               http_desc.sin.sin_port = htons(atoi(v->value));
+                       else if (!strcasecmp(v->name, "sslbindaddr")) {
+                               if ((hp = ast_gethostbyname(v->value, &ahp))) {
+                                       memcpy(&https_desc.sin.sin_addr, hp->h_addr, sizeof(https_desc.sin.sin_addr));
+                                       have_sslbindaddr = 1;
+                               } else {
+                                       ast_log(LOG_WARNING, "Invalid bind address '%s'\n", v->value);
+                               }
+                       } else if (!strcasecmp(v->name, "bindaddr")) {
                                if ((hp = ast_gethostbyname(v->value, &ahp))) {
-                                       memcpy(&sin.sin_addr, hp->h_addr, sizeof(sin.sin_addr));
+                                       memcpy(&http_desc.sin.sin_addr, hp->h_addr, sizeof(http_desc.sin.sin_addr));
                                } else {
                                        ast_log(LOG_WARNING, "Invalid bind address '%s'\n", v->value);
                                }
@@ -648,12 +889,16 @@ static int __ast_http_load(int reload)
                }
                ast_config_destroy(cfg);
        }
+       if (!have_sslbindaddr)
+               https_desc.sin.sin_addr = http_desc.sin.sin_addr;
        if (enabled)
-               sin.sin_family = AF_INET;
+               http_desc.sin.sin_family = https_desc.sin.sin_family = AF_INET;
        if (strcmp(prefix, newprefix))
                ast_copy_string(prefix, newprefix, sizeof(prefix));
        enablestatic = newenablestatic;
-       http_server_start(&sin);
+       server_start(&http_desc);
+       if (ssl_setup())
+               server_start(&https_desc);
        return 0;
 }
 
@@ -664,12 +909,17 @@ static int handle_show_http(int fd, int argc, char *argv[])
                return RESULT_SHOWUSAGE;
        ast_cli(fd, "HTTP Server Status:\n");
        ast_cli(fd, "Prefix: %s\n", prefix);
-       if (oldsin.sin_family)
-               ast_cli(fd, "Server Enabled and Bound to %s:%d\n\n",
-                       ast_inet_ntoa(oldsin.sin_addr),
-                       ntohs(oldsin.sin_port));
-       else
+       if (!http_desc.oldsin.sin_family)
                ast_cli(fd, "Server Disabled\n\n");
+       else {
+               ast_cli(fd, "Server Enabled and Bound to %s:%d\n\n",
+                       ast_inet_ntoa(http_desc.oldsin.sin_addr),
+                       ntohs(http_desc.oldsin.sin_port));
+               if (do_ssl)
+                       ast_cli(fd, "HTTPS Server Enabled and Bound to %s:%d\n\n",
+                               ast_inet_ntoa(https_desc.oldsin.sin_addr),
+                               ntohs(https_desc.oldsin.sin_port));
+       }
        ast_cli(fd, "Enabled URI's:\n");
        urih = uris;
        while(urih){
@@ -687,11 +937,11 @@ int ast_http_reload(void)
 }
 
 static char show_http_help[] =
-"Usage: http list status\n"
+"Usage: http show status\n"
 "       Lists status of internal HTTP engine\n";
 
 static struct ast_cli_entry cli_http[] = {
-       { { "http", "list", "status", NULL },
+       { { "http", "show", "status", NULL },
        handle_show_http, "Display HTTP server status",
        show_http_help },
 };