From 095350e6eb8458b720c0905257d1b6d040c3a55f Mon Sep 17 00:00:00 2001 From: Julien BLACHE Date: Wed, 22 Apr 2009 21:25:41 +0200 Subject: [PATCH] Introduce new evhttp-based HTTP server --- src/Makefile.am | 1 + src/httpd.c | 401 ++++++++++++++++++++++++++++++++++++++++++++++++ src/httpd.h | 11 ++ 3 files changed, 413 insertions(+) create mode 100644 src/httpd.c create mode 100644 src/httpd.h diff --git a/src/Makefile.am b/src/Makefile.am index c7236b17..32560f55 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -20,6 +20,7 @@ mt_daapd_SOURCES = main.c daapd.h webserver.c \ filescanner.c filescanner.h \ filescanner_ffmpeg.c filescanner_urlfile.c filescanner_m3u.c \ mdns_avahi.c mdns_avahi.h \ + httpd.c httpd.h \ db-generic.c db-generic.h ff-plugins.c ff-plugins.h \ scan-wma.c \ smart-parser.c smart-parser.h xml-rpc.c xml-rpc.h \ diff --git a/src/httpd.c b/src/httpd.c new file mode 100644 index 00000000..4a499e88 --- /dev/null +++ b/src/httpd.c @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2009 Julien BLACHE + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "daapd.h" +#include "err.h" +#include "conffile.h" +#include "httpd.h" + + +#define WEBFACE_ROOT "/usr/share/mt-daapd/admin-root/" + +struct content_type_map { + char *ext; + char *ctype; +}; + + +static struct content_type_map ext2ctype[] = + { + { ".html", "text/html; charset=utf-8" }, + { ".xml", "text/xml; charset=utf-8" }, + { ".css", "text/css; charset=utf-8" }, + { ".txt", "text/plain; charset=utf-8" }, + { ".js", "application/javascript; charset=utf-8" }, + { ".gif", "image/gif" }, + { ".ico", "image/x-ico" }, + { ".png", "image/png" }, + { NULL, NULL } + }; + +static int exit_pipe[2]; +static int httpd_exit; +static struct event_base *evbase_httpd; +static struct event exitev; +static struct evhttp *evhttpd; +static pthread_t tid_httpd; + + +/* Thread: httpd */ +static int +path_is_legal(char *path) +{ + return strncmp(WEBFACE_ROOT, path, strlen(WEBFACE_ROOT)); +} + +/* Thread: httpd */ +static void +redirect_to_index(struct evhttp_request *req, char *uri) +{ + char buf[256]; + int slashed; + int ret; + + slashed = (uri[strlen(uri) - 1] == '/'); + + ret = snprintf(buf, sizeof(buf), "%s%sindex.html", uri, (slashed) ? "" : "/"); + if ((ret < 0) || (ret >= sizeof(buf))) + { + DPRINTF(E_LOG, L_HTTPD, "Redirection URL exceeds buffer length\n"); + + evhttp_send_error(req, HTTP_NOTFOUND, "Not Found"); + return; + } + + evhttp_add_header(req->output_headers, "Location", buf); + evhttp_send_reply(req, HTTP_MOVETEMP, "Moved", NULL); +} + +/* Thread: httpd */ +static void +serve_file(struct evhttp_request *req, char *uri) +{ + char *ext; + char path[PATH_MAX]; + char *deref; + char *ctype; + struct evbuffer *evbuf; + struct stat sb; + int fd; + int i; + int ret; + + ret = snprintf(path, sizeof(path), "%s%s", WEBFACE_ROOT, uri + 1); /* skip starting '/' */ + if ((ret < 0) || (ret >= sizeof(path))) + { + DPRINTF(E_LOG, L_HTTPD, "Request exceeds PATH_MAX: %s\n", uri); + + evhttp_send_error(req, HTTP_NOTFOUND, "Not Found"); + + return; + } + + ret = lstat(path, &sb); + if (ret < 0) + { + DPRINTF(E_LOG, L_HTTPD, "Could not lstat() %s: %s\n", path, strerror(errno)); + + evhttp_send_error(req, HTTP_NOTFOUND, "Not Found"); + + return; + } + + if (S_ISDIR(sb.st_mode)) + { + redirect_to_index(req, uri); + + return; + } + else if (S_ISLNK(sb.st_mode)) + { + deref = realpath(path, NULL); + if (!deref) + { + DPRINTF(E_LOG, L_HTTPD, "Could not dereference %s: %s\n", path, strerror(errno)); + + evhttp_send_error(req, HTTP_NOTFOUND, "Not Found"); + + return; + } + + if (strlen(deref) + 1 > PATH_MAX) + { + DPRINTF(E_LOG, L_HTTPD, "Dereferenced path exceeds PATH_MAX: %s\n", path); + + evhttp_send_error(req, HTTP_NOTFOUND, "Not Found"); + + free(deref); + return; + } + + strcpy(path, deref); + free(deref); + + ret = stat(path, &sb); + if (ret < 0) + { + DPRINTF(E_LOG, L_HTTPD, "Could not stat() %s: %s\n", path, strerror(errno)); + + evhttp_send_error(req, HTTP_NOTFOUND, "Not Found"); + + return; + } + + if (S_ISDIR(sb.st_mode)) + { + redirect_to_index(req, uri); + + return; + } + } + + if (path_is_legal(path) != 0) + { + evhttp_send_error(req, 403, "Forbidden"); + + return; + } + + evbuf = evbuffer_new(); + if (!evbuf) + { + DPRINTF(E_LOG, L_HTTPD, "Could not create evbuffer\n"); + + evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal error"); + return; + } + + fd = open(path, O_RDONLY); + if (fd < 0) + { + DPRINTF(E_LOG, L_HTTPD, "Could not open %s: %s\n", path, strerror(errno)); + + evhttp_send_error(req, HTTP_NOTFOUND, "Not Found"); + return; + } + + ret = evbuffer_read(evbuf, fd, sb.st_size); + close(fd); + if (ret < 0) + { + DPRINTF(E_LOG, L_HTTPD, "Could not read file into evbuffer\n"); + + evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal error"); + return; + } + + ctype = "application/octet-stream"; + ext = strrchr(path, '.'); + if (ext) + { + for (i = 0; ext2ctype[i].ext; i++) + { + if (strcmp(ext, ext2ctype[i].ext) == 0) + { + ctype = ext2ctype[i].ctype; + break; + } + } + } + + evhttp_add_header(req->output_headers, "Content-Type", ctype); + + evhttp_send_reply(req, HTTP_OK, "OK", evbuf); + + evbuffer_free(evbuf); +} + +/* Thread: httpd */ +static void +webface_cb(struct evhttp_request *req, void *arg) +{ + const char *req_uri; + char *uri; + char *ptr; + + req_uri = evhttp_request_uri(req); + if (!req_uri) + { + redirect_to_index(req, "/"); + + return; + } + + uri = strdup(req_uri); + ptr = strchr(uri, '?'); + if (ptr) + { + DPRINTF(E_DBG, L_HTTPD, "Found query string\n"); + + *ptr = '\0'; + } + + ptr = uri; + uri = evhttp_decode_uri(uri); + free(ptr); + + serve_file(req, uri); + + free(uri); +} + +/* Thread: httpd */ +static void * +httpd(void *arg) +{ + event_base_dispatch(evbase_httpd); + + if (!httpd_exit) + DPRINTF(E_FATAL, L_HTTPD, "HTTPd event loop terminated ahead of time!\n"); + + pthread_exit(NULL); +} + +/* Thread: httpd */ +static void +exit_cb(int fd, short event, void *arg) +{ + event_base_loopbreak(evbase_httpd); + + httpd_exit = 1; +} + +/* Thread: main */ +int +httpd_init(void) +{ + unsigned short port; + int bindv6; + int ret; + + httpd_exit = 0; + + ret = pipe2(exit_pipe, O_CLOEXEC); + if (ret < 0) + { + DPRINTF(E_FATAL, L_HTTPD, "Could not create pipe: %s\n", strerror(errno)); + + return -1; + } + + evbase_httpd = event_base_new(); + if (!evbase_httpd) + { + DPRINTF(E_FATAL, L_HTTPD, "Could not create an event base\n"); + + goto evbase_fail; + } + + event_set(&exitev, exit_pipe[0], EV_READ, exit_cb, NULL); + event_base_set(evbase_httpd, &exitev); + event_add(&exitev, NULL); + + evhttpd = evhttp_new(evbase_httpd); + if (!evhttpd) + { + DPRINTF(E_FATAL, L_HTTPD, "Could not create HTTP server\n"); + + goto evhttp_fail; + } + + port = cfg_getint(cfg_getnsec(cfg, "library", 0), "port") /* tmp */ + 1; + + /* evhttp doesn't support IPv6 yet, so this is expected to fail */ + bindv6 = evhttp_bind_socket(evhttpd, "::", port); + if (bindv6 < 0) + DPRINTF(E_INF, L_HTTPD, "Could not bind IN6ADDR_ANY:%d (that's OK)\n", port); + + ret = evhttp_bind_socket(evhttpd, "0.0.0.0", port); + if (ret < 0) + { + if (bindv6 < 0) + { + DPRINTF(E_FATAL, L_HTTPD, "Could not bind INADDR_ANY:%d\n", port); + + goto bind_fail; + } + } + + evhttp_set_gencb(evhttpd, webface_cb, NULL); + + ret = pthread_create(&tid_httpd, NULL, httpd, NULL); + if (ret != 0) + { + DPRINTF(E_FATAL, L_HTTPD, "Could not spawn HTTPd thread: %s\n", strerror(errno)); + + goto thread_fail; + } + + return 0; + + thread_fail: + bind_fail: + evhttp_free(evhttpd); + evhttp_fail: + event_base_free(evbase_httpd); + evbase_fail: + close(exit_pipe[0]); + close(exit_pipe[1]); + + return -1; +} + +/* Thread: main */ +void +httpd_deinit(void) +{ + int dummy = 42; + int ret; + + ret = write(exit_pipe[1], &dummy, sizeof(dummy)); + if (ret != sizeof(dummy)) + { + DPRINTF(E_FATAL, L_HTTPD, "Could not write to exit fd: %s\n", strerror(errno)); + + return; + } + + ret = pthread_join(tid_httpd, NULL); + if (ret != 0) + { + DPRINTF(E_FATAL, L_HTTPD, "Could not join HTTPd thread: %s\n", strerror(errno)); + + return; + } + + close(exit_pipe[0]); + close(exit_pipe[1]); + evhttp_free(evhttpd); + event_base_free(evbase_httpd); +} diff --git a/src/httpd.h b/src/httpd.h new file mode 100644 index 00000000..03dbe6ad --- /dev/null +++ b/src/httpd.h @@ -0,0 +1,11 @@ + +#ifndef __HTTPD_H__ +#define __HTTPD_H__ + +int +httpd_init(void); + +void +httpd_deinit(void); + +#endif /* !__HTTPD_H__ */