diff --git a/configure.ac b/configure.ac index 595fbc27..25d6b5ab 100644 --- a/configure.ac +++ b/configure.ac @@ -50,7 +50,7 @@ AC_CHECK_HEADERS([sys/wait.h sys/param.h dirent.h getopt.h stdint.h], [], [AC_MSG_ERROR([[Missing header required to build OwnTone]])]) AC_CHECK_HEADERS([time.h], [], [AC_MSG_ERROR([[Missing header required to build OwnTone]])]) -AC_CHECK_FUNCS_ONCE([posix_fadvise pipe2]) +AC_CHECK_FUNCS_ONCE([posix_fadvise pipe2 syscall]) AC_CHECK_FUNCS([strptime strtok_r], [], [AC_MSG_ERROR([[Missing function required to build OwnTone]])]) @@ -148,13 +148,10 @@ OWNTONE_MODULES_CHECK([COMMON], [SQLITE3], [sqlite3 >= 3.5.0], [AC_MSG_RESULT([[runtime will tell]])]) ]) -OWNTONE_MODULES_CHECK([OWNTONE], [LIBEVENT], [libevent >= 2], - [event_base_new], [event2/event.h], - [dnl check for old version - PKG_CHECK_EXISTS([libevent >= 2.1.4], [], - [AC_DEFINE([HAVE_LIBEVENT2_OLD], 1, - [Define to 1 if you have libevent 2 (<2.1.4)])]) - ]) +OWNTONE_MODULES_CHECK([OWNTONE], [LIBEVENT], [libevent >= 2.1.4], + [event_base_new], [event2/event.h]) +OWNTONE_MODULES_CHECK([OWNTONE], [LIBEVENT_PTHREADS], [libevent_pthreads], + [evthread_use_pthreads], [event2/thread.h]) dnl Check for evhttp_connection_get_peer() signature AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[ @@ -269,11 +266,6 @@ OWNTONE_ARG_WITH_CHECK([OWNTONE_OPTS], [libwebsockets support], [libwebsockets], [libwebsockets >= 2.0.2]) AM_CONDITIONAL([COND_LIBWEBSOCKETS], [[test "x$with_libwebsockets" = "xyes"]]) -dnl Build with libevent_pthreads -OWNTONE_ARG_WITH_CHECK([OWNTONE_OPTS], [libevent_pthreads support], - [libevent_pthreads], [LIBEVENT_PTHREADS], [libevent_pthreads], - [evthread_use_pthreads], [event2/thread.h]) - dnl Build with Avahi (or Bonjour if not) OWNTONE_ARG_WITH_CHECK([OWNTONE_OPTS], [Avahi mDNS], [avahi], [AVAHI], [avahi-client >= 0.6.24], [avahi_client_new], [avahi-client/client.h]) diff --git a/docs/building.md b/docs/building.md index faf2c603..907fc746 100644 --- a/docs/building.md +++ b/docs/building.md @@ -224,7 +224,7 @@ Libraries: from - libconfuse from -- libevent 2.0+ (best with 2.1.4+) +- libevent 2.1.4+ from - MiniXML (aka mxml or libmxml) from diff --git a/src/Makefile.am b/src/Makefile.am index fa01a0a9..a64b5299 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -92,14 +92,15 @@ owntone_SOURCES = main.c \ library.c library.h \ $(MDNS_SRC) mdns.h \ remote_pairing.c remote_pairing.h \ - httpd.c httpd.h \ - httpd_rsp.c httpd_rsp.h \ + httpd_libevhttp.c \ + httpd.c httpd.h httpd_internal.h \ + httpd_rsp.c \ httpd_daap.c httpd_daap.h \ - httpd_dacp.c httpd_dacp.h \ - httpd_jsonapi.c httpd_jsonapi.h \ - httpd_streaming.c httpd_streaming.h \ - httpd_oauth.c httpd_oauth.h \ - httpd_artworkapi.c httpd_artworkapi.h \ + httpd_dacp.c \ + httpd_jsonapi.c \ + httpd_streaming.c \ + httpd_oauth.c \ + httpd_artworkapi.c \ http.c http.h \ dmap_common.c dmap_common.h \ transcode.c transcode.h \ @@ -117,9 +118,11 @@ owntone_SOURCES = main.c \ outputs/rtp_common.h outputs/rtp_common.c \ outputs/raop.c outputs/airplay.c $(PAIR_AP_SRC) \ outputs/airplay_events.c outputs/airplay_events.h \ - outputs/streaming.c outputs/dummy.c outputs/fifo.c outputs/rcp.c \ + outputs/streaming.c outputs/streaming.h \ + outputs/dummy.c outputs/fifo.c outputs/rcp.c \ $(ALSA_SRC) $(PULSEAUDIO_SRC) $(CHROMECAST_SRC) \ evrtsp/rtsp.c evrtsp/evrtsp.h evrtsp/rtsp-internal.h evrtsp/log.h \ + evthr.c evthr.h \ $(SPOTIFY_SRC) \ $(LASTFM_SRC) \ $(MPD_SRC) \ diff --git a/src/artwork.c b/src/artwork.c index f0c53a50..2135cd2e 100644 --- a/src/artwork.c +++ b/src/artwork.c @@ -582,20 +582,6 @@ size_calculate(int *dst_w, int *dst_h, int src_w, int src_h, int max_w, int max_ DPRINTF(E_DBG, L_ART, "Rescale required, destination width %d height %d\n", *dst_w, *dst_h); } -#ifdef HAVE_LIBEVENT2_OLD -// This is not how this function is actually defined in libevent 2.1+, but it -// works as a less optimal stand-in -int -evbuffer_add_buffer_reference(struct evbuffer *outbuf, struct evbuffer *inbuf) -{ - uint8_t *buf = evbuffer_pullup(inbuf, -1); - if (!buf) - return -1; - - return evbuffer_add_reference(outbuf, buf, evbuffer_get_length(inbuf), NULL, NULL); -} -#endif - /* * Either gets the artwork file given in "path" (rescaled if needed) or rescales * the artwork given in "inbuf". diff --git a/src/commands.c b/src/commands.c index 7043bff1..822c65e5 100644 --- a/src/commands.c +++ b/src/commands.c @@ -63,8 +63,8 @@ command_cb_async(struct commands_base *cmdbase, struct command *cmd) // Command is executed asynchronously cmdstate = cmd->func(cmd->arg, &cmd->ret); - // Only free arg if there are no pending events (used in worker.c) - if (cmdstate != COMMAND_PENDING && cmd->arg) + // Only free arg if there are no pending events (used in httpd.c) + if (cmdstate != COMMAND_PENDING) free(cmd->arg); free(cmd); diff --git a/src/dmap_common.c b/src/dmap_common.c index cb091093..3d5e52e2 100644 --- a/src/dmap_common.c +++ b/src/dmap_common.c @@ -25,7 +25,6 @@ #include "db.h" #include "misc.h" -#include "httpd.h" #include "logger.h" #include "dmap_common.h" #include "parsers/daap_parser.h" @@ -360,31 +359,6 @@ dmap_error_make(struct evbuffer *evbuf, const char *container, const char *errms dmap_add_string(evbuf, "msts", errmsg); } -void -dmap_send_error(struct evhttp_request *req, const char *container, const char *errmsg) -{ - struct evbuffer *evbuf; - - if (!req) - return; - - evbuf = evbuffer_new(); - if (!evbuf) - { - DPRINTF(E_LOG, L_DMAP, "Could not allocate evbuffer for DMAP error\n"); - - httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); - return; - } - - dmap_error_make(evbuf, container, errmsg); - - httpd_send_reply(req, HTTP_OK, "OK", evbuf, HTTPD_SEND_NO_GZIP); - - evbuffer_free(evbuf); -} - - int dmap_encode_file_metadata(struct evbuffer *songlist, struct evbuffer *song, struct db_media_file_info *dbmfi, const struct dmap_field **meta, int nmeta, int sort_tags, int force_wav) { diff --git a/src/dmap_common.h b/src/dmap_common.h index 2babd621..51410924 100644 --- a/src/dmap_common.h +++ b/src/dmap_common.h @@ -78,10 +78,6 @@ dmap_add_field(struct evbuffer *evbuf, const struct dmap_field *df, char *strval void dmap_error_make(struct evbuffer *evbuf, const char *container, const char *errmsg); -void -dmap_send_error(struct evhttp_request *req, const char *container, const char *errmsg); - - int dmap_encode_file_metadata(struct evbuffer *songlist, struct evbuffer *song, struct db_media_file_info *dbmfi, const struct dmap_field **meta, int nmeta, int sort_tags, int force_wav); diff --git a/src/evrtsp/rtsp.c b/src/evrtsp/rtsp.c index 4d467df7..4f70d48d 100644 --- a/src/evrtsp/rtsp.c +++ b/src/evrtsp/rtsp.c @@ -63,17 +63,6 @@ #include "log.h" #include "rtsp-internal.h" -// For compability with libevent 2.0 (HAVE_LIBEVENT2_OLD) -#if defined(_EVENT_HAVE_GETNAMEINFO) -# define EVENT__HAVE_GETNAMEINFO 1 -#endif -#if defined(_EVENT_HAVE_GETADDRINFO) -# define EVENT__HAVE_GETADDRINFO 1 -#endif -#if defined(_EVENT_HAVE_STRSEP) -# define EVENT__HAVE_STRSEP 1 -#endif - #ifndef EVENT__HAVE_GETNAMEINFO #define NI_MAXSERV 32 #define NI_MAXHOST 1025 diff --git a/src/evthr.c b/src/evthr.c new file mode 100644 index 00000000..0c236cdc --- /dev/null +++ b/src/evthr.c @@ -0,0 +1,467 @@ +/* +------------------- Thread handling borrowed from libevhtp --------------------- + +BSD 3-Clause License + +Copyright (c) 2010-2018, Mark Ellzey, Nathan French, Marcus Sundberg +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "evthr.h" + +#ifndef TAILQ_FOREACH_SAFE +#define TAILQ_FOREACH_SAFE(var, head, field, tvar) \ + for ((var) = TAILQ_FIRST((head)); \ + (var) && ((tvar) = TAILQ_NEXT((var), field), 1); \ + (var) = (tvar)) +#endif + +#define _evthr_read(thr, cmd, sock) \ + (recv(sock, cmd, sizeof(struct evthr_cmd), 0) == sizeof(struct evthr_cmd)) ? 1 : 0 + +#define EVTHR_SHARED_PIPE 1 + +struct evthr_cmd { + uint8_t stop; + void *args; + evthr_cb cb; +} __attribute__((packed)); + +struct evthr_pool { +#ifdef EVTHR_SHARED_PIPE + int rdr; + int wdr; +#endif + int nthreads; + TAILQ_HEAD(evthr_pool_slist, evthr) threads; +}; + +struct evthr { + int rdr; + int wdr; + char err; + struct event *event; + struct event_base *evbase; + pthread_mutex_t lock; + pthread_t *thr; + evthr_init_cb init_cb; + evthr_exit_cb exit_cb; + void *arg; + void *aux; +#ifdef EVTHR_SHARED_PIPE + int pool_rdr; + struct event *shared_pool_ev; +#endif + TAILQ_ENTRY(evthr) next; +}; + + +static void +_evthr_read_cmd(evutil_socket_t sock, short which, void *args) +{ + struct evthr *thread; + struct evthr_cmd cmd; + int stopped; + + if (!(thread = (struct evthr *)args)) { + return; + } + + stopped = 0; + + if (_evthr_read(thread, &cmd, sock) == 1) { + stopped = cmd.stop; + + if (cmd.cb != NULL) { + (cmd.cb)(thread, cmd.args, thread->arg); + } + } + + if (stopped == 1) { + event_base_loopbreak(thread->evbase); + } + + return; +} + +static void * +_evthr_loop(void *args) +{ + struct evthr *thread; + + if (!(thread = (struct evthr *)args)) { + return NULL; + } + + if (thread == NULL || thread->thr == NULL) { + pthread_exit(NULL); + } + + thread->evbase = event_base_new(); + thread->event = event_new(thread->evbase, thread->rdr, + EV_READ | EV_PERSIST, _evthr_read_cmd, args); + + event_add(thread->event, NULL); + +#ifdef EVTHR_SHARED_PIPE + if (thread->pool_rdr > 0) { + thread->shared_pool_ev = event_new(thread->evbase, thread->pool_rdr, + EV_READ | EV_PERSIST, _evthr_read_cmd, args); + event_add(thread->shared_pool_ev, NULL); + } +#endif + + pthread_mutex_lock(&thread->lock); + if (thread->init_cb != NULL) { + (thread->init_cb)(thread, thread->arg); + } + + pthread_mutex_unlock(&thread->lock); + + event_base_loop(thread->evbase, 0); + + pthread_mutex_lock(&thread->lock); + if (thread->exit_cb != NULL) { + (thread->exit_cb)(thread, thread->arg); + } + + pthread_mutex_unlock(&thread->lock); + + pthread_exit(NULL); +} + +static enum evthr_res +evthr_defer(struct evthr *thread, evthr_cb cb, void *arg) +{ + struct evthr_cmd cmd = { + .cb = cb, + .args = arg, + .stop = 0 + }; + + if (send(thread->wdr, &cmd, sizeof(cmd), 0) <= 0) { + return EVTHR_RES_RETRY; + } + + return EVTHR_RES_OK; +} + +static enum evthr_res +evthr_stop(struct evthr *thread) +{ + struct evthr_cmd cmd = { + .cb = NULL, + .args = NULL, + .stop = 1 + }; + + if (send(thread->wdr, &cmd, sizeof(struct evthr_cmd), 0) < 0) { + return EVTHR_RES_RETRY; + } + + pthread_join(*thread->thr, NULL); + return EVTHR_RES_OK; +} + +struct event_base * +evthr_get_base(struct evthr * thr) +{ + return thr ? thr->evbase : NULL; +} + +void +evthr_set_aux(struct evthr *thr, void *aux) +{ + if (thr) { + thr->aux = aux; + } +} + +void * +evthr_get_aux(struct evthr *thr) +{ + return thr ? thr->aux : NULL; +} + +static void +evthr_free(struct evthr *thread) +{ + if (thread == NULL) { + return; + } + + if (thread->rdr > 0) { + close(thread->rdr); + } + + if (thread->wdr > 0) { + close(thread->wdr); + } + + if (thread->thr) { + free(thread->thr); + } + + if (thread->event) { + event_free(thread->event); + } + +#ifdef EVTHR_SHARED_PIPE + if (thread->shared_pool_ev) { + event_free(thread->shared_pool_ev); + } +#endif + + if (thread->evbase) { + event_base_free(thread->evbase); + } + + free(thread); +} + +static struct evthr * +evthr_wexit_new(evthr_init_cb init_cb, evthr_exit_cb exit_cb, void *args) +{ + struct evthr *thread; + int fds[2]; + + if (evutil_socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == -1) { + return NULL; + } + + evutil_make_socket_nonblocking(fds[0]); + evutil_make_socket_nonblocking(fds[1]); + + if (!(thread = calloc(1, sizeof(struct evthr)))) { + return NULL; + } + + thread->thr = malloc(sizeof(pthread_t)); + thread->arg = args; + thread->rdr = fds[0]; + thread->wdr = fds[1]; + + thread->init_cb = init_cb; + thread->exit_cb = exit_cb; + + if (pthread_mutex_init(&thread->lock, NULL)) { + evthr_free(thread); + return NULL; + } + + return thread; +} + +static int +evthr_start(struct evthr *thread) +{ + if (thread == NULL || thread->thr == NULL) { + return -1; + } + + if (pthread_create(thread->thr, NULL, _evthr_loop, (void *)thread)) { + return -1; + } + + return 0; +} + +void +evthr_pool_free(struct evthr_pool *pool) +{ + struct evthr *thread; + struct evthr *save; + + if (pool == NULL) { + return; + } + + TAILQ_FOREACH_SAFE(thread, &pool->threads, next, save) { + TAILQ_REMOVE(&pool->threads, thread, next); + + evthr_free(thread); + } + + free(pool); +} + +enum evthr_res +evthr_pool_stop(struct evthr_pool *pool) +{ + struct evthr *thr; + struct evthr *save; + + if (pool == NULL) { + return EVTHR_RES_FATAL; + } + + TAILQ_FOREACH_SAFE(thr, &pool->threads, next, save) { + evthr_stop(thr); + } + + return EVTHR_RES_OK; +} + +static inline int +get_backlog_(struct evthr *thread) +{ + int backlog = 0; + + ioctl(thread->rdr, FIONREAD, &backlog); + + return (int)(backlog / sizeof(struct evthr_cmd)); +} + +enum evthr_res +evthr_pool_defer(struct evthr_pool *pool, evthr_cb cb, void *arg) +{ +#ifdef EVTHR_SHARED_PIPE + struct evthr_cmd cmd = { + .cb = cb, + .args = arg, + .stop = 0 + }; + + if (send(pool->wdr, &cmd, sizeof(cmd), 0) == -1) { + return EVTHR_RES_RETRY; + } + + return EVTHR_RES_OK; +#endif + struct evthr *thread = NULL; + struct evthr *min_thread = NULL; + int min_backlog = 0; + + if (pool == NULL) { + return EVTHR_RES_FATAL; + } + + if (cb == NULL) { + return EVTHR_RES_NOCB; + } + + TAILQ_FOREACH(thread, &pool->threads, next) { + int backlog = get_backlog_(thread); + + if (backlog == 0) { + min_thread = thread; + break; + } + + if (min_thread == NULL || backlog < min_backlog) { + min_thread = thread; + min_backlog = backlog; + } + } + + return evthr_defer(min_thread, cb, arg); +} + +struct evthr_pool * +evthr_pool_wexit_new(int nthreads, evthr_init_cb init_cb, evthr_exit_cb exit_cb, void *shared) +{ + struct evthr_pool *pool; + int i; + +#ifdef EVTHR_SHARED_PIPE + int fds[2]; +#endif + + if (nthreads == 0) { + return NULL; + } + + if (!(pool = calloc(1, sizeof(struct evthr_pool)))) { + return NULL; + } + + pool->nthreads = nthreads; + TAILQ_INIT(&pool->threads); + +#ifdef EVTHR_SHARED_PIPE + if (evutil_socketpair(AF_UNIX, SOCK_DGRAM, 0, fds) == -1) { + return NULL; + } + + evutil_make_socket_nonblocking(fds[0]); + evutil_make_socket_nonblocking(fds[1]); + + pool->rdr = fds[0]; + pool->wdr = fds[1]; +#endif + + for (i = 0; i < nthreads; i++) { + struct evthr * thread; + + if (!(thread = evthr_wexit_new(init_cb, exit_cb, shared))) { + evthr_pool_free(pool); + return NULL; + } + +#ifdef EVTHR_SHARED_PIPE + thread->pool_rdr = fds[0]; +#endif + + TAILQ_INSERT_TAIL(&pool->threads, thread, next); + } + + return pool; +} + +int +evthr_pool_start(struct evthr_pool *pool) +{ + struct evthr *evthr = NULL; + + if (pool == NULL) { + return -1; + } + + TAILQ_FOREACH(evthr, &pool->threads, next) { + if (evthr_start(evthr) < 0) { + return -1; + } + + usleep(5000); + } + + return 0; +} diff --git a/src/evthr.h b/src/evthr.h new file mode 100644 index 00000000..7f724f93 --- /dev/null +++ b/src/evthr.h @@ -0,0 +1,43 @@ +#ifndef __EVTHR_H__ +#define __EVTHR_H__ + +enum evthr_res { + EVTHR_RES_OK = 0, + EVTHR_RES_BACKLOG, + EVTHR_RES_RETRY, + EVTHR_RES_NOCB, + EVTHR_RES_FATAL +}; + +struct evthr_pool; +struct evthr; + +typedef void (*evthr_cb)(struct evthr *thr, void *cmd_arg, void *shared); +typedef void (*evthr_init_cb)(struct evthr *thr, void *shared); +typedef void (*evthr_exit_cb)(struct evthr *thr, void *shared); + +struct event_base * +evthr_get_base(struct evthr *thr); + +void +evthr_set_aux(struct evthr *thr, void *aux); + +void * +evthr_get_aux(struct evthr *thr); + +enum evthr_res +evthr_pool_defer(struct evthr_pool *pool, evthr_cb cb, void *arg); + +struct evthr_pool * +evthr_pool_wexit_new(int nthreads, evthr_init_cb init_cb, evthr_exit_cb exit_cb, void *shared); + +void +evthr_pool_free(struct evthr_pool *pool); + +enum evthr_res +evthr_pool_stop(struct evthr_pool *pool); + +int +evthr_pool_start(struct evthr_pool *pool); + +#endif /* !__EVTHR_H__ */ diff --git a/src/http.c b/src/http.c index 566b64b4..6a1604d4 100644 --- a/src/http.c +++ b/src/http.c @@ -40,7 +40,6 @@ #include #include "http.h" -#include "httpd.h" #include "logger.h" #include "misc.h" #include "conffile.h" @@ -240,9 +239,11 @@ http_form_urlencode(struct keyval *kv) int http_stream_setup(char **stream, const char *url) { + CURLU *url_handle; + CURLUcode rc; struct http_client_ctx ctx; - struct httpd_uri_parsed *parsed; struct evbuffer *evbuf; + char *path; const char *ext; char *line; char *pos; @@ -253,17 +254,28 @@ http_stream_setup(char **stream, const char *url) *stream = NULL; - parsed = httpd_uri_parse(url); - if (!parsed) + CHECK_NULL(L_HTTP, url_handle = curl_url()); + + rc = curl_url_set(url_handle, CURLUPART_URL, url, 0); + if (rc != 0) { DPRINTF(E_LOG, L_HTTP, "Couldn't parse internet playlist: '%s'\n", url); + curl_url_cleanup(url_handle); return -1; } - // parsed->path does not include query or fragment, so should work with any url's + rc = curl_url_get(url_handle, CURLUPART_PATH, &path, 0); + if (rc != 0) + { + DPRINTF(E_LOG, L_HTTP, "Couldn't find internet playlist path: '%s'\n", url); + curl_url_cleanup(url_handle); + return -1; + } + + // path does not include query or fragment, so should work with any url's // e.g. http://yp.shoutcast.com/sbin/tunein-station.pls?id=99179772#Air Jazz pl_format = PLAYLIST_UNK; - if (parsed->path && (ext = strrchr(parsed->path, '.'))) + if (path && (ext = strrchr(path, '.'))) { if (strcasecmp(ext, ".m3u") == 0) pl_format = PLAYLIST_M3U; @@ -271,7 +283,9 @@ http_stream_setup(char **stream, const char *url) pl_format = PLAYLIST_PLS; } - httpd_uri_free(parsed); + curl_free(path); + curl_url_cleanup(url_handle); + if (pl_format==PLAYLIST_UNK) { diff --git a/src/httpd.c b/src/httpd.c index 397ccb72..8a5087bf 100644 --- a/src/httpd.c +++ b/src/httpd.c @@ -27,25 +27,20 @@ #include #include #include -#include #include #include -#include #include #include #include #include -#ifdef HAVE_EVENTFD -# include +#ifdef HAVE_SYSCALL +#include // get thread ID #endif + #include -#include -#include -#ifdef HAVE_LIBEVENT2_OLD -# include -# include -#endif + +#include #include #include "logger.h" @@ -53,14 +48,9 @@ #include "conffile.h" #include "misc.h" #include "worker.h" +#include "evthr.h" #include "httpd.h" -#include "httpd_rsp.h" -#include "httpd_daap.h" -#include "httpd_dacp.h" -#include "httpd_jsonapi.h" -#include "httpd_streaming.h" -#include "httpd_oauth.h" -#include "httpd_artworkapi.h" +#include "httpd_internal.h" #include "transcode.h" #ifdef LASTFM # include "lastfm.h" @@ -69,7 +59,6 @@ # include "websocket.h" #endif - #define STREAM_CHUNK_SIZE (64 * 1024) #define ERR_PAGE "\n\n" \ "%d %s\n" \ @@ -81,6 +70,26 @@ #define HTTPD_STREAM_BPS 16 #define HTTPD_STREAM_CHANNELS 2 +extern struct httpd_module httpd_dacp; +extern struct httpd_module httpd_daap; +extern struct httpd_module httpd_jsonapi; +extern struct httpd_module httpd_artworkapi; +extern struct httpd_module httpd_streaming; +extern struct httpd_module httpd_oauth; +extern struct httpd_module httpd_rsp; + +// Must be in sync with enum httpd_modules +static struct httpd_module *httpd_modules[] = { + &httpd_dacp, + &httpd_daap, + &httpd_jsonapi, + &httpd_artworkapi, + &httpd_streaming, + &httpd_oauth, + &httpd_rsp, + NULL +}; + struct content_type_map { char *ext; @@ -88,9 +97,7 @@ struct content_type_map { }; struct stream_ctx { - struct evhttp_request *req; - uint8_t *buf; - struct evbuffer *evbuf; + struct httpd_request *hreq; struct event *ev; int id; int fd; @@ -116,27 +123,26 @@ static const struct content_type_map ext2ctype[] = { NULL, NULL } }; -static const char *http_reply_401 = "401 UnauthorizedAuthorization required"; - static char webroot_directory[PATH_MAX]; -struct event_base *evbase_httpd; -#ifdef HAVE_EVENTFD -static int exit_efd; -#else -static int exit_pipe[2]; -#endif -static int httpd_exit; -static struct event *exitev; -static struct evhttp *evhttpd; -static pthread_t tid_httpd; - -static const char *allow_origin; +static const char *httpd_allow_origin; static int httpd_port; -#ifdef HAVE_LIBEVENT2_OLD -struct stream_ctx *g_st; -#endif + +// The server is designed around a single thread listening for requests. When +// received, the request is passed to a thread from the worker pool, where a +// handler will process it and prepare a response for the httpd thread to send +// back. The idea is that the httpd thread never blocks. The handler in the +// worker thread can block, but shouldn't hold the thread if it is a long- +// running request (e.g. a long poll), because then we can run out of worker +// threads. The handler should use events to avoid this. Handlers, that are non- +// blocking and where the response must not be delayed can use +// HTTPD_HANDLER_REALTIME, then the httpd thread calls it directly (sync) +// instead of the async worker. In short, you shouldn't need to increase the +// below. +#define THREADPOOL_NTHREADS 1 + +static struct evthr_pool *httpd_threadpool; /* -------------------------------- HELPERS --------------------------------- */ @@ -167,107 +173,178 @@ scrobble_cb(void *arg) } #endif -/* - * This disabled in the commit after d8cdc89 because my tests work fine without - * it, and it seems that nowadays iTunes and Remote encodes the query just fine. - * However, I'm keeping it around for a while in case problems show up. If you - * are from the future, you can probably safely remove it for good. - * -static char * -httpd_fixup_uri(struct evhttp_request *req) + +/* --------------------------- MODULES INTERFACE ---------------------------- */ + +static void +modules_handlers_unset(struct httpd_uri_map *uri_map) { - struct evkeyvalq *headers; - const char *ua; - const char *uri; - const char *u; - const char *q; - char *fixed; - char *f; - int len; + struct httpd_uri_map *uri; - uri = evhttp_request_get_uri(req); - if (!uri) - return NULL; - - // No query string, nothing to do - q = strchr(uri, '?'); - if (!q) - return strdup(uri); - - headers = evhttp_request_get_input_headers(req); - ua = evhttp_find_header(headers, "User-Agent"); - if (!ua) - return strdup(uri); - - if ((strncmp(ua, "iTunes", strlen("iTunes")) != 0) - && (strncmp(ua, "Remote", strlen("Remote")) != 0) - && (strncmp(ua, "Roku", strlen("Roku")) != 0)) - return strdup(uri); - - // Reencode + as %2B and space as + in the query, - // which iTunes and Roku devices don't do - len = strlen(uri); - - u = q; - while (*u) + for (uri = uri_map; uri->preg; uri++) { - if (*u == '+') - len += 2; - - u++; + regfree(uri->preg); // Frees allocation by regcomp + free(uri->preg); // Frees our own calloc } +} - fixed = (char *)malloc(len + 1); - if (!fixed) - return NULL; +static int +modules_handlers_set(struct httpd_uri_map *uri_map) +{ + struct httpd_uri_map *uri; + char buf[64]; + int ret; - strncpy(fixed, uri, q - uri); - - f = fixed + (q - uri); - while (*q) + for (uri = uri_map; uri->handler; uri++) { - switch (*q) + uri->preg = calloc(1, sizeof(regex_t)); + if (!uri->preg) { - case '+': - *f = '%'; - f++; - *f = '2'; - f++; - *f = 'B'; - break; - - case ' ': - *f = '+'; - break; - - default: - *f = *q; - break; + DPRINTF(E_LOG, L_HTTPD, "Error setting URI handler, out of memory"); + goto error; } - q++; - f++; + ret = regcomp(uri->preg, uri->regexp, REG_EXTENDED | REG_NOSUB); + if (ret != 0) + { + regerror(ret, uri->preg, buf, sizeof(buf)); + DPRINTF(E_LOG, L_HTTPD, "Error setting URI handler, regexp error: %s\n", buf); + goto error; + } } - *f = '\0'; + return 0; - return fixed; + error: + modules_handlers_unset(uri_map); + return -1; +} + +static int +modules_init(void) +{ + struct httpd_module **ptr; + struct httpd_module *m; + + for (ptr = httpd_modules; *ptr; ptr++) + { + m = *ptr; + m->initialized = (!m->init || m->init() == 0); + if (!m->initialized) + { + DPRINTF(E_FATAL, L_HTTPD, "%s init failed\n", m->name); + return -1; + } + + if (modules_handlers_set(m->handlers) != 0) + { + DPRINTF(E_FATAL, L_HTTPD, "%s handler configuration failed\n", m->name); + return -1; + } + } + + return 0; +} + +static void +modules_deinit(void) +{ + struct httpd_module **ptr; + struct httpd_module *m; + + for (ptr = httpd_modules; *ptr; ptr++) + { + m = *ptr; + if (m->initialized && m->deinit) + m->deinit(); + + modules_handlers_unset(m->handlers); + } +} + +static struct httpd_module * +modules_search(const char *path) +{ + struct httpd_module **ptr; + struct httpd_module *m; + const char **test; + bool is_found = false; + + for (ptr = httpd_modules; *ptr; ptr++) + { + m = *ptr; + if (!m->subpaths || !m->request) + continue; + + for (test = m->subpaths; *test && !is_found; test++) + is_found = (strncmp(path, *test, strlen(*test)) == 0); + + for (test = m->fullpaths; *test && !is_found; test++) + is_found = (strcmp(path, *test) == 0); + + if (is_found) + return m; + } + + return NULL; } -*/ /* --------------------------- REQUEST HELPERS ------------------------------ */ +static void +cors_headers_add(struct httpd_request *hreq, const char *allow_origin) +{ + if (allow_origin) + httpd_header_add(hreq->out_headers, "Access-Control-Allow-Origin", httpd_allow_origin); + + httpd_header_add(hreq->out_headers, "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); + httpd_header_add(hreq->out_headers, "Access-Control-Allow-Headers", "authorization"); +} + +static bool +is_cors_preflight(struct httpd_request *hreq, const char *allow_origin) +{ + return ( hreq->method == HTTPD_METHOD_OPTIONS && hreq->in_headers && allow_origin && + httpd_header_find(hreq->in_headers, "Origin") && + httpd_header_find(hreq->in_headers, "Access-Control-Request-Method") ); +} void -httpd_redirect_to(struct evhttp_request *req, const char *path) +httpd_request_handler_set(struct httpd_request *hreq) { - struct evkeyvalq *headers; + struct httpd_uri_map *map; + int ret; - headers = evhttp_request_get_output_headers(req); - evhttp_add_header(headers, "Location", path); + // Path with e.g. /api -> JSON module + hreq->module = modules_search(hreq->path); + if (!hreq->module) + { + return; + } - httpd_send_reply(req, HTTP_MOVETEMP, "Moved", NULL, HTTPD_SEND_NO_GZIP); + for (map = hreq->module->handlers; map->handler; map++) + { + // Check if handler supports the current http request method + if (map->method && hreq->method && !(map->method & hreq->method)) + continue; + + ret = regexec(map->preg, hreq->path, 0, NULL, 0); + if (ret != 0) + continue; + + hreq->handler = map->handler; + hreq->is_async = !(map->flags & HTTPD_HANDLER_REALTIME); + break; + } +} + +void +httpd_redirect_to(struct httpd_request *hreq, const char *path) +{ + httpd_header_add(hreq->out_headers, "Location", path); + + httpd_send_reply(hreq, HTTP_MOVETEMP, "Moved", HTTPD_SEND_NO_GZIP); } /* @@ -282,23 +359,19 @@ httpd_redirect_to(struct evhttp_request *req, const char *path) * @return True if the given ETag matches the request-header-value "If-None-Match", otherwise false */ bool -httpd_request_etag_matches(struct evhttp_request *req, const char *etag) +httpd_request_etag_matches(struct httpd_request *hreq, const char *etag) { - struct evkeyvalq *input_headers; - struct evkeyvalq *output_headers; const char *none_match; - input_headers = evhttp_request_get_input_headers(req); - none_match = evhttp_find_header(input_headers, "If-None-Match"); + none_match = httpd_header_find(hreq->in_headers, "If-None-Match"); // Return not modified, if given timestamp matches "If-Modified-Since" request header if (none_match && (strcasecmp(etag, none_match) == 0)) return true; // Add cache headers to allow client side caching - output_headers = evhttp_request_get_output_headers(req); - evhttp_add_header(output_headers, "Cache-Control", "private,no-cache,max-age=0"); - evhttp_add_header(output_headers, "ETag", etag); + httpd_header_add(hreq->out_headers, "Cache-Control", "private,no-cache,max-age=0"); + httpd_header_add(hreq->out_headers, "ETag", etag); return false; } @@ -315,16 +388,13 @@ httpd_request_etag_matches(struct evhttp_request *req, const char *etag) * @return True if the given timestamp matches the request-header-value "If-Modified-Since", otherwise false */ bool -httpd_request_not_modified_since(struct evhttp_request *req, time_t mtime) +httpd_request_not_modified_since(struct httpd_request *hreq, time_t mtime) { - struct evkeyvalq *input_headers; - struct evkeyvalq *output_headers; char last_modified[1000]; const char *modified_since; struct tm timebuf; - input_headers = evhttp_request_get_input_headers(req); - modified_since = evhttp_find_header(input_headers, "If-Modified-Since"); + modified_since = httpd_header_find(hreq->in_headers, "If-Modified-Since"); strftime(last_modified, sizeof(last_modified), "%a, %d %b %Y %H:%M:%S %Z", gmtime_r(&mtime, &timebuf)); @@ -333,38 +403,31 @@ httpd_request_not_modified_since(struct evhttp_request *req, time_t mtime) return true; // Add cache headers to allow client side caching - output_headers = evhttp_request_get_output_headers(req); - evhttp_add_header(output_headers, "Cache-Control", "private,no-cache,max-age=0"); - evhttp_add_header(output_headers, "Last-Modified", last_modified); + httpd_header_add(hreq->out_headers, "Cache-Control", "private,no-cache,max-age=0"); + httpd_header_add(hreq->out_headers, "Last-Modified", last_modified); return false; } void -httpd_response_not_cachable(struct evhttp_request *req) +httpd_response_not_cachable(struct httpd_request *hreq) { - struct evkeyvalq *output_headers; - - output_headers = evhttp_request_get_output_headers(req); - // Remove potentially set cache control headers - evhttp_remove_header(output_headers, "Cache-Control"); - evhttp_remove_header(output_headers, "Last-Modified"); - evhttp_remove_header(output_headers, "ETag"); + httpd_header_remove(hreq->out_headers, "Cache-Control"); + httpd_header_remove(hreq->out_headers, "Last-Modified"); + httpd_header_remove(hreq->out_headers, "ETag"); // Tell clients that they are not allowed to cache this response - evhttp_add_header(output_headers, "Cache-Control", "no-store"); + httpd_header_add(hreq->out_headers, "Cache-Control", "no-store"); } static void -serve_file(struct evhttp_request *req, const char *uri) +serve_file(struct httpd_request *hreq) { char *ext; char path[PATH_MAX]; char deref[PATH_MAX]; char *ctype; - struct evbuffer *evbuf; - struct evkeyvalq *output_headers; struct stat sb; int fd; int i; @@ -373,16 +436,15 @@ serve_file(struct evhttp_request *req, const char *uri) int ret; /* Check authentication */ - if (!httpd_admin_check_auth(req)) + if (!httpd_admin_check_auth(hreq)) return; - ret = snprintf(path, sizeof(path), "%s%s", webroot_directory, uri); + ret = snprintf(path, sizeof(path), "%s%s", webroot_directory, hreq->path); if ((ret < 0) || (ret >= sizeof(path))) { - DPRINTF(E_LOG, L_HTTPD, "Request exceeds PATH_MAX: %s\n", uri); - - httpd_send_error(req, HTTP_NOTFOUND, "Not Found"); + DPRINTF(E_LOG, L_HTTPD, "Request exceeds PATH_MAX: %s\n", hreq->uri); + httpd_send_error(hreq, HTTP_NOTFOUND, "Not Found"); return; } @@ -390,7 +452,7 @@ serve_file(struct evhttp_request *req, const char *uri) { DPRINTF(E_LOG, L_HTTPD, "Could not dereference %s: %s\n", path, strerror(errno)); - httpd_send_error(req, HTTP_NOTFOUND, "Not Found"); + httpd_send_error(hreq, HTTP_NOTFOUND, "Not Found"); return; } @@ -398,8 +460,7 @@ serve_file(struct evhttp_request *req, const char *uri) { DPRINTF(E_LOG, L_HTTPD, "Dereferenced path exceeds PATH_MAX: %s\n", path); - httpd_send_error(req, HTTP_NOTFOUND, "Not Found"); - + httpd_send_error(hreq, HTTP_NOTFOUND, "Not Found"); return; } @@ -408,8 +469,7 @@ serve_file(struct evhttp_request *req, const char *uri) { DPRINTF(E_WARN, L_HTTPD, "Could not lstat() %s: %s\n", deref, strerror(errno)); - httpd_send_error(req, HTTP_NOTFOUND, "Not Found"); - + httpd_send_error(hreq, HTTP_NOTFOUND, "Not Found"); return; } @@ -422,7 +482,7 @@ serve_file(struct evhttp_request *req, const char *uri) { DPRINTF(E_LOG, L_HTTPD, "Could not dereference %s: %s\n", path, strerror(errno)); - httpd_send_error(req, HTTP_NOTFOUND, "Not Found"); + httpd_send_error(hreq, HTTP_NOTFOUND, "Not Found"); return; } @@ -430,7 +490,7 @@ serve_file(struct evhttp_request *req, const char *uri) { DPRINTF(E_LOG, L_HTTPD, "Dereferenced path exceeds PATH_MAX: %s\n", path); - httpd_send_error(req, HTTP_NOTFOUND, "Not Found"); + httpd_send_error(hreq, HTTP_NOTFOUND, "Not Found"); return; } @@ -439,7 +499,7 @@ serve_file(struct evhttp_request *req, const char *uri) if (ret < 0) { DPRINTF(E_LOG, L_HTTPD, "Could not stat() %s: %s\n", path, strerror(errno)); - httpd_send_error(req, HTTP_NOTFOUND, "Not Found"); + httpd_send_error(hreq, HTTP_NOTFOUND, "Not Found"); return; } } @@ -448,23 +508,14 @@ serve_file(struct evhttp_request *req, const char *uri) { DPRINTF(E_WARN, L_HTTPD, "Access to file outside the web root dir forbidden: %s\n", deref); - httpd_send_error(req, 403, "Forbidden"); + httpd_send_error(hreq, HTTP_FORBIDDEN, "Forbidden"); return; } - if (httpd_request_not_modified_since(req, sb.st_mtime)) + if (httpd_request_not_modified_since(hreq, sb.st_mtime)) { - httpd_send_reply(req, HTTP_NOTMODIFIED, NULL, NULL, HTTPD_SEND_NO_GZIP); - return; - } - - evbuf = evbuffer_new(); - if (!evbuf) - { - DPRINTF(E_LOG, L_HTTPD, "Could not create evbuffer\n"); - - httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal error"); + httpd_send_reply(hreq, HTTP_NOTMODIFIED, NULL, HTTPD_SEND_NO_GZIP); return; } @@ -473,12 +524,11 @@ serve_file(struct evhttp_request *req, const char *uri) { DPRINTF(E_LOG, L_HTTPD, "Could not open %s: %s\n", deref, strerror(errno)); - httpd_send_error(req, HTTP_NOTFOUND, "Not Found"); - evbuffer_free(evbuf); + httpd_send_error(hreq, HTTP_NOTFOUND, "Not Found"); return; } - ret = evbuffer_expand(evbuf, sb.st_size); + ret = evbuffer_expand(hreq->out_body, sb.st_size); if (ret < 0) { DPRINTF(E_LOG, L_HTTPD, "Out of memory for htdocs-file\n"); @@ -486,7 +536,7 @@ serve_file(struct evhttp_request *req, const char *uri) } while ((ret = read(fd, buf, sizeof(buf))) > 0) - evbuffer_add(evbuf, buf, ret); + evbuffer_add(hreq->out_body, buf, ret); if (ret < 0) { @@ -508,56 +558,57 @@ serve_file(struct evhttp_request *req, const char *uri) } } - output_headers = evhttp_request_get_output_headers(req); - evhttp_add_header(output_headers, "Content-Type", ctype); + httpd_header_add(hreq->out_headers, "Content-Type", ctype); - httpd_send_reply(req, HTTP_OK, "OK", evbuf, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_OK, "OK", HTTPD_SEND_NO_GZIP); - evbuffer_free(evbuf); close(fd); return; out_fail: - httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal error"); - evbuffer_free(evbuf); + httpd_send_error(hreq, HTTP_SERVUNAVAIL, "Internal error"); close(fd); } /* ---------------------------- STREAM HANDLING ----------------------------- */ +// This will triggered in a httpd thread, but since the reading may be in a +// worker thread we just want to trigger the read loop static void -stream_end(struct stream_ctx *st, int failed) +stream_chunk_resched_cb(httpd_connection *conn, void *arg) { - struct evhttp_connection *evcon; + struct stream_ctx *st = arg; - evcon = evhttp_request_get_connection(st->req); + // TODO not thread safe if st was freed in worker thread, but maybe not possible? + event_active(st->ev, 0, 0); +} - if (evcon) - evhttp_connection_set_closecb(evcon, NULL, NULL); +static void +stream_free(struct stream_ctx *st) +{ + if (!st) + return; - if (!failed) - evhttp_send_reply_end(st->req); - - evbuffer_free(st->evbuf); - event_free(st->ev); - - if (st->xcode) - transcode_cleanup(&st->xcode); - else - { - free(st->buf); - close(st->fd); - } - -#ifdef HAVE_LIBEVENT2_OLD - if (g_st == st) - g_st = NULL; -#endif + if (st->ev) + event_free(st->ev); + if (st->fd >= 0) + close(st->fd); + transcode_cleanup(&st->xcode); free(st); } +static void +stream_end(struct stream_ctx *st) +{ + DPRINTF(E_DBG, L_HTTPD, "Ending stream %d\n", st->id); + + httpd_send_reply_end(st->hreq); + + stream_free(st); +} + static void stream_end_register(struct stream_ctx *st) { @@ -573,45 +624,134 @@ stream_end_register(struct stream_ctx *st) } } -static void -stream_chunk_resched_cb(struct evhttp_connection *evcon, void *arg) +static struct stream_ctx * +stream_new(struct media_file_info *mfi, struct httpd_request *hreq, event_callback_fn stream_cb) { struct stream_ctx *st; - struct timeval tv; + + CHECK_NULL(L_HTTPD, st = calloc(1, sizeof(struct stream_ctx))); + st->fd = -1; + + st->ev = event_new(hreq->evbase, -1, EV_PERSIST, stream_cb, st); + if (!st->ev) + { + DPRINTF(E_LOG, L_HTTPD, "Could not create event for streaming\n"); + + httpd_send_error(hreq, HTTP_SERVUNAVAIL, "Internal Server Error"); + goto error; + } + + event_active(st->ev, 0, 0); + + st->id = mfi->id; + st->hreq = hreq; + return st; + + error: + stream_free(st); + return NULL; +} + +static struct stream_ctx * +stream_new_transcode(struct media_file_info *mfi, struct httpd_request *hreq, int64_t offset, int64_t end_offset, event_callback_fn stream_cb) +{ + struct stream_ctx *st; + struct media_quality quality = { HTTPD_STREAM_SAMPLE_RATE, HTTPD_STREAM_BPS, HTTPD_STREAM_CHANNELS, 0 }; + + st = stream_new(mfi, hreq, stream_cb); + if (!st) + { + goto error; + } + + st->xcode = transcode_setup(XCODE_PCM16_HEADER, &quality, mfi->data_kind, mfi->path, mfi->song_length, &st->size); + if (!st->xcode) + { + DPRINTF(E_WARN, L_HTTPD, "Transcoding setup failed, aborting streaming\n"); + + httpd_send_error(hreq, HTTP_SERVUNAVAIL, "Internal Server Error"); + goto error; + } + + st->stream_size = st->size - offset; + if (end_offset > 0) + st->stream_size -= (st->size - end_offset); + + st->start_offset = offset; + + return st; + + error: + stream_free(st); + return NULL; +} + +static struct stream_ctx * +stream_new_raw(struct media_file_info *mfi, struct httpd_request *hreq, int64_t offset, int64_t end_offset, event_callback_fn stream_cb) +{ + struct stream_ctx *st; + struct stat sb; + off_t pos; int ret; - st = (struct stream_ctx *)arg; + st = stream_new(mfi, hreq, stream_cb); + if (!st) + { + goto error; + } - evutil_timerclear(&tv); - ret = event_add(st->ev, &tv); + st->fd = open(mfi->path, O_RDONLY); + if (st->fd < 0) + { + DPRINTF(E_LOG, L_HTTPD, "Could not open %s: %s\n", mfi->path, strerror(errno)); + + httpd_send_error(hreq, HTTP_NOTFOUND, "Not Found"); + goto error; + } + + ret = stat(mfi->path, &sb); if (ret < 0) { - DPRINTF(E_LOG, L_HTTPD, "Could not re-add one-shot event for streaming\n"); + DPRINTF(E_LOG, L_HTTPD, "Could not stat() %s: %s\n", mfi->path, strerror(errno)); - stream_end(st, 0); + httpd_send_error(hreq, HTTP_NOTFOUND, "Not Found"); + goto error; } -} -#ifdef HAVE_LIBEVENT2_OLD -static void -stream_chunk_resched_cb_wrapper(struct bufferevent *bufev, void *arg) -{ - if (g_st) - stream_chunk_resched_cb(NULL, g_st); + st->size = sb.st_size; + + st->stream_size = st->size - offset; + if (end_offset > 0) + st->stream_size -= (st->size - end_offset); + + st->start_offset = offset; + st->offset = offset; + st->end_offset = end_offset; + + pos = lseek(st->fd, offset, SEEK_SET); + if (pos == (off_t) -1) + { + DPRINTF(E_LOG, L_HTTPD, "Could not seek into %s: %s\n", mfi->path, strerror(errno)); + + httpd_send_error(hreq, HTTP_BADREQUEST, "Bad Request"); + goto error; + } + + return st; + + error: + stream_free(st); + return NULL; } -#endif static void stream_chunk_xcode_cb(int fd, short event, void *arg) { - struct stream_ctx *st; - struct timeval tv; + struct stream_ctx *st = arg; int xcoded; int ret; - st = (struct stream_ctx *)arg; - - xcoded = transcode(st->evbuf, NULL, st->xcode, STREAM_CHUNK_SIZE); + xcoded = transcode(st->hreq->out_body, NULL, st->xcode, STREAM_CHUNK_SIZE); if (xcoded <= 0) { if (xcoded == 0) @@ -619,77 +759,54 @@ stream_chunk_xcode_cb(int fd, short event, void *arg) else DPRINTF(E_LOG, L_HTTPD, "Transcoding error, file id %d\n", st->id); - stream_end(st, 0); + stream_end(st); return; } DPRINTF(E_DBG, L_HTTPD, "Got %d bytes from transcode; streaming file id %d\n", xcoded, st->id); - /* Consume transcoded data until we meet start_offset */ + // Consume transcoded data until we meet start_offset if (st->start_offset > st->offset) { ret = st->start_offset - st->offset; if (ret < xcoded) { - evbuffer_drain(st->evbuf, ret); + evbuffer_drain(st->hreq->out_body, ret); st->offset += ret; ret = xcoded - ret; } else { - evbuffer_drain(st->evbuf, xcoded); + evbuffer_drain(st->hreq->out_body, xcoded); st->offset += xcoded; - goto consume; + // Reschedule immediately - consume up to start_offset + event_active(st->ev, 0, 0); + return; } } else ret = xcoded; -#ifdef HAVE_LIBEVENT2_OLD - evhttp_send_reply_chunk(st->req, st->evbuf); - - struct evhttp_connection *evcon = evhttp_request_get_connection(st->req); - struct bufferevent *bufev = evhttp_connection_get_bufferevent(evcon); - - g_st = st; // Can't pass st to callback so use global - limits libevent 2.0 to a single stream - bufev->writecb = stream_chunk_resched_cb_wrapper; -#else - evhttp_send_reply_chunk_with_cb(st->req, st->evbuf, stream_chunk_resched_cb, st); -#endif + httpd_send_reply_chunk(st->hreq, stream_chunk_resched_cb, st); st->offset += ret; stream_end_register(st); - - return; - - consume: /* reschedule immediately - consume up to start_offset */ - evutil_timerclear(&tv); - ret = event_add(st->ev, &tv); - if (ret < 0) - { - DPRINTF(E_LOG, L_HTTPD, "Could not re-add one-shot event for streaming (xcode)\n"); - - stream_end(st, 0); - return; - } } static void stream_chunk_raw_cb(int fd, short event, void *arg) { - struct stream_ctx *st; + struct stream_ctx *st = arg; size_t chunk_size; int ret; - st = (struct stream_ctx *)arg; - if (st->end_offset && (st->offset > st->end_offset)) { - stream_end(st, 0); + stream_end(st); return; } @@ -698,7 +815,7 @@ stream_chunk_raw_cb(int fd, short event, void *arg) else chunk_size = STREAM_CHUNK_SIZE; - ret = read(st->fd, st->buf, chunk_size); + ret = evbuffer_read(st->hreq->out_body, st->fd, chunk_size); if (ret <= 0) { if (ret == 0) @@ -706,25 +823,13 @@ stream_chunk_raw_cb(int fd, short event, void *arg) else DPRINTF(E_LOG, L_HTTPD, "Streaming error, file id %d\n", st->id); - stream_end(st, 0); + stream_end(st); return; } DPRINTF(E_DBG, L_HTTPD, "Read %d bytes; streaming file id %d\n", ret, st->id); - evbuffer_add(st->evbuf, st->buf, ret); - -#ifdef HAVE_LIBEVENT2_OLD - evhttp_send_reply_chunk(st->req, st->evbuf); - - struct evhttp_connection *evcon = evhttp_request_get_connection(st->req); - struct bufferevent *bufev = evhttp_connection_get_bufferevent(evcon); - - g_st = st; // Can't pass st to callback so use global - limits libevent 2.0 to a single stream - bufev->writecb = stream_chunk_resched_cb_wrapper; -#else - evhttp_send_reply_chunk_with_cb(st->req, st->evbuf, stream_chunk_resched_cb, st); -#endif + httpd_send_reply_chunk(st->hreq, stream_chunk_resched_cb, st); st->offset += ret; @@ -732,341 +837,91 @@ stream_chunk_raw_cb(int fd, short event, void *arg) } static void -stream_fail_cb(struct evhttp_connection *evcon, void *arg) +stream_fail_cb(void *arg) { - struct stream_ctx *st; + struct stream_ctx *st = arg; - st = (struct stream_ctx *)arg; - - DPRINTF(E_WARN, L_HTTPD, "Connection failed; stopping streaming of file ID %d\n", st->id); - - /* Stop streaming */ - event_del(st->ev); - - stream_end(st, 1); + stream_free(st); } -/* ---------------------------- MAIN HTTPD THREAD --------------------------- */ +/* ---------------------------- REQUEST CALLBACKS --------------------------- */ -static void * -httpd(void *arg) +// Worker thread, invoked by request_cb() below +static void +request_async_cb(void *arg) { - int ret; + struct httpd_request *hreq = *(struct httpd_request **)arg; - ret = db_perthread_init(); - if (ret < 0) +#ifdef HAVE_SYSCALL + DPRINTF(E_DBG, hreq->module->logdomain, "%s request '%s' in worker thread %ld\n", hreq->module->name, hreq->uri, syscall(SYS_gettid)); +#endif + + // Some handlers require an evbase to schedule events + hreq->evbase = worker_evbase_get(); + hreq->module->request(hreq); +} + +// httpd thread +static void +request_cb(struct httpd_request *hreq, void *arg) +{ + if (is_cors_preflight(hreq, httpd_allow_origin)) { - DPRINTF(E_LOG, L_HTTPD, "Error: DB init failed\n"); - - pthread_exit(NULL); + httpd_send_reply(hreq, HTTP_OK, "OK", HTTPD_SEND_NO_GZIP); + return; } - - event_base_dispatch(evbase_httpd); - - if (!httpd_exit) - DPRINTF(E_FATAL, L_HTTPD, "HTTPd event loop terminated ahead of time!\n"); - - db_perthread_deinit(); - - pthread_exit(NULL); -} - -static void -exit_cb(int fd, short event, void *arg) -{ - event_base_loopbreak(evbase_httpd); - - httpd_exit = 1; -} - -static void -httpd_gen_cb(struct evhttp_request *req, void *arg) -{ - struct evkeyvalq *input_headers; - struct evkeyvalq *output_headers; - struct httpd_uri_parsed *parsed; - const char *uri; - - // Clear the proxy request flag set by evhttp if the request URI was absolute. - // It has side-effects on Connection: keep-alive - req->flags &= ~EVHTTP_PROXY_REQUEST; - - // Did we get a CORS preflight request? - input_headers = evhttp_request_get_input_headers(req); - if ( input_headers && allow_origin && - (evhttp_request_get_command(req) == EVHTTP_REQ_OPTIONS) && - evhttp_find_header(input_headers, "Origin") && - evhttp_find_header(input_headers, "Access-Control-Request-Method") ) + else if (!hreq->uri || !hreq->uri_parsed) { - output_headers = evhttp_request_get_output_headers(req); - - evhttp_add_header(output_headers, "Access-Control-Allow-Origin", allow_origin); - evhttp_add_header(output_headers, "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); - evhttp_add_header(output_headers, "Access-Control-Allow-Headers", "authorization"); - - // In this case there is no reason to go through httpd_send_reply - evhttp_send_reply(req, HTTP_OK, "OK", NULL); + DPRINTF(E_WARN, L_HTTPD, "Invalid URI in request: '%s'\n", hreq->uri); + httpd_redirect_to(hreq, "/"); + return; + } + else if (!hreq->path) + { + DPRINTF(E_WARN, L_HTTPD, "Invalid path in request: '%s'\n", hreq->uri); + httpd_redirect_to(hreq, "/"); return; } - uri = evhttp_request_get_uri(req); - if (!uri) + httpd_request_handler_set(hreq); + if (hreq->module && hreq->is_async) { - DPRINTF(E_WARN, L_HTTPD, "No URI in request\n"); - httpd_redirect_to(req, "/"); - return; + worker_execute(request_async_cb, &hreq, sizeof(struct httpd_request *), 0); + } + else if (hreq->module) + { + DPRINTF(E_DBG, hreq->module->logdomain, "%s request: '%s'\n", hreq->module->name, hreq->uri); + hreq->evbase = httpd_backend_evbase_get(hreq->backend); + hreq->module->request(hreq); + } + else + { + // Serve web interface files + DPRINTF(E_DBG, L_HTTPD, "HTTP request: '%s'\n", hreq->uri); + serve_file(hreq); } - parsed = httpd_uri_parse(uri); - if (!parsed || !parsed->path) - { - httpd_redirect_to(req, "/"); - goto out; - } - - if (strcmp(parsed->path, "/") == 0) - { - goto serve_file; - } - - /* Dispatch protocol-specific handlers */ - if (dacp_is_request(parsed->path)) - { - dacp_request(req, parsed); - goto out; - } - else if (daap_is_request(parsed->path)) - { - daap_request(req, parsed); - goto out; - } - else if (jsonapi_is_request(parsed->path)) - { - jsonapi_request(req, parsed); - goto out; - } - else if (artworkapi_is_request(parsed->path)) - { - artworkapi_request(req, parsed); - goto out; - } - else if (streaming_is_request(parsed->path)) - { - streaming_request(req, parsed); - goto out; - } - else if (oauth_is_request(parsed->path)) - { - oauth_request(req, parsed); - goto out; - } - else if (rsp_is_request(parsed->path)) - { - rsp_request(req, parsed); - goto out; - } - - DPRINTF(E_DBG, L_HTTPD, "HTTP request: '%s'\n", parsed->uri); - - /* Serve web interface files */ - serve_file: - serve_file(req, parsed->path); - - out: - httpd_uri_free(parsed); + // Don't touch hreq here, if async it has been passed to a worker thread } /* ------------------------------- HTTPD API -------------------------------- */ void -httpd_uri_free(struct httpd_uri_parsed *parsed) +httpd_stream_file(struct httpd_request *hreq, int id) { - int i; - - if (!parsed) - return; - - free(parsed->uri_decoded); - free(parsed->path); - for (i = 0; i < ARRAY_SIZE(parsed->path_parts); i++) - { - free(parsed->path_parts[i]); - } - - evhttp_clear_headers(&(parsed->ev_query)); - - if (parsed->ev_uri) - evhttp_uri_free(parsed->ev_uri); - - free(parsed); -} - -struct httpd_uri_parsed * -httpd_uri_parse(const char *uri) -{ - struct httpd_uri_parsed *parsed; - char *path = NULL; - const char *query; - char *path_part; - char *ptr; - int i; - int ret; - - CHECK_NULL(L_HTTPD, parsed = calloc(1, sizeof(struct httpd_uri_parsed))); - - parsed->uri = uri; - - parsed->ev_uri = evhttp_uri_parse_with_flags(parsed->uri, EVHTTP_URI_NONCONFORMANT); - if (!parsed->ev_uri) - { - DPRINTF(E_LOG, L_HTTPD, "Could not parse request: '%s'\n", parsed->uri); - goto error; - } - - parsed->uri_decoded = evhttp_uridecode(parsed->uri, 0, NULL); - if (!parsed->uri_decoded) - { - DPRINTF(E_LOG, L_HTTPD, "Could not URI decode request: '%s'\n", parsed->uri); - goto error; - } - - query = evhttp_uri_get_query(parsed->ev_uri); - if (query && strchr(query, '=')) - { - ret = evhttp_parse_query_str(query, &(parsed->ev_query)); - if (ret < 0) - { - DPRINTF(E_LOG, L_DAAP, "Invalid query '%s' in request: '%s'\n", query, parsed->uri); - goto error; - } - } - - path = strdup(evhttp_uri_get_path(parsed->ev_uri)); - if (!path) - { - DPRINTF(E_WARN, L_HTTPD, "No path in request: '%s'\n", parsed->uri); - return parsed; - } - - parsed->path = evhttp_uridecode(path, 0, NULL); - if (!parsed->path) - { - DPRINTF(E_LOG, L_HTTPD, "Could not URI decode path: '%s'\n", path); - goto error; - } - - path_part = strtok_r(path, "/", &ptr); - for (i = 0; (i < ARRAY_SIZE(parsed->path_parts) && path_part); i++) - { - parsed->path_parts[i] = evhttp_uridecode(path_part, 0, NULL); - path_part = strtok_r(NULL, "/", &ptr); - } - - if (path_part) - { - // If "path_part" is not NULL, we have path tokens that could not be parsed into the "parsed->path_parts" array - DPRINTF(E_LOG, L_HTTPD, "URI path has too many components (%d): '%s'\n", i, parsed->path); - goto error; - } - - free(path); - - return parsed; - - error: - free(path); - httpd_uri_free(parsed); - return NULL; -} - -struct httpd_request * -httpd_request_parse(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed, const char *user_agent, struct httpd_uri_map *uri_map) -{ - struct httpd_request *hreq; - struct evhttp_connection *evcon; - struct evkeyvalq *headers; - int req_method; - int i; - int ret; - - CHECK_NULL(L_HTTPD, hreq = calloc(1, sizeof(struct httpd_request))); - - // Note req is allowed to be NULL - hreq->req = req; - hreq->uri_parsed = uri_parsed; - hreq->query = &(uri_parsed->ev_query); - req_method = 0; - - if (req) - { - headers = evhttp_request_get_input_headers(req); - hreq->user_agent = evhttp_find_header(headers, "User-Agent"); - - evcon = evhttp_request_get_connection(req); - if (evcon) - httpd_peer_get(&hreq->peer_address, &hreq->peer_port, evcon); - else - DPRINTF(E_LOG, L_HTTPD, "Connection to client lost or missing\n"); - - req_method = evhttp_request_get_command(req); - } - - if (user_agent) - hreq->user_agent = user_agent; - - // Find a handler for the path - for (i = 0; uri_map[i].handler; i++) - { - // Check if handler supports the current http request method - if (uri_map[i].method && req_method && !(req_method & uri_map[i].method)) - continue; - - ret = regexec(&uri_map[i].preg, uri_parsed->path, 0, NULL, 0); - if (ret == 0) - { - hreq->handler = uri_map[i].handler; - return hreq; // Success - } - } - - // Handler not found, that's an error - free(hreq); - - return NULL; -} - -/* Thread: httpd */ -void -httpd_stream_file(struct evhttp_request *req, int id) -{ - struct media_quality quality = { HTTPD_STREAM_SAMPLE_RATE, HTTPD_STREAM_BPS, HTTPD_STREAM_CHANNELS, 0 }; - struct media_file_info *mfi; - struct stream_ctx *st; - void (*stream_cb)(int fd, short event, void *arg); - struct stat sb; - struct timeval tv; - struct evhttp_connection *evcon; - struct evkeyvalq *input_headers; - struct evkeyvalq *output_headers; + struct media_file_info *mfi = NULL; + struct stream_ctx *st = NULL; const char *param; const char *param_end; - const char *ua; - const char *client_codecs; char buf[64]; - int64_t offset; - int64_t end_offset; - off_t pos; + int64_t offset = 0; + int64_t end_offset = 0; int transcode; int ret; - offset = 0; - end_offset = 0; - - input_headers = evhttp_request_get_input_headers(req); - - param = evhttp_find_header(input_headers, "Range"); + param = httpd_header_find(hreq->in_headers, "Range"); if (param) { DPRINTF(E_DBG, L_HTTPD, "Found Range header: %s\n", param); @@ -1105,198 +960,86 @@ httpd_stream_file(struct evhttp_request *req, int id) { DPRINTF(E_LOG, L_HTTPD, "Item %d not found\n", id); - evhttp_send_error(req, HTTP_NOTFOUND, "Not Found"); - return; + httpd_send_error(hreq, HTTP_NOTFOUND, "Not Found"); + goto error; } if (mfi->data_kind != DATA_KIND_FILE) { DPRINTF(E_LOG, L_HTTPD, "Could not serve '%s' to client, not a file\n", mfi->path); - evhttp_send_error(req, 500, "Cannot stream non-file content"); - goto out_free_mfi; + httpd_send_error(hreq, HTTP_INTERNAL, "Cannot stream non-file content"); + goto error; } - st = (struct stream_ctx *)malloc(sizeof(struct stream_ctx)); - if (!st) - { - DPRINTF(E_LOG, L_HTTPD, "Out of memory for struct stream_ctx\n"); - - evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); - goto out_free_mfi; - } - memset(st, 0, sizeof(struct stream_ctx)); - st->fd = -1; - - ua = evhttp_find_header(input_headers, "User-Agent"); - client_codecs = evhttp_find_header(input_headers, "Accept-Codecs"); - - transcode = transcode_needed(ua, client_codecs, mfi->codectype); - - output_headers = evhttp_request_get_output_headers(req); + param = httpd_header_find(hreq->in_headers, "Accept-Codecs"); + transcode = transcode_needed(hreq->user_agent, param, mfi->codectype); if (transcode) { DPRINTF(E_INFO, L_HTTPD, "Preparing to transcode %s\n", mfi->path); - stream_cb = stream_chunk_xcode_cb; + st = stream_new_transcode(mfi, hreq, offset, end_offset, stream_chunk_xcode_cb); + if (!st) + goto error; - st->xcode = transcode_setup(XCODE_PCM16_HEADER, &quality, mfi->data_kind, mfi->path, mfi->song_length, &st->size); - if (!st->xcode) - { - DPRINTF(E_WARN, L_HTTPD, "Transcoding setup failed, aborting streaming\n"); - - evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); - - goto out_free_st; - } - - if (!evhttp_find_header(output_headers, "Content-Type")) - evhttp_add_header(output_headers, "Content-Type", "audio/wav"); + if (!httpd_header_find(hreq->out_headers, "Content-Type")) + httpd_header_add(hreq->out_headers, "Content-Type", "audio/wav"); } else { - /* Stream the raw file */ DPRINTF(E_INFO, L_HTTPD, "Preparing to stream %s\n", mfi->path); - st->buf = (uint8_t *)malloc(STREAM_CHUNK_SIZE); - if (!st->buf) - { - DPRINTF(E_LOG, L_HTTPD, "Out of memory for raw streaming buffer\n"); + st = stream_new_raw(mfi, hreq, offset, end_offset, stream_chunk_raw_cb); + if (!st) + goto error; - evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); - - goto out_free_st; - } - - stream_cb = stream_chunk_raw_cb; - - st->fd = open(mfi->path, O_RDONLY); - if (st->fd < 0) - { - DPRINTF(E_LOG, L_HTTPD, "Could not open %s: %s\n", mfi->path, strerror(errno)); - - evhttp_send_error(req, HTTP_NOTFOUND, "Not Found"); - - goto out_cleanup; - } - - ret = stat(mfi->path, &sb); - if (ret < 0) - { - DPRINTF(E_LOG, L_HTTPD, "Could not stat() %s: %s\n", mfi->path, strerror(errno)); - - evhttp_send_error(req, HTTP_NOTFOUND, "Not Found"); - - goto out_cleanup; - } - st->size = sb.st_size; - - pos = lseek(st->fd, offset, SEEK_SET); - if (pos == (off_t) -1) - { - DPRINTF(E_LOG, L_HTTPD, "Could not seek into %s: %s\n", mfi->path, strerror(errno)); - - evhttp_send_error(req, HTTP_BADREQUEST, "Bad Request"); - - goto out_cleanup; - } - st->offset = offset; - st->end_offset = end_offset; - - /* Content-Type for video files is different than for audio files - * and overrides whatever may have been set previously, like - * application/x-dmap-tagged when we're speaking DAAP. - */ + // Content-Type for video files is different than for audio files and + // overrides whatever may have been set previously, like + // application/x-dmap-tagged when we're speaking DAAP. if (mfi->has_video) { - /* Front Row and others expect video/ */ + // Front Row and others expect video/ ret = snprintf(buf, sizeof(buf), "video/%s", mfi->type); if ((ret < 0) || (ret >= sizeof(buf))) DPRINTF(E_LOG, L_HTTPD, "Content-Type too large for buffer, dropping\n"); else { - evhttp_remove_header(output_headers, "Content-Type"); - evhttp_add_header(output_headers, "Content-Type", buf); + httpd_header_remove(hreq->out_headers, "Content-Type"); + httpd_header_add(hreq->out_headers, "Content-Type", buf); } } - /* If no Content-Type has been set and we're streaming audio, add a proper - * Content-Type for the file we're streaming. Remember DAAP streams audio - * with application/x-dmap-tagged as the Content-Type (ugh!). - */ - else if (!evhttp_find_header(output_headers, "Content-Type") && mfi->type) + // If no Content-Type has been set and we're streaming audio, add a proper + // Content-Type for the file we're streaming. Remember DAAP streams audio + // with application/x-dmap-tagged as the Content-Type (ugh!). + else if (!httpd_header_find(hreq->out_headers, "Content-Type") && mfi->type) { ret = snprintf(buf, sizeof(buf), "audio/%s", mfi->type); if ((ret < 0) || (ret >= sizeof(buf))) DPRINTF(E_LOG, L_HTTPD, "Content-Type too large for buffer, dropping\n"); else - evhttp_add_header(output_headers, "Content-Type", buf); + httpd_header_add(hreq->out_headers, "Content-Type", buf); } } - st->evbuf = evbuffer_new(); - if (!st->evbuf) - { - DPRINTF(E_LOG, L_HTTPD, "Could not allocate an evbuffer for streaming\n"); - - evhttp_clear_headers(output_headers); - evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); - - goto out_cleanup; - } - - ret = evbuffer_expand(st->evbuf, STREAM_CHUNK_SIZE); - if (ret != 0) - { - DPRINTF(E_LOG, L_HTTPD, "Could not expand evbuffer for streaming\n"); - - evhttp_clear_headers(output_headers); - evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); - - goto out_cleanup; - } - - st->ev = event_new(evbase_httpd, -1, EV_TIMEOUT, stream_cb, st); - evutil_timerclear(&tv); - if (!st->ev || (event_add(st->ev, &tv) < 0)) - { - DPRINTF(E_LOG, L_HTTPD, "Could not add one-shot event for streaming\n"); - - evhttp_clear_headers(output_headers); - evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); - - goto out_cleanup; - } - - st->id = mfi->id; - st->start_offset = offset; - st->stream_size = st->size; - st->req = req; - if ((offset == 0) && (end_offset == 0)) { - /* If we are not decoding, send the Content-Length. We don't do - * that if we are decoding because we can only guesstimate the - * size in this case and the error margin is unknown and variable. - */ + // If we are not decoding, send the Content-Length. We don't do that if we + // are decoding because we can only guesstimate the size in this case and + // the error margin is unknown and variable. if (!transcode) { ret = snprintf(buf, sizeof(buf), "%" PRIi64, (int64_t)st->size); if ((ret < 0) || (ret >= sizeof(buf))) DPRINTF(E_LOG, L_HTTPD, "Content-Length too large for buffer, dropping\n"); else - evhttp_add_header(output_headers, "Content-Length", buf); + httpd_header_add(hreq->out_headers, "Content-Length", buf); } - evhttp_send_reply_start(req, HTTP_OK, "OK"); + httpd_send_reply_start(hreq, HTTP_OK, "OK"); } else { - if (offset > 0) - st->stream_size -= offset; - if (end_offset > 0) - st->stream_size -= (st->size - end_offset); - DPRINTF(E_DBG, L_HTTPD, "Stream request with range %" PRIi64 "-%" PRIi64 "\n", offset, end_offset); ret = snprintf(buf, sizeof(buf), "bytes %" PRIi64 "-%" PRIi64 "/%" PRIi64, @@ -1304,21 +1047,21 @@ httpd_stream_file(struct evhttp_request *req, int id) if ((ret < 0) || (ret >= sizeof(buf))) DPRINTF(E_LOG, L_HTTPD, "Content-Range too large for buffer, dropping\n"); else - evhttp_add_header(output_headers, "Content-Range", buf); + httpd_header_add(hreq->out_headers, "Content-Range", buf); ret = snprintf(buf, sizeof(buf), "%" PRIi64, ((end_offset) ? end_offset + 1 : (int64_t)st->size) - offset); if ((ret < 0) || (ret >= sizeof(buf))) DPRINTF(E_LOG, L_HTTPD, "Content-Length too large for buffer, dropping\n"); else - evhttp_add_header(output_headers, "Content-Length", buf); + httpd_header_add(hreq->out_headers, "Content-Length", buf); - evhttp_send_reply_start(req, 206, "Partial Content"); + httpd_send_reply_start(hreq, 206, "Partial Content"); } #ifdef HAVE_POSIX_FADVISE if (!transcode) { - /* Hint the OS */ + // Hint the OS if ( (ret = posix_fadvise(st->fd, st->start_offset, st->stream_size, POSIX_FADV_WILLNEED)) != 0 || (ret = posix_fadvise(st->fd, st->start_offset, st->stream_size, POSIX_FADV_SEQUENTIAL)) != 0 || (ret = posix_fadvise(st->fd, st->start_offset, st->stream_size, POSIX_FADV_NOREUSE)) != 0 ) @@ -1326,28 +1069,15 @@ httpd_stream_file(struct evhttp_request *req, int id) } #endif - evcon = evhttp_request_get_connection(req); - - evhttp_connection_set_closecb(evcon, stream_fail_cb, st); + httpd_request_close_cb_set(hreq, stream_fail_cb, st); DPRINTF(E_INFO, L_HTTPD, "Kicking off streaming for %s\n", mfi->path); free_mfi(mfi, 0); - return; - out_cleanup: - if (st->evbuf) - evbuffer_free(st->evbuf); - if (st->xcode) - transcode_cleanup(&st->xcode); - if (st->buf) - free(st->buf); - if (st->fd > 0) - close(st->fd); - out_free_st: - free(st); - out_free_mfi: + error: + stream_free(st); free_mfi(mfi, 0); } @@ -1418,116 +1148,106 @@ httpd_gzip_deflate(struct evbuffer *in) return NULL; } + +// The httpd_send functions below can be called from a worker thread (with +// hreq->is_async) or directly from the httpd thread. In the former case, they +// will command sending from the httpd thread, since it is not safe to access +// the backend (evhttp) from a worker thread. hreq will be freed (again, +// possibly async) if the type is either _COMPLETE or _END. + void -httpd_send_reply(struct evhttp_request *req, int code, const char *reason, struct evbuffer *evbuf, enum httpd_send_flags flags) +httpd_send_reply(struct httpd_request *hreq, int code, const char *reason, enum httpd_send_flags flags) { struct evbuffer *gzbuf; - struct evkeyvalq *input_headers; - struct evkeyvalq *output_headers; + struct evbuffer *save; const char *param; int do_gzip; - if (!req) + if (!hreq->backend) return; - input_headers = evhttp_request_get_input_headers(req); - output_headers = evhttp_request_get_output_headers(req); - do_gzip = ( (!(flags & HTTPD_SEND_NO_GZIP)) && - evbuf && (evbuffer_get_length(evbuf) > 512) && - (param = evhttp_find_header(input_headers, "Accept-Encoding")) && + (evbuffer_get_length(hreq->out_body) > 512) && + (param = httpd_header_find(hreq->in_headers, "Accept-Encoding")) && (strstr(param, "gzip") || strstr(param, "*")) ); - if (allow_origin) - evhttp_add_header(output_headers, "Access-Control-Allow-Origin", allow_origin); + cors_headers_add(hreq, httpd_allow_origin); - if (do_gzip && (gzbuf = httpd_gzip_deflate(evbuf))) + if (do_gzip && (gzbuf = httpd_gzip_deflate(hreq->out_body))) { DPRINTF(E_DBG, L_HTTPD, "Gzipping response\n"); - evhttp_add_header(output_headers, "Content-Encoding", "gzip"); - evhttp_send_reply(req, code, reason, gzbuf); - evbuffer_free(gzbuf); + httpd_header_add(hreq->out_headers, "Content-Encoding", "gzip"); + save = hreq->out_body; + hreq->out_body = gzbuf; + evbuffer_free(save); + } - // Drain original buffer, as would be after evhttp_send_reply() - evbuffer_drain(evbuf, evbuffer_get_length(evbuf)); - } - else - { - evhttp_send_reply(req, code, reason, evbuf); - } + httpd_send(hreq, HTTPD_REPLY_COMPLETE, code, reason, NULL, NULL); +} + +void +httpd_send_reply_start(struct httpd_request *hreq, int code, const char *reason) +{ + cors_headers_add(hreq, httpd_allow_origin); + + httpd_send(hreq, HTTPD_REPLY_START, code, reason, NULL, NULL); +} + +void +httpd_send_reply_chunk(struct httpd_request *hreq, httpd_connection_chunkcb cb, void *arg) +{ + httpd_send(hreq, HTTPD_REPLY_CHUNK, 0, NULL, cb, arg); +} + +void +httpd_send_reply_end(struct httpd_request *hreq) +{ + httpd_send(hreq, HTTPD_REPLY_END, 0, NULL, NULL, NULL); } // This is a modified version of evhttp_send_error (credit libevent) void -httpd_send_error(struct evhttp_request* req, int error, const char* reason) +httpd_send_error(struct httpd_request *hreq, int error, const char *reason) { - struct evkeyvalq *output_headers; - struct evbuffer *evbuf; + evbuffer_drain(hreq->out_body, -1); + httpd_headers_clear(hreq->out_headers); - if (!allow_origin) - { - evhttp_send_error(req, error, reason); - return; - } + cors_headers_add(hreq, httpd_allow_origin); - output_headers = evhttp_request_get_output_headers(req); + httpd_header_add(hreq->out_headers, "Content-Type", "text/html"); + httpd_header_add(hreq->out_headers, "Connection", "close"); - evhttp_clear_headers(output_headers); + evbuffer_add_printf(hreq->out_body, ERR_PAGE, error, reason, reason); - evhttp_add_header(output_headers, "Access-Control-Allow-Origin", allow_origin); - evhttp_add_header(output_headers, "Content-Type", "text/html"); - evhttp_add_header(output_headers, "Connection", "close"); - - evbuf = evbuffer_new(); - if (!evbuf) - DPRINTF(E_LOG, L_HTTPD, "Could not allocate evbuffer for error page\n"); - else - evbuffer_add_printf(evbuf, ERR_PAGE, error, reason, reason); - - evhttp_send_reply(req, error, reason, evbuf); - - if (evbuf) - evbuffer_free(evbuf); + httpd_send(hreq, HTTPD_REPLY_COMPLETE, error, reason, NULL, NULL); } bool -httpd_admin_check_auth(struct evhttp_request *req) +httpd_admin_check_auth(struct httpd_request *hreq) { - struct evhttp_connection *evcon; - const char *addr; - uint16_t port; const char *passwd; int ret; - evcon = evhttp_request_get_connection(req); - if (!evcon) - { - DPRINTF(E_LOG, L_HTTPD, "Connection to client lost or missing\n"); - return false; - } - - httpd_peer_get(&addr, &port, evcon); - - if (net_peer_address_is_trusted(addr)) + if (net_peer_address_is_trusted(hreq->peer_address)) return true; passwd = cfg_getstr(cfg_getsec(cfg, "general"), "admin_password"); if (!passwd) { - DPRINTF(E_LOG, L_HTTPD, "Web interface request to '%s' denied: No password set in the config\n", evhttp_request_get_uri(req)); + DPRINTF(E_LOG, L_HTTPD, "Web interface request to '%s' denied: No password set in the config\n", hreq->uri); - httpd_send_error(req, 403, "Forbidden"); + httpd_send_error(hreq, HTTP_FORBIDDEN, "Forbidden"); return false; } DPRINTF(E_DBG, L_HTTPD, "Checking web interface authentication\n"); - ret = httpd_basic_auth(req, "admin", passwd, PACKAGE " web interface"); + ret = httpd_basic_auth(hreq, "admin", passwd, PACKAGE " web interface"); if (ret != 0) { - DPRINTF(E_LOG, L_HTTPD, "Web interface request to '%s' denied: Incorrect password\n", evhttp_request_get_uri(req)); + DPRINTF(E_LOG, L_HTTPD, "Web interface request to '%s' denied: Incorrect password\n", hreq->uri); // httpd_basic_auth has sent a reply return false; @@ -1539,18 +1259,15 @@ httpd_admin_check_auth(struct evhttp_request *req) } int -httpd_basic_auth(struct evhttp_request *req, const char *user, const char *passwd, const char *realm) +httpd_basic_auth(struct httpd_request *hreq, const char *user, const char *passwd, const char *realm) { - struct evbuffer *evbuf; - struct evkeyvalq *headers; char header[256]; const char *auth; char *authuser; char *authpwd; int ret; - headers = evhttp_request_get_input_headers(req); - auth = evhttp_find_header(headers, "Authorization"); + auth = httpd_header_find(hreq->in_headers, "Authorization"); if (!auth) { DPRINTF(E_DBG, L_HTTPD, "No Authorization header\n"); @@ -1614,37 +1331,59 @@ httpd_basic_auth(struct evhttp_request *req, const char *user, const char *passw ret = snprintf(header, sizeof(header), "Basic realm=\"%s\"", realm); if ((ret < 0) || (ret >= sizeof(header))) { - httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); + httpd_send_error(hreq, HTTP_SERVUNAVAIL, "Internal Server Error"); return -1; } - evbuf = evbuffer_new(); - if (!evbuf) - { - httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); - return -1; - } + httpd_header_add(hreq->out_headers, "WWW-Authenticate", header); - headers = evhttp_request_get_output_headers(req); - evhttp_add_header(headers, "WWW-Authenticate", header); + evbuffer_add_printf(hreq->out_body, ERR_PAGE, HTTP_UNAUTHORIZED, "Unauthorized", "Authorization required"); - evbuffer_add(evbuf, http_reply_401, strlen(http_reply_401)); - - httpd_send_reply(req, 401, "Unauthorized", evbuf, HTTPD_SEND_NO_GZIP); - - evbuffer_free(evbuf); + httpd_send_reply(hreq, HTTP_UNAUTHORIZED, "Unauthorized", HTTPD_SEND_NO_GZIP); return -1; } -void -httpd_peer_get(const char **address, ev_uint16_t *port, struct evhttp_connection *evcon) +static int +bind_test(short unsigned port) { -#ifdef HAVE_EVHTTP_CONNECTION_GET_PEER_CONST_CHAR - evhttp_connection_get_peer(evcon, address, port); -#else - evhttp_connection_get_peer(evcon, (char **)address, port); -#endif + int fd; + + fd = net_bind(&port, SOCK_STREAM, "httpd init"); + if (fd < 0) + return -1; + + close(fd); + return 0; +} + +static void +thread_init_cb(struct evthr *thr, void *shared) +{ + struct event_base *evbase; + httpd_server *server; + + thread_setname(pthread_self(), "httpd"); + + CHECK_ERR(L_HTTPD, db_perthread_init()); + CHECK_NULL(L_HTTPD, evbase = evthr_get_base(thr)); + CHECK_NULL(L_HTTPD, server = httpd_server_new(evbase, httpd_port, request_cb, NULL)); + + // For CORS headers + httpd_server_allow_origin_set(server, httpd_allow_origin); + + evthr_set_aux(thr, server); +} + +static void +thread_exit_cb(struct evthr *thr, void *shared) +{ + httpd_server *server; + + server = evthr_get_aux(thr); + httpd_server_free(server); + + db_perthread_deinit(); } /* Thread: main */ @@ -1654,83 +1393,45 @@ httpd_init(const char *webroot) struct stat sb; int ret; - httpd_exit = 0; - DPRINTF(E_DBG, L_HTTPD, "Starting web server with root directory '%s'\n", webroot); ret = stat(webroot, &sb); if (ret < 0) { DPRINTF(E_LOG, L_HTTPD, "Could not stat() web root directory '%s': %s\n", webroot, strerror(errno)); - return -1; } if (!S_ISDIR(sb.st_mode)) { DPRINTF(E_LOG, L_HTTPD, "Web root directory '%s' is not a directory\n", webroot); - return -1; } if (!realpath(webroot, webroot_directory)) { DPRINTF(E_LOG, L_HTTPD, "Web root directory '%s' could not be dereferenced: %s\n", webroot, strerror(errno)); - return -1; } - evbase_httpd = event_base_new(); - if (!evbase_httpd) - { - DPRINTF(E_FATAL, L_HTTPD, "Could not create an event base\n"); + // Read config + httpd_port = cfg_getint(cfg_getsec(cfg, "library"), "port"); + httpd_allow_origin = cfg_getstr(cfg_getsec(cfg, "general"), "allow_origin"); + if (strlen(httpd_allow_origin) == 0) + httpd_allow_origin = NULL; + // Test that the port is free. We do it here because we can make a nicer exit + // than we can in thread_init_cb(), where the actual binding takes place. + ret = bind_test(httpd_port); + if (ret < 0) + { + DPRINTF(E_FATAL, L_HTTPD, "Could not create HTTP server on port %d (server already running?)\n", httpd_port); return -1; - } + } - ret = rsp_init(); + // Prepare modules, e.g. httpd_daap + ret = modules_init(); if (ret < 0) { - DPRINTF(E_FATAL, L_HTTPD, "RSP protocol init failed\n"); - - goto rsp_fail; - } - - ret = daap_init(); - if (ret < 0) - { - DPRINTF(E_FATAL, L_HTTPD, "DAAP protocol init failed\n"); - - goto daap_fail; - } - - ret = dacp_init(); - if (ret < 0) - { - DPRINTF(E_FATAL, L_HTTPD, "DACP protocol init failed\n"); - - goto dacp_fail; - } - - ret = jsonapi_init(); - if (ret < 0) - { - DPRINTF(E_FATAL, L_HTTPD, "JSON api init failed\n"); - - goto jsonapi_fail; - } - - ret = artworkapi_init(); - if (ret < 0) - { - DPRINTF(E_FATAL, L_HTTPD, "Artwork init failed\n"); - - goto artworkapi_fail; - } - - ret = oauth_init(); - if (ret < 0) - { - DPRINTF(E_FATAL, L_HTTPD, "OAuth init failed\n"); - - goto oauth_fail; + DPRINTF(E_FATAL, L_HTTPD, "Modules init failed\n"); + goto error; } #ifdef HAVE_LIBWEBSOCKETS @@ -1738,119 +1439,28 @@ httpd_init(const char *webroot) if (ret < 0) { DPRINTF(E_FATAL, L_HTTPD, "Websocket init failed\n"); - - goto websocket_fail; + goto error; } #endif - streaming_init(); - -#ifdef HAVE_EVENTFD - exit_efd = eventfd(0, EFD_CLOEXEC); - if (exit_efd < 0) + httpd_threadpool = evthr_pool_wexit_new(THREADPOOL_NTHREADS, thread_init_cb, thread_exit_cb, NULL); + if (!httpd_threadpool) { - DPRINTF(E_FATAL, L_HTTPD, "Could not create eventfd: %s\n", strerror(errno)); - - goto pipe_fail; + DPRINTF(E_LOG, L_HTTPD, "Could not create httpd thread pool\n"); + goto error; } - exitev = event_new(evbase_httpd, exit_efd, EV_READ, exit_cb, NULL); -#else -# ifdef HAVE_PIPE2 - ret = pipe2(exit_pipe, O_CLOEXEC); -# else - ret = pipe(exit_pipe); -# endif + ret = evthr_pool_start(httpd_threadpool); if (ret < 0) { - DPRINTF(E_FATAL, L_HTTPD, "Could not create pipe: %s\n", strerror(errno)); - - goto pipe_fail; + DPRINTF(E_LOG, L_HTTPD, "Could not spawn worker threads\n"); + goto error; } - exitev = event_new(evbase_httpd, exit_pipe[0], EV_READ, exit_cb, NULL); -#endif /* HAVE_EVENTFD */ - if (!exitev) - { - DPRINTF(E_FATAL, L_HTTPD, "Could not create exit event\n"); - - goto exitev_fail; - } - event_add(exitev, NULL); - - evhttpd = evhttp_new(evbase_httpd); - if (!evhttpd) - { - DPRINTF(E_FATAL, L_HTTPD, "Could not create HTTP server\n"); - - goto evhttpd_fail; - } - - // For CORS headers - allow_origin = cfg_getstr(cfg_getsec(cfg, "general"), "allow_origin"); - if (allow_origin) - { - if (strlen(allow_origin) != 0) - evhttp_set_allowed_methods(evhttpd, EVHTTP_REQ_GET | EVHTTP_REQ_POST | EVHTTP_REQ_PUT | EVHTTP_REQ_DELETE | EVHTTP_REQ_HEAD | EVHTTP_REQ_OPTIONS); - else - allow_origin = NULL; - } - - httpd_port = cfg_getint(cfg_getsec(cfg, "library"), "port"); - - ret = net_evhttp_bind(evhttpd, httpd_port, "httpd"); - if (ret < 0) - { - DPRINTF(E_FATAL, L_HTTPD, "Could not bind to port %d (server already running?)\n", httpd_port); - goto bind_fail; - } - - evhttp_set_gencb(evhttpd, httpd_gen_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; - } - - thread_setname(tid_httpd, "httpd"); - return 0; - thread_fail: - bind_fail: - evhttp_free(evhttpd); - evhttpd_fail: - event_free(exitev); - exitev_fail: -#ifdef HAVE_EVENTFD - close(exit_efd); -#else - close(exit_pipe[0]); - close(exit_pipe[1]); -#endif - pipe_fail: - streaming_deinit(); -#ifdef HAVE_LIBWEBSOCKETS - websocket_deinit(); - websocket_fail: -#endif - oauth_deinit(); - oauth_fail: - artworkapi_deinit(); - artworkapi_fail: - jsonapi_deinit(); - jsonapi_fail: - dacp_deinit(); - dacp_fail: - daap_deinit(); - daap_fail: - rsp_deinit(); - rsp_fail: - event_base_free(evbase_httpd); - + error: + httpd_deinit(); return -1; } @@ -1858,53 +1468,13 @@ httpd_init(const char *webroot) void httpd_deinit(void) { - int ret; + // Give modules a chance to hang up connections nicely + modules_deinit(); -#ifdef HAVE_EVENTFD - ret = eventfd_write(exit_efd, 1); - if (ret < 0) - { - DPRINTF(E_FATAL, L_HTTPD, "Could not send exit event: %s\n", strerror(errno)); - - return; - } -#else - int dummy = 42; - - 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; - } -#endif - - ret = pthread_join(tid_httpd, NULL); - if (ret != 0) - { - DPRINTF(E_FATAL, L_HTTPD, "Could not join HTTPd thread: %s\n", strerror(errno)); - - return; - } - - streaming_deinit(); #ifdef HAVE_LIBWEBSOCKETS websocket_deinit(); #endif - oauth_deinit(); - jsonapi_deinit(); - rsp_deinit(); - dacp_deinit(); - daap_deinit(); -#ifdef HAVE_EVENTFD - close(exit_efd); -#else - close(exit_pipe[0]); - close(exit_pipe[1]); -#endif - event_free(exitev); - evhttp_free(evhttpd); - event_base_free(evbase_httpd); + evthr_pool_stop(httpd_threadpool); + evthr_pool_free(httpd_threadpool); } diff --git a/src/httpd.h b/src/httpd.h index 1dd6e825..8926f707 100644 --- a/src/httpd.h +++ b/src/httpd.h @@ -2,112 +2,7 @@ #ifndef __HTTPD_H__ #define __HTTPD_H__ -#include -#include -#include -#include #include -#include - -enum httpd_send_flags -{ - HTTPD_SEND_NO_GZIP = (1 << 0), -}; - -/* - * Contains a parsed version of the URI httpd got. The URI may have been - * complete: - * scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment] - * or relative: - * [/path][?query][#fragment] - * - * We are interested in the path and the query, so they are disassembled to - * path_parts and ev_query. If the request is http://x:3689/foo/bar?key1=val1, - * then part_parts[0] is "foo", [1] is "bar" and the rest is null. - * - * Each path_part is an allocated URI decoded string. - */ -struct httpd_uri_parsed -{ - const char *uri; - struct evhttp_uri *ev_uri; - struct evkeyvalq ev_query; - char *uri_decoded; - char *path; - char *path_parts[31]; -}; - -/* - * A collection of pointers to request data that the reply handlers may need. - * Also has the function pointer to the reply handler and a pointer to a reply - * evbuffer. - */ -struct httpd_request { - // User-agent (if available) - const char *user_agent; - // The parsed request URI given to us by httpd_uri_parse - struct httpd_uri_parsed *uri_parsed; - // Shortcut to &uri_parsed->ev_query - struct evkeyvalq *query; - // http request struct (if available) - struct evhttp_request *req; - // Source IP address (ipv4 or ipv6) and port of the request (if available) - const char *peer_address; - unsigned short peer_port; - // A pointer to extra data that the module handling the request might need - void *extra_data; - - // Reply evbuffer - struct evbuffer *reply; - - // A pointer to the handler that will process the request - int (*handler)(struct httpd_request *hreq); -}; - -/* - * Maps a regex of the request path to a handler of the request - */ -struct httpd_uri_map -{ - int method; - char *regexp; - int (*handler)(struct httpd_request *hreq); - regex_t preg; -}; - -/* - * Helper to free the parsed uri struct - */ -void -httpd_uri_free(struct httpd_uri_parsed *parsed); - -/* - * Parse an URI into the struct - */ -struct httpd_uri_parsed * -httpd_uri_parse(const char *uri); - -/* - * Parse a request into the httpd_request struct. It can later be freed with - * free(), unless the module has allocated something to *extra_data. Note that - * the pointers in the returned struct are only valid as long as the inputs are - * still valid. If req is not null, then we will find the user-agent from the - * request headers, except if provided as an argument to this function. - */ -struct httpd_request * -httpd_request_parse(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed, const char *user_agent, struct httpd_uri_map *uri_map); - -void -httpd_stream_file(struct evhttp_request *req, int id); - -bool -httpd_request_not_modified_since(struct evhttp_request *req, time_t mtime); - -bool -httpd_request_etag_matches(struct evhttp_request *req, const char *etag); - -void -httpd_response_not_cachable(struct evhttp_request *req); /* * Gzips an evbuffer @@ -118,52 +13,6 @@ httpd_response_not_cachable(struct evhttp_request *req); struct evbuffer * httpd_gzip_deflate(struct evbuffer *in); -/* - * This wrapper around evhttp_send_reply should be used whenever a request may - * come from a browser. It will automatically gzip if feasible, but the caller - * may direct it not to. It will set CORS headers as appropriate. Should be - * thread safe. - * - * @in req The evhttp request struct - * @in code HTTP code, e.g. 200 - * @in reason A brief explanation of the error - if NULL the standard meaning - of the error code will be used - * @in evbuf Data for the response body - * @in flags See flags above - */ -void -httpd_send_reply(struct evhttp_request *req, int code, const char *reason, struct evbuffer *evbuf, enum httpd_send_flags flags); - -/* - * This is a substitute for evhttp_send_error that should be used whenever an - * error may be returned to a browser. It will set CORS headers as appropriate, - * which is not possible with evhttp_send_error, because it clears the headers. - * Should be thread safe. - * - * @in req The evhttp request struct - * @in error HTTP code, e.g. 200 - * @in reason A brief explanation of the error - if NULL the standard meaning - of the error code will be used - */ -void -httpd_send_error(struct evhttp_request *req, int error, const char *reason); - -/* - * Redirects to the given path - */ -void -httpd_redirect_to(struct evhttp_request *req, const char *path); - - -bool -httpd_admin_check_auth(struct evhttp_request *req); - -int -httpd_basic_auth(struct evhttp_request *req, const char *user, const char *passwd, const char *realm); - -void -httpd_peer_get(const char **address, ev_uint16_t *port, struct evhttp_connection *evcon); - int httpd_init(const char *webroot); diff --git a/src/httpd_artworkapi.c b/src/httpd_artworkapi.c index 4414d90b..48ed9161 100644 --- a/src/httpd_artworkapi.c +++ b/src/httpd_artworkapi.c @@ -20,12 +20,11 @@ # include #endif -#include #include #include #include -#include "httpd_artworkapi.h" +#include "httpd_internal.h" #include "logger.h" #include "misc.h" #include "player.h" @@ -40,20 +39,20 @@ request_process(struct httpd_request *hreq, uint32_t *max_w, uint32_t *max_h) *max_w = 0; *max_h = 0; - param = evhttp_find_header(hreq->query, "maxwidth"); + param = httpd_query_value_find(hreq->query, "maxwidth"); if (param) { ret = safe_atou32(param, max_w); if (ret < 0) - DPRINTF(E_LOG, L_WEB, "Invalid width in request: '%s'\n", hreq->uri_parsed->uri); + DPRINTF(E_LOG, L_WEB, "Invalid width in request: '%s'\n", hreq->uri); } - param = evhttp_find_header(hreq->query, "maxheight"); + param = httpd_query_value_find(hreq->query, "maxheight"); if (param) { ret = safe_atou32(param, max_h); if (ret < 0) - DPRINTF(E_LOG, L_WEB, "Invalid height in request: '%s'\n", hreq->uri_parsed->uri); + DPRINTF(E_LOG, L_WEB, "Invalid height in request: '%s'\n", hreq->uri); } return 0; @@ -62,14 +61,10 @@ request_process(struct httpd_request *hreq, uint32_t *max_w, uint32_t *max_h) static int response_process(struct httpd_request *hreq, int format) { - struct evkeyvalq *headers; - - headers = evhttp_request_get_output_headers(hreq->req); - if (format == ART_FMT_PNG) - evhttp_add_header(headers, "Content-Type", "image/png"); + httpd_header_add(hreq->out_headers, "Content-Type", "image/png"); else if (format == ART_FMT_JPEG) - evhttp_add_header(headers, "Content-Type", "image/jpeg"); + httpd_header_add(hreq->out_headers, "Content-Type", "image/jpeg"); else return HTTP_NOCONTENT; @@ -92,7 +87,7 @@ artworkapi_reply_nowplaying(struct httpd_request *hreq) if (ret != 0) return HTTP_NOTFOUND; - ret = artwork_get_item(hreq->reply, id, max_w, max_h, 0); + ret = artwork_get_item(hreq->out_body, id, max_w, max_h, 0); return response_process(hreq, ret); } @@ -109,11 +104,11 @@ artworkapi_reply_item(struct httpd_request *hreq) if (ret != 0) return ret; - ret = safe_atou32(hreq->uri_parsed->path_parts[2], &id); + ret = safe_atou32(hreq->path_parts[2], &id); if (ret != 0) return HTTP_BADREQUEST; - ret = artwork_get_item(hreq->reply, id, max_w, max_h, 0); + ret = artwork_get_item(hreq->out_body, id, max_w, max_h, 0); return response_process(hreq, ret); } @@ -130,111 +125,73 @@ artworkapi_reply_group(struct httpd_request *hreq) if (ret != 0) return ret; - ret = safe_atou32(hreq->uri_parsed->path_parts[2], &id); + ret = safe_atou32(hreq->path_parts[2], &id); if (ret != 0) return HTTP_BADREQUEST; - ret = artwork_get_group(hreq->reply, id, max_w, max_h, 0); + ret = artwork_get_group(hreq->out_body, id, max_w, max_h, 0); return response_process(hreq, ret); } static struct httpd_uri_map artworkapi_handlers[] = { - { EVHTTP_REQ_GET, "^/artwork/nowplaying$", artworkapi_reply_nowplaying }, - { EVHTTP_REQ_GET, "^/artwork/item/[[:digit:]]+$", artworkapi_reply_item }, - { EVHTTP_REQ_GET, "^/artwork/group/[[:digit:]]+$", artworkapi_reply_group }, + { HTTPD_METHOD_GET, "^/artwork/nowplaying$", artworkapi_reply_nowplaying }, + { HTTPD_METHOD_GET, "^/artwork/item/[[:digit:]]+$", artworkapi_reply_item }, + { HTTPD_METHOD_GET, "^/artwork/group/[[:digit:]]+$", artworkapi_reply_group }, { 0, NULL, NULL } }; /* ------------------------------- API --------------------------------- */ -void -artworkapi_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed) + +static void +artworkapi_request(struct httpd_request *hreq) { - struct httpd_request *hreq; int status_code; - DPRINTF(E_DBG, L_WEB, "Artwork api request: '%s'\n", uri_parsed->uri); - - if (!httpd_admin_check_auth(req)) + if (!httpd_admin_check_auth(hreq)) return; - hreq = httpd_request_parse(req, uri_parsed, NULL, artworkapi_handlers); - if (!hreq) + if (!hreq->handler) { - DPRINTF(E_LOG, L_WEB, "Unrecognized path '%s' in artwork api request: '%s'\n", uri_parsed->path, uri_parsed->uri); + DPRINTF(E_LOG, L_WEB, "Unrecognized path in artwork api request: '%s'\n", hreq->uri); - httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); + httpd_send_error(hreq, HTTP_BADREQUEST, "Bad Request"); return; } - CHECK_NULL(L_WEB, hreq->reply = evbuffer_new()); - status_code = hreq->handler(hreq); switch (status_code) { case HTTP_OK: /* 200 OK */ - httpd_send_reply(req, status_code, "OK", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, status_code, "OK", HTTPD_SEND_NO_GZIP); break; case HTTP_NOCONTENT: /* 204 No Content */ - httpd_send_reply(req, status_code, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, status_code, "No Content", HTTPD_SEND_NO_GZIP); break; case HTTP_NOTMODIFIED: /* 304 Not Modified */ - httpd_send_reply(req, HTTP_NOTMODIFIED, NULL, NULL, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_NOTMODIFIED, NULL, HTTPD_SEND_NO_GZIP); break; case HTTP_BADREQUEST: /* 400 Bad Request */ - httpd_send_error(req, status_code, "Bad Request"); + httpd_send_error(hreq, status_code, "Bad Request"); break; case HTTP_NOTFOUND: /* 404 Not Found */ - httpd_send_error(req, status_code, "Not Found"); + httpd_send_error(hreq, status_code, "Not Found"); break; case HTTP_INTERNAL: /* 500 Internal Server Error */ default: - httpd_send_error(req, HTTP_INTERNAL, "Internal Server Error"); + httpd_send_error(hreq, HTTP_INTERNAL, "Internal Server Error"); } - - evbuffer_free(hreq->reply); - free(hreq); } -int -artworkapi_is_request(const char *path) +struct httpd_module httpd_artworkapi = { - if (strncmp(path, "/artwork/", strlen("/artwork/")) == 0) - return 1; - - return 0; -} - -int -artworkapi_init(void) -{ - char buf[64]; - int i; - int ret; - - for (i = 0; artworkapi_handlers[i].handler; i++) - { - ret = regcomp(&artworkapi_handlers[i].preg, artworkapi_handlers[i].regexp, REG_EXTENDED | REG_NOSUB); - if (ret != 0) - { - regerror(ret, &artworkapi_handlers[i].preg, buf, sizeof(buf)); - - DPRINTF(E_FATAL, L_WEB, "artwork api init failed; regexp error: %s\n", buf); - return -1; - } - } - - return 0; -} - -void -artworkapi_deinit(void) -{ - int i; - - for (i = 0; artworkapi_handlers[i].handler; i++) - regfree(&artworkapi_handlers[i].preg); -} + .name = "Artwork API", + .type = MODULE_ARTWORKAPI, + .logdomain = L_WEB, + .subpaths = { "/artwork/", NULL }, + .handlers = artworkapi_handlers, + .request = artworkapi_request, +}; diff --git a/src/httpd_artworkapi.h b/src/httpd_artworkapi.h deleted file mode 100644 index 7c7163c5..00000000 --- a/src/httpd_artworkapi.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef __HTTPD_ARTWORK_H__ -#define __HTTPD_ARTWORK_H__ - -#include "httpd.h" - -int -artworkapi_init(void); - -void -artworkapi_deinit(void); - -void -artworkapi_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed); - -int -artworkapi_is_request(const char *path); - -#endif diff --git a/src/httpd_daap.c b/src/httpd_daap.c index e87d1f45..5fed4db9 100644 --- a/src/httpd_daap.c +++ b/src/httpd_daap.c @@ -42,8 +42,8 @@ #include #include -#include +#include "httpd_internal.h" #include "httpd_daap.h" #include "logger.h" #include "db.h" @@ -55,9 +55,6 @@ #include "cache.h" -/* httpd event base, from httpd.c */ -extern struct event_base *evbase_httpd; - /* Max number of sessions and session timeout * Many clients (including iTunes) don't seem to respect the timeout capability * that we announce, and just keep using the same session. Therefore we take a @@ -99,7 +96,7 @@ struct daap_session { }; struct daap_update_request { - struct evhttp_request *req; + struct httpd_request *hreq; /* Refresh tiemout */ struct event *timeout; @@ -246,12 +243,14 @@ daap_session_add(bool is_remote, int request_session_id) return s; } - /* ---------------------- UPDATE REQUESTS HANDLERS -------------------------- */ static void update_free(struct daap_update_request *ur) { + if (!ur) + return; + if (ur->timeout) event_free(ur->timeout); @@ -285,45 +284,28 @@ update_remove(struct daap_update_request *ur) static void update_refresh_cb(int fd, short event, void *arg) { - struct daap_update_request *ur; - struct evhttp_connection *evcon; - struct evbuffer *reply; - - ur = (struct daap_update_request *)arg; - - CHECK_NULL(L_DAAP, reply = evbuffer_new()); - CHECK_ERR(L_DAAP, evbuffer_expand(reply, 32)); + struct daap_update_request *ur = arg; + struct httpd_request *hreq = ur->hreq; current_rev++; /* Send back current revision */ - dmap_add_container(reply, "mupd", 24); - dmap_add_int(reply, "mstt", 200); /* 12 */ - dmap_add_int(reply, "musr", current_rev); /* 12 */ + dmap_add_container(hreq->out_body, "mupd", 24); + dmap_add_int(hreq->out_body, "mstt", 200); /* 12 */ + dmap_add_int(hreq->out_body, "musr", current_rev); /* 12 */ - evcon = evhttp_request_get_connection(ur->req); - evhttp_connection_set_closecb(evcon, NULL, NULL); - - httpd_send_reply(ur->req, HTTP_OK, "OK", reply, 0); + httpd_send_reply(hreq, HTTP_OK, "OK", 0); update_remove(ur); } static void -update_fail_cb(struct evhttp_connection *evcon, void *arg) +update_fail_cb(void *arg) { - struct evhttp_connection *evc; - struct daap_update_request *ur; - - ur = (struct daap_update_request *)arg; + struct daap_update_request *ur = arg; DPRINTF(E_DBG, L_DAAP, "Update request: client closed connection\n"); - evc = evhttp_request_get_connection(ur->req); - if (evc) - evhttp_connection_set_closecb(evc, NULL, NULL); - - evhttp_request_free(ur->req); update_remove(ur); } @@ -513,7 +495,7 @@ query_params_set(struct query_params *qp, int *sort_headers, struct httpd_reques memset(qp, 0, sizeof(struct query_params)); - param = evhttp_find_header(hreq->query, "index"); + param = httpd_query_value_find(hreq->query, "index"); if (param) { if (param[0] == '-') /* -n, last n entries */ @@ -559,7 +541,7 @@ query_params_set(struct query_params *qp, int *sort_headers, struct httpd_reques qp->idx_type = I_SUB; qp->sort = S_NONE; - param = evhttp_find_header(hreq->query, "sort"); + param = httpd_query_value_find(hreq->query, "sort"); if (param) { if (strcmp(param, "name") == 0) @@ -580,7 +562,7 @@ query_params_set(struct query_params *qp, int *sort_headers, struct httpd_reques if (sort_headers) { *sort_headers = 0; - param = evhttp_find_header(hreq->query, "include-sort-headers"); + param = httpd_query_value_find(hreq->query, "include-sort-headers"); if (param && (strcmp(param, "1") == 0)) { *sort_headers = 1; @@ -588,9 +570,9 @@ query_params_set(struct query_params *qp, int *sort_headers, struct httpd_reques } } - param = evhttp_find_header(hreq->query, "query"); + param = httpd_query_value_find(hreq->query, "query"); if (!param) - param = evhttp_find_header(hreq->query, "filter"); + param = httpd_query_value_find(hreq->query, "filter"); if (param) { @@ -675,26 +657,26 @@ daap_reply_send(struct httpd_request *hreq, enum daap_reply_result result) switch (result) { case DAAP_REPLY_LOGOUT: - httpd_send_reply(hreq->req, 204, "Logout Successful", hreq->reply, 0); + httpd_send_reply(hreq, HTTP_NOCONTENT, "Logout Successful", 0); break; case DAAP_REPLY_NO_CONTENT: - httpd_send_reply(hreq->req, HTTP_NOCONTENT, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_NOCONTENT, "No Content", HTTPD_SEND_NO_GZIP); break; case DAAP_REPLY_OK: - httpd_send_reply(hreq->req, HTTP_OK, "OK", hreq->reply, 0); + httpd_send_reply(hreq, HTTP_OK, "OK", 0); break; case DAAP_REPLY_OK_NO_GZIP: case DAAP_REPLY_ERROR: - httpd_send_reply(hreq->req, HTTP_OK, "OK", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_OK, "OK", HTTPD_SEND_NO_GZIP); break; case DAAP_REPLY_FORBIDDEN: - httpd_send_error(hreq->req, 403, "Forbidden"); + httpd_send_error(hreq, HTTP_FORBIDDEN, "Forbidden"); break; case DAAP_REPLY_BAD_REQUEST: - httpd_send_error(hreq->req, HTTP_BADREQUEST, "Bad Request"); + httpd_send_error(hreq, HTTP_BADREQUEST, "Bad Request"); break; case DAAP_REPLY_SERVUNAVAIL: - httpd_send_error(hreq->req, HTTP_SERVUNAVAIL, "Internal Server Error"); + httpd_send_error(hreq, HTTP_SERVUNAVAIL, "Internal Server Error"); break; case DAAP_REPLY_NO_CONNECTION: case DAAP_REPLY_NONE: @@ -718,17 +700,17 @@ daap_request_authorize(struct httpd_request *hreq) // with httpd_basic_auth() if a library password is set. Remote clients will // also call /login, but they should not get a httpd_basic_auth(), instead // daap_reply_login() will take care of auth. - if (session->is_remote && (strcmp(hreq->uri_parsed->path, "/login") == 0)) + if (session->is_remote && (strcmp(hreq->path, "/login") == 0)) return 0; - param = evhttp_find_header(hreq->query, "session-id"); + param = httpd_query_value_find(hreq->query, "session-id"); if (param) { if (session->id == 0) { - DPRINTF(E_LOG, L_DAAP, "Unauthorized request from '%s', DAAP session not found: '%s'\n", hreq->peer_address, hreq->uri_parsed->uri); + DPRINTF(E_LOG, L_DAAP, "Unauthorized request from '%s', DAAP session not found: '%s'\n", hreq->peer_address, hreq->uri); - httpd_send_error(hreq->req, 401, "Unauthorized"); + httpd_send_error(hreq, HTTP_UNAUTHORIZED, "Unauthorized");; return -1; } @@ -741,16 +723,16 @@ daap_request_authorize(struct httpd_request *hreq) return 0; // If no valid session then we may need to authenticate - if ((strcmp(hreq->uri_parsed->path, "/server-info") == 0) - || (strcmp(hreq->uri_parsed->path, "/logout") == 0) - || (strcmp(hreq->uri_parsed->path, "/content-codes") == 0) - || (strncmp(hreq->uri_parsed->path, "/databases/1/items/", strlen("/databases/1/items/")) == 0)) + if ((strcmp(hreq->path, "/server-info") == 0) + || (strcmp(hreq->path, "/logout") == 0) + || (strcmp(hreq->path, "/content-codes") == 0) + || (strncmp(hreq->path, "/databases/1/items/", strlen("/databases/1/items/")) == 0)) return 0; // No authentication DPRINTF(E_DBG, L_DAAP, "Checking authentication for library\n"); // We don't care about the username - ret = httpd_basic_auth(hreq->req, NULL, passwd, cfg_getstr(cfg_getsec(cfg, "library"), "name")); + ret = httpd_basic_auth(hreq, NULL, passwd, cfg_getstr(cfg_getsec(cfg, "library"), "name")); if (ret != 0) { DPRINTF(E_LOG, L_DAAP, "Unsuccessful library authorization attempt from '%s'\n", hreq->peer_address); @@ -764,13 +746,12 @@ daap_request_authorize(struct httpd_request *hreq) /* --------------------------- REPLY HANDLERS ------------------------------- */ /* Note that some handlers can be called without a connection (needed for */ /* cache regeneration), while others cannot. Those that cannot should check */ -/* that hreq->req is not a null pointer. */ +/* that hreq->backend is not null. */ static enum daap_reply_result daap_reply_server_info(struct httpd_request *hreq) { struct evbuffer *content; - struct evkeyvalq *headers; char *name; char *passwd; const char *clientver; @@ -778,7 +759,7 @@ daap_reply_server_info(struct httpd_request *hreq) int mpro; int apro; - if (!hreq->req) + if (!hreq->backend) { DPRINTF(E_LOG, L_DAAP, "Bug! daap_reply_server_info() cannot be called without an actual connection\n"); return DAAP_REPLY_NO_CONNECTION; @@ -793,8 +774,7 @@ daap_reply_server_info(struct httpd_request *hreq) mpro = 2 << 16 | 10; apro = 3 << 16 | 12; - headers = evhttp_request_get_input_headers(hreq->req); - if (headers && (clientver = evhttp_find_header(headers, "Client-DAAP-Version"))) + if (hreq->in_headers && (clientver = httpd_header_find(hreq->in_headers, "Client-DAAP-Version"))) { if (strcmp(clientver, "1.0") == 0) { @@ -864,9 +844,9 @@ daap_reply_server_info(struct httpd_request *hreq) // Create container len = evbuffer_get_length(content); - dmap_add_container(hreq->reply, "msrv", len); + dmap_add_container(hreq->out_body, "msrv", len); - CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->reply, content)); + CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->out_body, content)); evbuffer_free(content); @@ -887,19 +867,19 @@ daap_reply_content_codes(struct httpd_request *hreq) for (i = 0; i < nfields; i++) len += 8 + 12 + 10 + 8 + strlen(dmap_fields[i].desc); - CHECK_ERR(L_DAAP, evbuffer_expand(hreq->reply, len + 8)); + CHECK_ERR(L_DAAP, evbuffer_expand(hreq->out_body, len + 8)); - dmap_add_container(hreq->reply, "mccr", len); - dmap_add_int(hreq->reply, "mstt", 200); + dmap_add_container(hreq->out_body, "mccr", len); + dmap_add_int(hreq->out_body, "mstt", 200); for (i = 0; i < nfields; i++) { len = 12 + 10 + 8 + strlen(dmap_fields[i].desc); - dmap_add_container(hreq->reply, "mdcl", len); - dmap_add_string(hreq->reply, "mcnm", dmap_fields[i].tag); /* 12 */ - dmap_add_string(hreq->reply, "mcna", dmap_fields[i].desc); /* 8 + strlen(desc) */ - dmap_add_short(hreq->reply, "mcty", dmap_fields[i].type); /* 10 */ + dmap_add_container(hreq->out_body, "mdcl", len); + dmap_add_string(hreq->out_body, "mcnm", dmap_fields[i].tag); /* 12 */ + dmap_add_string(hreq->out_body, "mcna", dmap_fields[i].desc); /* 8 + strlen(desc) */ + dmap_add_short(hreq->out_body, "mcty", dmap_fields[i].type); /* 10 */ } return DAAP_REPLY_OK; @@ -915,9 +895,9 @@ daap_reply_login(struct httpd_request *hreq) int request_session_id; int ret; - CHECK_ERR(L_DAAP, evbuffer_expand(hreq->reply, 32)); + CHECK_ERR(L_DAAP, evbuffer_expand(hreq->out_body, 32)); - param = evhttp_find_header(hreq->query, "pairing-guid"); + param = httpd_query_value_find(hreq->query, "pairing-guid"); if (param && !net_peer_address_is_trusted(hreq->peer_address)) { if (strlen(param) < 3) @@ -948,7 +928,7 @@ daap_reply_login(struct httpd_request *hreq) DPRINTF(E_INFO, L_DAAP, "Client (unknown user-agent) logging in from %s\n", hreq->peer_address); } - param = evhttp_find_header(hreq->query, "request-session-id"); + param = httpd_query_value_find(hreq->query, "request-session-id"); if (param) { ret = safe_atoi32(param, &request_session_id); @@ -964,13 +944,13 @@ daap_reply_login(struct httpd_request *hreq) session = daap_session_add(adhoc->is_remote, request_session_id); if (!session) { - dmap_error_make(hreq->reply, "mlog", "Could not start session"); + dmap_error_make(hreq->out_body, "mlog", "Could not start session"); return DAAP_REPLY_ERROR; } - dmap_add_container(hreq->reply, "mlog", 24); - dmap_add_int(hreq->reply, "mstt", 200); /* 12 */ - dmap_add_int(hreq->reply, "mlid", session->id); /* 12 */ + dmap_add_container(hreq->out_body, "mlog", 24); + dmap_add_int(hreq->out_body, "mstt", 200); /* 12 */ + dmap_add_int(hreq->out_body, "mlid", session->id); /* 12 */ return DAAP_REPLY_OK; } @@ -992,19 +972,17 @@ static enum daap_reply_result daap_reply_update(struct httpd_request *hreq) { struct daap_update_request *ur; - struct evhttp_connection *evcon; - struct bufferevent *bufev; const char *param; int reqd_rev; int ret; - if (!hreq->req) + if (!hreq->backend) { DPRINTF(E_LOG, L_DAAP, "Bug! daap_reply_update() cannot be called without an actual connection\n"); return DAAP_REPLY_NO_CONNECTION; } - param = evhttp_find_header(hreq->query, "revision-number"); + param = httpd_query_value_find(hreq->query, "revision-number"); if (!param) { DPRINTF(E_DBG, L_DAAP, "Missing revision-number in client update request\n"); @@ -1018,18 +996,18 @@ daap_reply_update(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DAAP, "Parameter revision-number not an integer\n"); - dmap_error_make(hreq->reply, "mupd", "Invalid request"); + dmap_error_make(hreq->out_body, "mupd", "Invalid request"); return DAAP_REPLY_ERROR; } if (reqd_rev == 1) /* Or revision is not valid */ { - CHECK_ERR(L_DAAP, evbuffer_expand(hreq->reply, 32)); + CHECK_ERR(L_DAAP, evbuffer_expand(hreq->out_body, 32)); /* Send back current revision */ - dmap_add_container(hreq->reply, "mupd", 24); - dmap_add_int(hreq->reply, "mstt", 200); /* 12 */ - dmap_add_int(hreq->reply, "musr", current_rev); /* 12 */ + dmap_add_container(hreq->out_body, "mupd", 24); + dmap_add_int(hreq->out_body, "mstt", 200); /* 12 */ + dmap_add_int(hreq->out_body, "musr", current_rev); /* 12 */ return DAAP_REPLY_OK; } @@ -1040,13 +1018,13 @@ daap_reply_update(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DAAP, "Out of memory for update request\n"); - dmap_error_make(hreq->reply, "mupd", "Out of memory"); + dmap_error_make(hreq->out_body, "mupd", "Out of memory"); return DAAP_REPLY_ERROR; } if (DAAP_UPDATE_REFRESH > 0) { - ur->timeout = evtimer_new(evbase_httpd, update_refresh_cb, ur); + ur->timeout = evtimer_new(hreq->evbase, update_refresh_cb, ur); if (ur->timeout) ret = evtimer_add(ur->timeout, &daap_update_refresh_tv); else @@ -1056,14 +1034,14 @@ daap_reply_update(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DAAP, "Out of memory for update request event\n"); - dmap_error_make(hreq->reply, "mupd", "Could not register timer"); + dmap_error_make(hreq->out_body, "mupd", "Could not register timer"); update_free(ur); return DAAP_REPLY_ERROR; } } /* NOTE: we may need to keep reqd_rev in there too */ - ur->req = hreq->req; + ur->hreq = hreq; ur->next = update_requests; update_requests = ur; @@ -1071,20 +1049,7 @@ daap_reply_update(struct httpd_request *hreq) /* If the connection fails before we have an update to push out * to the client, we need to know. */ - evcon = evhttp_request_get_connection(hreq->req); - if (evcon) - { - evhttp_connection_set_closecb(evcon, update_fail_cb, ur); - - // This is a workaround for some versions of libevent (2.0, but possibly - // also 2.1) that don't detect if the client hangs up, and thus don't - // clean up and never call update_fail_cb(). See github issue #870 and - // https://github.com/libevent/libevent/issues/666. It should probably be - // removed again in the future. The workaround is also present in dacp.c - bufev = evhttp_connection_get_bufferevent(evcon); - if (bufev) - bufferevent_enable(bufev, EV_READ); - } + httpd_request_close_cb_set(hreq, update_fail_cb, ur); return DAAP_REPLY_NONE; } @@ -1112,7 +1077,7 @@ daap_reply_dblist(struct httpd_request *hreq) CHECK_NULL(L_DAAP, content = evbuffer_new()); CHECK_NULL(L_DAAP, item = evbuffer_new()); CHECK_ERR(L_DAAP, evbuffer_expand(item, 512)); - CHECK_ERR(L_DAAP, evbuffer_expand(hreq->reply, 1024)); + CHECK_ERR(L_DAAP, evbuffer_expand(hreq->out_body, 1024)); // Add db entry for library with dbid = 1 dmap_add_int(item, "miid", 1); @@ -1155,14 +1120,14 @@ daap_reply_dblist(struct httpd_request *hreq) // Create container len = evbuffer_get_length(content); - dmap_add_container(hreq->reply, "avdb", len + 53); - dmap_add_int(hreq->reply, "mstt", 200); /* 12 */ - dmap_add_char(hreq->reply, "muty", 0); /* 9 */ - dmap_add_int(hreq->reply, "mtco", 2); /* 12 */ - dmap_add_int(hreq->reply, "mrco", 2); /* 12 */ - dmap_add_container(hreq->reply, "mlcl", len); /* 8 */ + dmap_add_container(hreq->out_body, "avdb", len + 53); + dmap_add_int(hreq->out_body, "mstt", 200); /* 12 */ + dmap_add_char(hreq->out_body, "muty", 0); /* 9 */ + dmap_add_int(hreq->out_body, "mtco", 2); /* 12 */ + dmap_add_int(hreq->out_body, "mrco", 2); /* 12 */ + dmap_add_container(hreq->out_body, "mlcl", len); /* 8 */ - CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->reply, content)); + CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->out_body, content)); evbuffer_free(item); evbuffer_free(content); @@ -1177,7 +1142,6 @@ daap_reply_songlist_generic(struct httpd_request *hreq, int playlist) struct db_media_file_info dbmfi; struct evbuffer *song; struct evbuffer *songlist; - struct evkeyvalq *headers; struct daap_session *s; const struct dmap_field **meta = NULL; struct sort_ctx *sctx; @@ -1218,11 +1182,11 @@ daap_reply_songlist_generic(struct httpd_request *hreq, int playlist) CHECK_NULL(L_DAAP, songlist = evbuffer_new()); CHECK_NULL(L_DAAP, song = evbuffer_new()); CHECK_NULL(L_DAAP, sctx = daap_sort_context_new()); - CHECK_ERR(L_DAAP, evbuffer_expand(hreq->reply, 61)); + CHECK_ERR(L_DAAP, evbuffer_expand(hreq->out_body, 61)); CHECK_ERR(L_DAAP, evbuffer_expand(songlist, 4096)); CHECK_ERR(L_DAAP, evbuffer_expand(song, 512)); - param = evhttp_find_header(hreq->query, "meta"); + param = httpd_query_value_find(hreq->query, "meta"); if (!param) { DPRINTF(E_DBG, L_DAAP, "No meta parameter in query, using default\n"); @@ -1246,15 +1210,14 @@ daap_reply_songlist_generic(struct httpd_request *hreq, int playlist) { DPRINTF(E_LOG, L_DAAP, "Could not start query\n"); - dmap_error_make(hreq->reply, tag, "Could not start query"); + dmap_error_make(hreq->out_body, tag, "Could not start query"); goto error; } client_codecs = NULL; - if (!s->is_remote && hreq->req) + if (!s->is_remote && hreq->in_headers) { - headers = evhttp_request_get_input_headers(hreq->req); - client_codecs = evhttp_find_header(headers, "Accept-Codecs"); + client_codecs = httpd_header_find(hreq->in_headers, "Accept-Codecs"); } nsongs = 0; @@ -1312,13 +1275,13 @@ daap_reply_songlist_generic(struct httpd_request *hreq, int playlist) if (ret == -100) { - dmap_error_make(hreq->reply, tag, "Out of memory"); + dmap_error_make(hreq->out_body, tag, "Out of memory"); goto error; } else if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Error fetching results\n"); - dmap_error_make(hreq->reply, tag, "Error fetching query results"); + dmap_error_make(hreq->out_body, tag, "Error fetching query results"); goto error; } @@ -1327,25 +1290,25 @@ daap_reply_songlist_generic(struct httpd_request *hreq, int playlist) if (sort_headers) { daap_sort_finalize(sctx); - dmap_add_container(hreq->reply, tag, len + evbuffer_get_length(sctx->headerlist) + 61); + dmap_add_container(hreq->out_body, tag, len + evbuffer_get_length(sctx->headerlist) + 61); } else - dmap_add_container(hreq->reply, tag, len + 53); + dmap_add_container(hreq->out_body, tag, len + 53); - dmap_add_int(hreq->reply, "mstt", 200); /* 12 */ - dmap_add_char(hreq->reply, "muty", 0); /* 9 */ - dmap_add_int(hreq->reply, "mtco", qp.results); /* 12 */ - dmap_add_int(hreq->reply, "mrco", nsongs); /* 12 */ - dmap_add_container(hreq->reply, "mlcl", len); /* 8 */ + dmap_add_int(hreq->out_body, "mstt", 200); /* 12 */ + dmap_add_char(hreq->out_body, "muty", 0); /* 9 */ + dmap_add_int(hreq->out_body, "mtco", qp.results); /* 12 */ + dmap_add_int(hreq->out_body, "mrco", nsongs); /* 12 */ + dmap_add_container(hreq->out_body, "mlcl", len); /* 8 */ - CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->reply, songlist)); + CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->out_body, songlist)); if (sort_headers) { len = evbuffer_get_length(sctx->headerlist); - dmap_add_container(hreq->reply, "mshl", len); /* 8 */ + dmap_add_container(hreq->out_body, "mshl", len); /* 8 */ - CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->reply, sctx->headerlist)); + CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->out_body, sctx->headerlist)); } free(meta); @@ -1378,10 +1341,10 @@ daap_reply_plsonglist(struct httpd_request *hreq) int playlist; int ret; - ret = safe_atoi32(hreq->uri_parsed->path_parts[3], &playlist); + ret = safe_atoi32(hreq->path_parts[3], &playlist); if (ret < 0) { - dmap_error_make(hreq->reply, "apso", "Invalid playlist ID"); + dmap_error_make(hreq->out_body, "apso", "Invalid playlist ID"); return DAAP_REPLY_ERROR; } @@ -1424,10 +1387,10 @@ daap_reply_playlists(struct httpd_request *hreq) cfg_radiopl = cfg_getbool(cfg_getsec(cfg, "library"), "radio_playlists"); - ret = safe_atoi32(hreq->uri_parsed->path_parts[1], &database); + ret = safe_atoi32(hreq->path_parts[1], &database); if (ret < 0) { - dmap_error_make(hreq->reply, "aply", "Invalid database ID"); + dmap_error_make(hreq->out_body, "aply", "Invalid database ID"); return DAAP_REPLY_ERROR; } @@ -1436,11 +1399,11 @@ daap_reply_playlists(struct httpd_request *hreq) CHECK_NULL(L_DAAP, playlistlist = evbuffer_new()); CHECK_NULL(L_DAAP, playlist = evbuffer_new()); - CHECK_ERR(L_DAAP, evbuffer_expand(hreq->reply, 61)); + CHECK_ERR(L_DAAP, evbuffer_expand(hreq->out_body, 61)); CHECK_ERR(L_DAAP, evbuffer_expand(playlistlist, 1024)); CHECK_ERR(L_DAAP, evbuffer_expand(playlist, 128)); - param = evhttp_find_header(hreq->query, "meta"); + param = httpd_query_value_find(hreq->query, "meta"); if (!param) { DPRINTF(E_LOG, L_DAAP, "No meta parameter in query, using default\n"); @@ -1453,7 +1416,7 @@ daap_reply_playlists(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DAAP, "Failed to parse meta parameter in DAAP query\n"); - dmap_error_make(hreq->reply, "aply", "Failed to parse query"); + dmap_error_make(hreq->out_body, "aply", "Failed to parse query"); goto error; } @@ -1462,7 +1425,7 @@ daap_reply_playlists(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DAAP, "Could not start query\n"); - dmap_error_make(hreq->reply, "aply", "Could not start query"); + dmap_error_make(hreq->out_body, "aply", "Could not start query"); goto error; } @@ -1578,26 +1541,26 @@ daap_reply_playlists(struct httpd_request *hreq) if (ret == -100) { - dmap_error_make(hreq->reply, "aply", "Out of memory"); + dmap_error_make(hreq->out_body, "aply", "Out of memory"); goto error; } else if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Error fetching results\n"); - dmap_error_make(hreq->reply, "aply", "Error fetching query results"); + dmap_error_make(hreq->out_body, "aply", "Error fetching query results"); goto error; } /* Add header to evbuf, add playlistlist to evbuf */ len = evbuffer_get_length(playlistlist); - dmap_add_container(hreq->reply, "aply", len + 53); - dmap_add_int(hreq->reply, "mstt", 200); /* 12 */ - dmap_add_char(hreq->reply, "muty", 0); /* 9 */ - dmap_add_int(hreq->reply, "mtco", qp.results); /* 12 */ - dmap_add_int(hreq->reply,"mrco", npls); /* 12 */ - dmap_add_container(hreq->reply, "mlcl", len); + dmap_add_container(hreq->out_body, "aply", len + 53); + dmap_add_int(hreq->out_body, "mstt", 200); /* 12 */ + dmap_add_char(hreq->out_body, "muty", 0); /* 9 */ + dmap_add_int(hreq->out_body, "mtco", qp.results); /* 12 */ + dmap_add_int(hreq->out_body,"mrco", npls); /* 12 */ + dmap_add_container(hreq->out_body, "mlcl", len); - CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->reply, playlistlist)); + CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->out_body, playlistlist)); free(meta); evbuffer_free(playlist); @@ -1638,7 +1601,7 @@ daap_reply_groups(struct httpd_request *hreq) int i; int ret; - param = evhttp_find_header(hreq->query, "group-type"); + param = httpd_query_value_find(hreq->query, "group-type"); if (param && strcmp(param, "artists") == 0) { // Request from Remote may have the form: @@ -1662,11 +1625,11 @@ daap_reply_groups(struct httpd_request *hreq) CHECK_NULL(L_DAAP, grouplist = evbuffer_new()); CHECK_NULL(L_DAAP, group = evbuffer_new()); CHECK_NULL(L_DAAP, sctx = daap_sort_context_new()); - CHECK_ERR(L_DAAP, evbuffer_expand(hreq->reply, 61)); + CHECK_ERR(L_DAAP, evbuffer_expand(hreq->out_body, 61)); CHECK_ERR(L_DAAP, evbuffer_expand(grouplist, 1024)); CHECK_ERR(L_DAAP, evbuffer_expand(group, 128)); - param = evhttp_find_header(hreq->query, "meta"); + param = httpd_query_value_find(hreq->query, "meta"); if (!param) { DPRINTF(E_LOG, L_DAAP, "No meta parameter in query, using default\n"); @@ -1679,7 +1642,7 @@ daap_reply_groups(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DAAP, "Failed to parse meta parameter in DAAP query\n"); - dmap_error_make(hreq->reply, tag, "Failed to parse query"); + dmap_error_make(hreq->out_body, tag, "Failed to parse query"); goto error; } @@ -1688,7 +1651,7 @@ daap_reply_groups(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DAAP, "Could not start query\n"); - dmap_error_make(hreq->reply, tag, "Could not start query"); + dmap_error_make(hreq->out_body, tag, "Could not start query"); goto error; } @@ -1780,13 +1743,13 @@ daap_reply_groups(struct httpd_request *hreq) if (ret == -100) { - dmap_error_make(hreq->reply, tag, "Out of memory"); + dmap_error_make(hreq->out_body, tag, "Out of memory"); goto error; } else if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Error fetching results\n"); - dmap_error_make(hreq->reply, tag, "Error fetching query results"); + dmap_error_make(hreq->out_body, tag, "Error fetching query results"); goto error; } @@ -1795,25 +1758,25 @@ daap_reply_groups(struct httpd_request *hreq) if (sort_headers) { daap_sort_finalize(sctx); - dmap_add_container(hreq->reply, tag, len + evbuffer_get_length(sctx->headerlist) + 61); + dmap_add_container(hreq->out_body, tag, len + evbuffer_get_length(sctx->headerlist) + 61); } else - dmap_add_container(hreq->reply, tag, len + 53); + dmap_add_container(hreq->out_body, tag, len + 53); - dmap_add_int(hreq->reply, "mstt", 200); /* 12 */ - dmap_add_char(hreq->reply, "muty", 0); /* 9 */ - dmap_add_int(hreq->reply, "mtco", qp.results); /* 12 */ - dmap_add_int(hreq->reply,"mrco", ngrp); /* 12 */ - dmap_add_container(hreq->reply, "mlcl", len); /* 8 */ + dmap_add_int(hreq->out_body, "mstt", 200); /* 12 */ + dmap_add_char(hreq->out_body, "muty", 0); /* 9 */ + dmap_add_int(hreq->out_body, "mtco", qp.results); /* 12 */ + dmap_add_int(hreq->out_body,"mrco", ngrp); /* 12 */ + dmap_add_container(hreq->out_body, "mlcl", len); /* 8 */ - CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->reply, grouplist)); + CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->out_body, grouplist)); if (sort_headers) { len = evbuffer_get_length(sctx->headerlist); - dmap_add_container(hreq->reply, "mshl", len); /* 8 */ + dmap_add_container(hreq->out_body, "mshl", len); /* 8 */ - CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->reply, sctx->headerlist)); + CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->out_body, sctx->headerlist)); } free(meta); @@ -1848,36 +1811,36 @@ daap_reply_browse(struct httpd_request *hreq) int nitems; int ret; - if (strcmp(hreq->uri_parsed->path_parts[3], "artists") == 0) + if (strcmp(hreq->path_parts[3], "artists") == 0) { tag = "abar"; query_params_set(&qp, &sort_headers, hreq, Q_BROWSE_ARTISTS); } - else if (strcmp(hreq->uri_parsed->path_parts[3], "albums") == 0) + else if (strcmp(hreq->path_parts[3], "albums") == 0) { tag = "abal"; query_params_set(&qp, &sort_headers, hreq, Q_BROWSE_ALBUMS); } - else if (strcmp(hreq->uri_parsed->path_parts[3], "genres") == 0) + else if (strcmp(hreq->path_parts[3], "genres") == 0) { tag = "abgn"; query_params_set(&qp, &sort_headers, hreq, Q_BROWSE_GENRES); } - else if (strcmp(hreq->uri_parsed->path_parts[3], "composers") == 0) + else if (strcmp(hreq->path_parts[3], "composers") == 0) { tag = "abcp"; query_params_set(&qp, &sort_headers, hreq, Q_BROWSE_COMPOSERS); } else { - DPRINTF(E_LOG, L_DAAP, "Invalid DAAP browse request type '%s'\n", hreq->uri_parsed->path_parts[3]); - dmap_error_make(hreq->reply, "abro", "Invalid browse type"); + DPRINTF(E_LOG, L_DAAP, "Invalid DAAP browse request type '%s'\n", hreq->path_parts[3]); + dmap_error_make(hreq->out_body, "abro", "Invalid browse type"); return DAAP_REPLY_ERROR; } CHECK_NULL(L_DAAP, itemlist = evbuffer_new()); CHECK_NULL(L_DAAP, sctx = daap_sort_context_new()); - CHECK_ERR(L_DAAP, evbuffer_expand(hreq->reply, 52)); + CHECK_ERR(L_DAAP, evbuffer_expand(hreq->out_body, 52)); CHECK_ERR(L_DAAP, evbuffer_expand(itemlist, 1024)); // Just a starting alloc, it'll expand as needed ret = db_query_start(&qp); @@ -1885,7 +1848,7 @@ daap_reply_browse(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DAAP, "Could not start query\n"); - dmap_error_make(hreq->reply, "abro", "Could not start query"); + dmap_error_make(hreq->out_body, "abro", "Could not start query"); goto error; } @@ -1913,7 +1876,7 @@ daap_reply_browse(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DAAP, "Error fetching/building results\n"); - dmap_error_make(hreq->reply, "abro", "Error fetching/building query results"); + dmap_error_make(hreq->out_body, "abro", "Error fetching/building query results"); goto error; } @@ -1921,24 +1884,24 @@ daap_reply_browse(struct httpd_request *hreq) if (sort_headers) { daap_sort_finalize(sctx); - dmap_add_container(hreq->reply, "abro", len + evbuffer_get_length(sctx->headerlist) + 52); + dmap_add_container(hreq->out_body, "abro", len + evbuffer_get_length(sctx->headerlist) + 52); } else - dmap_add_container(hreq->reply, "abro", len + 44); + dmap_add_container(hreq->out_body, "abro", len + 44); - dmap_add_int(hreq->reply, "mstt", 200); /* 12 */ - dmap_add_int(hreq->reply, "mtco", qp.results); /* 12 */ - dmap_add_int(hreq->reply, "mrco", nitems); /* 12 */ - dmap_add_container(hreq->reply, tag, len); /* 8 */ + dmap_add_int(hreq->out_body, "mstt", 200); /* 12 */ + dmap_add_int(hreq->out_body, "mtco", qp.results); /* 12 */ + dmap_add_int(hreq->out_body, "mrco", nitems); /* 12 */ + dmap_add_container(hreq->out_body, tag, len); /* 8 */ - CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->reply, itemlist)); + CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->out_body, itemlist)); if (sort_headers) { len = evbuffer_get_length(sctx->headerlist); - dmap_add_container(hreq->reply, "mshl", len); /* 8 */ + dmap_add_container(hreq->out_body, "mshl", len); /* 8 */ - CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->reply, sctx->headerlist)); + CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->out_body, sctx->headerlist)); } daap_sort_context_free(sctx); @@ -1959,7 +1922,6 @@ daap_reply_browse(struct httpd_request *hreq) static enum daap_reply_result daap_reply_extra_data(struct httpd_request *hreq) { - struct evkeyvalq *headers; char clen[32]; const char *param; char *ctype; @@ -1969,22 +1931,22 @@ daap_reply_extra_data(struct httpd_request *hreq) int max_h; int ret; - if (!hreq->req) + if (!hreq->backend) { DPRINTF(E_LOG, L_DAAP, "Bug! daap_reply_extra_data() cannot be called without an actual connection\n"); return DAAP_REPLY_NO_CONNECTION; } - ret = safe_atoi32(hreq->uri_parsed->path_parts[3], &id); + ret = safe_atoi32(hreq->path_parts[3], &id); if (ret < 0) { - DPRINTF(E_LOG, L_DAAP, "Could not convert id parameter to integer: '%s'\n", hreq->uri_parsed->path_parts[3]); + DPRINTF(E_LOG, L_DAAP, "Could not convert id parameter to integer: '%s'\n", hreq->path_parts[3]); return DAAP_REPLY_BAD_REQUEST; } - if (evhttp_find_header(hreq->query, "mw") && evhttp_find_header(hreq->query, "mh")) + if (httpd_query_value_find(hreq->query, "mw") && httpd_query_value_find(hreq->query, "mh")) { - param = evhttp_find_header(hreq->query, "mw"); + param = httpd_query_value_find(hreq->query, "mw"); ret = safe_atoi32(param, &max_w); if (ret < 0) { @@ -1992,7 +1954,7 @@ daap_reply_extra_data(struct httpd_request *hreq) return DAAP_REPLY_BAD_REQUEST; } - param = evhttp_find_header(hreq->query, "mh"); + param = httpd_query_value_find(hreq->query, "mh"); ret = safe_atoi32(param, &max_h); if (ret < 0) { @@ -2008,12 +1970,12 @@ daap_reply_extra_data(struct httpd_request *hreq) max_h = 0; } - if (strcmp(hreq->uri_parsed->path_parts[2], "groups") == 0) - ret = artwork_get_group(hreq->reply, id, max_w, max_h, 0); - else if (strcmp(hreq->uri_parsed->path_parts[2], "items") == 0) - ret = artwork_get_item(hreq->reply, id, max_w, max_h, 0); + if (strcmp(hreq->path_parts[2], "groups") == 0) + ret = artwork_get_group(hreq->out_body, id, max_w, max_h, 0); + else if (strcmp(hreq->path_parts[2], "items") == 0) + ret = artwork_get_item(hreq->out_body, id, max_w, max_h, 0); - len = evbuffer_get_length(hreq->reply); + len = evbuffer_get_length(hreq->out_body); switch (ret) { @@ -2027,16 +1989,15 @@ daap_reply_extra_data(struct httpd_request *hreq) default: if (len > 0) - evbuffer_drain(hreq->reply, len); + evbuffer_drain(hreq->out_body, len); goto no_artwork; } - headers = evhttp_request_get_output_headers(hreq->req); - evhttp_remove_header(headers, "Content-Type"); - evhttp_add_header(headers, "Content-Type", ctype); + httpd_header_remove(hreq->out_headers, "Content-Type"); + httpd_header_add(hreq->out_headers, "Content-Type", ctype); snprintf(clen, sizeof(clen), "%ld", (long)len); - evhttp_add_header(headers, "Content-Length", clen); + httpd_header_add(hreq->out_headers, "Content-Length", clen); return DAAP_REPLY_OK_NO_GZIP; @@ -2050,17 +2011,17 @@ daap_stream(struct httpd_request *hreq) int id; int ret; - if (!hreq->req) + if (!hreq->backend) { DPRINTF(E_LOG, L_DAAP, "Bug! daap_stream() cannot be called without an actual connection\n"); return DAAP_REPLY_NO_CONNECTION; } - ret = safe_atoi32(hreq->uri_parsed->path_parts[3], &id); + ret = safe_atoi32(hreq->path_parts[3], &id); if (ret < 0) return DAAP_REPLY_BAD_REQUEST; - httpd_stream_file(hreq->req, id); + httpd_stream_file(hreq, id); return DAAP_REPLY_NONE; } @@ -2138,16 +2099,16 @@ daap_reply_dmap_test(struct httpd_request *hreq) dmap_add_field(test, &dmap_TST8, buf, 0); dmap_add_field(test, &dmap_TST9, buf, 0); - dmap_add_container(hreq->reply, dmap_TEST.tag, evbuffer_get_length(test)); + dmap_add_container(hreq->out_body, dmap_TEST.tag, evbuffer_get_length(test)); - ret = evbuffer_add_buffer(hreq->reply, test); + ret = evbuffer_add_buffer(hreq->out_body, test); evbuffer_free(test); if (ret < 0) { DPRINTF(E_LOG, L_DAAP, "Could not add test results to DMAP test reply\n"); - dmap_error_make(hreq->reply, dmap_TEST.tag, "Out of memory"); + dmap_error_make(hreq->out_body, dmap_TEST.tag, "Out of memory"); return DAAP_REPLY_ERROR; } @@ -2175,7 +2136,7 @@ static struct httpd_uri_map daap_handlers[] = }, { .regexp = "^/update$", - .handler = daap_reply_update + .handler = daap_reply_update, }, { .regexp = "^/activity$", @@ -2237,11 +2198,9 @@ static struct httpd_uri_map daap_handlers[] = * iTunes 12.1 gives us an absolute request-uri for streaming like * http://10.1.1.20:3689/databases/1/items/1.mp3 */ -void -daap_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed) +static void +daap_request(struct httpd_request *hreq) { - struct httpd_request *hreq; - struct evkeyvalq *headers; struct timespec start; struct timespec end; struct daap_session session; @@ -2250,24 +2209,20 @@ daap_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed) int ret; int msec; - DPRINTF(E_DBG, L_DAAP, "DAAP request: '%s'\n", uri_parsed->uri); - - hreq = httpd_request_parse(req, uri_parsed, NULL, daap_handlers); - if (!hreq) + if (!hreq->handler) { - DPRINTF(E_LOG, L_DAAP, "Unrecognized path '%s' in DAAP request: '%s'\n", uri_parsed->path, uri_parsed->uri); - - httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); + DPRINTF(E_LOG, L_DAAP, "Unrecognized path in DAAP request: '%s'\n", hreq->uri); + daap_reply_send(hreq, DAAP_REPLY_BAD_REQUEST); return; } // Check if we have a session and point hreq->extra_data to it - param = evhttp_find_header(hreq->query, "session-id"); + param = httpd_query_value_find(hreq->query, "session-id"); if (param) { ret = safe_atoi32(param, &id); if (ret < 0) - DPRINTF(E_LOG, L_DAAP, "Ignoring non-numeric session id in DAAP request: '%s'\n", uri_parsed->uri); + DPRINTF(E_LOG, L_DAAP, "Ignoring non-numeric session id in DAAP request: '%s'\n", hreq->uri); else hreq->extra_data = daap_session_get(id); } @@ -2276,39 +2231,31 @@ daap_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed) if (!hreq->extra_data) { memset(&session, 0, sizeof(struct daap_session)); - session.is_remote = (evhttp_find_header(hreq->query, "pairing-guid") != NULL); + session.is_remote = (httpd_query_value_find(hreq->query, "pairing-guid") != NULL); hreq->extra_data = &session; } ret = daap_request_authorize(hreq); if (ret < 0) { - free(hreq); return; } // Set reply headers - headers = evhttp_request_get_output_headers(req); - evhttp_add_header(headers, "Accept-Ranges", "bytes"); - evhttp_add_header(headers, "DAAP-Server", PACKAGE_NAME "/" VERSION); + httpd_header_add(hreq->out_headers, "Accept-Ranges", "bytes"); + httpd_header_add(hreq->out_headers, "DAAP-Server", PACKAGE_NAME "/" VERSION); // Content-Type for all replies, even the actual audio streaming. Note that // video streaming will override this Content-Type with a more appropriate // video/ Content-Type as expected by clients like Front Row. - evhttp_add_header(headers, "Content-Type", "application/x-dmap-tagged"); - - // Now we create the actual reply - CHECK_NULL(L_DAAP, hreq->reply = evbuffer_new()); + httpd_header_add(hreq->out_headers, "Content-Type", "application/x-dmap-tagged"); // Try the cache - ret = cache_daap_get(hreq->reply, uri_parsed->uri); + ret = cache_daap_get(hreq->out_body, hreq->uri); if (ret == 0) { // The cache will return the data gzipped, so httpd_send_reply won't need to do it - evhttp_add_header(headers, "Content-Encoding", "gzip"); - httpd_send_reply(req, HTTP_OK, "OK", hreq->reply, HTTPD_SEND_NO_GZIP); // TODO not all want this reply - - evbuffer_free(hreq->reply); - free(hreq); + httpd_header_add(hreq->out_headers, "Content-Encoding", "gzip"); + httpd_send_reply(hreq, HTTP_OK, "OK", HTTPD_SEND_NO_GZIP); // TODO not all want this reply return; } @@ -2325,38 +2272,7 @@ daap_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed) DPRINTF(E_DBG, L_DAAP, "DAAP request handled in %d milliseconds\n", msec); if (ret == DAAP_REPLY_OK && msec > cache_daap_threshold() && hreq->user_agent) - cache_daap_add(uri_parsed->uri, hreq->user_agent, ((struct daap_session *)hreq->extra_data)->is_remote, msec); - - evbuffer_free(hreq->reply); - free(hreq); -} - -int -daap_is_request(const char *path) -{ - if (strncmp(path, "/databases/", strlen("/databases/")) == 0) - return 1; - if (strcmp(path, "/databases") == 0) - return 1; - if (strcmp(path, "/server-info") == 0) - return 1; - if (strcmp(path, "/content-codes") == 0) - return 1; - if (strcmp(path, "/login") == 0) - return 1; - if (strcmp(path, "/update") == 0) - return 1; - if (strcmp(path, "/activity") == 0) - return 1; - if (strcmp(path, "/logout") == 0) - return 1; - -#ifdef DMAP_TEST - if (strcmp(path, "/dmap-test") == 0) - return 1; -#endif - - return 0; + cache_daap_add(hreq->uri, hreq->user_agent, ((struct daap_session *)hreq->extra_data)->is_remote, msec); } int @@ -2377,24 +2293,24 @@ struct evbuffer * daap_reply_build(const char *uri, const char *user_agent, int is_remote) { struct httpd_request *hreq; - struct httpd_uri_parsed *uri_parsed; - struct evbuffer *reply; + struct evbuffer *out_body = NULL; struct daap_session session; int ret; DPRINTF(E_DBG, L_DAAP, "Building reply for DAAP request: '%s'\n", uri); - reply = NULL; - - uri_parsed = httpd_uri_parse(uri); - if (!uri_parsed) - return NULL; - - hreq = httpd_request_parse(NULL, uri_parsed, user_agent, daap_handlers); + hreq = httpd_request_new(NULL, NULL, uri, user_agent); if (!hreq) { - DPRINTF(E_LOG, L_DAAP, "Cannot build reply, unrecognized path '%s' in request: '%s'\n", uri_parsed->path, uri_parsed->uri); - goto out_free_uri; + DPRINTF(E_LOG, L_DAAP, "Error building request: '%s'\n", uri); + goto out; + } + + httpd_request_handler_set(hreq); + if (!hreq->handler) + { + DPRINTF(E_LOG, L_DAAP, "Cannot build reply, unrecognized path in request: '%s'\n", uri); + goto out; } memset(&session, 0, sizeof(struct daap_session)); @@ -2402,61 +2318,36 @@ daap_reply_build(const char *uri, const char *user_agent, int is_remote) hreq->extra_data = &session; - CHECK_NULL(L_DAAP, hreq->reply = evbuffer_new()); - ret = hreq->handler(hreq); if (ret < 0) { - evbuffer_free(hreq->reply); - goto out_free_hreq; + goto out; } - reply = hreq->reply; + // Take ownership of the reply + out_body = hreq->out_body; + hreq->out_body = NULL; - out_free_hreq: - free(hreq); - out_free_uri: - httpd_uri_free(uri_parsed); + out: + httpd_request_free(hreq); - return reply; + return out_body; } -int +static int daap_init(void) { - char buf[64]; - int i; - int ret; - srand((unsigned)time(NULL)); current_rev = 2; - update_requests = NULL; - - for (i = 0; daap_handlers[i].handler; i++) - { - ret = regcomp(&daap_handlers[i].preg, daap_handlers[i].regexp, REG_EXTENDED | REG_NOSUB); - if (ret != 0) - { - regerror(ret, &daap_handlers[i].preg, buf, sizeof(buf)); - - DPRINTF(E_FATAL, L_DAAP, "DAAP init failed; regexp error: %s\n", buf); - return -1; - } - } return 0; } -void +static void daap_deinit(void) { struct daap_session *s; struct daap_update_request *ur; - struct evhttp_connection *evcon; - int i; - - for (i = 0; daap_handlers[i].handler; i++) - regfree(&daap_handlers[i].preg); for (s = daap_sessions; daap_sessions; s = daap_sessions) { @@ -2468,13 +2359,24 @@ daap_deinit(void) { update_requests = ur->next; - evcon = evhttp_request_get_connection(ur->req); - if (evcon) - { - evhttp_connection_set_closecb(evcon, NULL, NULL); - evhttp_connection_free(evcon); - } - + daap_reply_send(ur->hreq, DAAP_REPLY_SERVUNAVAIL); update_free(ur); } } + +struct httpd_module httpd_daap = +{ + .name = "DAAP", + .type = MODULE_DAAP, + .logdomain = L_DAAP, + .subpaths = { "/databases/", NULL }, +#ifdef DMAP_TEST + .fullpaths = { "/databases", "/server-info", "/content-codes", "/login", "/update", "/activity", "/logout", "/dmap-test", NULL }, +#else + .fullpaths = { "/databases", "/server-info", "/content-codes", "/login", "/update", "/activity", "/logout", NULL }, +#endif + .handlers = daap_handlers, + .init = daap_init, + .deinit = daap_deinit, + .request = daap_request, +}; diff --git a/src/httpd_daap.h b/src/httpd_daap.h index 53fb6e55..2efd4b42 100644 --- a/src/httpd_daap.h +++ b/src/httpd_daap.h @@ -2,20 +2,6 @@ #ifndef __HTTPD_DAAP_H__ #define __HTTPD_DAAP_H__ -#include "httpd.h" - -int -daap_init(void); - -void -daap_deinit(void); - -void -daap_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed); - -int -daap_is_request(const char *path); - int daap_session_is_valid(int id); diff --git a/src/httpd_dacp.c b/src/httpd_dacp.c index bee56149..f8ec8c9d 100644 --- a/src/httpd_dacp.c +++ b/src/httpd_dacp.c @@ -26,19 +26,14 @@ #include #include #include -#include #include #include #include -#ifdef HAVE_EVENTFD -# include -#endif - +#include #include -#include -#include "httpd_dacp.h" +#include "httpd_internal.h" #include "httpd_daap.h" #include "logger.h" #include "misc.h" @@ -51,11 +46,9 @@ #define DACP_VOLUME_STEP 5 -/* httpd event base, from httpd.c */ -extern struct event_base *evbase_httpd; - struct dacp_update_request { - struct evhttp_request *req; + struct httpd_request *hreq; + struct event *updateev; struct dacp_update_request *next; }; @@ -130,31 +123,30 @@ dacp_propset_userrating(const char *value, struct httpd_request *hreq); /* gperf static hash, dacp_prop.gperf */ #include "dacp_prop_hash.h" - -/* Play status update */ -#ifdef HAVE_EVENTFD -static int update_efd; -#else -static int update_pipe[2]; -#endif -static struct event *updateev; -/* Next revision number the client should call with */ -static int current_rev; - -/* Play status update requests */ +// Play status update requests static struct dacp_update_request *update_requests; +static pthread_mutex_t update_request_lck; +// Next revision number the client should call with +static int update_current_rev; -/* Seek timer */ -static struct event *seek_timer; -static int seek_target; - -/* If an item is removed from the library while in the queue, we replace it with this */ +// If an item is removed from the library while in the queue, we replace it with this static struct media_file_info dummy_mfi; static struct db_queue_item dummy_queue_item; /* -------------------------------- HELPERS --------------------------------- */ +static void +dacp_send_error(struct httpd_request *hreq, const char *container, const char *errmsg) +{ + if (!hreq) + return; + + dmap_error_make(hreq->out_body, container, errmsg); + + httpd_send_reply(hreq, HTTP_OK, "OK", HTTPD_SEND_NO_GZIP); +} + static void dacp_nowplaying(struct evbuffer *evbuf, struct player_status *status, struct db_queue_item *queue_item) { @@ -542,15 +534,13 @@ speaker_enum_cb(struct player_speaker_info *spk, void *arg) static int speaker_get(struct player_speaker_info *speaker_info, struct httpd_request *hreq, const char *req_name) { - struct evkeyvalq *headers; const char *remote; uint32_t active_remote; int ret; - headers = evhttp_request_get_input_headers(hreq->req); - remote = evhttp_find_header(headers, "Active-Remote"); + remote = httpd_header_find(hreq->in_headers, "Active-Remote"); - if (!headers || !remote || (safe_atou32(remote, &active_remote) < 0)) + if (!remote || (safe_atou32(remote, &active_remote) < 0)) { DPRINTF(E_LOG, L_DACP, "'%s' request from '%s' has invalid Active-Remote: '%s'\n", req_name, hreq->peer_address, remote); return -1; @@ -583,6 +573,8 @@ speaker_volume_step(struct player_speaker_info *speaker_info, int step) static void seek_timer_cb(int fd, short what, void *arg) { + intptr_t seek_target_packed = (intptr_t)arg; + int seek_target = seek_target_packed; int ret; DPRINTF(E_DBG, L_DACP, "Seek timer expired, target %d ms\n", seek_target); @@ -610,7 +602,7 @@ dacp_request_authorize(struct httpd_request *hreq) if (net_peer_address_is_trusted(hreq->peer_address)) return 0; - param = evhttp_find_header(hreq->query, "session-id"); + param = httpd_query_value_find(hreq->query, "session-id"); if (!param) { DPRINTF(E_LOG, L_DACP, "No session-id specified in request\n"); @@ -633,9 +625,9 @@ dacp_request_authorize(struct httpd_request *hreq) return 0; invalid: - DPRINTF(E_LOG, L_DACP, "Unauthorized request '%s' from '%s' (is peer trusted in your config?)\n", hreq->uri_parsed->uri, hreq->peer_address); + DPRINTF(E_LOG, L_DACP, "Unauthorized request '%s' from '%s' (is peer trusted in your config?)\n", hreq->uri, hreq->peer_address); - httpd_send_error(hreq->req, 403, "Forbidden"); + httpd_send_error(hreq, HTTP_FORBIDDEN, "Forbidden"); return -1; } @@ -643,7 +635,7 @@ dacp_request_authorize(struct httpd_request *hreq) /* ---------------------- UPDATE REQUESTS HANDLERS -------------------------- */ static int -make_playstatusupdate(struct evbuffer *evbuf) +make_playstatusupdate(struct evbuffer *evbuf, int current_rev) { struct player_status status; struct db_queue_item *queue_item = NULL; @@ -709,115 +701,42 @@ make_playstatusupdate(struct evbuffer *evbuf) } static void -playstatusupdate_cb(int fd, short what, void *arg) +playstatusupdate_cb(int fd, short what, void *arg); + +static struct dacp_update_request * +update_request_new(struct httpd_request *hreq) { struct dacp_update_request *ur; - struct evbuffer *evbuf; - struct evbuffer *update; - struct evhttp_connection *evcon; - uint8_t *buf; - size_t len; - int ret; -#ifdef HAVE_EVENTFD - eventfd_t count; + CHECK_NULL(L_DACP, ur = calloc(1, sizeof(struct dacp_update_request))); + CHECK_NULL(L_DACP, ur->updateev = event_new(hreq->evbase, -1, 0, playstatusupdate_cb, ur)); + ur->hreq = hreq; - ret = eventfd_read(update_efd, &count); - if (ret < 0) - { - DPRINTF(E_LOG, L_DACP, "Could not read playstatusupdate event counter: %s\n", strerror(errno)); - - goto readd; - } -#else - int dummy; - - read(update_pipe[0], &dummy, sizeof(dummy)); -#endif - - current_rev++; - - if (!update_requests) - goto readd; - - CHECK_NULL(L_DACP, evbuf = evbuffer_new()); - CHECK_NULL(L_DACP, update = evbuffer_new()); - - ret = make_playstatusupdate(update); - if (ret < 0) - goto out_free_update; - - len = evbuffer_get_length(update); - - for (ur = update_requests; update_requests; ur = update_requests) - { - update_requests = ur->next; - - evcon = evhttp_request_get_connection(ur->req); - if (evcon) - evhttp_connection_set_closecb(evcon, NULL, NULL); - - // Only copy buffer if we actually need to reuse it - if (ur->next) - { - buf = evbuffer_pullup(update, -1); - evbuffer_add(evbuf, buf, len); - httpd_send_reply(ur->req, HTTP_OK, "OK", evbuf, 0); - } - else - httpd_send_reply(ur->req, HTTP_OK, "OK", update, 0); - - free(ur); - } - - out_free_update: - evbuffer_free(update); - evbuffer_free(evbuf); - readd: - ret = event_add(updateev, NULL); - if (ret < 0) - DPRINTF(E_LOG, L_DACP, "Couldn't re-add event for playstatusupdate\n"); -} - -/* Thread: player */ -static void -dacp_playstatus_update_handler(short event_mask) -{ - int ret; - -#ifdef HAVE_EVENTFD - ret = eventfd_write(update_efd, 1); - if (ret < 0) - DPRINTF(E_LOG, L_DACP, "Could not send status update event: %s\n", strerror(errno)); -#else - int dummy = 42; - - ret = write(update_pipe[1], &dummy, sizeof(dummy)); - if (ret != sizeof(dummy)) - DPRINTF(E_LOG, L_DACP, "Could not write to status update fd: %s\n", strerror(errno)); -#endif + return ur; } static void -update_fail_cb(struct evhttp_connection *evcon, void *arg) +update_request_free(struct dacp_update_request *ur) +{ + if (!ur) + return; + + if (ur->updateev) + event_free(ur->updateev); + + free(ur); +} + +static void +update_request_remove(struct dacp_update_request **head, struct dacp_update_request *ur) { - struct dacp_update_request *ur; struct dacp_update_request *p; - struct evhttp_connection *evc; - ur = (struct dacp_update_request *)arg; - - DPRINTF(E_DBG, L_DACP, "Update request: client closed connection\n"); - - evc = evhttp_request_get_connection(ur->req); - if (evc) - evhttp_connection_set_closecb(evc, NULL, NULL); - - if (ur == update_requests) - update_requests = ur->next; + if (ur == *head) + *head = ur->next; else { - for (p = update_requests; p && (p->next != ur); p = p->next) + for (p = *head; p && (p->next != ur); p = p->next) ; if (!p) @@ -829,8 +748,55 @@ update_fail_cb(struct evhttp_connection *evcon, void *arg) p->next = ur->next; } - evhttp_request_free(ur->req); - free(ur); + update_request_free(ur); +} + +static void +playstatusupdate_cb(int fd, short what, void *arg) +{ + struct dacp_update_request *ur = arg; + struct httpd_request *hreq = ur->hreq; + int ret; + + ret = make_playstatusupdate(hreq->out_body, update_current_rev); + if (ret < 0) + goto error; + + httpd_send_reply(hreq, HTTP_OK, "OK", 0); + + pthread_mutex_lock(&update_request_lck); + update_request_remove(&update_requests, ur); + pthread_mutex_unlock(&update_request_lck); + + error: + return; +} + +static void +update_fail_cb(void *arg) +{ + struct dacp_update_request *ur = arg; + + DPRINTF(E_DBG, L_DACP, "Update request: client closed connection\n"); + + pthread_mutex_lock(&update_request_lck); + update_request_remove(&update_requests, ur); + pthread_mutex_unlock(&update_request_lck); +} + +/* Thread: player */ +static void +dacp_playstatus_update_handler(short event_mask) +{ + struct dacp_update_request *ur; + + pthread_mutex_lock(&update_request_lck); + update_current_rev++; + for (ur = update_requests; ur; ur = ur->next) + { + event_active(ur->updateev, 0, 0); + } + pthread_mutex_unlock(&update_request_lck); } @@ -955,7 +921,7 @@ dacp_propset_volume(const char *value, struct httpd_request *hreq) return; } - param = evhttp_find_header(hreq->query, "speaker-id"); + param = httpd_query_value_find(hreq->query, "speaker-id"); if (param) { ret = safe_atou64(param, &id); @@ -970,7 +936,7 @@ dacp_propset_volume(const char *value, struct httpd_request *hreq) return; } - param = evhttp_find_header(hreq->query, "include-speaker-id"); + param = httpd_query_value_find(hreq->query, "include-speaker-id"); if (param) { ret = safe_atou64(param, &id); @@ -1037,6 +1003,8 @@ static void dacp_propset_playingtime(const char *value, struct httpd_request *hreq) { struct timeval tv; + int seek_target; + intptr_t seek_target_packed; int ret; ret = safe_atoi32(value, &seek_target); @@ -1047,9 +1015,12 @@ dacp_propset_playingtime(const char *value, struct httpd_request *hreq) return; } + seek_target_packed = seek_target; + evutil_timerclear(&tv); tv.tv_usec = 200 * 1000; - evtimer_add(seek_timer, &tv); + + event_base_once(hreq->evbase, -1, EV_TIMEOUT, seek_timer_cb, (void *)seek_target_packed, &tv); } static void @@ -1102,9 +1073,9 @@ dacp_propset_userrating(const char *value, struct httpd_request *hreq) return; } - param = evhttp_find_header(hreq->query, "item-spec"); // Remote + param = httpd_query_value_find(hreq->query, "item-spec"); // Remote if (!param) - param = evhttp_find_header(hreq->query, "song-spec"); // Retune + param = httpd_query_value_find(hreq->query, "song-spec"); // Retune if (!param) { @@ -1169,33 +1140,33 @@ static int dacp_reply_ctrlint(struct httpd_request *hreq) { /* /ctrl-int */ - CHECK_ERR(L_DACP, evbuffer_expand(hreq->reply, 256)); + CHECK_ERR(L_DACP, evbuffer_expand(hreq->out_body, 256)); /* If tags are added or removed container sizes should be adjusted too */ - dmap_add_container(hreq->reply, "caci", 194); /* 8, unknown dacp container - size of content */ - dmap_add_int(hreq->reply, "mstt", 200); /* 12, dmap.status */ - dmap_add_char(hreq->reply, "muty", 0); /* 9, dmap.updatetype */ - dmap_add_int(hreq->reply, "mtco", 1); /* 12, dmap.specifiedtotalcount */ - dmap_add_int(hreq->reply, "mrco", 1); /* 12, dmap.returnedcount */ - dmap_add_container(hreq->reply, "mlcl", 141); /* 8, dmap.listing - size of content */ - dmap_add_container(hreq->reply, "mlit", 133); /* 8, dmap.listingitem - size of content */ - dmap_add_int(hreq->reply, "miid", 1); /* 12, dmap.itemid - database ID */ - dmap_add_char(hreq->reply, "cmik", 1); /* 9, unknown */ + dmap_add_container(hreq->out_body, "caci", 194); /* 8, unknown dacp container - size of content */ + dmap_add_int(hreq->out_body, "mstt", 200); /* 12, dmap.status */ + dmap_add_char(hreq->out_body, "muty", 0); /* 9, dmap.updatetype */ + dmap_add_int(hreq->out_body, "mtco", 1); /* 12, dmap.specifiedtotalcount */ + dmap_add_int(hreq->out_body, "mrco", 1); /* 12, dmap.returnedcount */ + dmap_add_container(hreq->out_body, "mlcl", 141); /* 8, dmap.listing - size of content */ + dmap_add_container(hreq->out_body, "mlit", 133); /* 8, dmap.listingitem - size of content */ + dmap_add_int(hreq->out_body, "miid", 1); /* 12, dmap.itemid - database ID */ + dmap_add_char(hreq->out_body, "cmik", 1); /* 9, unknown */ - dmap_add_int(hreq->reply, "cmpr", (2 << 16 | 2)); /* 12, dmcp.protocolversion */ - dmap_add_int(hreq->reply, "capr", (2 << 16 | 5)); /* 12, dacp.protocolversion */ + dmap_add_int(hreq->out_body, "cmpr", (2 << 16 | 2)); /* 12, dmcp.protocolversion */ + dmap_add_int(hreq->out_body, "capr", (2 << 16 | 5)); /* 12, dacp.protocolversion */ - dmap_add_char(hreq->reply, "cmsp", 1); /* 9, unknown */ - dmap_add_char(hreq->reply, "aeFR", 0x64); /* 9, unknown */ - dmap_add_char(hreq->reply, "cmsv", 1); /* 9, unknown */ - dmap_add_char(hreq->reply, "cass", 1); /* 9, unknown */ - dmap_add_char(hreq->reply, "caov", 1); /* 9, unknown */ - dmap_add_char(hreq->reply, "casu", 1); /* 9, unknown */ - dmap_add_char(hreq->reply, "ceSG", 1); /* 9, unknown */ - dmap_add_char(hreq->reply, "cmrl", 1); /* 9, unknown */ - dmap_add_long(hreq->reply, "ceSX", (1 << 1 | 1)); /* 16, unknown dacp - lowest bit announces support for playqueue-contents/-edit */ + dmap_add_char(hreq->out_body, "cmsp", 1); /* 9, unknown */ + dmap_add_char(hreq->out_body, "aeFR", 0x64); /* 9, unknown */ + dmap_add_char(hreq->out_body, "cmsv", 1); /* 9, unknown */ + dmap_add_char(hreq->out_body, "cass", 1); /* 9, unknown */ + dmap_add_char(hreq->out_body, "caov", 1); /* 9, unknown */ + dmap_add_char(hreq->out_body, "casu", 1); /* 9, unknown */ + dmap_add_char(hreq->out_body, "ceSG", 1); /* 9, unknown */ + dmap_add_char(hreq->out_body, "cmrl", 1); /* 9, unknown */ + dmap_add_long(hreq->out_body, "ceSX", (1 << 1 | 1)); /* 16, unknown dacp - lowest bit announces support for playqueue-contents/-edit */ - httpd_send_reply(hreq->req, HTTP_OK, "OK", hreq->reply, 0); + httpd_send_reply(hreq, HTTP_OK, "OK", 0); return 0; } @@ -1215,7 +1186,7 @@ dacp_reply_cue_play(struct httpd_request *hreq) /* /cue?command=play&query=...&sort=...&index=N */ - param = evhttp_find_header(hreq->query, "clear-first"); + param = httpd_query_value_find(hreq->query, "clear-first"); if (param) { ret = safe_atoi32(param, &clear); @@ -1231,17 +1202,17 @@ dacp_reply_cue_play(struct httpd_request *hreq) player_get_status(&status); - cuequery = evhttp_find_header(hreq->query, "query"); + cuequery = httpd_query_value_find(hreq->query, "query"); if (cuequery) { - sort = evhttp_find_header(hreq->query, "sort"); + sort = httpd_query_value_find(hreq->query, "sort"); ret = dacp_queueitem_add(cuequery, NULL, sort, 0, 0); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not build song queue\n"); - dmap_send_error(hreq->req, "cacr", "Could not build song queue"); + dacp_send_error(hreq, "cacr", "Could not build song queue"); return -1; } } @@ -1250,12 +1221,12 @@ dacp_reply_cue_play(struct httpd_request *hreq) player_playback_stop(); } - param = evhttp_find_header(hreq->query, "dacp.shufflestate"); + param = httpd_query_value_find(hreq->query, "dacp.shufflestate"); if (param) dacp_propset_shufflestate(param, NULL); pos = 0; - param = evhttp_find_header(hreq->query, "index"); + param = httpd_query_value_find(hreq->query, "index"); if (param) { ret = safe_atou32(param, &pos); @@ -1264,10 +1235,10 @@ dacp_reply_cue_play(struct httpd_request *hreq) } /* If selection was from Up Next queue or history queue (command will be playnow), then index is relative */ - if ((param = evhttp_find_header(hreq->query, "command")) && (strcmp(param, "playnow") == 0)) + if ((param = httpd_query_value_find(hreq->query, "command")) && (strcmp(param, "playnow") == 0)) { /* If mode parameter is -1 or 1, the index is relative to the history queue, otherwise to the Up Next queue */ - param = evhttp_find_header(hreq->query, "mode"); + param = httpd_query_value_find(hreq->query, "mode"); if (param && ((strcmp(param, "-1") == 0) || (strcmp(param, "1") == 0))) { /* Play from history queue */ @@ -1281,7 +1252,7 @@ dacp_reply_cue_play(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Could not start playback from history\n"); - dmap_send_error(hreq->req, "cacr", "Playback failed to start"); + dacp_send_error(hreq, "cacr", "Playback failed to start"); return -1; } } @@ -1289,7 +1260,7 @@ dacp_reply_cue_play(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Could not start playback from history\n"); - dmap_send_error(hreq->req, "cacr", "Playback failed to start"); + dacp_send_error(hreq, "cacr", "Playback failed to start"); return -1; } } @@ -1304,7 +1275,7 @@ dacp_reply_cue_play(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Could not fetch item from queue: pos=%d, now playing=%d\n", pos, status.item_id); - dmap_send_error(hreq->req, "cacr", "Playback failed to start"); + dacp_send_error(hreq, "cacr", "Playback failed to start"); return -1; } } @@ -1316,7 +1287,7 @@ dacp_reply_cue_play(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Could not fetch item from queue: pos=%d\n", pos); - dmap_send_error(hreq->req, "cacr", "Playback failed to start"); + dacp_send_error(hreq, "cacr", "Playback failed to start"); return -1; } } @@ -1327,19 +1298,19 @@ dacp_reply_cue_play(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Could not start playback\n"); - dmap_send_error(hreq->req, "cacr", "Playback failed to start"); + dacp_send_error(hreq, "cacr", "Playback failed to start"); return -1; } player_get_status(&status); - CHECK_ERR(L_DACP, evbuffer_expand(hreq->reply, 64)); + CHECK_ERR(L_DACP, evbuffer_expand(hreq->out_body, 64)); - dmap_add_container(hreq->reply, "cacr", 24); /* 8 + len */ - dmap_add_int(hreq->reply, "mstt", 200); /* 12 */ - dmap_add_int(hreq->reply, "miid", status.id);/* 12 */ + dmap_add_container(hreq->out_body, "cacr", 24); /* 8 + len */ + dmap_add_int(hreq->out_body, "mstt", 200); /* 12 */ + dmap_add_int(hreq->out_body, "miid", status.id);/* 12 */ - httpd_send_reply(hreq->req, HTTP_OK, "OK", hreq->reply, 0); + httpd_send_reply(hreq, HTTP_OK, "OK", 0); return 0; } @@ -1353,13 +1324,13 @@ dacp_reply_cue_clear(struct httpd_request *hreq) db_queue_clear(0); - CHECK_ERR(L_DACP, evbuffer_expand(hreq->reply, 64)); + CHECK_ERR(L_DACP, evbuffer_expand(hreq->out_body, 64)); - dmap_add_container(hreq->reply, "cacr", 24); /* 8 + len */ - dmap_add_int(hreq->reply, "mstt", 200); /* 12 */ - dmap_add_int(hreq->reply, "miid", 0); /* 12 */ + dmap_add_container(hreq->out_body, "cacr", 24); /* 8 + len */ + dmap_add_int(hreq->out_body, "mstt", 200); /* 12 */ + dmap_add_int(hreq->out_body, "miid", 0); /* 12 */ - httpd_send_reply(hreq->req, HTTP_OK, "OK", hreq->reply, 0); + httpd_send_reply(hreq, HTTP_OK, "OK", 0); return 0; } @@ -1374,12 +1345,12 @@ dacp_reply_cue(struct httpd_request *hreq) if (ret < 0) return -1; - param = evhttp_find_header(hreq->query, "command"); + param = httpd_query_value_find(hreq->query, "command"); if (!param) { DPRINTF(E_LOG, L_DACP, "No command in cue request\n"); - dmap_send_error(hreq->req, "cacr", "No command in cue request"); + dacp_send_error(hreq, "cacr", "No command in cue request"); return -1; } @@ -1391,7 +1362,7 @@ dacp_reply_cue(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Unknown cue command %s\n", param); - dmap_send_error(hreq->req, "cacr", "Unknown command in cue request"); + dacp_send_error(hreq, "cacr", "Unknown command in cue request"); return -1; } } @@ -1408,12 +1379,12 @@ dacp_reply_play(struct httpd_request *hreq) ret = player_playback_start(); if (ret < 0) { - httpd_send_error(hreq->req, 500, "Internal Server Error"); + httpd_send_error(hreq, HTTP_INTERNAL, "Internal Server Error"); return -1; } /* 204 No Content is the canonical reply */ - httpd_send_reply(hreq->req, HTTP_NOCONTENT, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_NOCONTENT, "No Content", HTTPD_SEND_NO_GZIP); return 0; } @@ -1440,10 +1411,10 @@ dacp_reply_playspec(struct httpd_request *hreq) return -1; /* Check for shuffle */ - shuffle = evhttp_find_header(hreq->query, "dacp.shufflestate"); + shuffle = httpd_query_value_find(hreq->query, "dacp.shufflestate"); /* Playlist ID */ - param = evhttp_find_header(hreq->query, "container-spec"); + param = httpd_query_value_find(hreq->query, "container-spec"); if (!param) { DPRINTF(E_LOG, L_DACP, "No container-spec in playspec request\n"); @@ -1472,9 +1443,9 @@ dacp_reply_playspec(struct httpd_request *hreq) if (!shuffle) { /* Start song ID */ - if ((param = evhttp_find_header(hreq->query, "item-spec"))) + if ((param = httpd_query_value_find(hreq->query, "item-spec"))) plid = 0; // This is a podcast/audiobook - just play a single item, not a playlist - else if (!(param = evhttp_find_header(hreq->query, "container-item-spec"))) + else if (!(param = httpd_query_value_find(hreq->query, "container-item-spec"))) { DPRINTF(E_LOG, L_DACP, "No container-item-spec/item-spec in playspec request\n"); goto out_fail; @@ -1545,11 +1516,11 @@ dacp_reply_playspec(struct httpd_request *hreq) } /* 204 No Content is the canonical reply */ - httpd_send_reply(hreq->req, HTTP_NOCONTENT, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_NOCONTENT, "No Content", HTTPD_SEND_NO_GZIP); return 0; out_fail: - httpd_send_error(hreq->req, 500, "Internal Server Error"); + httpd_send_error(hreq, HTTP_INTERNAL, "Internal Server Error"); return -1; } @@ -1566,7 +1537,7 @@ dacp_reply_stop(struct httpd_request *hreq) player_playback_stop(); /* 204 No Content is the canonical reply */ - httpd_send_reply(hreq->req, HTTP_NOCONTENT, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_NOCONTENT, "No Content", HTTPD_SEND_NO_GZIP); return 0; } @@ -1583,7 +1554,7 @@ dacp_reply_pause(struct httpd_request *hreq) player_playback_pause(); /* 204 No Content is the canonical reply */ - httpd_send_reply(hreq->req, HTTP_NOCONTENT, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_NOCONTENT, "No Content", HTTPD_SEND_NO_GZIP); return 0; } @@ -1610,13 +1581,13 @@ dacp_reply_playpause(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Player returned an error for start after pause\n"); - httpd_send_error(hreq->req, 500, "Internal Server Error"); + httpd_send_error(hreq, HTTP_INTERNAL, "Internal Server Error"); return -1; } } /* 204 No Content is the canonical reply */ - httpd_send_reply(hreq->req, HTTP_NOCONTENT, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_NOCONTENT, "No Content", HTTPD_SEND_NO_GZIP); return 0; } @@ -1635,7 +1606,7 @@ dacp_reply_nextitem(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Player returned an error for nextitem\n"); - httpd_send_error(hreq->req, 500, "Internal Server Error"); + httpd_send_error(hreq, HTTP_INTERNAL, "Internal Server Error"); return -1; } @@ -1644,12 +1615,12 @@ dacp_reply_nextitem(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Player returned an error for start after nextitem\n"); - httpd_send_error(hreq->req, 500, "Internal Server Error"); + httpd_send_error(hreq, HTTP_INTERNAL, "Internal Server Error"); return -1; } /* 204 No Content is the canonical reply */ - httpd_send_reply(hreq->req, HTTP_NOCONTENT, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_NOCONTENT, "No Content", HTTPD_SEND_NO_GZIP); return 0; } @@ -1668,7 +1639,7 @@ dacp_reply_previtem(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Player returned an error for previtem\n"); - httpd_send_error(hreq->req, 500, "Internal Server Error"); + httpd_send_error(hreq, HTTP_INTERNAL, "Internal Server Error"); return -1; } @@ -1677,12 +1648,12 @@ dacp_reply_previtem(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Player returned an error for start after previtem\n"); - httpd_send_error(hreq->req, 500, "Internal Server Error"); + httpd_send_error(hreq, HTTP_INTERNAL, "Internal Server Error"); return -1; } /* 204 No Content is the canonical reply */ - httpd_send_reply(hreq->req, HTTP_NOCONTENT, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_NOCONTENT, "No Content", HTTPD_SEND_NO_GZIP); return 0; } @@ -1699,7 +1670,7 @@ dacp_reply_beginff(struct httpd_request *hreq) /* TODO */ /* 204 No Content is the canonical reply */ - httpd_send_reply(hreq->req, HTTP_NOCONTENT, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_NOCONTENT, "No Content", HTTPD_SEND_NO_GZIP); return 0; } @@ -1716,7 +1687,7 @@ dacp_reply_beginrew(struct httpd_request *hreq) /* TODO */ /* 204 No Content is the canonical reply */ - httpd_send_reply(hreq->req, HTTP_NOCONTENT, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_NOCONTENT, "No Content", HTTPD_SEND_NO_GZIP); return 0; } @@ -1733,7 +1704,7 @@ dacp_reply_playresume(struct httpd_request *hreq) /* TODO */ /* 204 No Content is the canonical reply */ - httpd_send_reply(hreq->req, HTTP_NOCONTENT, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_NOCONTENT, "No Content", HTTPD_SEND_NO_GZIP); return 0; } @@ -1764,7 +1735,7 @@ dacp_reply_playqueuecontents(struct httpd_request *hreq) DPRINTF(E_DBG, L_DACP, "Fetching playqueue contents\n"); span = 50; /* Default */ - param = evhttp_find_header(hreq->query, "span"); + param = httpd_query_value_find(hreq->query, "span"); if (param) { ret = safe_atoi32(param, &span); @@ -1773,7 +1744,7 @@ dacp_reply_playqueuecontents(struct httpd_request *hreq) } CHECK_NULL(L_DACP, songlist = evbuffer_new()); - CHECK_ERR(L_DACP, evbuffer_expand(hreq->reply, 128)); + CHECK_ERR(L_DACP, evbuffer_expand(hreq->out_body, 128)); player_get_status(&status); @@ -1868,24 +1839,24 @@ dacp_reply_playqueuecontents(struct httpd_request *hreq) /* Final construction of reply */ playlist_length = evbuffer_get_length(playlists); - dmap_add_container(hreq->reply, "ceQR", 79 + playlist_length + songlist_length); /* size of entire container */ - dmap_add_int(hreq->reply, "mstt", 200); /* 12, dmap.status */ - dmap_add_int(hreq->reply, "mtco", abs(span)); /* 12 */ - dmap_add_int(hreq->reply, "mrco", count); /* 12 */ - dmap_add_char(hreq->reply, "ceQu", 0); /* 9 */ - dmap_add_container(hreq->reply, "mlcl", 8 + playlist_length + songlist_length); /* 8 */ - dmap_add_container(hreq->reply, "ceQS", playlist_length); /* 8 */ + dmap_add_container(hreq->out_body, "ceQR", 79 + playlist_length + songlist_length); /* size of entire container */ + dmap_add_int(hreq->out_body, "mstt", 200); /* 12, dmap.status */ + dmap_add_int(hreq->out_body, "mtco", abs(span)); /* 12 */ + dmap_add_int(hreq->out_body, "mrco", count); /* 12 */ + dmap_add_char(hreq->out_body, "ceQu", 0); /* 9 */ + dmap_add_container(hreq->out_body, "mlcl", 8 + playlist_length + songlist_length); /* 8 */ + dmap_add_container(hreq->out_body, "ceQS", playlist_length); /* 8 */ - CHECK_ERR(L_DACP, evbuffer_add_buffer(hreq->reply, playlists)); - CHECK_ERR(L_DACP, evbuffer_add_buffer(hreq->reply, songlist)); + CHECK_ERR(L_DACP, evbuffer_add_buffer(hreq->out_body, playlists)); + CHECK_ERR(L_DACP, evbuffer_add_buffer(hreq->out_body, songlist)); evbuffer_free(playlists); evbuffer_free(songlist); - dmap_add_char(hreq->reply, "apsm", status.shuffle); /* 9, daap.playlistshufflemode - not part of mlcl container */ - dmap_add_char(hreq->reply, "aprm", status.repeat); /* 9, daap.playlistrepeatmode - not part of mlcl container */ + dmap_add_char(hreq->out_body, "apsm", status.shuffle); /* 9, daap.playlistshufflemode - not part of mlcl container */ + dmap_add_char(hreq->out_body, "aprm", status.repeat); /* 9, daap.playlistrepeatmode - not part of mlcl container */ - httpd_send_reply(hreq->req, HTTP_OK, "OK", hreq->reply, 0); + httpd_send_reply(hreq, HTTP_OK, "OK", 0); return 0; @@ -1893,7 +1864,7 @@ dacp_reply_playqueuecontents(struct httpd_request *hreq) DPRINTF(E_LOG, L_DACP, "Database error in dacp_reply_playqueuecontents\n"); evbuffer_free(songlist); - dmap_send_error(hreq->req, "ceQR", "Database error"); + dacp_send_error(hreq, "ceQR", "Database error"); return -1; } @@ -1904,7 +1875,7 @@ dacp_reply_playqueueedit_clear(struct httpd_request *hreq) const char *param; struct player_status status; - param = evhttp_find_header(hreq->query, "mode"); + param = httpd_query_value_find(hreq->query, "mode"); /* * The mode parameter contains the playlist to be cleared. @@ -1919,11 +1890,11 @@ dacp_reply_playqueueedit_clear(struct httpd_request *hreq) db_queue_clear(status.item_id); } - dmap_add_container(hreq->reply, "cacr", 24); /* 8 + len */ - dmap_add_int(hreq->reply, "mstt", 200); /* 12 */ - dmap_add_int(hreq->reply, "miid", 0); /* 12 */ + dmap_add_container(hreq->out_body, "cacr", 24); /* 8 + len */ + dmap_add_int(hreq->out_body, "mstt", 200); /* 12 */ + dmap_add_int(hreq->out_body, "miid", 0); /* 12 */ - httpd_send_reply(hreq->req, HTTP_OK, "OK", hreq->reply, 0); + httpd_send_reply(hreq, HTTP_OK, "OK", 0); return 0; } @@ -1955,7 +1926,7 @@ dacp_reply_playqueueedit_add(struct httpd_request *hreq) mode = 1; - param = evhttp_find_header(hreq->query, "mode"); + param = httpd_query_value_find(hreq->query, "mode"); if (param) { ret = safe_atoi32(param, &mode); @@ -1963,7 +1934,7 @@ dacp_reply_playqueueedit_add(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Invalid mode value in playqueue-edit request\n"); - dmap_send_error(hreq->req, "cacr", "Invalid request"); + dacp_send_error(hreq, "cacr", "Invalid request"); return -1; } } @@ -1977,16 +1948,16 @@ dacp_reply_playqueueedit_add(struct httpd_request *hreq) if (mode == 2) player_shuffle_set(1); - editquery = evhttp_find_header(hreq->query, "query"); + editquery = httpd_query_value_find(hreq->query, "query"); if (!editquery) { DPRINTF(E_LOG, L_DACP, "Could not add song queue, DACP query missing\n"); - dmap_send_error(hreq->req, "cacr", "Invalid request"); + dacp_send_error(hreq, "cacr", "Invalid request"); return -1; } - sort = evhttp_find_header(hreq->query, "sort"); + sort = httpd_query_value_find(hreq->query, "sort"); // if sort param is missing and an album or artist is added to the queue, set sort to "album" if (!sort && (strstr(editquery, "daap.songalbumid:") || strstr(editquery, "daap.songartistid:"))) @@ -1995,9 +1966,9 @@ dacp_reply_playqueueedit_add(struct httpd_request *hreq) } // only use queryfilter if mode is not equal 0 (add to up next), 3 (play next) or 5 (add to up next) - queuefilter = (mode == 0 || mode == 3 || mode == 5) ? NULL : evhttp_find_header(hreq->query, "queuefilter"); + queuefilter = (mode == 0 || mode == 3 || mode == 5) ? NULL : httpd_query_value_find(hreq->query, "queuefilter"); - querymodifier = evhttp_find_header(hreq->query, "query-modifier"); + querymodifier = httpd_query_value_find(hreq->query, "query-modifier"); if (!querymodifier || (strcmp(querymodifier, "containers") != 0)) { quirkyquery = (mode == 1) && strstr(editquery, "dmap.itemid:") && ((!queuefilter) || strstr(queuefilter, "(null)")); @@ -2011,7 +1982,7 @@ dacp_reply_playqueueedit_add(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Invalid playlist id in request: %s\n", editquery); - dmap_send_error(hreq->req, "cacr", "Invalid request"); + dacp_send_error(hreq, "cacr", "Invalid request"); return -1; } @@ -2023,7 +1994,7 @@ dacp_reply_playqueueedit_add(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Could not build song queue\n"); - dmap_send_error(hreq->req, "cacr", "Invalid request"); + dacp_send_error(hreq, "cacr", "Invalid request"); return -1; } @@ -2056,12 +2027,12 @@ dacp_reply_playqueueedit_add(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Could not start playback\n"); - dmap_send_error(hreq->req, "cacr", "Playback failed to start"); + dacp_send_error(hreq, "cacr", "Playback failed to start"); return -1; } /* 204 No Content is the canonical reply */ - httpd_send_reply(hreq->req, HTTP_NOCONTENT, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_NOCONTENT, "No Content", HTTPD_SEND_NO_GZIP); return 0; } @@ -2083,7 +2054,7 @@ dacp_reply_playqueueedit_move(struct httpd_request *hreq) int src; int dst; - param = evhttp_find_header(hreq->query, "edit-params"); + param = httpd_query_value_find(hreq->query, "edit-params"); if (param) { ret = safe_atoi32(strchr(param, ':') + 1, &src); @@ -2091,7 +2062,7 @@ dacp_reply_playqueueedit_move(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Invalid edit-params move-from value in playqueue-edit request\n"); - dmap_send_error(hreq->req, "cacr", "Invalid request"); + dacp_send_error(hreq, "cacr", "Invalid request"); return -1; } @@ -2100,7 +2071,7 @@ dacp_reply_playqueueedit_move(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Invalid edit-params move-to value in playqueue-edit request\n"); - dmap_send_error(hreq->req, "cacr", "Invalid request"); + dacp_send_error(hreq, "cacr", "Invalid request"); return -1; } @@ -2109,7 +2080,7 @@ dacp_reply_playqueueedit_move(struct httpd_request *hreq) } /* 204 No Content is the canonical reply */ - httpd_send_reply(hreq->req, HTTP_NOCONTENT, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_NOCONTENT, "No Content", HTTPD_SEND_NO_GZIP); return 0; } @@ -2127,7 +2098,7 @@ dacp_reply_playqueueedit_remove(struct httpd_request *hreq) int item_index; int ret; - param = evhttp_find_header(hreq->query, "items"); + param = httpd_query_value_find(hreq->query, "items"); if (param) { ret = safe_atoi32(param, &item_index); @@ -2135,7 +2106,7 @@ dacp_reply_playqueueedit_remove(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Invalid edit-params remove item value in playqueue-edit request\n"); - dmap_send_error(hreq->req, "cacr", "Invalid request"); + dacp_send_error(hreq, "cacr", "Invalid request"); return -1; } @@ -2145,7 +2116,7 @@ dacp_reply_playqueueedit_remove(struct httpd_request *hreq) } /* 204 No Content is the canonical reply */ - httpd_send_reply(hreq->req, HTTP_NOCONTENT, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_NOCONTENT, "No Content", HTTPD_SEND_NO_GZIP); return 0; } @@ -2203,12 +2174,12 @@ dacp_reply_playqueueedit(struct httpd_request *hreq) if (ret < 0) return -1; - param = evhttp_find_header(hreq->query, "command"); + param = httpd_query_value_find(hreq->query, "command"); if (!param) { DPRINTF(E_LOG, L_DACP, "No command in playqueue-edit request\n"); - dmap_send_error(hreq->req, "cmst", "Invalid request"); + dacp_send_error(hreq, "cmst", "Invalid request"); return -1; } @@ -2226,7 +2197,7 @@ dacp_reply_playqueueedit(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Unknown playqueue-edit command %s\n", param); - dmap_send_error(hreq->req, "cmst", "Invalid request"); + dacp_send_error(hreq, "cmst", "Invalid request"); return -1; } } @@ -2235,8 +2206,6 @@ static int dacp_reply_playstatusupdate(struct httpd_request *hreq) { struct dacp_update_request *ur; - struct evhttp_connection *evcon; - struct bufferevent *bufev; const char *param; int reqd_rev; int ret; @@ -2245,12 +2214,12 @@ dacp_reply_playstatusupdate(struct httpd_request *hreq) if (ret < 0) return -1; - param = evhttp_find_header(hreq->query, "revision-number"); + param = httpd_query_value_find(hreq->query, "revision-number"); if (!param) { DPRINTF(E_LOG, L_DACP, "Missing revision-number in update request\n"); - dmap_send_error(hreq->req, "cmst", "Invalid request"); + dacp_send_error(hreq, "cmst", "Invalid request"); return -1; } @@ -2259,56 +2228,42 @@ dacp_reply_playstatusupdate(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Parameter revision-number not an integer\n"); - dmap_send_error(hreq->req, "cmst", "Invalid request"); + dacp_send_error(hreq, "cmst", "Invalid request"); return -1; } // Caller didn't use current revision number. It was probably his first // request so we will give him status immediately, incl. which revision number // to use when he calls again. - if (reqd_rev != current_rev) + if (reqd_rev != update_current_rev) { - ret = make_playstatusupdate(hreq->reply); + ret = make_playstatusupdate(hreq->out_body, update_current_rev); if (ret < 0) - httpd_send_error(hreq->req, 500, "Internal Server Error"); + httpd_send_error(hreq, HTTP_INTERNAL, "Internal Server Error"); else - httpd_send_reply(hreq->req, HTTP_OK, "OK", hreq->reply, 0); + httpd_send_reply(hreq, HTTP_OK, "OK", 0); return ret; } // Else, just let the request hang until we have changes to push back - ur = calloc(1, sizeof(struct dacp_update_request)); + ur = update_request_new(hreq); if (!ur) { DPRINTF(E_LOG, L_DACP, "Out of memory for update request\n"); - dmap_send_error(hreq->req, "cmst", "Out of memory"); + dacp_send_error(hreq, "cmst", "Out of memory"); return -1; } - ur->req = hreq->req; - + pthread_mutex_lock(&update_request_lck); ur->next = update_requests; update_requests = ur; + pthread_mutex_unlock(&update_request_lck); - /* If the connection fails before we have an update to push out - * to the client, we need to know. - */ - evcon = evhttp_request_get_connection(hreq->req); - if (evcon) - { - evhttp_connection_set_closecb(evcon, update_fail_cb, ur); - - // This is a workaround for some versions of libevent (2.0, but possibly - // also 2.1) that don't detect if the client hangs up, and thus don't - // clean up and never call update_fail_cb(). See github issue #870 and - // https://github.com/libevent/libevent/issues/666. It should probably be - // removed again in the future. The workaround is also present in daap.c - bufev = evhttp_connection_get_bufferevent(evcon); - if (bufev) - bufferevent_enable(bufev, EV_READ); - } + // If the connection fails before we have an update to push out to the client, + // we need to know. + httpd_request_close_cb_set(hreq, update_fail_cb, ur); return 0; } @@ -2317,7 +2272,6 @@ static int dacp_reply_nowplayingartwork(struct httpd_request *hreq) { char clen[32]; - struct evkeyvalq *headers; const char *param; char *ctype; size_t len; @@ -2330,7 +2284,7 @@ dacp_reply_nowplayingartwork(struct httpd_request *hreq) if (ret < 0) return -1; - param = evhttp_find_header(hreq->query, "mw"); + param = httpd_query_value_find(hreq->query, "mw"); if (!param) { DPRINTF(E_LOG, L_DACP, "Request for artwork without mw parameter\n"); @@ -2344,7 +2298,7 @@ dacp_reply_nowplayingartwork(struct httpd_request *hreq) goto error; } - param = evhttp_find_header(hreq->query, "mh"); + param = httpd_query_value_find(hreq->query, "mh"); if (!param) { DPRINTF(E_LOG, L_DACP, "Request for artwork without mh parameter\n"); @@ -2362,8 +2316,8 @@ dacp_reply_nowplayingartwork(struct httpd_request *hreq) if (ret < 0) goto no_artwork; - ret = artwork_get_item(hreq->reply, id, max_w, max_h, 0); - len = evbuffer_get_length(hreq->reply); + ret = artwork_get_item(hreq->out_body, id, max_w, max_h, 0); + len = evbuffer_get_length(hreq->out_body); switch (ret) { @@ -2377,26 +2331,25 @@ dacp_reply_nowplayingartwork(struct httpd_request *hreq) default: if (len > 0) - evbuffer_drain(hreq->reply, len); + evbuffer_drain(hreq->out_body, len); goto no_artwork; } - headers = evhttp_request_get_output_headers(hreq->req); - evhttp_remove_header(headers, "Content-Type"); - evhttp_add_header(headers, "Content-Type", ctype); + httpd_header_remove(hreq->out_headers, "Content-Type"); + httpd_header_add(hreq->out_headers, "Content-Type", ctype); snprintf(clen, sizeof(clen), "%ld", (long)len); - evhttp_add_header(headers, "Content-Length", clen); + httpd_header_add(hreq->out_headers, "Content-Length", clen); - httpd_send_reply(hreq->req, HTTP_OK, "OK", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_OK, "OK", HTTPD_SEND_NO_GZIP); return 0; no_artwork: - httpd_send_error(hreq->req, HTTP_NOTFOUND, "Not Found"); + httpd_send_error(hreq, HTTP_NOTFOUND, "Not Found"); return 0; error: - httpd_send_error(hreq->req, HTTP_BADREQUEST, "Bad Request"); + httpd_send_error(hreq, HTTP_BADREQUEST, "Bad Request"); return -1; } @@ -2418,12 +2371,12 @@ dacp_reply_getproperty(struct httpd_request *hreq) if (ret < 0) return -1; - param = evhttp_find_header(hreq->query, "properties"); + param = httpd_query_value_find(hreq->query, "properties"); if (!param) { DPRINTF(E_WARN, L_DACP, "Invalid DACP getproperty request, no properties\n"); - dmap_send_error(hreq->req, "cmgt", "Invalid request"); + dacp_send_error(hreq, "cmgt", "Invalid request"); return -1; } @@ -2432,7 +2385,7 @@ dacp_reply_getproperty(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Could not duplicate properties parameter; out of memory\n"); - dmap_send_error(hreq->req, "cmgt", "Out of memory"); + dacp_send_error(hreq, "cmgt", "Out of memory"); return -1; } @@ -2441,7 +2394,7 @@ dacp_reply_getproperty(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Could not allocate evbuffer for properties list\n"); - dmap_send_error(hreq->req, "cmgt", "Out of memory"); + dacp_send_error(hreq, "cmgt", "Out of memory"); goto out_free_propstr; } @@ -2454,7 +2407,7 @@ dacp_reply_getproperty(struct httpd_request *hreq) { DPRINTF(E_LOG, L_DACP, "Could not fetch queue_item for item-id %d\n", status.item_id); - dmap_send_error(hreq->req, "cmgt", "Server error"); + dacp_send_error(hreq, "cmgt", "Server error"); goto out_free_proplist; } } @@ -2482,14 +2435,14 @@ dacp_reply_getproperty(struct httpd_request *hreq) free_queue_item(queue_item, 0); len = evbuffer_get_length(proplist); - dmap_add_container(hreq->reply, "cmgt", 12 + len); - dmap_add_int(hreq->reply, "mstt", 200); /* 12 */ + dmap_add_container(hreq->out_body, "cmgt", 12 + len); + dmap_add_int(hreq->out_body, "mstt", 200); /* 12 */ - CHECK_ERR(L_DACP, evbuffer_add_buffer(hreq->reply, proplist)); + CHECK_ERR(L_DACP, evbuffer_add_buffer(hreq->out_body, proplist)); evbuffer_free(proplist); - httpd_send_reply(hreq->req, HTTP_OK, "OK", hreq->reply, 0); + httpd_send_reply(hreq, HTTP_OK, "OK", 0); return 0; @@ -2502,11 +2455,27 @@ dacp_reply_getproperty(struct httpd_request *hreq) return -1; } +static void +setproperty_cb(const char *key, const char *val, void *arg) +{ + struct httpd_request *hreq = arg; + const struct dacp_prop_map *dpm = dacp_find_prop(key, strlen(key)); + + if (!dpm) + { + DPRINTF(E_SPAM, L_DACP, "Unknown DACP property %s\n", key); + return; + } + + if (dpm->propset) + dpm->propset(val, hreq); + else + DPRINTF(E_WARN, L_DACP, "No setter method for DACP property %s\n", dpm->desc); +} + static int dacp_reply_setproperty(struct httpd_request *hreq) { - const struct dacp_prop_map *dpm; - struct evkeyval *param; int ret; ret = dacp_request_authorize(hreq); @@ -2525,24 +2494,10 @@ dacp_reply_setproperty(struct httpd_request *hreq) /* /ctrl-int/1/setproperty?dacp.shufflestate=1&session-id=100 */ - TAILQ_FOREACH(param, hreq->query, next) - { - dpm = dacp_find_prop(param->key, strlen(param->key)); - - if (!dpm) - { - DPRINTF(E_SPAM, L_DACP, "Unknown DACP property %s\n", param->key); - continue; - } - - if (dpm->propset) - dpm->propset(param->value, hreq); - else - DPRINTF(E_WARN, L_DACP, "No setter method for DACP property %s\n", dpm->desc); - } + httpd_query_iterate(hreq->query, setproperty_cb, hreq); /* 204 No Content is the canonical reply */ - httpd_send_reply(hreq->req, HTTP_NOCONTENT, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_NOCONTENT, "No Content", HTTPD_SEND_NO_GZIP); return 0; } @@ -2563,14 +2518,14 @@ dacp_reply_getspeakers(struct httpd_request *hreq) player_speaker_enumerate(speaker_enum_cb, spklist); len = evbuffer_get_length(spklist); - dmap_add_container(hreq->reply, "casp", 12 + len); - dmap_add_int(hreq->reply, "mstt", 200); /* 12 */ + dmap_add_container(hreq->out_body, "casp", 12 + len); + dmap_add_int(hreq->out_body, "mstt", 200); /* 12 */ - evbuffer_add_buffer(hreq->reply, spklist); + evbuffer_add_buffer(hreq->out_body, spklist); evbuffer_free(spklist); - httpd_send_reply(hreq->req, HTTP_OK, "OK", hreq->reply, 0); + httpd_send_reply(hreq, HTTP_OK, "OK", 0); return 0; } @@ -2589,12 +2544,12 @@ dacp_reply_setspeakers(struct httpd_request *hreq) if (ret < 0) return -1; - param = evhttp_find_header(hreq->query, "speaker-id"); + param = httpd_query_value_find(hreq->query, "speaker-id"); if (!param) { DPRINTF(E_LOG, L_DACP, "Missing speaker-id parameter in DACP setspeakers request\n"); - httpd_send_error(hreq->req, HTTP_BADREQUEST, "Bad Request"); + httpd_send_error(hreq, HTTP_BADREQUEST, "Bad Request"); return -1; } @@ -2652,15 +2607,15 @@ dacp_reply_setspeakers(struct httpd_request *hreq) /* Password problem */ if (ret == -2) - httpd_send_error(hreq->req, 902, ""); + httpd_send_error(hreq, 902, ""); else - httpd_send_error(hreq->req, 500, "Internal Server Error"); + httpd_send_error(hreq, HTTP_INTERNAL, "Internal Server Error"); return -1; } /* 204 No Content is the canonical reply */ - httpd_send_reply(hreq->req, HTTP_NOCONTENT, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_NOCONTENT, "No Content", HTTPD_SEND_NO_GZIP); return 0; } @@ -2674,19 +2629,19 @@ dacp_reply_volumeup(struct httpd_request *hreq) ret = speaker_get(&speaker_info, hreq, "volumeup"); if (ret < 0) { - httpd_send_error(hreq->req, HTTP_BADREQUEST, "Bad Request"); + httpd_send_error(hreq, HTTP_BADREQUEST, "Bad Request"); return -1; } ret = speaker_volume_step(&speaker_info, DACP_VOLUME_STEP); if (ret < 0) { - httpd_send_error(hreq->req, HTTP_BADREQUEST, "Bad Request"); + httpd_send_error(hreq, HTTP_BADREQUEST, "Bad Request"); return -1; } /* 204 No Content is the canonical reply */ - httpd_send_reply(hreq->req, HTTP_NOCONTENT, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_NOCONTENT, "No Content", HTTPD_SEND_NO_GZIP); return 0; } @@ -2700,19 +2655,19 @@ dacp_reply_volumedown(struct httpd_request *hreq) ret = speaker_get(&speaker_info, hreq, "volumedown"); if (ret < 0) { - httpd_send_error(hreq->req, HTTP_BADREQUEST, "Bad Request"); + httpd_send_error(hreq, HTTP_BADREQUEST, "Bad Request"); return -1; } ret = speaker_volume_step(&speaker_info, -DACP_VOLUME_STEP); if (ret < 0) { - httpd_send_error(hreq->req, HTTP_BADREQUEST, "Bad Request"); + httpd_send_error(hreq, HTTP_BADREQUEST, "Bad Request"); return -1; } /* 204 No Content is the canonical reply */ - httpd_send_reply(hreq->req, HTTP_NOCONTENT, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_NOCONTENT, "No Content", HTTPD_SEND_NO_GZIP); return 0; } @@ -2726,7 +2681,7 @@ dacp_reply_mutetoggle(struct httpd_request *hreq) ret = speaker_get(&speaker_info, hreq, "mutetoggle"); if (ret < 0) { - httpd_send_error(hreq->req, HTTP_BADREQUEST, "Bad Request"); + httpd_send_error(hreq, HTTP_BADREQUEST, "Bad Request"); return -1; } @@ -2734,12 +2689,12 @@ dacp_reply_mutetoggle(struct httpd_request *hreq) ret = speaker_info.selected ? player_speaker_disable(speaker_info.id) : player_speaker_enable(speaker_info.id); if (ret < 0) { - httpd_send_error(hreq->req, 500, "Internal Server Error"); + httpd_send_error(hreq, HTTP_INTERNAL, "Internal Server Error"); return -1; } /* 204 No Content is the canonical reply */ - httpd_send_reply(hreq->req, HTTP_NOCONTENT, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_NOCONTENT, "No Content", HTTPD_SEND_NO_GZIP); return 0; } @@ -2800,7 +2755,7 @@ static struct httpd_uri_map dacp_handlers[] = }, { .regexp = "^/ctrl-int/[[:digit:]]+/playstatusupdate$", - .handler = dacp_reply_playstatusupdate + .handler = dacp_reply_playstatusupdate, }, { .regexp = "^/ctrl-int/[[:digit:]]+/playqueue-contents$", @@ -2851,57 +2806,27 @@ static struct httpd_uri_map dacp_handlers[] = /* ------------------------------- DACP API --------------------------------- */ -void -dacp_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed) +static void +dacp_request(struct httpd_request *hreq) { - struct httpd_request *hreq; - struct evkeyvalq *headers; - - DPRINTF(E_DBG, L_DACP, "DACP request: '%s'\n", uri_parsed->uri); - - hreq = httpd_request_parse(req, uri_parsed, NULL, dacp_handlers); - if (!hreq) + if (!hreq->handler) { - DPRINTF(E_LOG, L_DACP, "Unrecognized path '%s' in DACP request: '%s'\n", uri_parsed->path, uri_parsed->uri); + DPRINTF(E_LOG, L_DACP, "Unrecognized path in DACP request: '%s'\n", hreq->uri); - httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); + httpd_send_error(hreq, HTTP_BADREQUEST, "Bad Request"); return; } - headers = evhttp_request_get_output_headers(req); - evhttp_add_header(headers, "DAAP-Server", PACKAGE_NAME "/" VERSION); + httpd_header_add(hreq->out_headers, "DAAP-Server", PACKAGE_NAME "/" VERSION); /* Content-Type for all DACP replies; can be overriden as needed */ - evhttp_add_header(headers, "Content-Type", "application/x-dmap-tagged"); - - CHECK_NULL(L_DACP, hreq->reply = evbuffer_new()); + httpd_header_add(hreq->out_headers, "Content-Type", "application/x-dmap-tagged"); hreq->handler(hreq); - - evbuffer_free(hreq->reply); - free(hreq); } -int -dacp_is_request(const char *path) -{ - if (strncmp(path, "/ctrl-int/", strlen("/ctrl-int/")) == 0) - return 1; - if (strcmp(path, "/ctrl-int") == 0) - return 1; - - return 0; -} - -int +static int dacp_init(void) { - char buf[64]; - int i; - int ret; - - current_rev = 2; - update_requests = NULL; - dummy_mfi.id = DB_MEDIA_FILE_NON_PERSISTENT_ID; dummy_mfi.title = CFG_NAME_UNKNOWN_TITLE; dummy_mfi.artist = CFG_NAME_UNKNOWN_ARTIST; @@ -2914,109 +2839,38 @@ dacp_init(void) dummy_queue_item.album = CFG_NAME_UNKNOWN_ALBUM; dummy_queue_item.genre = CFG_NAME_UNKNOWN_GENRE; -#ifdef HAVE_EVENTFD - update_efd = eventfd(0, EFD_CLOEXEC); - if (update_efd < 0) - { - DPRINTF(E_LOG, L_DACP, "Could not create update eventfd: %s\n", strerror(errno)); - - return -1; - } -#else -# ifdef HAVE_PIPE2 - ret = pipe2(update_pipe, O_CLOEXEC); -# else - ret = pipe(update_pipe); -# endif - if (ret < 0) - { - DPRINTF(E_LOG, L_DACP, "Could not create update pipe: %s\n", strerror(errno)); - - return -1; - } -#endif /* HAVE_EVENTFD */ - - for (i = 0; dacp_handlers[i].handler; i++) - { - ret = regcomp(&dacp_handlers[i].preg, dacp_handlers[i].regexp, REG_EXTENDED | REG_NOSUB); - if (ret != 0) - { - regerror(ret, &dacp_handlers[i].preg, buf, sizeof(buf)); - - DPRINTF(E_FATAL, L_DACP, "DACP init failed; regexp error: %s\n", buf); - goto regexp_fail; - } - } - -#ifdef HAVE_EVENTFD - updateev = event_new(evbase_httpd, update_efd, EV_READ, playstatusupdate_cb, NULL); -#else - updateev = event_new(evbase_httpd, update_pipe[0], EV_READ, playstatusupdate_cb, NULL); -#endif - if (!updateev) - { - DPRINTF(E_LOG, L_DACP, "Could not create update event\n"); - - return -1; - } - event_add(updateev, NULL); - - seek_timer = evtimer_new(evbase_httpd, seek_timer_cb, NULL); - if (!seek_timer) - { - DPRINTF(E_LOG, L_DACP, "Could not create seek_timer event\n"); - - return -1; - } - + CHECK_ERR(L_DACP, mutex_init(&update_request_lck)); + update_current_rev = 2; listener_add(dacp_playstatus_update_handler, LISTENER_PLAYER | LISTENER_VOLUME | LISTENER_QUEUE); return 0; - - regexp_fail: -#ifdef HAVE_EVENTFD - close(update_efd); -#else - close(update_pipe[0]); - close(update_pipe[1]); -#endif - return -1; } -void +static void dacp_deinit(void) { struct dacp_update_request *ur; - struct evhttp_connection *evcon; - int i; listener_remove(dacp_playstatus_update_handler); - event_free(seek_timer); - - for (i = 0; dacp_handlers[i].handler; i++) - regfree(&dacp_handlers[i].preg); - for (ur = update_requests; update_requests; ur = update_requests) { update_requests = ur->next; - evcon = evhttp_request_get_connection(ur->req); - if (evcon) - { - evhttp_connection_set_closecb(evcon, NULL, NULL); - evhttp_connection_free(evcon); - } - - free(ur); + httpd_send_error(ur->hreq, HTTP_SERVUNAVAIL, "Service Unavailable"); + update_request_free(ur); } - - event_free(updateev); - -#ifdef HAVE_EVENTFD - close(update_efd); -#else - close(update_pipe[0]); - close(update_pipe[1]); -#endif } + +struct httpd_module httpd_dacp = +{ + .name = "DACP", + .type = MODULE_DACP, + .logdomain = L_DACP, + .subpaths = { "/ctrl-int/", NULL }, + .fullpaths = { "/ctrl-int", NULL }, + .handlers = dacp_handlers, + .init = dacp_init, + .deinit = dacp_deinit, + .request = dacp_request, +}; diff --git a/src/httpd_dacp.h b/src/httpd_dacp.h deleted file mode 100644 index 307543dc..00000000 --- a/src/httpd_dacp.h +++ /dev/null @@ -1,19 +0,0 @@ - -#ifndef __HTTPD_DACP_H__ -#define __HTTPD_DACP_H__ - -#include "httpd.h" - -int -dacp_init(void); - -void -dacp_deinit(void); - -void -dacp_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed); - -int -dacp_is_request(const char *path); - -#endif /* !__HTTPD_DACP_H__ */ diff --git a/src/httpd_internal.h b/src/httpd_internal.h new file mode 100644 index 00000000..0157601e --- /dev/null +++ b/src/httpd_internal.h @@ -0,0 +1,366 @@ + +#ifndef __HTTPD_INTERNAL_H__ +#define __HTTPD_INTERNAL_H__ + +#include +#include +#include + +#ifdef HAVE_CONFIG_H +# include +#endif + +/* Response codes from event2/http.h */ +#define HTTP_CONTINUE 100 /**< client should proceed to send */ +#define HTTP_SWITCH_PROTOCOLS 101 /**< switching to another protocol */ +#define HTTP_PROCESSING 102 /**< processing the request, but no response is available yet */ +#define HTTP_EARLYHINTS 103 /**< return some response headers */ +#define HTTP_OK 200 /**< request completed ok */ +#define HTTP_CREATED 201 /**< new resource is created */ +#define HTTP_ACCEPTED 202 /**< accepted for processing */ +#define HTTP_NONAUTHORITATIVE 203 /**< returning a modified version of the origin's response */ +#define HTTP_NOCONTENT 204 /**< request does not have content */ +#define HTTP_MOVEPERM 301 /**< the uri moved permanently */ +#define HTTP_MOVETEMP 302 /**< the uri moved temporarily */ +#define HTTP_NOTMODIFIED 304 /**< page was not modified from last */ +#define HTTP_BADREQUEST 400 /**< invalid http request was made */ +#define HTTP_UNAUTHORIZED 401 /**< authentication is required */ +#define HTTP_PAYMENTREQUIRED 402 /**< user exceeded limit on requests */ +#define HTTP_FORBIDDEN 403 /**< user not having the necessary permissions */ +#define HTTP_NOTFOUND 404 /**< could not find content for uri */ +#define HTTP_BADMETHOD 405 /**< method not allowed for this uri */ +#define HTTP_ENTITYTOOLARGE 413 /**< request is larger than the server is able to process */ +#define HTTP_EXPECTATIONFAILED 417 /**< we can't handle this expectation */ +#define HTTP_INTERNAL 500 /**< internal error */ +#define HTTP_NOTIMPLEMENTED 501 /**< not implemented */ +#define HTTP_BADGATEWAY 502 /**< received an invalid response from the upstream */ +#define HTTP_SERVUNAVAIL 503 /**< the server is not available */ + + +struct httpd_request; + +// Declaring here instead of including event2/http.h makes it easier to support +// other backends than evhttp in the future, e.g. libevhtp +struct httpd_server; +struct evhttp_connection; +struct evhttp_request; +struct evkeyvalq; +struct httpd_uri_parsed; + +typedef struct httpd_server httpd_server; +typedef struct evhttp_connection httpd_connection; +typedef struct evhttp_request httpd_backend; +typedef struct evkeyvalq httpd_headers; +typedef struct evkeyvalq httpd_query; +typedef struct httpd_uri_parsed httpd_uri_parsed; +typedef struct httpd_backend_data httpd_backend_data; + +typedef char *httpd_uri_path_parts[31]; +typedef void (*httpd_request_cb)(struct httpd_request *hreq, void *arg); +typedef void (*httpd_close_cb)(void *arg); +typedef void (*httpd_connection_chunkcb)(httpd_connection *conn, void *arg); +typedef void (*httpd_query_iteratecb)(const char *key, const char *val, void *arg); + +enum httpd_methods +{ + HTTPD_METHOD_GET = 1 << 0, + HTTPD_METHOD_POST = 1 << 1, + HTTPD_METHOD_HEAD = 1 << 2, + HTTPD_METHOD_PUT = 1 << 3, + HTTPD_METHOD_DELETE = 1 << 4, + HTTPD_METHOD_OPTIONS = 1 << 5, + HTTPD_METHOD_TRACE = 1 << 6, + HTTPD_METHOD_CONNECT = 1 << 7, + HTTPD_METHOD_PATCH = 1 << 8, +}; + +#define HTTPD_F_REPLY_LAST (1 << 15) +enum httpd_reply_type +{ + HTTPD_REPLY_START = 1, + HTTPD_REPLY_CHUNK = 2, + HTTPD_REPLY_END = HTTPD_F_REPLY_LAST | 1, + HTTPD_REPLY_COMPLETE = HTTPD_F_REPLY_LAST | 2, +}; + +enum httpd_send_flags +{ + HTTPD_SEND_NO_GZIP = (1 << 0), +}; + + +/*---------------------------------- MODULES ---------------------------------*/ + +// Must be in sync with modules[] in httpd.c +enum httpd_modules +{ + MODULE_DACP, + MODULE_DAAP, + MODULE_JSONAPI, + MODULE_ARTWORKAPI, + MODULE_STREAMING, + MODULE_OAUTH, + MODULE_RSP, +}; + +enum httpd_handler_flags +{ + // Most requests are pushed to a worker thread, but some handlers deal with + // requests that must be answered quickly. Can only be used for nonblocking + // handlers. + HTTPD_HANDLER_REALTIME = (1 << 0), +}; + +struct httpd_module +{ + const char *name; + enum httpd_modules type; + char initialized; + int logdomain; + + // Null-terminated list of URL subpath that the module accepts e.g., /subpath/morepath/file.mp3 + const char *subpaths[16]; + // Null-terminated list of URL fullparhs that the module accepts e.g., /fullpath + const char *fullpaths[16]; + // Pointer to the module's handler definitions + struct httpd_uri_map *handlers; + + int (*init)(void); + void (*deinit)(void); + void (*request)(struct httpd_request *); +}; + +/* + * Maps a regex of the request path to a handler of the request + */ +struct httpd_uri_map +{ + enum httpd_methods method; + char *regexp; + int (*handler)(struct httpd_request *hreq); + void *preg; + int flags; // See enum httpd_handler_flags +}; + + +/*------------------------------- HTTPD STRUCTS ------------------------------*/ + +/* + * A collection of pointers to request data that the reply handlers may need. + * Also has the function pointer to the reply handler and a pointer to a reply + * evbuffer. + */ +struct httpd_request { + // Request method + enum httpd_methods method; + // Backend private request object + httpd_backend *backend; + // For storing data that the actual backend doesn't have readily available + httpd_backend_data *backend_data; + // User-agent (if available) + const char *user_agent; + // Source IP address (ipv4 or ipv6) and port of the request (if available) + const char *peer_address; + unsigned short peer_port; + + // The original, request URI. The URI may have been complete: + // scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment] + // or relative: + // [/path][?query][#fragment] + const char *uri; + // URI decoded path from the request URI + const char *path; + // If the request is http://x:3689/foo/bar?key1=val1, then part_parts[0] is + // "foo", [1] is "bar" and the rest is null. Each path_part is an allocated + // URI decoded string. + httpd_uri_path_parts path_parts; + // Struct with the query, used with httpd_query_ functions + httpd_query *query; + // Backend private parser URI object + httpd_uri_parsed *uri_parsed; + + // Request headers + httpd_headers *in_headers; + // Request body + struct evbuffer *in_body; + // Response headers + httpd_headers *out_headers; + // Response body + struct evbuffer *out_body; + + // Our httpd module that will process this request + struct httpd_module *module; + // A pointer to the handler that will process the request + int (*handler)(struct httpd_request *hreq); + // Is the processing defered to a worker thread + bool is_async; + // Handler thread's evbase in case the handler needs to scehdule an event + struct event_base *evbase; + // A pointer to extra data that the module handling the request might need + void *extra_data; +}; + + +/*------------------------------ HTTPD FUNCTIONS -----------------------------*/ + +void +httpd_stream_file(struct httpd_request *hreq, int id); + +void +httpd_request_handler_set(struct httpd_request *hreq); + +bool +httpd_request_not_modified_since(struct httpd_request *hreq, time_t mtime); + +bool +httpd_request_etag_matches(struct httpd_request *hreq, const char *etag); + +void +httpd_response_not_cachable(struct httpd_request *hreq); + +/* + * This wrapper around evhttp_send_reply should be used whenever a request may + * come from a browser. It will automatically gzip if feasible, but the caller + * may direct it not to. It will set CORS headers as appropriate. Should be + * thread safe. + * + * @in req The http request struct + * @in code HTTP code, e.g. 200 + * @in reason A brief explanation of the error - if NULL the standard meaning + of the error code will be used + * @in flags See flags above + */ +void +httpd_send_reply(struct httpd_request *hreq, int code, const char *reason, enum httpd_send_flags flags); + +void +httpd_send_reply_start(struct httpd_request *hreq, int code, const char *reason); + +void +httpd_send_reply_chunk(struct httpd_request *hreq, httpd_connection_chunkcb cb, void *arg); + +void +httpd_send_reply_end(struct httpd_request *hreq); + +/* + * This is a substitute for evhttp_send_error that should be used whenever an + * error may be returned to a browser. It will set CORS headers as appropriate, + * which is not possible with evhttp_send_error, because it clears the headers. + * Should be thread safe. + * + * @in req The http request struct + * @in error HTTP code, e.g. 200 + * @in reason A brief explanation of the error - if NULL the standard meaning + of the error code will be used + */ +void +httpd_send_error(struct httpd_request *hreq, int error, const char *reason); + + +void +httpd_redirect_to(struct httpd_request *hreq, const char *path); + +bool +httpd_admin_check_auth(struct httpd_request *hreq); + +int +httpd_basic_auth(struct httpd_request *hreq, const char *user, const char *passwd, const char *realm); + + +/*-------------------------- WRAPPERS FOR EVHTTP -----------------------------*/ + +const char * +httpd_query_value_find(httpd_query *query, const char *key); + +void +httpd_query_iterate(httpd_query *query, httpd_query_iteratecb cb, void *arg); + +void +httpd_query_clear(httpd_query *query); + +const char * +httpd_header_find(httpd_headers *headers, const char *key); + +void +httpd_header_remove(httpd_headers *headers, const char *key); + +void +httpd_header_add(httpd_headers *headers, const char *key, const char *val); + +void +httpd_headers_clear(httpd_headers *headers); + +void +httpd_request_close_cb_set(struct httpd_request *hreq, httpd_close_cb cb, void *arg); + +void +httpd_request_free(struct httpd_request *hreq); + +struct httpd_request * +httpd_request_new(httpd_backend *backend, httpd_server *server, const char *uri, const char *user_agent); + +void +httpd_server_free(httpd_server *server); + +httpd_server * +httpd_server_new(struct event_base *evbase, unsigned short port, httpd_request_cb cb, void *arg); + +void +httpd_server_allow_origin_set(httpd_server *server, bool allow); + + +/*----------------- Only called by httpd.c to send raw replies ---------------*/ + +void +httpd_send(struct httpd_request *hreq, enum httpd_reply_type type, int code, const char *reason, + httpd_connection_chunkcb cb, void *cbarg); + + +/*---------- Only called by httpd.c to populate struct httpd_request ---------*/ + +httpd_backend_data * +httpd_backend_data_create(httpd_backend *backend, httpd_server *server); + +void +httpd_backend_data_free(httpd_backend_data *backend_data); + +struct event_base * +httpd_backend_evbase_get(httpd_backend *backend); + +const char * +httpd_backend_uri_get(httpd_backend *backend, httpd_backend_data *backend_data); + +httpd_headers * +httpd_backend_input_headers_get(httpd_backend *backend); + +httpd_headers * +httpd_backend_output_headers_get(httpd_backend *backend); + +struct evbuffer * +httpd_backend_input_buffer_get(httpd_backend *backend); + +int +httpd_backend_peer_get(const char **addr, uint16_t *port, httpd_backend *backend, httpd_backend_data *backend_data); + +int +httpd_backend_method_get(enum httpd_methods *method, httpd_backend *backend); + +httpd_uri_parsed * +httpd_uri_parsed_create(httpd_backend *backend); + +httpd_uri_parsed * +httpd_uri_parsed_create_fromuri(const char *uri); + +void +httpd_uri_parsed_free(httpd_uri_parsed *uri_parsed); + +httpd_query * +httpd_uri_query_get(httpd_uri_parsed *parsed); + +const char * +httpd_uri_path_get(httpd_uri_parsed *parsed); + +void +httpd_uri_path_parts_get(httpd_uri_path_parts *part_parts, httpd_uri_parsed *parsed); + +#endif /* !__HTTPD_INTERNAL_H__ */ diff --git a/src/httpd_jsonapi.c b/src/httpd_jsonapi.c index 56cdb771..cd7e319b 100644 --- a/src/httpd_jsonapi.c +++ b/src/httpd_jsonapi.c @@ -34,14 +34,13 @@ #include #include #include -#include #include #include #include #include #include -#include "httpd_jsonapi.h" +#include "httpd_internal.h" #include "conffile.h" #include "db.h" #ifdef LASTFM @@ -68,13 +67,13 @@ static char *default_playlist_directory; /* -------------------------------- HELPERS --------------------------------- */ static bool -is_modified(struct evhttp_request *req, const char *key) +is_modified(struct httpd_request *hreq, const char *key) { int64_t db_update = 0; db_admin_getint64(&db_update, key); - return (!db_update || !httpd_request_not_modified_since(req, (time_t)db_update)); + return (!db_update || !httpd_request_not_modified_since(hreq, (time_t)db_update)); } static inline void @@ -764,7 +763,7 @@ query_params_limit_set(struct query_params *query_params, struct httpd_request * query_params->limit = -1; query_params->offset = 0; - param = evhttp_find_header(hreq->query, "limit"); + param = httpd_query_value_find(hreq->query, "limit"); if (param) { query_params->idx_type = I_SUB; @@ -775,7 +774,7 @@ query_params_limit_set(struct query_params *query_params, struct httpd_request * return -1; } - param = evhttp_find_header(hreq->query, "offset"); + param = httpd_query_value_find(hreq->query, "offset"); if (param && safe_atoi32(param, &query_params->offset) < 0) { DPRINTF(E_LOG, L_WEB, "Invalid value for query parameter 'offset' (%s)\n", param); @@ -867,7 +866,7 @@ jsonapi_reply_config(struct httpd_request *hreq) json_object_object_add(jreply, "allow_modifying_stored_playlists", json_object_new_boolean(allow_modifying_stored_playlists)); safe_json_add_string(jreply, "default_playlist_directory", default_playlist_directory); - CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); + CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(jreply))); jparse_free(jreply); @@ -974,7 +973,7 @@ jsonapi_reply_settings_get(struct httpd_request *hreq) json_object_object_add(jreply, "categories", json_categories); - CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); + CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(jreply))); jparse_free(jreply); @@ -989,7 +988,7 @@ jsonapi_reply_settings_category_get(struct httpd_request *hreq) json_object *jreply; - categoryname = hreq->uri_parsed->path_parts[2]; + categoryname = hreq->path_parts[2]; category = settings_category_get(categoryname); if (!category) @@ -1006,7 +1005,7 @@ jsonapi_reply_settings_category_get(struct httpd_request *hreq) return HTTP_INTERNAL; } - CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); + CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(jreply))); jparse_free(jreply); @@ -1023,8 +1022,8 @@ jsonapi_reply_settings_option_get(struct httpd_request *hreq) json_object *jreply; - categoryname = hreq->uri_parsed->path_parts[2]; - optionname = hreq->uri_parsed->path_parts[3]; + categoryname = hreq->path_parts[2]; + optionname = hreq->path_parts[3]; category = settings_category_get(categoryname); if (!category) @@ -1048,7 +1047,7 @@ jsonapi_reply_settings_option_get(struct httpd_request *hreq) return HTTP_INTERNAL; } - CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); + CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(jreply))); jparse_free(jreply); @@ -1062,7 +1061,6 @@ jsonapi_reply_settings_option_put(struct httpd_request *hreq) const char *optionname; struct settings_category *category; struct settings_option *option; - struct evbuffer *in_evbuf; json_object* request; int intval; bool boolval; @@ -1070,8 +1068,8 @@ jsonapi_reply_settings_option_put(struct httpd_request *hreq) int ret; - categoryname = hreq->uri_parsed->path_parts[2]; - optionname = hreq->uri_parsed->path_parts[3]; + categoryname = hreq->path_parts[2]; + optionname = hreq->path_parts[3]; category = settings_category_get(categoryname); if (!category) @@ -1088,8 +1086,7 @@ jsonapi_reply_settings_option_put(struct httpd_request *hreq) return HTTP_NOTFOUND; } - in_evbuf = evhttp_request_get_input_buffer(hreq->req); - request = jparse_obj_from_evbuffer(in_evbuf); + request = jparse_obj_from_evbuffer(hreq->in_body); if (!request) { DPRINTF(E_LOG, L_WEB, "Missing request body for setting option '%s' (type %d)\n", optionname, option->type); @@ -1137,8 +1134,8 @@ jsonapi_reply_settings_option_delete(struct httpd_request *hreq) int ret; - categoryname = hreq->uri_parsed->path_parts[2]; - optionname = hreq->uri_parsed->path_parts[3]; + categoryname = hreq->path_parts[2]; + optionname = hreq->path_parts[3]; category = settings_category_get(categoryname); if (!category) @@ -1238,7 +1235,7 @@ jsonapi_reply_library(struct httpd_request *hreq) } } - CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); + CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(jreply))); jparse_free(jreply); return HTTP_OK; @@ -1252,7 +1249,7 @@ jsonapi_reply_update(struct httpd_request *hreq) { const char *param; - param = evhttp_find_header(hreq->query, "scan_kind"); + param = httpd_query_value_find(hreq->query, "scan_kind"); library_rescan(db_scan_kind_enum(param)); return HTTP_NOCONTENT; @@ -1263,7 +1260,7 @@ jsonapi_reply_meta_rescan(struct httpd_request *hreq) { const char *param; - param = evhttp_find_header(hreq->query, "scan_kind"); + param = httpd_query_value_find(hreq->query, "scan_kind"); library_metarescan(db_scan_kind_enum(param)); return HTTP_NOCONTENT; @@ -1331,7 +1328,7 @@ jsonapi_reply_spotify(struct httpd_request *hreq) json_object_object_add(jreply, "enabled", json_object_new_boolean(false)); #endif - CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); + CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(jreply))); jparse_free(jreply); @@ -1365,7 +1362,7 @@ jsonapi_reply_lastfm(struct httpd_request *hreq) json_object_object_add(jreply, "enabled", json_object_new_boolean(enabled)); json_object_object_add(jreply, "scrobbling_enabled", json_object_new_boolean(scrobbling_enabled)); - CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); + CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(jreply))); jparse_free(jreply); @@ -1379,7 +1376,6 @@ static int jsonapi_reply_lastfm_login(struct httpd_request *hreq) { #ifdef LASTFM - struct evbuffer *in_evbuf; json_object *request; const char *user; const char *password; @@ -1390,8 +1386,7 @@ jsonapi_reply_lastfm_login(struct httpd_request *hreq) DPRINTF(E_DBG, L_WEB, "Received LastFM login request\n"); - in_evbuf = evhttp_request_get_input_buffer(hreq->req); - request = jparse_obj_from_evbuffer(in_evbuf); + request = jparse_obj_from_evbuffer(hreq->in_body); if (!request) { DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n"); @@ -1434,7 +1429,7 @@ jsonapi_reply_lastfm_login(struct httpd_request *hreq) json_object_object_add(jreply, "errors", errors); } - CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); + CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(jreply))); jparse_free(jreply); @@ -1466,13 +1461,11 @@ jsonapi_reply_lastfm_logout(struct httpd_request *hreq) static int jsonapi_reply_pairing_pair(struct httpd_request *hreq) { - struct evbuffer *evbuf; json_object* request; const char* pin; int ret; - evbuf = evhttp_request_get_input_buffer(hreq->req); - request = jparse_obj_from_evbuffer(evbuf); + request = jparse_obj_from_evbuffer(hreq->in_body); if (!request) { DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n"); @@ -1532,7 +1525,7 @@ jsonapi_reply_pairing_get(struct httpd_request *hreq) json_object_object_add(jreply, "active", json_object_new_boolean(false)); } - CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); + CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(jreply))); jparse_free(jreply); free(remote_name); @@ -1591,10 +1584,10 @@ jsonapi_reply_outputs_get_byid(struct httpd_request *hreq) json_object *jreply; int ret; - ret = safe_atou64(hreq->uri_parsed->path_parts[2], &output_id); + ret = safe_atou64(hreq->path_parts[2], &output_id); if (ret < 0) { - DPRINTF(E_LOG, L_WEB, "No valid output id given to outputs endpoint '%s'\n", hreq->uri_parsed->path); + DPRINTF(E_LOG, L_WEB, "No valid output id given to outputs endpoint '%s'\n", hreq->path); return HTTP_BADREQUEST; } @@ -1603,13 +1596,13 @@ jsonapi_reply_outputs_get_byid(struct httpd_request *hreq) if (ret < 0) { - DPRINTF(E_LOG, L_WEB, "No output found for '%s'\n", hreq->uri_parsed->path); + DPRINTF(E_LOG, L_WEB, "No output found for '%s'\n", hreq->path); return HTTP_BADREQUEST; } jreply = speaker_to_json(&speaker_info); - CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); + CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(jreply))); jparse_free(jreply); @@ -1623,23 +1616,21 @@ static int jsonapi_reply_outputs_put_byid(struct httpd_request *hreq) { uint64_t output_id; - struct evbuffer *in_evbuf; json_object* request; bool selected; int volume; const char *pin; int ret; - ret = safe_atou64(hreq->uri_parsed->path_parts[2], &output_id); + ret = safe_atou64(hreq->path_parts[2], &output_id); if (ret < 0) { - DPRINTF(E_LOG, L_WEB, "No valid output id given to outputs endpoint '%s'\n", hreq->uri_parsed->path); + DPRINTF(E_LOG, L_WEB, "No valid output id given to outputs endpoint '%s'\n", hreq->path); return HTTP_BADREQUEST; } - in_evbuf = evhttp_request_get_input_buffer(hreq->req); - request = jparse_obj_from_evbuffer(in_evbuf); + request = jparse_obj_from_evbuffer(hreq->in_body); if (!request) { DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n"); @@ -1689,10 +1680,10 @@ jsonapi_reply_outputs_toggle_byid(struct httpd_request *hreq) struct player_speaker_info spk; int ret; - ret = safe_atou64(hreq->uri_parsed->path_parts[2], &output_id); + ret = safe_atou64(hreq->path_parts[2], &output_id); if (ret < 0) { - DPRINTF(E_LOG, L_WEB, "No valid output id given to outputs endpoint '%s'\n", hreq->uri_parsed->path); + DPRINTF(E_LOG, L_WEB, "No valid output id given to outputs endpoint '%s'\n", hreq->path); return HTTP_BADREQUEST; } @@ -1700,7 +1691,7 @@ jsonapi_reply_outputs_toggle_byid(struct httpd_request *hreq) ret = player_speaker_get_byid(&spk, output_id); if (ret < 0) { - DPRINTF(E_LOG, L_WEB, "No output found for the given output id, toggle failed for '%s'\n", hreq->uri_parsed->path); + DPRINTF(E_LOG, L_WEB, "No output found for the given output id, toggle failed for '%s'\n", hreq->path); return HTTP_BADREQUEST; } @@ -1731,7 +1722,7 @@ jsonapi_reply_outputs(struct httpd_request *hreq) jreply = json_object_new_object(); json_object_object_add(jreply, "outputs", outputs); - CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); + CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(jreply))); jparse_free(jreply); @@ -1741,12 +1732,10 @@ jsonapi_reply_outputs(struct httpd_request *hreq) static int jsonapi_reply_verification(struct httpd_request *hreq) { - struct evbuffer *in_evbuf; json_object* request; const char* message; - in_evbuf = evhttp_request_get_input_buffer(hreq->req); - request = jparse_obj_from_evbuffer(in_evbuf); + request = jparse_obj_from_evbuffer(hreq->in_body); if (!request) { DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n"); @@ -1769,15 +1758,13 @@ jsonapi_reply_verification(struct httpd_request *hreq) static int jsonapi_reply_outputs_set(struct httpd_request *hreq) { - struct evbuffer *in_evbuf; json_object *request; json_object *outputs; json_object *output_id; int nspk, i, ret; uint64_t *ids; - in_evbuf = evhttp_request_get_input_buffer(hreq->req); - request = jparse_obj_from_evbuffer(in_evbuf); + request = jparse_obj_from_evbuffer(hreq->in_body); if (!request) { DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n"); @@ -1902,11 +1889,11 @@ jsonapi_reply_player_play(struct httpd_request *hreq) const char *param; int ret; - if ((param = evhttp_find_header(hreq->query, "item_id"))) + if ((param = httpd_query_value_find(hreq->query, "item_id"))) { return play_item_with_id(param); } - else if ((param = evhttp_find_header(hreq->query, "position"))) + else if ((param = httpd_query_value_find(hreq->query, "position"))) { return play_item_at_position(param); } @@ -2033,8 +2020,8 @@ jsonapi_reply_player_seek(struct httpd_request *hreq) int seek_ms; int ret; - param_pos = evhttp_find_header(hreq->query, "position_ms"); - param_seek = evhttp_find_header(hreq->query, "seek_ms"); + param_pos = httpd_query_value_find(hreq->query, "position_ms"); + param_seek = httpd_query_value_find(hreq->query, "seek_ms"); if (!param_pos && !param_seek) return HTTP_BADREQUEST; @@ -2144,7 +2131,7 @@ jsonapi_reply_player(struct httpd_request *hreq) } } - CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply))); + CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(reply))); jparse_free(reply); @@ -2472,7 +2459,7 @@ jsonapi_reply_queue_tracks_add(struct httpd_request *hreq) int ret = 0; - param_pos = evhttp_find_header(hreq->query, "position"); + param_pos = httpd_query_value_find(hreq->query, "position"); if (param_pos) { if (safe_atoi32(param_pos, &pos) < 0) @@ -2487,8 +2474,8 @@ jsonapi_reply_queue_tracks_add(struct httpd_request *hreq) else pos = -1; - param_uris = evhttp_find_header(hreq->query, "uris"); - param_expression = evhttp_find_header(hreq->query, "expression"); + param_uris = httpd_query_value_find(hreq->query, "uris"); + param_expression = httpd_query_value_find(hreq->query, "expression"); if (!param_uris && !param_expression) { @@ -2498,7 +2485,7 @@ jsonapi_reply_queue_tracks_add(struct httpd_request *hreq) } // if query parameter "clear" is "true", stop playback and clear the queue before adding new queue items - param = evhttp_find_header(hreq->query, "clear"); + param = httpd_query_value_find(hreq->query, "clear"); if (param && strcmp(param, "true") == 0) { player_playback_stop(); @@ -2506,7 +2493,7 @@ jsonapi_reply_queue_tracks_add(struct httpd_request *hreq) } // if query parameter "shuffle" is present, update the shuffle state before adding new queue items - param = evhttp_find_header(hreq->query, "shuffle"); + param = httpd_query_value_find(hreq->query, "shuffle"); if (param) { shuffle = (strcmp(param, "true") == 0); @@ -2520,7 +2507,7 @@ jsonapi_reply_queue_tracks_add(struct httpd_request *hreq) else { // This overrides the value specified in query - param = evhttp_find_header(hreq->query, "limit"); + param = httpd_query_value_find(hreq->query, "limit"); if (param && safe_atoi32(param, &limit) == 0) ret = queue_tracks_add_byexpression(param_expression, pos, limit, &total_count); else @@ -2532,7 +2519,7 @@ jsonapi_reply_queue_tracks_add(struct httpd_request *hreq) reply = json_object_new_object(); json_object_object_add(reply, "count", json_object_new_int(total_count)); - ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); + ret = evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(reply)); jparse_free(reply); } @@ -2540,10 +2527,10 @@ jsonapi_reply_queue_tracks_add(struct httpd_request *hreq) return HTTP_INTERNAL; // If query parameter "playback" is "start", start playback after successfully adding new items - param = evhttp_find_header(hreq->query, "playback"); + param = httpd_query_value_find(hreq->query, "playback"); if (param && strcmp(param, "start") == 0) { - if ((param = evhttp_find_header(hreq->query, "playback_from_position"))) + if ((param = httpd_query_value_find(hreq->query, "playback_from_position"))) ret = (play_item_at_position(param) == HTTP_NOCONTENT) ? 0 : -1; else ret = player_playback_start(); @@ -2597,12 +2584,12 @@ jsonapi_reply_queue_tracks_update(struct httpd_request *hreq) player_get_status(&status); - if (strcmp(hreq->uri_parsed->path_parts[3], "now_playing") != 0) + if (strcmp(hreq->path_parts[3], "now_playing") != 0) { - ret = safe_atou32(hreq->uri_parsed->path_parts[3], &item_id); + ret = safe_atou32(hreq->path_parts[3], &item_id); if (ret < 0) { - DPRINTF(E_LOG, L_WEB, "No valid item id given: '%s'\n", hreq->uri_parsed->path); + DPRINTF(E_LOG, L_WEB, "No valid item id given: '%s'\n", hreq->path); return HTTP_BADREQUEST; } } @@ -2612,27 +2599,27 @@ jsonapi_reply_queue_tracks_update(struct httpd_request *hreq) queue_item = db_queue_fetch_byitemid(item_id); if (!queue_item) { - DPRINTF(E_LOG, L_WEB, "No valid item id given, or now_playing given but not playing: '%s'\n", hreq->uri_parsed->path); + DPRINTF(E_LOG, L_WEB, "No valid item id given, or now_playing given but not playing: '%s'\n", hreq->path); return HTTP_BADREQUEST; } ret = HTTP_OK; is_changed = false; - if ((param = evhttp_find_header(hreq->query, "new_position"))) + if ((param = httpd_query_value_find(hreq->query, "new_position"))) ret = update_pos(item_id, param, status.shuffle); - if ((param = evhttp_find_header(hreq->query, "title"))) + if ((param = httpd_query_value_find(hreq->query, "title"))) update_str(&is_changed, &queue_item->title, param); - if ((param = evhttp_find_header(hreq->query, "album"))) + if ((param = httpd_query_value_find(hreq->query, "album"))) update_str(&is_changed, &queue_item->album, param); - if ((param = evhttp_find_header(hreq->query, "artist"))) + if ((param = httpd_query_value_find(hreq->query, "artist"))) update_str(&is_changed, &queue_item->artist, param); - if ((param = evhttp_find_header(hreq->query, "album_artist"))) + if ((param = httpd_query_value_find(hreq->query, "album_artist"))) update_str(&is_changed, &queue_item->album_artist, param); - if ((param = evhttp_find_header(hreq->query, "composer"))) + if ((param = httpd_query_value_find(hreq->query, "composer"))) update_str(&is_changed, &queue_item->composer, param); - if ((param = evhttp_find_header(hreq->query, "genre"))) + if ((param = httpd_query_value_find(hreq->query, "genre"))) update_str(&is_changed, &queue_item->genre, param); - if ((param = evhttp_find_header(hreq->query, "artwork_url"))) + if ((param = httpd_query_value_find(hreq->query, "artwork_url"))) update_str(&is_changed, &queue_item->artwork_url, param); if (ret != HTTP_OK) @@ -2650,10 +2637,10 @@ jsonapi_reply_queue_tracks_delete(struct httpd_request *hreq) uint32_t item_id; int ret; - ret = safe_atou32(hreq->uri_parsed->path_parts[3], &item_id); + ret = safe_atou32(hreq->path_parts[3], &item_id); if (ret < 0) { - DPRINTF(E_LOG, L_WEB, "No valid item id given '%s'\n", hreq->uri_parsed->path); + DPRINTF(E_LOG, L_WEB, "No valid item id given '%s'\n", hreq->path); return HTTP_BADREQUEST; } @@ -2697,7 +2684,7 @@ jsonapi_reply_queue(struct httpd_request *hreq) db_queue_get_count(&count); snprintf(etag, sizeof(etag), "%d", version); - if (httpd_request_etag_matches(hreq->req, etag)) + if (httpd_request_etag_matches(hreq, etag)) return HTTP_NOTMODIFIED; memset(&query_params, 0, sizeof(struct query_params)); @@ -2713,7 +2700,7 @@ jsonapi_reply_queue(struct httpd_request *hreq) if (status.shuffle) query_params.sort = S_SHUFFLE_POS; - param = evhttp_find_header(hreq->query, "id"); + param = httpd_query_value_find(hreq->query, "id"); if (param && strcmp(param, "now_playing") == 0) { query_params.filter = db_mprintf("id = %d", status.item_id); @@ -2724,10 +2711,10 @@ jsonapi_reply_queue(struct httpd_request *hreq) } else { - param = evhttp_find_header(hreq->query, "start"); + param = httpd_query_value_find(hreq->query, "start"); if (param && safe_atoi32(param, &start_pos) == 0) { - param = evhttp_find_header(hreq->query, "end"); + param = httpd_query_value_find(hreq->query, "end"); if (!param || safe_atoi32(param, &end_pos) != 0) { end_pos = start_pos + 1; @@ -2753,7 +2740,7 @@ jsonapi_reply_queue(struct httpd_request *hreq) json_object_array_add(items, item); } - ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); + ret = evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "outputs: Couldn't add outputs to response buffer.\n"); @@ -2774,7 +2761,7 @@ jsonapi_reply_player_repeat(struct httpd_request *hreq) { const char *param; - param = evhttp_find_header(hreq->query, "state"); + param = httpd_query_value_find(hreq->query, "state"); if (!param) return HTTP_BADREQUEST; @@ -2800,7 +2787,7 @@ jsonapi_reply_player_shuffle(struct httpd_request *hreq) const char *param; bool shuffle; - param = evhttp_find_header(hreq->query, "state"); + param = httpd_query_value_find(hreq->query, "state"); if (!param) return HTTP_BADREQUEST; @@ -2816,7 +2803,7 @@ jsonapi_reply_player_consume(struct httpd_request *hreq) const char *param; bool consume; - param = evhttp_find_header(hreq->query, "state"); + param = httpd_query_value_find(hreq->query, "state"); if (!param) return HTTP_BADREQUEST; @@ -2895,7 +2882,7 @@ jsonapi_reply_player_volume(struct httpd_request *hreq) step = 0; // Parse and validate parameters - param_volume = evhttp_find_header(hreq->query, "volume"); + param_volume = httpd_query_value_find(hreq->query, "volume"); if (param_volume) { ret = safe_atoi32(param_volume, &volume); @@ -2903,7 +2890,7 @@ jsonapi_reply_player_volume(struct httpd_request *hreq) return HTTP_BADREQUEST; } - param_step = evhttp_find_header(hreq->query, "step"); + param_step = httpd_query_value_find(hreq->query, "step"); if (param_step) { ret = safe_atoi32(param_step, &step); @@ -2918,7 +2905,7 @@ jsonapi_reply_player_volume(struct httpd_request *hreq) return HTTP_BADREQUEST; } - param = evhttp_find_header(hreq->query, "output_id"); + param = httpd_query_value_find(hreq->query, "output_id"); if (param) { // Update volume for individual output @@ -2953,11 +2940,11 @@ jsonapi_reply_library_artists(struct httpd_request *hreq) int total; int ret = 0; - if (!is_modified(hreq->req, DB_ADMIN_DB_UPDATE)) + if (!is_modified(hreq, DB_ADMIN_DB_UPDATE)) return HTTP_NOTMODIFIED; media_kind = 0; - param = evhttp_find_header(hreq->query, "media_kind"); + param = httpd_query_value_find(hreq->query, "media_kind"); if (param) { media_kind = db_media_kind_enum(param); @@ -2992,7 +2979,7 @@ jsonapi_reply_library_artists(struct httpd_request *hreq) json_object_object_add(reply, "offset", json_object_new_int(query_params.offset)); json_object_object_add(reply, "limit", json_object_new_int(query_params.limit)); - ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); + ret = evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add artists to response buffer.\n"); @@ -3014,10 +3001,10 @@ jsonapi_reply_library_artist(struct httpd_request *hreq) int ret = 0; bool notfound = false; - if (!is_modified(hreq->req, DB_ADMIN_DB_UPDATE)) + if (!is_modified(hreq, DB_ADMIN_DB_UPDATE)) return HTTP_NOTMODIFIED; - artist_id = hreq->uri_parsed->path_parts[3]; + artist_id = hreq->path_parts[3]; reply = fetch_artist(¬found, artist_id); if (!reply) @@ -3026,7 +3013,7 @@ jsonapi_reply_library_artist(struct httpd_request *hreq) goto error; } - ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); + ret = evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add artists to response buffer.\n"); @@ -3049,10 +3036,10 @@ jsonapi_reply_library_artist_albums(struct httpd_request *hreq) int total; int ret = 0; - if (!is_modified(hreq->req, DB_ADMIN_DB_UPDATE)) + if (!is_modified(hreq, DB_ADMIN_DB_UPDATE)) return HTTP_NOTMODIFIED; - artist_id = hreq->uri_parsed->path_parts[3]; + artist_id = hreq->path_parts[3]; reply = json_object_new_object(); items = json_object_new_array(); @@ -3078,7 +3065,7 @@ jsonapi_reply_library_artist_albums(struct httpd_request *hreq) json_object_object_add(reply, "offset", json_object_new_int(query_params.offset)); json_object_object_add(reply, "limit", json_object_new_int(query_params.limit)); - ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); + ret = evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add albums to response buffer.\n"); @@ -3102,11 +3089,11 @@ jsonapi_reply_library_albums(struct httpd_request *hreq) int total; int ret = 0; - if (!is_modified(hreq->req, DB_ADMIN_DB_UPDATE)) + if (!is_modified(hreq, DB_ADMIN_DB_UPDATE)) return HTTP_NOTMODIFIED; media_kind = 0; - param = evhttp_find_header(hreq->query, "media_kind"); + param = httpd_query_value_find(hreq->query, "media_kind"); if (param) { media_kind = db_media_kind_enum(param); @@ -3141,7 +3128,7 @@ jsonapi_reply_library_albums(struct httpd_request *hreq) json_object_object_add(reply, "offset", json_object_new_int(query_params.offset)); json_object_object_add(reply, "limit", json_object_new_int(query_params.limit)); - ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); + ret = evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add albums to response buffer.\n"); @@ -3163,10 +3150,10 @@ jsonapi_reply_library_album(struct httpd_request *hreq) int ret = 0; bool notfound = false; - if (!is_modified(hreq->req, DB_ADMIN_DB_UPDATE)) + if (!is_modified(hreq, DB_ADMIN_DB_UPDATE)) return HTTP_NOTMODIFIED; - album_id = hreq->uri_parsed->path_parts[3]; + album_id = hreq->path_parts[3]; reply = fetch_album(¬found, album_id); if (!reply) @@ -3175,7 +3162,7 @@ jsonapi_reply_library_album(struct httpd_request *hreq) goto error; } - ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); + ret = evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add artists to response buffer.\n"); @@ -3198,10 +3185,10 @@ jsonapi_reply_library_album_tracks(struct httpd_request *hreq) int total; int ret = 0; - if (!is_modified(hreq->req, DB_ADMIN_DB_MODIFIED)) + if (!is_modified(hreq, DB_ADMIN_DB_MODIFIED)) return HTTP_NOTMODIFIED; - album_id = hreq->uri_parsed->path_parts[3]; + album_id = hreq->path_parts[3]; reply = json_object_new_object(); items = json_object_new_array(); @@ -3227,7 +3214,7 @@ jsonapi_reply_library_album_tracks(struct httpd_request *hreq) json_object_object_add(reply, "offset", json_object_new_int(query_params.offset)); json_object_object_add(reply, "limit", json_object_new_int(query_params.limit)); - ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); + ret = evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add tracks to response buffer.\n"); @@ -3247,11 +3234,11 @@ jsonapi_reply_library_album_tracks_put_byid(struct httpd_request *hreq) int64_t album_id;; int ret; - ret = safe_atoi64(hreq->uri_parsed->path_parts[3], &album_id); + ret = safe_atoi64(hreq->path_parts[3], &album_id); if (ret < 0) return HTTP_INTERNAL; - param = evhttp_find_header(hreq->query, "play_count"); + param = httpd_query_value_find(hreq->query, "play_count"); if (!param) return HTTP_BADREQUEST; @@ -3282,10 +3269,10 @@ jsonapi_reply_library_tracks_get_byid(struct httpd_request *hreq) int ret = 0; bool notfound = false; - if (!is_modified(hreq->req, DB_ADMIN_DB_MODIFIED)) + if (!is_modified(hreq, DB_ADMIN_DB_MODIFIED)) return HTTP_NOTMODIFIED; - track_id = hreq->uri_parsed->path_parts[3]; + track_id = hreq->path_parts[3]; memset(&query_params, 0, sizeof(struct query_params)); @@ -3309,7 +3296,7 @@ jsonapi_reply_library_tracks_get_byid(struct httpd_request *hreq) reply = track_to_json(&dbmfi); - ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); + ret = evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add track to response buffer.\n"); @@ -3327,7 +3314,6 @@ jsonapi_reply_library_tracks_get_byid(struct httpd_request *hreq) static int jsonapi_reply_library_tracks_put(struct httpd_request *hreq) { - struct evbuffer *in_evbuf; json_object *request = NULL; json_object *tracks; json_object *track = NULL; @@ -3337,8 +3323,7 @@ jsonapi_reply_library_tracks_put(struct httpd_request *hreq) int32_t track_id; int i; - in_evbuf = evhttp_request_get_input_buffer(hreq->req); - request = jparse_obj_from_evbuffer(in_evbuf); + request = jparse_obj_from_evbuffer(hreq->in_body); if (!request) { DPRINTF(E_LOG, L_WEB, "Failed to read json tracks request\n"); @@ -3413,11 +3398,11 @@ jsonapi_reply_library_tracks_put_byid(struct httpd_request *hreq) uint32_t val; int ret; - ret = safe_atoi32(hreq->uri_parsed->path_parts[3], &track_id); + ret = safe_atoi32(hreq->path_parts[3], &track_id); if (ret < 0) return HTTP_INTERNAL; - param = evhttp_find_header(hreq->query, "play_count"); + param = httpd_query_value_find(hreq->query, "play_count"); if (param) { if (strcmp(param, "increment") == 0) @@ -3435,7 +3420,7 @@ jsonapi_reply_library_tracks_put_byid(struct httpd_request *hreq) } } - param = evhttp_find_header(hreq->query, "rating"); + param = httpd_query_value_find(hreq->query, "rating"); if (param) { ret = safe_atou32(param, &val); @@ -3451,7 +3436,7 @@ jsonapi_reply_library_tracks_put_byid(struct httpd_request *hreq) } // Retreive marked tracks via "/api/search?type=tracks&expression=usermark+=+1" - param = evhttp_find_header(hreq->query, "usermark"); + param = httpd_query_value_find(hreq->query, "usermark"); if (param) { ret = safe_atou32(param, &val); @@ -3481,10 +3466,10 @@ jsonapi_reply_library_track_playlists(struct httpd_request *hreq) int total; int ret = 0; - if (!is_modified(hreq->req, DB_ADMIN_DB_MODIFIED)) + if (!is_modified(hreq, DB_ADMIN_DB_MODIFIED)) return HTTP_NOTMODIFIED; - track_id = hreq->uri_parsed->path_parts[3]; + track_id = hreq->path_parts[3]; if (safe_atoi32(track_id, &id) < 0) { DPRINTF(E_LOG, L_WEB, "Error converting track id '%s' to int.\n", track_id); @@ -3519,7 +3504,7 @@ jsonapi_reply_library_track_playlists(struct httpd_request *hreq) json_object_object_add(reply, "offset", json_object_new_int(query_params.offset)); json_object_object_add(reply, "limit", json_object_new_int(query_params.limit)); - ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); + ret = evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "track playlists: Couldn't add playlists to response buffer.\n"); @@ -3543,7 +3528,7 @@ jsonapi_reply_library_playlists(struct httpd_request *hreq) int total; int ret = 0; - if (!is_modified(hreq->req, DB_ADMIN_DB_UPDATE)) + if (!is_modified(hreq, DB_ADMIN_DB_UPDATE)) return HTTP_NOTMODIFIED; reply = json_object_new_object(); @@ -3570,7 +3555,7 @@ jsonapi_reply_library_playlists(struct httpd_request *hreq) json_object_object_add(reply, "offset", json_object_new_int(query_params.offset)); json_object_object_add(reply, "limit", json_object_new_int(query_params.limit)); - ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); + ret = evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add playlists to response buffer.\n"); @@ -3591,10 +3576,10 @@ jsonapi_reply_library_playlist_get(struct httpd_request *hreq) int ret = 0; bool notfound = false; - if (!is_modified(hreq->req, DB_ADMIN_DB_UPDATE)) + if (!is_modified(hreq, DB_ADMIN_DB_UPDATE)) return HTTP_NOTMODIFIED; - ret = safe_atou32(hreq->uri_parsed->path_parts[3], &playlist_id); + ret = safe_atou32(hreq->path_parts[3], &playlist_id); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Could not parse playlist id to integer\n"); @@ -3621,7 +3606,7 @@ jsonapi_reply_library_playlist_get(struct httpd_request *hreq) goto error; } - ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); + ret = evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add playlist to response buffer.\n"); @@ -3665,14 +3650,14 @@ jsonapi_reply_library_playlist_put(struct httpd_request *hreq) const char *param; int ret; - ret = safe_atou32(hreq->uri_parsed->path_parts[3], &playlist_id); + ret = safe_atou32(hreq->path_parts[3], &playlist_id); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Could not parse playlist id to integer\n"); return HTTP_BADREQUEST; } - if ((param = evhttp_find_header(hreq->query, "query_limit"))) + if ((param = httpd_query_value_find(hreq->query, "query_limit"))) ret = playlist_attrib_query_limit_set(playlist_id, param); else ret = -1; @@ -3694,12 +3679,12 @@ jsonapi_reply_library_playlist_tracks(struct httpd_request *hreq) int ret = 0; // Due to smart playlists possibly changing their tracks between rescans, disable caching in clients - httpd_response_not_cachable(hreq->req); + httpd_response_not_cachable(hreq); - ret = safe_atoi32(hreq->uri_parsed->path_parts[3], &playlist_id); + ret = safe_atoi32(hreq->path_parts[3], &playlist_id); if (ret < 0) { - DPRINTF(E_LOG, L_WEB, "No valid playlist id given '%s'\n", hreq->uri_parsed->path); + DPRINTF(E_LOG, L_WEB, "No valid playlist id given '%s'\n", hreq->path); return HTTP_BADREQUEST; } @@ -3725,7 +3710,7 @@ jsonapi_reply_library_playlist_tracks(struct httpd_request *hreq) json_object_object_add(reply, "offset", json_object_new_int(query_params.offset)); json_object_object_add(reply, "limit", json_object_new_int(query_params.limit)); - ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); + ret = evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "playlist tracks: Couldn't add tracks to response buffer.\n"); @@ -3745,10 +3730,10 @@ jsonapi_reply_library_playlist_delete(struct httpd_request *hreq) uint32_t pl_id; int ret; - ret = safe_atou32(hreq->uri_parsed->path_parts[3], &pl_id); + ret = safe_atou32(hreq->path_parts[3], &pl_id); if (ret < 0) { - DPRINTF(E_LOG, L_WEB, "No valid playlist id given '%s'\n", hreq->uri_parsed->path); + DPRINTF(E_LOG, L_WEB, "No valid playlist id given '%s'\n", hreq->path); return HTTP_BADREQUEST; } @@ -3768,14 +3753,14 @@ jsonapi_reply_library_playlist_playlists(struct httpd_request *hreq) int total; int ret = 0; - if (!is_modified(hreq->req, DB_ADMIN_DB_MODIFIED)) + if (!is_modified(hreq, DB_ADMIN_DB_MODIFIED)) return HTTP_NOTMODIFIED; - ret = safe_atoi32(hreq->uri_parsed->path_parts[3], &playlist_id); + ret = safe_atoi32(hreq->path_parts[3], &playlist_id); if (ret < 0) { - DPRINTF(E_LOG, L_WEB, "No valid playlist id given '%s'\n", hreq->uri_parsed->path); + DPRINTF(E_LOG, L_WEB, "No valid playlist id given '%s'\n", hreq->path); return HTTP_BADREQUEST; } @@ -3803,7 +3788,7 @@ jsonapi_reply_library_playlist_playlists(struct httpd_request *hreq) json_object_object_add(reply, "offset", json_object_new_int(query_params.offset)); json_object_object_add(reply, "limit", json_object_new_int(query_params.limit)); - ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); + ret = evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "playlist tracks: Couldn't add tracks to response buffer.\n"); @@ -3824,11 +3809,11 @@ jsonapi_reply_library_playlist_tracks_put_byid(struct httpd_request *hreq) int playlist_id; int ret; - ret = safe_atoi32(hreq->uri_parsed->path_parts[3], &playlist_id); + ret = safe_atoi32(hreq->path_parts[3], &playlist_id); if (ret < 0) return HTTP_INTERNAL; - param = evhttp_find_header(hreq->query, "play_count"); + param = httpd_query_value_find(hreq->query, "play_count"); if (!param) return HTTP_BADREQUEST; @@ -3857,7 +3842,7 @@ jsonapi_reply_queue_save(struct httpd_request *hreq) char *playlist_name = NULL; int ret = 0; - if ((param = evhttp_find_header(hreq->query, "name")) == NULL) + if ((param = httpd_query_value_find(hreq->query, "name")) == NULL) { DPRINTF(E_LOG, L_WEB, "Invalid argument, missing 'name'\n"); return HTTP_BADREQUEST; @@ -3907,14 +3892,14 @@ jsonapi_reply_library_browse(struct httpd_request *hreq) int total; int ret; - if (!is_modified(hreq->req, DB_ADMIN_DB_UPDATE)) + if (!is_modified(hreq, DB_ADMIN_DB_UPDATE)) return HTTP_NOTMODIFIED; - browse_type = hreq->uri_parsed->path_parts[2]; + browse_type = hreq->path_parts[2]; DPRINTF(E_DBG, L_WEB, "Browse query with type '%s'\n", browse_type); media_kind = 0; - param = evhttp_find_header(hreq->query, "media_kind"); + param = httpd_query_value_find(hreq->query, "media_kind"); if (param) { media_kind = db_media_kind_enum(param); @@ -3964,7 +3949,7 @@ jsonapi_reply_library_browse(struct httpd_request *hreq) json_object_object_add(reply, "offset", json_object_new_int(query_params.offset)); json_object_object_add(reply, "limit", json_object_new_int(query_params.limit)); - ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); + ret = evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add browse items to response buffer.\n"); @@ -3988,11 +3973,11 @@ jsonapi_reply_library_browseitem(struct httpd_request *hreq) json_object *reply; int ret; - if (!is_modified(hreq->req, DB_ADMIN_DB_UPDATE)) + if (!is_modified(hreq, DB_ADMIN_DB_UPDATE)) return HTTP_NOTMODIFIED; - browse_type = hreq->uri_parsed->path_parts[2]; - item_name = hreq->uri_parsed->path_parts[3]; + browse_type = hreq->path_parts[2]; + item_name = hreq->path_parts[3]; DPRINTF(E_DBG, L_WEB, "Browse item query with type '%s'\n", browse_type); reply = json_object_new_object(); @@ -4037,7 +4022,7 @@ jsonapi_reply_library_browseitem(struct httpd_request *hreq) goto error; } - ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); + ret = evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add browse item to response buffer.\n"); @@ -4063,13 +4048,13 @@ jsonapi_reply_library_count(struct httpd_request *hreq) json_object *jreply; int ret; - if (!is_modified(hreq->req, DB_ADMIN_DB_UPDATE)) + if (!is_modified(hreq, DB_ADMIN_DB_UPDATE)) return HTTP_NOTMODIFIED; memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_COUNT_ITEMS; - param_expression = evhttp_find_header(hreq->query, "expression"); + param_expression = httpd_query_value_find(hreq->query, "expression"); if (param_expression) { memset(&smartpl_expression, 0, sizeof(struct smartpl)); @@ -4102,7 +4087,7 @@ jsonapi_reply_library_count(struct httpd_request *hreq) free(qp.filter); - CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); + CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(jreply))); jparse_free(jreply); return HTTP_OK; @@ -4123,7 +4108,7 @@ jsonapi_reply_library_files(struct httpd_request *hreq) int total; int ret; - param = evhttp_find_header(hreq->query, "directory"); + param = httpd_query_value_find(hreq->query, "directory"); directory_id = DIR_FILE; if (param) @@ -4196,7 +4181,7 @@ jsonapi_reply_library_files(struct httpd_request *hreq) json_object_object_add(playlists, "limit", json_object_new_int(query_params.limit)); // Build JSON response - ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); + ret = evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add directories to response buffer.\n"); @@ -4215,7 +4200,7 @@ jsonapi_reply_library_add(struct httpd_request *hreq) const char *url; int ret; - url = evhttp_find_header(hreq->query, "url"); + url = httpd_query_value_find(hreq->query, "url"); if (!url) { DPRINTF(E_LOG, L_WEB, "Missing URL parameter for library add\n"); @@ -4528,9 +4513,9 @@ jsonapi_reply_search(struct httpd_request *hreq) reply = NULL; - param_type = evhttp_find_header(hreq->query, "type"); - param_query = evhttp_find_header(hreq->query, "query"); - param_expression = evhttp_find_header(hreq->query, "expression"); + param_type = httpd_query_value_find(hreq->query, "type"); + param_query = httpd_query_value_find(hreq->query, "query"); + param_expression = httpd_query_value_find(hreq->query, "expression"); if (!param_type || (!param_query && !param_expression)) { @@ -4540,7 +4525,7 @@ jsonapi_reply_search(struct httpd_request *hreq) } media_kind = 0; - param_media_kind = evhttp_find_header(hreq->query, "media_kind"); + param_media_kind = httpd_query_value_find(hreq->query, "media_kind"); if (param_media_kind) { media_kind = db_media_kind_enum(param_media_kind); @@ -4601,7 +4586,7 @@ jsonapi_reply_search(struct httpd_request *hreq) goto error; } - ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); + ret = evbuffer_add_printf(hreq->out_body, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "playlist tracks: Couldn't add tracks to response buffer.\n"); @@ -4635,80 +4620,80 @@ jsonapi_reply_library_backup(struct httpd_request *hreq) static struct httpd_uri_map adm_handlers[] = { - { EVHTTP_REQ_GET, "^/api/config$", jsonapi_reply_config }, - { EVHTTP_REQ_GET, "^/api/settings$", jsonapi_reply_settings_get }, - { EVHTTP_REQ_GET, "^/api/settings/[A-Za-z0-9_]+$", jsonapi_reply_settings_category_get }, - { EVHTTP_REQ_GET, "^/api/settings/[A-Za-z0-9_]+/[A-Za-z0-9_]+$", jsonapi_reply_settings_option_get }, - { EVHTTP_REQ_PUT, "^/api/settings/[A-Za-z0-9_]+/[A-Za-z0-9_]+$", jsonapi_reply_settings_option_put }, - { EVHTTP_REQ_DELETE, "^/api/settings/[A-Za-z0-9_]+/[A-Za-z0-9_]+$", jsonapi_reply_settings_option_delete }, - { EVHTTP_REQ_GET, "^/api/library$", jsonapi_reply_library }, - { EVHTTP_REQ_GET | - EVHTTP_REQ_PUT, "^/api/update$", jsonapi_reply_update }, - { EVHTTP_REQ_PUT, "^/api/rescan$", jsonapi_reply_meta_rescan }, - { EVHTTP_REQ_GET, "^/api/spotify-logout$", jsonapi_reply_spotify_logout }, - { EVHTTP_REQ_GET, "^/api/spotify$", jsonapi_reply_spotify }, - { EVHTTP_REQ_GET, "^/api/pairing$", jsonapi_reply_pairing_get }, - { EVHTTP_REQ_POST, "^/api/pairing$", jsonapi_reply_pairing_pair }, - { EVHTTP_REQ_POST, "^/api/lastfm-login$", jsonapi_reply_lastfm_login }, - { EVHTTP_REQ_GET, "^/api/lastfm-logout$", jsonapi_reply_lastfm_logout }, - { EVHTTP_REQ_GET, "^/api/lastfm$", jsonapi_reply_lastfm }, - { EVHTTP_REQ_POST, "^/api/verification$", jsonapi_reply_verification }, + { HTTPD_METHOD_GET, "^/api/config$", jsonapi_reply_config }, + { HTTPD_METHOD_GET, "^/api/settings$", jsonapi_reply_settings_get }, + { HTTPD_METHOD_GET, "^/api/settings/[A-Za-z0-9_]+$", jsonapi_reply_settings_category_get }, + { HTTPD_METHOD_GET, "^/api/settings/[A-Za-z0-9_]+/[A-Za-z0-9_]+$", jsonapi_reply_settings_option_get }, + { HTTPD_METHOD_PUT, "^/api/settings/[A-Za-z0-9_]+/[A-Za-z0-9_]+$", jsonapi_reply_settings_option_put }, + { HTTPD_METHOD_DELETE, "^/api/settings/[A-Za-z0-9_]+/[A-Za-z0-9_]+$", jsonapi_reply_settings_option_delete }, + { HTTPD_METHOD_GET, "^/api/library$", jsonapi_reply_library }, + { HTTPD_METHOD_GET | + HTTPD_METHOD_PUT, "^/api/update$", jsonapi_reply_update }, + { HTTPD_METHOD_PUT, "^/api/rescan$", jsonapi_reply_meta_rescan }, + { HTTPD_METHOD_GET, "^/api/spotify-logout$", jsonapi_reply_spotify_logout }, + { HTTPD_METHOD_GET, "^/api/spotify$", jsonapi_reply_spotify }, + { HTTPD_METHOD_GET, "^/api/pairing$", jsonapi_reply_pairing_get }, + { HTTPD_METHOD_POST, "^/api/pairing$", jsonapi_reply_pairing_pair }, + { HTTPD_METHOD_POST, "^/api/lastfm-login$", jsonapi_reply_lastfm_login }, + { HTTPD_METHOD_GET, "^/api/lastfm-logout$", jsonapi_reply_lastfm_logout }, + { HTTPD_METHOD_GET, "^/api/lastfm$", jsonapi_reply_lastfm }, + { HTTPD_METHOD_POST, "^/api/verification$", jsonapi_reply_verification }, - { EVHTTP_REQ_GET, "^/api/outputs$", jsonapi_reply_outputs }, - { EVHTTP_REQ_PUT, "^/api/outputs/set$", jsonapi_reply_outputs_set }, - { EVHTTP_REQ_POST, "^/api/select-outputs$", jsonapi_reply_outputs_set }, // deprecated: use "/api/outputs/set" - { EVHTTP_REQ_GET, "^/api/outputs/[[:digit:]]+$", jsonapi_reply_outputs_get_byid }, - { EVHTTP_REQ_PUT, "^/api/outputs/[[:digit:]]+$", jsonapi_reply_outputs_put_byid }, - { EVHTTP_REQ_PUT, "^/api/outputs/[[:digit:]]+/toggle$", jsonapi_reply_outputs_toggle_byid }, + { HTTPD_METHOD_GET, "^/api/outputs$", jsonapi_reply_outputs }, + { HTTPD_METHOD_PUT, "^/api/outputs/set$", jsonapi_reply_outputs_set }, + { HTTPD_METHOD_POST, "^/api/select-outputs$", jsonapi_reply_outputs_set }, // deprecated: use "/api/outputs/set" + { HTTPD_METHOD_GET, "^/api/outputs/[[:digit:]]+$", jsonapi_reply_outputs_get_byid }, + { HTTPD_METHOD_PUT, "^/api/outputs/[[:digit:]]+$", jsonapi_reply_outputs_put_byid }, + { HTTPD_METHOD_PUT, "^/api/outputs/[[:digit:]]+/toggle$", jsonapi_reply_outputs_toggle_byid }, - { EVHTTP_REQ_GET, "^/api/player$", jsonapi_reply_player }, - { EVHTTP_REQ_PUT, "^/api/player/play$", jsonapi_reply_player_play }, - { EVHTTP_REQ_PUT, "^/api/player/pause$", jsonapi_reply_player_pause }, - { EVHTTP_REQ_PUT, "^/api/player/stop$", jsonapi_reply_player_stop }, - { EVHTTP_REQ_PUT, "^/api/player/toggle$", jsonapi_reply_player_toggle }, - { EVHTTP_REQ_PUT, "^/api/player/next$", jsonapi_reply_player_next }, - { EVHTTP_REQ_PUT, "^/api/player/previous$", jsonapi_reply_player_previous }, - { EVHTTP_REQ_PUT, "^/api/player/shuffle$", jsonapi_reply_player_shuffle }, - { EVHTTP_REQ_PUT, "^/api/player/repeat$", jsonapi_reply_player_repeat }, - { EVHTTP_REQ_PUT, "^/api/player/consume$", jsonapi_reply_player_consume }, - { EVHTTP_REQ_PUT, "^/api/player/volume$", jsonapi_reply_player_volume }, - { EVHTTP_REQ_PUT, "^/api/player/seek$", jsonapi_reply_player_seek }, + { HTTPD_METHOD_GET, "^/api/player$", jsonapi_reply_player }, + { HTTPD_METHOD_PUT, "^/api/player/play$", jsonapi_reply_player_play }, + { HTTPD_METHOD_PUT, "^/api/player/pause$", jsonapi_reply_player_pause }, + { HTTPD_METHOD_PUT, "^/api/player/stop$", jsonapi_reply_player_stop }, + { HTTPD_METHOD_PUT, "^/api/player/toggle$", jsonapi_reply_player_toggle }, + { HTTPD_METHOD_PUT, "^/api/player/next$", jsonapi_reply_player_next }, + { HTTPD_METHOD_PUT, "^/api/player/previous$", jsonapi_reply_player_previous }, + { HTTPD_METHOD_PUT, "^/api/player/shuffle$", jsonapi_reply_player_shuffle }, + { HTTPD_METHOD_PUT, "^/api/player/repeat$", jsonapi_reply_player_repeat }, + { HTTPD_METHOD_PUT, "^/api/player/consume$", jsonapi_reply_player_consume }, + { HTTPD_METHOD_PUT, "^/api/player/volume$", jsonapi_reply_player_volume }, + { HTTPD_METHOD_PUT, "^/api/player/seek$", jsonapi_reply_player_seek }, - { EVHTTP_REQ_GET, "^/api/queue$", jsonapi_reply_queue }, - { EVHTTP_REQ_PUT, "^/api/queue/clear$", jsonapi_reply_queue_clear }, - { EVHTTP_REQ_POST, "^/api/queue/items/add$", jsonapi_reply_queue_tracks_add }, - { EVHTTP_REQ_PUT, "^/api/queue/items/[[:digit:]]+$", jsonapi_reply_queue_tracks_update }, - { EVHTTP_REQ_PUT, "^/api/queue/items/now_playing$", jsonapi_reply_queue_tracks_update }, - { EVHTTP_REQ_DELETE, "^/api/queue/items/[[:digit:]]+$", jsonapi_reply_queue_tracks_delete }, - { EVHTTP_REQ_POST, "^/api/queue/save$", jsonapi_reply_queue_save}, + { HTTPD_METHOD_GET, "^/api/queue$", jsonapi_reply_queue }, + { HTTPD_METHOD_PUT, "^/api/queue/clear$", jsonapi_reply_queue_clear }, + { HTTPD_METHOD_POST, "^/api/queue/items/add$", jsonapi_reply_queue_tracks_add }, + { HTTPD_METHOD_PUT, "^/api/queue/items/[[:digit:]]+$", jsonapi_reply_queue_tracks_update }, + { HTTPD_METHOD_PUT, "^/api/queue/items/now_playing$", jsonapi_reply_queue_tracks_update }, + { HTTPD_METHOD_DELETE, "^/api/queue/items/[[:digit:]]+$", jsonapi_reply_queue_tracks_delete }, + { HTTPD_METHOD_POST, "^/api/queue/save$", jsonapi_reply_queue_save}, - { EVHTTP_REQ_GET, "^/api/library/playlists$", jsonapi_reply_library_playlists }, - { EVHTTP_REQ_GET, "^/api/library/playlists/[[:digit:]]+$", jsonapi_reply_library_playlist_get }, - { EVHTTP_REQ_PUT, "^/api/library/playlists/[[:digit:]]+$", jsonapi_reply_library_playlist_put }, - { EVHTTP_REQ_GET, "^/api/library/playlists/[[:digit:]]+/tracks$", jsonapi_reply_library_playlist_tracks }, - { EVHTTP_REQ_PUT, "^/api/library/playlists/[[:digit:]]+/tracks", jsonapi_reply_library_playlist_tracks_put_byid}, -// { EVHTTP_REQ_POST, "^/api/library/playlists/[[:digit:]]+/tracks$", jsonapi_reply_library_playlists_tracks }, - { EVHTTP_REQ_DELETE, "^/api/library/playlists/[[:digit:]]+$", jsonapi_reply_library_playlist_delete }, - { EVHTTP_REQ_GET, "^/api/library/playlists/[[:digit:]]+/playlists", jsonapi_reply_library_playlist_playlists }, - { EVHTTP_REQ_GET, "^/api/library/artists$", jsonapi_reply_library_artists }, - { EVHTTP_REQ_GET, "^/api/library/artists/[[:digit:]]+$", jsonapi_reply_library_artist }, - { EVHTTP_REQ_GET, "^/api/library/artists/[[:digit:]]+/albums$", jsonapi_reply_library_artist_albums }, - { EVHTTP_REQ_GET, "^/api/library/albums$", jsonapi_reply_library_albums }, - { EVHTTP_REQ_GET, "^/api/library/albums/[[:digit:]]+$", jsonapi_reply_library_album }, - { EVHTTP_REQ_GET, "^/api/library/albums/[[:digit:]]+/tracks$", jsonapi_reply_library_album_tracks }, - { EVHTTP_REQ_PUT, "^/api/library/albums/[[:digit:]]+/tracks$", jsonapi_reply_library_album_tracks_put_byid }, - { EVHTTP_REQ_PUT, "^/api/library/tracks$", jsonapi_reply_library_tracks_put }, - { EVHTTP_REQ_GET, "^/api/library/tracks/[[:digit:]]+$", jsonapi_reply_library_tracks_get_byid }, - { EVHTTP_REQ_PUT, "^/api/library/tracks/[[:digit:]]+$", jsonapi_reply_library_tracks_put_byid }, - { EVHTTP_REQ_GET, "^/api/library/tracks/[[:digit:]]+/playlists$", jsonapi_reply_library_track_playlists }, - { EVHTTP_REQ_GET, "^/api/library/(genres|composers)$", jsonapi_reply_library_browse }, - { EVHTTP_REQ_GET, "^/api/library/(genres|composers)/.*$", jsonapi_reply_library_browseitem }, - { EVHTTP_REQ_GET, "^/api/library/count$", jsonapi_reply_library_count }, - { EVHTTP_REQ_GET, "^/api/library/files$", jsonapi_reply_library_files }, - { EVHTTP_REQ_POST, "^/api/library/add$", jsonapi_reply_library_add }, - { EVHTTP_REQ_PUT, "^/api/library/backup$", jsonapi_reply_library_backup }, + { HTTPD_METHOD_GET, "^/api/library/playlists$", jsonapi_reply_library_playlists }, + { HTTPD_METHOD_GET, "^/api/library/playlists/[[:digit:]]+$", jsonapi_reply_library_playlist_get }, + { HTTPD_METHOD_PUT, "^/api/library/playlists/[[:digit:]]+$", jsonapi_reply_library_playlist_put }, + { HTTPD_METHOD_GET, "^/api/library/playlists/[[:digit:]]+/tracks$", jsonapi_reply_library_playlist_tracks }, + { HTTPD_METHOD_PUT, "^/api/library/playlists/[[:digit:]]+/tracks", jsonapi_reply_library_playlist_tracks_put_byid}, +// { HTTPD_METHOD_POST, "^/api/library/playlists/[[:digit:]]+/tracks$", jsonapi_reply_library_playlists_tracks }, + { HTTPD_METHOD_DELETE, "^/api/library/playlists/[[:digit:]]+$", jsonapi_reply_library_playlist_delete }, + { HTTPD_METHOD_GET, "^/api/library/playlists/[[:digit:]]+/playlists", jsonapi_reply_library_playlist_playlists }, + { HTTPD_METHOD_GET, "^/api/library/artists$", jsonapi_reply_library_artists }, + { HTTPD_METHOD_GET, "^/api/library/artists/[[:digit:]]+$", jsonapi_reply_library_artist }, + { HTTPD_METHOD_GET, "^/api/library/artists/[[:digit:]]+/albums$", jsonapi_reply_library_artist_albums }, + { HTTPD_METHOD_GET, "^/api/library/albums$", jsonapi_reply_library_albums }, + { HTTPD_METHOD_GET, "^/api/library/albums/[[:digit:]]+$", jsonapi_reply_library_album }, + { HTTPD_METHOD_GET, "^/api/library/albums/[[:digit:]]+/tracks$", jsonapi_reply_library_album_tracks }, + { HTTPD_METHOD_PUT, "^/api/library/albums/[[:digit:]]+/tracks$", jsonapi_reply_library_album_tracks_put_byid }, + { HTTPD_METHOD_PUT, "^/api/library/tracks$", jsonapi_reply_library_tracks_put }, + { HTTPD_METHOD_GET, "^/api/library/tracks/[[:digit:]]+$", jsonapi_reply_library_tracks_get_byid }, + { HTTPD_METHOD_PUT, "^/api/library/tracks/[[:digit:]]+$", jsonapi_reply_library_tracks_put_byid }, + { HTTPD_METHOD_GET, "^/api/library/tracks/[[:digit:]]+/playlists$", jsonapi_reply_library_track_playlists }, + { HTTPD_METHOD_GET, "^/api/library/(genres|composers)$", jsonapi_reply_library_browse }, + { HTTPD_METHOD_GET, "^/api/library/(genres|composers)/.*$", jsonapi_reply_library_browseitem }, + { HTTPD_METHOD_GET, "^/api/library/count$", jsonapi_reply_library_count }, + { HTTPD_METHOD_GET, "^/api/library/files$", jsonapi_reply_library_files }, + { HTTPD_METHOD_POST, "^/api/library/add$", jsonapi_reply_library_add }, + { HTTPD_METHOD_PUT, "^/api/library/backup$", jsonapi_reply_library_backup }, - { EVHTTP_REQ_GET, "^/api/search$", jsonapi_reply_search }, + { HTTPD_METHOD_GET, "^/api/search$", jsonapi_reply_search }, { 0, NULL, NULL } }; @@ -4716,100 +4701,63 @@ static struct httpd_uri_map adm_handlers[] = /* ------------------------------- JSON API --------------------------------- */ -void -jsonapi_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed) +static void +jsonapi_request(struct httpd_request *hreq) { - struct httpd_request *hreq; - struct evkeyvalq *headers; int status_code; - DPRINTF(E_DBG, L_WEB, "JSON api request: '%s'\n", uri_parsed->uri); - - if (!httpd_admin_check_auth(req)) - return; - - hreq = httpd_request_parse(req, uri_parsed, NULL, adm_handlers); - if (!hreq) + if (!httpd_admin_check_auth(hreq)) { - DPRINTF(E_LOG, L_WEB, "Unrecognized path '%s' in JSON api request: '%s'\n", uri_parsed->path, uri_parsed->uri); - - httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); return; } - CHECK_NULL(L_WEB, hreq->reply = evbuffer_new()); + if (!hreq->handler) + { + DPRINTF(E_LOG, L_WEB, "Unrecognized JSON API request: '%s'\n", hreq->uri); + httpd_send_error(hreq, HTTP_BADREQUEST, "Bad Request"); + return; + } status_code = hreq->handler(hreq); if (status_code >= 400) - DPRINTF(E_LOG, L_WEB, "JSON api request failed with error code %d (%s)\n", status_code, uri_parsed->uri); + DPRINTF(E_LOG, L_WEB, "JSON api request failed with error code %d (%s)\n", status_code, hreq->uri); switch (status_code) { case HTTP_OK: /* 200 OK */ - headers = evhttp_request_get_output_headers(req); - evhttp_add_header(headers, "Content-Type", "application/json"); - httpd_send_reply(req, status_code, "OK", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_header_add(hreq->out_headers, "Content-Type", "application/json"); + httpd_send_reply(hreq, status_code, "OK", HTTPD_SEND_NO_GZIP); break; case HTTP_NOCONTENT: /* 204 No Content */ - httpd_send_reply(req, status_code, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, status_code, "No Content", HTTPD_SEND_NO_GZIP); break; case HTTP_NOTMODIFIED: /* 304 Not Modified */ - httpd_send_reply(req, HTTP_NOTMODIFIED, NULL, NULL, HTTPD_SEND_NO_GZIP); + httpd_send_reply(hreq, HTTP_NOTMODIFIED, NULL, HTTPD_SEND_NO_GZIP); break; - case HTTP_BADREQUEST: /* 400 Bad Request */ - httpd_send_error(req, status_code, "Bad Request"); + httpd_send_error(hreq, status_code, "Bad Request"); break; case 403: - httpd_send_error(req, status_code, "Forbidden"); + httpd_send_error(hreq, status_code, "Forbidden"); break; case HTTP_NOTFOUND: /* 404 Not Found */ - httpd_send_error(req, status_code, "Not Found"); + httpd_send_error(hreq, status_code, "Not Found"); break; case HTTP_SERVUNAVAIL: /* 503 */ - httpd_send_error(req, status_code, "Service Unavailable"); + httpd_send_error(hreq, status_code, "Service Unavailable"); break; case HTTP_INTERNAL: /* 500 Internal Server Error */ default: - httpd_send_error(req, HTTP_INTERNAL, "Internal Server Error"); + httpd_send_error(hreq, HTTP_INTERNAL, "Internal Server Error"); break; } - - evbuffer_free(hreq->reply); - free(hreq); } -int -jsonapi_is_request(const char *path) -{ - if (strncmp(path, "/api/", strlen("/api/")) == 0) - return 1; - if (strcmp(path, "/api") == 0) - return 1; - - return 0; -} - -int +static int jsonapi_init(void) { - char buf[64]; char *temp_path; - int i; - int ret; - - for (i = 0; adm_handlers[i].handler; i++) - { - ret = regcomp(&adm_handlers[i].preg, adm_handlers[i].regexp, REG_EXTENDED | REG_NOSUB); - if (ret != 0) - { - regerror(ret, &adm_handlers[i].preg, buf, sizeof(buf)); - - DPRINTF(E_FATAL, L_WEB, "JSON api init failed; regexp error: %s\n", buf); - return -1; - } - } default_playlist_directory = NULL; allow_modifying_stored_playlists = cfg_getbool(cfg_getsec(cfg, "library"), "allow_modifying_stored_playlists"); @@ -4837,13 +4785,20 @@ jsonapi_init(void) return 0; } -void +static void jsonapi_deinit(void) { - int i; - - for (i = 0; adm_handlers[i].handler; i++) - regfree(&adm_handlers[i].preg); - free(default_playlist_directory); } + +struct httpd_module httpd_jsonapi = +{ + .name = "JSON API", + .type = MODULE_JSONAPI, + .logdomain = L_WEB, + .subpaths = { "/api/", NULL }, + .handlers = adm_handlers, + .init = jsonapi_init, + .deinit = jsonapi_deinit, + .request = jsonapi_request, +}; diff --git a/src/httpd_jsonapi.h b/src/httpd_jsonapi.h deleted file mode 100644 index e6113ea6..00000000 --- a/src/httpd_jsonapi.h +++ /dev/null @@ -1,19 +0,0 @@ - -#ifndef __HTTPD_JSONAPI_H__ -#define __HTTPD_JSONAPI_H__ - -#include "httpd.h" - -int -jsonapi_init(void); - -void -jsonapi_deinit(void); - -void -jsonapi_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed); - -int -jsonapi_is_request(const char *path); - -#endif /* !__HTTPD_JSONAPI_H__ */ diff --git a/src/httpd_libevhttp.c b/src/httpd_libevhttp.c new file mode 100644 index 00000000..6949df8a --- /dev/null +++ b/src/httpd_libevhttp.c @@ -0,0 +1,652 @@ +/* + * Copyright (C) 2023 Espen Jürgensen + * + * 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 // TAILQ_FOREACH +#include // listen() + +#include +#include // flags in struct evhttp +#include +#include +#include + +#include + +#include "misc.h" +#include "logger.h" +#include "commands.h" +#include "httpd_internal.h" + +// #define DEBUG_ALLOC 1 + +#ifdef DEBUG_ALLOC +static pthread_mutex_t debug_alloc_lck = PTHREAD_MUTEX_INITIALIZER; +static int debug_alloc_count; +#endif + +struct httpd_uri_parsed +{ + struct evhttp_uri *ev_uri; + struct evkeyvalq query; + char *path; + httpd_uri_path_parts path_parts; +}; + +struct httpd_server +{ + int fd; + struct evhttp *evhttp; + struct commands_base *cmdbase; + httpd_request_cb request_cb; + void *request_cb_arg; +}; + +struct httpd_reply +{ + struct httpd_request *hreq; + enum httpd_reply_type type; + int code; + const char *reason; + httpd_connection_chunkcb chunkcb; + void *cbarg; +}; + +struct httpd_disconnect +{ + pthread_mutex_t lock; + struct event *ev; + httpd_close_cb cb; + void *cbarg; +}; + +struct httpd_backend_data +{ + // Pointer to server instance processing the request + struct httpd_server *server; + // If caller wants a callback on disconnect + struct httpd_disconnect disconnect; +}; + +// Forward +static void +closecb_worker(evutil_socket_t fd, short event, void *arg); + + +const char * +httpd_query_value_find(httpd_query *query, const char *key) +{ + return evhttp_find_header(query, key); +} + +void +httpd_query_iterate(httpd_query *query, httpd_query_iteratecb cb, void *arg) +{ + struct evkeyval *param; + + TAILQ_FOREACH(param, query, next) + { + cb(param->key, param->value, arg); + } +} + +void +httpd_query_clear(httpd_query *query) +{ + evhttp_clear_headers(query); +} + +const char * +httpd_header_find(httpd_headers *headers, const char *key) +{ + return evhttp_find_header(headers, key); +} + +void +httpd_header_remove(httpd_headers *headers, const char *key) +{ + evhttp_remove_header(headers, key); +} + +void +httpd_header_add(httpd_headers *headers, const char *key, const char *val) +{ + evhttp_add_header(headers, key, val); +} + +void +httpd_headers_clear(httpd_headers *headers) +{ + evhttp_clear_headers(headers); +} + +void +httpd_request_close_cb_set(struct httpd_request *hreq, httpd_close_cb cb, void *arg) +{ + struct httpd_disconnect *disconnect = &hreq->backend_data->disconnect; + + pthread_mutex_lock(&disconnect->lock); + + disconnect->cb = cb; + disconnect->cbarg = arg; + + if (hreq->is_async) + { + if (disconnect->ev) + event_free(disconnect->ev); + + if (disconnect->cb) + disconnect->ev = event_new(hreq->evbase, -1, 0, closecb_worker, hreq); + else + disconnect->ev = NULL; + } + + pthread_mutex_unlock(&disconnect->lock); +} + +void +httpd_request_free(struct httpd_request *hreq) +{ +#ifdef DEBUG_ALLOC + pthread_mutex_lock(&debug_alloc_lck); + debug_alloc_count--; + pthread_mutex_unlock(&debug_alloc_lck); + DPRINTF(E_DBG, L_HTTPD, "DEALLOC hreq - count is %d\n", debug_alloc_count); +#endif + + if (!hreq) + return; + + if (hreq->out_body) + evbuffer_free(hreq->out_body); + + httpd_uri_parsed_free(hreq->uri_parsed); + httpd_backend_data_free(hreq->backend_data); + free(hreq); +} + +struct httpd_request * +httpd_request_new(httpd_backend *backend, httpd_server *server, const char *uri, const char *user_agent) +{ + struct httpd_request *hreq; + httpd_backend_data *backend_data; + + CHECK_NULL(L_HTTPD, hreq = calloc(1, sizeof(struct httpd_request))); + +#ifdef DEBUG_ALLOC + pthread_mutex_lock(&debug_alloc_lck); + debug_alloc_count++; + pthread_mutex_unlock(&debug_alloc_lck); + DPRINTF(E_DBG, L_HTTPD, "ALLOC hreq - count is %d\n", debug_alloc_count); +#endif + + // Populate hreq by getting values from the backend (or from the caller) + hreq->backend = backend; + if (backend) + { + backend_data = httpd_backend_data_create(backend, server); + hreq->backend_data = backend_data; + + hreq->uri = httpd_backend_uri_get(backend, backend_data); + hreq->uri_parsed = httpd_uri_parsed_create(backend); + + hreq->in_headers = httpd_backend_input_headers_get(backend); + hreq->out_headers = httpd_backend_output_headers_get(backend); + hreq->in_body = httpd_backend_input_buffer_get(backend); + httpd_backend_method_get(&hreq->method, backend); + httpd_backend_peer_get(&hreq->peer_address, &hreq->peer_port, backend, backend_data); + + hreq->user_agent = httpd_header_find(hreq->in_headers, "User-Agent"); + } + else + { + hreq->uri = uri; + hreq->uri_parsed = httpd_uri_parsed_create_fromuri(uri); + + hreq->user_agent = user_agent; + } + + if (!hreq->uri_parsed) + { + DPRINTF(E_LOG, L_HTTPD, "Unable to parse URI '%s' in request from '%s'\n", hreq->uri, hreq->peer_address); + goto error; + } + + // Don't write directly to backend's buffer. This way we are sure we own the + // buffer even if there is no backend. + CHECK_NULL(L_HTTPD, hreq->out_body = evbuffer_new()); + + hreq->path = httpd_uri_path_get(hreq->uri_parsed); + hreq->query = httpd_uri_query_get(hreq->uri_parsed); + httpd_uri_path_parts_get(&hreq->path_parts, hreq->uri_parsed); + + return hreq; + + error: + httpd_request_free(hreq); + return NULL; +} + +// Since this is async, libevent will already have closed the connection, so +// the parts of hreq that are from httpd_connection will now be invalid e.g. +// peer_address. +static void +closecb_worker(evutil_socket_t fd, short event, void *arg) +{ + struct httpd_request *hreq = arg; + struct httpd_disconnect *disconnect = &hreq->backend_data->disconnect; + + pthread_mutex_lock(&disconnect->lock); + + if (disconnect->cb) + disconnect->cb(disconnect->cbarg); + + pthread_mutex_unlock(&disconnect->lock); + + httpd_send_reply_end(hreq); // hreq is now deallocated +} + +static void +closecb_httpd(httpd_connection *conn, void *arg) +{ + struct httpd_request *hreq = arg; + struct httpd_disconnect *disconnect = &hreq->backend_data->disconnect; + + DPRINTF(E_WARN, hreq->module->logdomain, "Connection to '%s' was closed\n", hreq->peer_address); + + // The disconnect event may occur while a worker thread is accessing hreq, or + // has an event scheduled that will do so, so we have to be careful to let it + // finish and cancel events. + pthread_mutex_lock(&disconnect->lock); + if (hreq->is_async) + { + if (disconnect->cb) + event_active(disconnect->ev, 0, 0); + + pthread_mutex_unlock(&disconnect->lock); + return; + } + pthread_mutex_unlock(&disconnect->lock); + + if (!disconnect->cb) + return; + + disconnect->cb(disconnect->cbarg); + httpd_send_reply_end(hreq); // hreq is now deallocated +} + +static void +gencb_httpd(httpd_backend *backend, void *arg) +{ + httpd_server *server = arg; + struct httpd_request *hreq; + struct bufferevent *bufev; + + // Clear the proxy request flag set by evhttp if the request URI was absolute. + // It has side-effects on Connection: keep-alive + backend->flags &= ~EVHTTP_PROXY_REQUEST; + + // This is a workaround for some versions of libevent (2.0 and 2.1) that don't + // detect if the client hangs up, and thus don't clean up and never call the + // connection close cb(). See github issue #870 and + // https://github.com/libevent/libevent/issues/666. It should probably be + // removed again in the future. + bufev = evhttp_connection_get_bufferevent(evhttp_request_get_connection(backend)); + if (bufev) + bufferevent_enable(bufev, EV_READ); + + hreq = httpd_request_new(backend, server, NULL, NULL); + if (!hreq) + { + evhttp_send_error(backend, HTTP_INTERNAL, "Internal error"); + return; + } + + // We must hook connection close, so we can assure that conn close callbacks + // to handlers running in a worker are made in the same thread. + evhttp_connection_set_closecb(evhttp_request_get_connection(backend), closecb_httpd, hreq); + + server->request_cb(hreq, server->request_cb_arg); +} + +void +httpd_server_free(httpd_server *server) +{ + if (!server) + return; + + if (server->fd > 0) + close(server->fd); + + if (server->evhttp) + evhttp_free(server->evhttp); + + commands_base_free(server->cmdbase); + free(server); +} + +httpd_server * +httpd_server_new(struct event_base *evbase, unsigned short port, httpd_request_cb cb, void *arg) +{ + httpd_server *server; + int ret; + + CHECK_NULL(L_HTTPD, server = calloc(1, sizeof(httpd_server))); + CHECK_NULL(L_HTTPD, server->evhttp = evhttp_new(evbase)); + CHECK_NULL(L_HTTPD, server->cmdbase = commands_base_new(evbase, NULL)); + + server->request_cb = cb; + server->request_cb_arg = arg; + + server->fd = net_bind_with_reuseport(&port, SOCK_STREAM | SOCK_NONBLOCK, "httpd"); + if (server->fd <= 0) + goto error; + + // Backlog of 128 is the same that libevent uses + ret = listen(server->fd, 128); + if (ret < 0) + goto error; + + ret = evhttp_accept_socket(server->evhttp, server->fd); + if (ret < 0) + goto error; + + evhttp_set_gencb(server->evhttp, gencb_httpd, server); + + return server; + + error: + httpd_server_free(server); + return NULL; +} + +void +httpd_server_allow_origin_set(httpd_server *server, bool allow) +{ + evhttp_set_allowed_methods(server->evhttp, EVHTTP_REQ_GET | EVHTTP_REQ_POST | EVHTTP_REQ_PUT | EVHTTP_REQ_DELETE | EVHTTP_REQ_HEAD | EVHTTP_REQ_OPTIONS); +} + +// No locking of hreq required here, we're in the httpd thread, and the worker +// thread is waiting at commands_exec_sync() +static void +send_reply_and_free(struct httpd_reply *reply) +{ + struct httpd_request *hreq = reply->hreq; + httpd_connection *conn; + +// DPRINTF(E_DBG, L_HTTPD, "Send from httpd thread, type %d, backend %p\n", reply->type, hreq->backend); + + if (reply->type & HTTPD_F_REPLY_LAST) + { + conn = evhttp_request_get_connection(hreq->backend); + if (conn) + evhttp_connection_set_closecb(conn, NULL, NULL); + } + + switch (reply->type) + { + case HTTPD_REPLY_COMPLETE: + evhttp_send_reply(hreq->backend, reply->code, reply->reason, hreq->out_body); + break; + case HTTPD_REPLY_START: + evhttp_send_reply_start(hreq->backend, reply->code, reply->reason); + break; + case HTTPD_REPLY_CHUNK: + evhttp_send_reply_chunk_with_cb(hreq->backend, hreq->out_body, reply->chunkcb, reply->cbarg); + break; + case HTTPD_REPLY_END: + evhttp_send_reply_end(hreq->backend); + break; + } +} + +static enum command_state +send_reply_and_free_cb(void *arg, int *retval) +{ + struct httpd_reply *reply = arg; + + send_reply_and_free(reply); + + return COMMAND_END; +} + +void +httpd_send(struct httpd_request *hreq, enum httpd_reply_type type, int code, const char *reason, httpd_connection_chunkcb cb, void *cbarg) +{ + struct httpd_server *server = hreq->backend_data->server; + struct httpd_reply reply = { + .hreq = hreq, + .type = type, + .code = code, + .chunkcb = cb, + .cbarg = cbarg, + .reason = reason, + }; + + if (type & HTTPD_F_REPLY_LAST) + httpd_request_close_cb_set(hreq, NULL, NULL); + + // Sending async is not a option, because then the worker thread might touch + // hreq before we have completed sending the current chunk + if (hreq->is_async) + commands_exec_sync(server->cmdbase, send_reply_and_free_cb, NULL, &reply); + else + send_reply_and_free(&reply); + + if (type & HTTPD_F_REPLY_LAST) + httpd_request_free(hreq); +} + +httpd_backend_data * +httpd_backend_data_create(httpd_backend *backend, httpd_server *server) +{ + httpd_backend_data *backend_data; + + CHECK_NULL(L_HTTPD, backend_data = calloc(1, sizeof(httpd_backend_data))); + CHECK_ERR(L_HTTPD, mutex_init(&backend_data->disconnect.lock)); + backend_data->server = server; + + return backend_data; +} + +void +httpd_backend_data_free(httpd_backend_data *backend_data) +{ + if (!backend_data) + return; + + if (backend_data->disconnect.ev) + event_free(backend_data->disconnect.ev); + + free(backend_data); +} + +struct event_base * +httpd_backend_evbase_get(httpd_backend *backend) +{ + httpd_connection *conn = evhttp_request_get_connection(backend); + if (!conn) + return NULL; + + return evhttp_connection_get_base(conn); +} + +const char * +httpd_backend_uri_get(httpd_backend *backend, httpd_backend_data *backend_data) +{ + return evhttp_request_get_uri(backend); +} + +httpd_headers * +httpd_backend_input_headers_get(httpd_backend *backend) +{ + return evhttp_request_get_input_headers(backend); +} + +httpd_headers * +httpd_backend_output_headers_get(httpd_backend *backend) +{ + return evhttp_request_get_output_headers(backend); +} + +struct evbuffer * +httpd_backend_input_buffer_get(httpd_backend *backend) +{ + return evhttp_request_get_input_buffer(backend); +} + +struct evbuffer * +httpd_backend_output_buffer_get(httpd_backend *backend) +{ + return evhttp_request_get_output_buffer(backend); +} + +int +httpd_backend_peer_get(const char **addr, uint16_t *port, httpd_backend *backend, httpd_backend_data *backend_data) +{ + httpd_connection *conn = evhttp_request_get_connection(backend); + if (!conn) + return -1; + +#ifdef HAVE_EVHTTP_CONNECTION_GET_PEER_CONST_CHAR + evhttp_connection_get_peer(conn, addr, port); +#else + evhttp_connection_get_peer(conn, (char **)addr, port); +#endif + return 0; +} + +int +httpd_backend_method_get(enum httpd_methods *method, httpd_backend *backend) +{ + enum evhttp_cmd_type cmd = evhttp_request_get_command(backend); + + switch (cmd) + { + case EVHTTP_REQ_GET: *method = HTTPD_METHOD_GET; break; + case EVHTTP_REQ_POST: *method = HTTPD_METHOD_POST; break; + case EVHTTP_REQ_HEAD: *method = HTTPD_METHOD_HEAD; break; + case EVHTTP_REQ_PUT: *method = HTTPD_METHOD_PUT; break; + case EVHTTP_REQ_DELETE: *method = HTTPD_METHOD_DELETE; break; + case EVHTTP_REQ_OPTIONS: *method = HTTPD_METHOD_OPTIONS; break; + case EVHTTP_REQ_TRACE: *method = HTTPD_METHOD_TRACE; break; + case EVHTTP_REQ_CONNECT: *method = HTTPD_METHOD_CONNECT; break; + case EVHTTP_REQ_PATCH: *method = HTTPD_METHOD_PATCH; break; + default: *method = HTTPD_METHOD_GET; return -1; + } + + return 0; +} + +httpd_uri_parsed * +httpd_uri_parsed_create(httpd_backend *backend) +{ + const char *uri = evhttp_request_get_uri(backend); + + return httpd_uri_parsed_create_fromuri(uri); +} + +httpd_uri_parsed * +httpd_uri_parsed_create_fromuri(const char *uri) +{ + struct httpd_uri_parsed *parsed; + const char *query; + char *path = NULL; + char *path_part; + char *ptr; + int i; + + parsed = calloc(1, sizeof(struct httpd_uri_parsed)); + if (!parsed) + goto error; + + parsed->ev_uri = evhttp_uri_parse_with_flags(uri, EVHTTP_URI_NONCONFORMANT); + if (!parsed->ev_uri) + goto error; + + query = evhttp_uri_get_query(parsed->ev_uri); + if (query && strchr(query, '=') && evhttp_parse_query_str(query, &(parsed->query)) < 0) + goto error; + + path = strdup(evhttp_uri_get_path(parsed->ev_uri)); + if (!path || !(parsed->path = evhttp_uridecode(path, 0, NULL))) + goto error; + + path_part = strtok_r(path, "/", &ptr); + for (i = 0; (i < ARRAY_SIZE(parsed->path_parts) && path_part); i++) + { + parsed->path_parts[i] = evhttp_uridecode(path_part, 0, NULL); + path_part = strtok_r(NULL, "/", &ptr); + } + + // If "path_part" is not NULL, we have path tokens that could not be parsed into the "parsed->path_parts" array + if (path_part) + goto error; + + free(path); + return parsed; + + error: + free(path); + httpd_uri_parsed_free(parsed); + return NULL; +} + +void +httpd_uri_parsed_free(httpd_uri_parsed *parsed) +{ + int i; + + if (!parsed) + return; + + free(parsed->path); + for (i = 0; i < ARRAY_SIZE(parsed->path_parts); i++) + free(parsed->path_parts[i]); + + httpd_query_clear(&(parsed->query)); + + if (parsed->ev_uri) + evhttp_uri_free(parsed->ev_uri); + + free(parsed); +} + +httpd_query * +httpd_uri_query_get(httpd_uri_parsed *parsed) +{ + return &parsed->query; +} + +const char * +httpd_uri_path_get(httpd_uri_parsed *parsed) +{ + return parsed->path; +} + +void +httpd_uri_path_parts_get(httpd_uri_path_parts *path_parts, httpd_uri_parsed *parsed) +{ + memcpy(path_parts, parsed->path_parts, sizeof(httpd_uri_path_parts)); +} diff --git a/src/httpd_oauth.c b/src/httpd_oauth.c index 202b2633..951aef49 100644 --- a/src/httpd_oauth.c +++ b/src/httpd_oauth.c @@ -28,7 +28,7 @@ #include #include -#include "httpd_oauth.h" +#include "httpd_internal.h" #include "logger.h" #include "misc.h" #include "conffile.h" @@ -54,12 +54,12 @@ oauth_reply_spotify(struct httpd_request *hreq) ret = spotifywebapi_oauth_callback(hreq->query, redirect_uri, &errmsg); if (ret < 0) { - DPRINTF(E_LOG, L_WEB, "Could not parse Spotify OAuth callback '%s': %s\n", hreq->uri_parsed->uri, errmsg); - httpd_send_error(hreq->req, HTTP_INTERNAL, errmsg); + DPRINTF(E_LOG, L_WEB, "Could not parse Spotify OAuth callback '%s': %s\n", hreq->uri, errmsg); + httpd_send_error(hreq, HTTP_INTERNAL, errmsg); return -1; } - httpd_redirect_to(hreq->req, "/#/settings/online-services"); + httpd_redirect_to(hreq, "/#/settings/online-services"); return 0; } @@ -69,7 +69,7 @@ oauth_reply_spotify(struct httpd_request *hreq) { DPRINTF(E_LOG, L_WEB, "This version was built without support for Spotify\n"); - httpd_send_error(hreq->req, HTTP_NOTFOUND, "This version was built without support for Spotify"); + httpd_send_error(hreq, HTTP_NOTFOUND, "This version was built without support for Spotify"); return -1; } @@ -90,65 +90,27 @@ static struct httpd_uri_map oauth_handlers[] = /* ------------------------------- OAUTH API -------------------------------- */ -void -oauth_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed) +static void +oauth_request(struct httpd_request *hreq) { - struct httpd_request *hreq; - - DPRINTF(E_LOG, L_WEB, "OAuth request: '%s'\n", uri_parsed->uri); - - hreq = httpd_request_parse(req, uri_parsed, NULL, oauth_handlers); - if (!hreq) + if (!hreq->handler) { - DPRINTF(E_LOG, L_WEB, "Unrecognized path '%s' in OAuth request: '%s'\n", uri_parsed->path, uri_parsed->uri); + DPRINTF(E_LOG, L_WEB, "Unrecognized path in OAuth request: '%s'\n", hreq->uri); - httpd_send_error(req, HTTP_NOTFOUND, NULL); + httpd_send_error(hreq, HTTP_NOTFOUND, NULL); return; } hreq->handler(hreq); - - free(hreq); } -int -oauth_is_request(const char *path) +struct httpd_module httpd_oauth = { - if (strncmp(path, "/oauth/", strlen("/oauth/")) == 0) - return 1; - if (strcmp(path, "/oauth") == 0) - return 1; - - return 0; -} - -int -oauth_init(void) -{ - char buf[64]; - int i; - int ret; - - for (i = 0; oauth_handlers[i].handler; i++) - { - ret = regcomp(&oauth_handlers[i].preg, oauth_handlers[i].regexp, REG_EXTENDED | REG_NOSUB); - if (ret != 0) - { - regerror(ret, &oauth_handlers[i].preg, buf, sizeof(buf)); - - DPRINTF(E_FATAL, L_WEB, "OAuth init failed; regexp error: %s\n", buf); - return -1; - } - } - - return 0; -} - -void -oauth_deinit(void) -{ - int i; - - for (i = 0; oauth_handlers[i].handler; i++) - regfree(&oauth_handlers[i].preg); -} + .name = "OAuth", + .type = MODULE_OAUTH, + .logdomain = L_WEB, + .subpaths = { "/oauth/", NULL }, + .fullpaths = { "/oauth", NULL }, + .handlers = oauth_handlers, + .request = oauth_request, +}; diff --git a/src/httpd_oauth.h b/src/httpd_oauth.h deleted file mode 100644 index 71ff1495..00000000 --- a/src/httpd_oauth.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef __HTTPD_OAUTH_H__ -#define __HTTPD_OAUTH_H__ - -#include "httpd.h" - -void -oauth_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed); - -int -oauth_is_request(const char *path); - -int -oauth_init(void); - -void -oauth_deinit(void); - -#endif /* !__HTTPD_OAUTH_H__ */ diff --git a/src/httpd_rsp.c b/src/httpd_rsp.c index 16c4b730..7bfce8f4 100644 --- a/src/httpd_rsp.c +++ b/src/httpd_rsp.c @@ -32,12 +32,11 @@ #include "mxml-compat.h" -#include "httpd_rsp.h" +#include "httpd_internal.h" #include "logger.h" #include "db.h" #include "conffile.h" #include "misc.h" -#include "httpd.h" #include "transcode.h" #include "parsers/rsp_parser.h" @@ -120,28 +119,17 @@ static const struct field_map rsp_fields[] = /* -------------------------------- HELPERS --------------------------------- */ -static struct evbuffer * -mxml_to_evbuf(mxml_node_t *tree) +static int +mxml_to_evbuf(struct evbuffer *evbuf, mxml_node_t *tree) { - struct evbuffer *evbuf; char *xml; int ret; - evbuf = evbuffer_new(); - if (!evbuf) - { - DPRINTF(E_LOG, L_RSP, "Could not create evbuffer for RSP reply\n"); - - return NULL; - } - xml = mxmlSaveAllocString(tree, MXML_NO_CALLBACK); if (!xml) { DPRINTF(E_LOG, L_RSP, "Could not finalize RSP reply\n"); - - evbuffer_free(evbuf); - return NULL; + return -1; } ret = evbuffer_add(evbuf, xml, strlen(xml)); @@ -149,22 +137,19 @@ mxml_to_evbuf(mxml_node_t *tree) if (ret < 0) { DPRINTF(E_LOG, L_RSP, "Could not load evbuffer for RSP reply\n"); - - evbuffer_free(evbuf); - return NULL; + return -1; } - return evbuf; + return 0; } static void -rsp_send_error(struct evhttp_request *req, char *errmsg) +rsp_send_error(struct httpd_request *hreq, char *errmsg) { - struct evbuffer *evbuf; - struct evkeyvalq *headers; mxml_node_t *reply; mxml_node_t *status; mxml_node_t *node; + int ret; /* We'd use mxmlNewXML(), but then we can't put any attributes * on the root node and we need some. @@ -187,23 +172,19 @@ rsp_send_error(struct evhttp_request *req, char *errmsg) node = mxmlNewElement(status, "totalrecords"); mxmlNewText(node, 0, "0"); - evbuf = mxml_to_evbuf(reply); + ret = mxml_to_evbuf(hreq->out_body, reply); mxmlDelete(reply); - if (!evbuf) + if (ret < 0) { - httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); - + httpd_send_error(hreq, HTTP_SERVUNAVAIL, "Internal Server Error"); return; } - headers = evhttp_request_get_output_headers(req); - evhttp_add_header(headers, "Content-Type", "text/xml; charset=utf-8"); - evhttp_add_header(headers, "Connection", "close"); + httpd_header_add(hreq->out_headers, "Content-Type", "text/xml; charset=utf-8"); + httpd_header_add(hreq->out_headers, "Connection", "close"); - httpd_send_reply(req, HTTP_OK, "OK", evbuf, HTTPD_SEND_NO_GZIP); - - evbuffer_free(evbuf); + httpd_send_reply(hreq, HTTP_OK, "OK", HTTPD_SEND_NO_GZIP); } static int @@ -215,25 +196,25 @@ query_params_set(struct query_params *qp, struct httpd_request *hreq) int ret; qp->offset = 0; - param = evhttp_find_header(hreq->query, "offset"); + param = httpd_query_value_find(hreq->query, "offset"); if (param) { ret = safe_atoi32(param, &qp->offset); if (ret < 0) { - rsp_send_error(hreq->req, "Invalid offset"); + rsp_send_error(hreq, "Invalid offset"); return -1; } } qp->limit = 0; - param = evhttp_find_header(hreq->query, "limit"); + param = httpd_query_value_find(hreq->query, "limit"); if (param) { ret = safe_atoi32(param, &qp->limit); if (ret < 0) { - rsp_send_error(hreq->req, "Invalid limit"); + rsp_send_error(hreq, "Invalid limit"); return -1; } } @@ -244,7 +225,7 @@ query_params_set(struct query_params *qp, struct httpd_request *hreq) qp->idx_type = I_NONE; qp->filter = NULL; - param = evhttp_find_header(hreq->query, "query"); + param = httpd_query_value_find(hreq->query, "query"); if (param) { ret = snprintf(query, sizeof(query), "%s", param); @@ -278,28 +259,23 @@ query_params_set(struct query_params *qp, struct httpd_request *hreq) } static void -rsp_send_reply(struct evhttp_request *req, mxml_node_t *reply) +rsp_send_reply(struct httpd_request *hreq, mxml_node_t *reply) { - struct evbuffer *evbuf; - struct evkeyvalq *headers; + int ret; - evbuf = mxml_to_evbuf(reply); + ret = mxml_to_evbuf(hreq->out_body, reply); mxmlDelete(reply); - if (!evbuf) + if (ret < 0) { - rsp_send_error(req, "Could not finalize reply"); - + rsp_send_error(hreq, "Could not finalize reply"); return; } - headers = evhttp_request_get_output_headers(req); - evhttp_add_header(headers, "Content-Type", "text/xml; charset=utf-8"); - evhttp_add_header(headers, "Connection", "close"); + httpd_header_add(hreq->out_headers, "Content-Type", "text/xml; charset=utf-8"); + httpd_header_add(hreq->out_headers, "Connection", "close"); - httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); - - evbuffer_free(evbuf); + httpd_send_reply(hreq, HTTP_OK, "OK", 0); } static int @@ -318,7 +294,7 @@ rsp_request_authorize(struct httpd_request *hreq) DPRINTF(E_DBG, L_RSP, "Checking authentication for library\n"); // We don't care about the username - ret = httpd_basic_auth(hreq->req, NULL, passwd, cfg_getstr(cfg_getsec(cfg, "library"), "name")); + ret = httpd_basic_auth(hreq, NULL, passwd, cfg_getstr(cfg_getsec(cfg, "library"), "name")); if (ret != 0) { DPRINTF(E_LOG, L_RSP, "Unsuccessful library authorization attempt from '%s'\n", hreq->peer_address); @@ -382,7 +358,7 @@ rsp_reply_info(struct httpd_request *hreq) node = mxmlNewElement(info, "name"); mxmlNewText(node, 0, library); - rsp_send_reply(hreq->req, reply); + rsp_send_reply(hreq, reply); return 0; } @@ -411,7 +387,7 @@ rsp_reply_db(struct httpd_request *hreq) { DPRINTF(E_LOG, L_RSP, "Could not start query\n"); - rsp_send_error(hreq->req, "Could not start query"); + rsp_send_error(hreq, "Could not start query"); return -1; } @@ -465,7 +441,7 @@ rsp_reply_db(struct httpd_request *hreq) mxmlDelete(reply); db_query_end(&qp); - rsp_send_error(hreq->req, "Error fetching query results"); + rsp_send_error(hreq, "Error fetching query results"); return -1; } @@ -479,7 +455,7 @@ rsp_reply_db(struct httpd_request *hreq) db_query_end(&qp); - rsp_send_reply(hreq->req, reply); + rsp_send_reply(hreq, reply); return 0; } @@ -489,7 +465,6 @@ rsp_reply_playlist(struct httpd_request *hreq) { struct query_params qp; struct db_media_file_info dbmfi; - struct evkeyvalq *headers; const char *param; const char *ua; const char *client_codecs; @@ -508,10 +483,10 @@ rsp_reply_playlist(struct httpd_request *hreq) memset(&qp, 0, sizeof(struct query_params)); - ret = safe_atoi32(hreq->uri_parsed->path_parts[2], &qp.id); + ret = safe_atoi32(hreq->path_parts[2], &qp.id); if (ret < 0) { - rsp_send_error(hreq->req, "Invalid playlist ID"); + rsp_send_error(hreq, "Invalid playlist ID"); return -1; } @@ -523,7 +498,7 @@ rsp_reply_playlist(struct httpd_request *hreq) qp.sort = S_NAME; mode = F_FULL; - param = evhttp_find_header(hreq->query, "type"); + param = httpd_query_value_find(hreq->query, "type"); if (param) { if (strcasecmp(param, "full") == 0) @@ -547,7 +522,7 @@ rsp_reply_playlist(struct httpd_request *hreq) { DPRINTF(E_LOG, L_RSP, "Could not start query\n"); - rsp_send_error(hreq->req, "Could not start query"); + rsp_send_error(hreq, "Could not start query"); if (qp.filter) free(qp.filter); @@ -587,10 +562,8 @@ rsp_reply_playlist(struct httpd_request *hreq) /* Items block (all items) */ while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0) { - headers = evhttp_request_get_input_headers(hreq->req); - - ua = evhttp_find_header(headers, "User-Agent"); - client_codecs = evhttp_find_header(headers, "Accept-Codecs"); + ua = httpd_header_find(hreq->in_headers, "User-Agent"); + client_codecs = httpd_header_find(hreq->in_headers, "Accept-Codecs"); transcode = transcode_needed(ua, client_codecs, dbmfi.codectype); @@ -658,7 +631,7 @@ rsp_reply_playlist(struct httpd_request *hreq) mxmlDelete(reply); db_query_end(&qp); - rsp_send_error(hreq->req, "Error fetching query results"); + rsp_send_error(hreq, "Error fetching query results"); return -1; } @@ -672,7 +645,7 @@ rsp_reply_playlist(struct httpd_request *hreq) db_query_end(&qp); - rsp_send_reply(hreq->req, reply); + rsp_send_reply(hreq, reply); return 0; } @@ -691,34 +664,34 @@ rsp_reply_browse(struct httpd_request *hreq) memset(&qp, 0, sizeof(struct query_params)); - if (strcmp(hreq->uri_parsed->path_parts[3], "artist") == 0) + if (strcmp(hreq->path_parts[3], "artist") == 0) { qp.type = Q_BROWSE_ARTISTS; } - else if (strcmp(hreq->uri_parsed->path_parts[3], "genre") == 0) + else if (strcmp(hreq->path_parts[3], "genre") == 0) { qp.type = Q_BROWSE_GENRES; } - else if (strcmp(hreq->uri_parsed->path_parts[3], "album") == 0) + else if (strcmp(hreq->path_parts[3], "album") == 0) { qp.type = Q_BROWSE_ALBUMS; } - else if (strcmp(hreq->uri_parsed->path_parts[3], "composer") == 0) + else if (strcmp(hreq->path_parts[3], "composer") == 0) { qp.type = Q_BROWSE_COMPOSERS; } else { - DPRINTF(E_LOG, L_RSP, "Unsupported browse type '%s'\n", hreq->uri_parsed->path_parts[3]); + DPRINTF(E_LOG, L_RSP, "Unsupported browse type '%s'\n", hreq->path_parts[3]); - rsp_send_error(hreq->req, "Unsupported browse type"); + rsp_send_error(hreq, "Unsupported browse type"); return -1; } - ret = safe_atoi32(hreq->uri_parsed->path_parts[2], &qp.id); + ret = safe_atoi32(hreq->path_parts[2], &qp.id); if (ret < 0) { - rsp_send_error(hreq->req, "Invalid playlist ID"); + rsp_send_error(hreq, "Invalid playlist ID"); return -1; } @@ -731,7 +704,7 @@ rsp_reply_browse(struct httpd_request *hreq) { DPRINTF(E_LOG, L_RSP, "Could not start query\n"); - rsp_send_error(hreq->req, "Could not start query"); + rsp_send_error(hreq, "Could not start query"); if (qp.filter) free(qp.filter); @@ -784,7 +757,7 @@ rsp_reply_browse(struct httpd_request *hreq) mxmlDelete(reply); db_query_end(&qp); - rsp_send_error(hreq->req, "Error fetching query results"); + rsp_send_error(hreq, "Error fetching query results"); return -1; } @@ -798,7 +771,7 @@ rsp_reply_browse(struct httpd_request *hreq) db_query_end(&qp); - rsp_send_reply(hreq->req, reply); + rsp_send_reply(hreq, reply); return 0; } @@ -809,14 +782,14 @@ rsp_stream(struct httpd_request *hreq) int id; int ret; - ret = safe_atoi32(hreq->uri_parsed->path_parts[2], &id); + ret = safe_atoi32(hreq->path_parts[2], &id); if (ret < 0) { - httpd_send_error(hreq->req, HTTP_BADREQUEST, "Bad Request"); + httpd_send_error(hreq, HTTP_BADREQUEST, "Bad Request"); return -1; } - httpd_stream_file(hreq->req, id); + httpd_stream_file(hreq, id); return 0; } @@ -852,7 +825,7 @@ static struct httpd_uri_map rsp_handlers[] = }, { .regexp = "^/rsp/stream/[[:digit:]]+$", - .handler = rsp_stream + .handler = rsp_stream, }, { .regexp = NULL, @@ -863,74 +836,45 @@ static struct httpd_uri_map rsp_handlers[] = /* -------------------------------- RSP API --------------------------------- */ -void -rsp_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed) +static void +rsp_request(struct httpd_request *hreq) { - struct httpd_request *hreq; int ret; - DPRINTF(E_DBG, L_RSP, "RSP request: '%s'\n", uri_parsed->uri); - - hreq = httpd_request_parse(req, uri_parsed, NULL, rsp_handlers); - if (!hreq) + if (!hreq->handler) { - DPRINTF(E_LOG, L_RSP, "Unrecognized path '%s' in RSP request: '%s'\n", uri_parsed->path, uri_parsed->uri); + DPRINTF(E_LOG, L_RSP, "Unrecognized path in RSP request: '%s'\n", hreq->uri); - rsp_send_error(req, "Server error"); + rsp_send_error(hreq, "Server error"); return; } ret = rsp_request_authorize(hreq); if (ret < 0) { - rsp_send_error(req, "Access denied"); + rsp_send_error(hreq, "Access denied"); free(hreq); return; } hreq->handler(hreq); - - free(hreq); } -int -rsp_is_request(const char *path) -{ - if (strncmp(path, "/rsp/", strlen("/rsp/")) == 0) - return 1; - - return 0; -} - -int +static int rsp_init(void) { - char buf[64]; - int i; - int ret; - snprintf(rsp_filter_files, sizeof(rsp_filter_files), "f.data_kind = %d", DATA_KIND_FILE); - for (i = 0; rsp_handlers[i].handler; i++) - { - ret = regcomp(&rsp_handlers[i].preg, rsp_handlers[i].regexp, REG_EXTENDED | REG_NOSUB); - if (ret != 0) - { - regerror(ret, &rsp_handlers[i].preg, buf, sizeof(buf)); - - DPRINTF(E_FATAL, L_RSP, "RSP init failed; regexp error: %s\n", buf); - return -1; - } - } - return 0; } -void -rsp_deinit(void) +struct httpd_module httpd_rsp = { - int i; - - for (i = 0; rsp_handlers[i].handler; i++) - regfree(&rsp_handlers[i].preg); -} + .name = "RSP", + .type = MODULE_RSP, + .logdomain = L_RSP, + .subpaths = { "/rsp/", NULL }, + .handlers = rsp_handlers, + .init = rsp_init, + .request = rsp_request, +}; diff --git a/src/httpd_rsp.h b/src/httpd_rsp.h deleted file mode 100644 index 637afcfe..00000000 --- a/src/httpd_rsp.h +++ /dev/null @@ -1,19 +0,0 @@ - -#ifndef __HTTPD_RSP_H__ -#define __HTTPD_RSP_H__ - -#include "httpd.h" - -int -rsp_init(void); - -void -rsp_deinit(void); - -void -rsp_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed); - -int -rsp_is_request(const char *path); - -#endif /* !__HTTPD_RSP_H__ */ diff --git a/src/httpd_streaming.c b/src/httpd_streaming.c index b078b925..1306e390 100644 --- a/src/httpd_streaming.c +++ b/src/httpd_streaming.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Espen Jürgensen + * Copyright (C) 2023 Espen Jürgensen * * 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 @@ -23,265 +23,66 @@ #include #include #include -#include -#include - #include #include -#include +#include #include +#include -#include "httpd_streaming.h" +#include "httpd_internal.h" +#include "outputs/streaming.h" #include "logger.h" #include "conffile.h" -#include "transcode.h" -#include "player.h" -#include "listener.h" -#include "db.h" -/* httpd event base, from httpd.c */ -extern struct event_base *evbase_httpd; - -// Seconds between sending silence when player is idle -// (to prevent client from hanging up) -#define STREAMING_SILENCE_INTERVAL 1 -// How many bytes we try to read at a time from the httpd pipe -#define STREAMING_READ_SIZE STOB(352, 16, 2) - -#define STREAMING_MP3_SAMPLE_RATE 44100 -#define STREAMING_MP3_BPS 16 -#define STREAMING_MP3_CHANNELS 2 -#define STREAMING_MP3_BIT_RATE 192000 - - -// Linked list of mp3 streaming requests struct streaming_session { - struct evhttp_request *req; - struct streaming_session *next; + struct httpd_request *hreq; - bool require_icy; // Client requested icy meta - size_t bytes_sent; // Audio bytes sent since last metablock + int fd; + struct event *readev; + struct evbuffer *readbuf; + size_t bytes_sent; + + bool icy_is_requested; + size_t icy_remaining; }; -static pthread_mutex_t streaming_sessions_lck; -static struct streaming_session *streaming_sessions; -// Means we're not able to encode to mp3 -static bool streaming_not_supported; +static struct media_quality streaming_default_quality = { + .sample_rate = 44100, + .bits_per_sample = 16, + .channels = 2, + .bit_rate = 128000, +}; -// Interval for sending silence when playback is paused -static struct timeval streaming_silence_tv = { STREAMING_SILENCE_INTERVAL, 0 }; -// Input buffer, output buffer and encoding ctx for transcode -static struct encode_ctx *streaming_encode_ctx; -static struct evbuffer *streaming_encoded_data; -static struct media_quality streaming_quality_in; -static struct media_quality streaming_quality_out = { STREAMING_MP3_SAMPLE_RATE, STREAMING_MP3_BPS, STREAMING_MP3_CHANNELS, STREAMING_MP3_BIT_RATE }; +/* ------------------------------ ICY metadata -------------------------------*/ -// Used for pushing events and data from the player -static struct event *streamingev; -static struct event *metaev; -static struct player_status streaming_player_status; -static int streaming_player_changed; -static int streaming_pipe[2]; -static int streaming_meta[2]; +// To test mp3 and ICY tagm it is good to use: +// mpv --display-tags=* http://localhost:3689/stream.mp3 #define STREAMING_ICY_METALEN_MAX 4080 // 255*16 incl header/footer (16bytes) #define STREAMING_ICY_METATITLELEN_MAX 4064 // STREAMING_ICY_METALEN_MAX -16 (not incl header/footer) -/* As streaming quality goes up, we send more data to the remote client. With a - * smaller ICY_METAINT value we have to splice metadata more frequently - on - * some devices with small input buffers, a higher quality stream and low - * ICY_METAINT can lead to stuttering as observed on a Roku Soundbridge - */ -#define STREAMING_ICY_METAINT_DEFAULT 16384 -static unsigned short streaming_icy_metaint = STREAMING_ICY_METAINT_DEFAULT; -static unsigned streaming_icy_clients; +// As streaming quality goes up, we send more data to the remote client. With a +// smaller ICY_METAINT value we have to splice metadata more frequently - on +// some devices with small input buffers, a higher quality stream and low +// ICY_METAINT can lead to stuttering as observed on a Roku Soundbridge +static unsigned short streaming_icy_metaint = 16384; + +static pthread_mutex_t streaming_metadata_lck; static char streaming_icy_title[STREAMING_ICY_METATITLELEN_MAX]; -static void -streaming_close_cb(struct evhttp_connection *evcon, void *arg) -{ - struct streaming_session *this; - struct streaming_session *session; - struct streaming_session *prev; - const char *address; - ev_uint16_t port; - - this = (struct streaming_session *)arg; - - httpd_peer_get(&address, &port, evcon); - DPRINTF(E_INFO, L_STREAMING, "Stopping mp3 streaming to %s:%d\n", address, (int)port); - - pthread_mutex_lock(&streaming_sessions_lck); - if (!streaming_sessions) - { - // This close comes during deinit() - we don't free `this` since it is - // already a dangling ptr (free'd in deinit()) at this stage - pthread_mutex_unlock(&streaming_sessions_lck); - return; - } - - prev = NULL; - for (session = streaming_sessions; session; session = session->next) - { - if (session->req == this->req) - break; - - prev = session; - } - - if (!session) - { - DPRINTF(E_LOG, L_STREAMING, "Bug! Got a failure callback for an unknown stream (%s:%d)\n", address, (int)port); - free(this); - pthread_mutex_unlock(&streaming_sessions_lck); - return; - } - - if (!prev) - streaming_sessions = session->next; - else - prev->next = session->next; - - if (session->require_icy) - --streaming_icy_clients; - - // Valgrind says libevent doesn't free the request on disconnect (even though it owns it - libevent bug?), - // so we do it with a reply end - evhttp_send_reply_end(session->req); - free(session); - - if (!streaming_sessions) - { - DPRINTF(E_INFO, L_STREAMING, "No more clients, will stop streaming\n"); - event_del(streamingev); - event_del(metaev); - } - - pthread_mutex_unlock(&streaming_sessions_lck); -} - -static void -streaming_end(void) -{ - struct streaming_session *session; - struct evhttp_connection *evcon; - const char *address; - ev_uint16_t port; - - pthread_mutex_lock(&streaming_sessions_lck); - for (session = streaming_sessions; streaming_sessions; session = streaming_sessions) - { - evcon = evhttp_request_get_connection(session->req); - if (evcon) - { - evhttp_connection_set_closecb(evcon, NULL, NULL); - httpd_peer_get(&address, &port, evcon); - DPRINTF(E_INFO, L_STREAMING, "Force close stream to %s:%d\n", address, (int)port); - } - evhttp_send_reply_end(session->req); - - streaming_sessions = session->next; - free(session); - } - pthread_mutex_unlock(&streaming_sessions_lck); - - event_del(streamingev); - event_del(metaev); -} - -static void -streaming_meta_cb(evutil_socket_t fd, short event, void *arg) -{ - struct media_quality quality; - struct decode_ctx *decode_ctx; - int ret; - - transcode_encode_cleanup(&streaming_encode_ctx); - - ret = read(fd, &quality, sizeof(struct media_quality)); - if (ret != sizeof(struct media_quality)) - goto error; - - decode_ctx = NULL; - if (quality.bits_per_sample == 16) - decode_ctx = transcode_decode_setup_raw(XCODE_PCM16, &quality); - else if (quality.bits_per_sample == 24) - decode_ctx = transcode_decode_setup_raw(XCODE_PCM24, &quality); - else if (quality.bits_per_sample == 32) - decode_ctx = transcode_decode_setup_raw(XCODE_PCM32, &quality); - - if (!decode_ctx) - goto error; - - streaming_encode_ctx = transcode_encode_setup(XCODE_MP3, &streaming_quality_out, decode_ctx, NULL, 0, 0); - transcode_decode_cleanup(&decode_ctx); - if (!streaming_encode_ctx) - { - DPRINTF(E_LOG, L_STREAMING, "Will not be able to stream MP3, libav does not support MP3 encoding: %d/%d/%d @ %d\n", streaming_quality_out.sample_rate, streaming_quality_out.bits_per_sample, streaming_quality_out.channels, streaming_quality_out.bit_rate); - streaming_not_supported = 1; - streaming_end(); - return; - } - - streaming_quality_in = quality; - streaming_not_supported = 0; - - return; - - error: - DPRINTF(E_LOG, L_STREAMING, "Unknown or unsupported quality of input data (%d/%d/%d), cannot MP3 encode\n", quality.sample_rate, quality.bits_per_sample, quality.channels); - streaming_not_supported = 1; - streaming_end(); -} - -static int -encode_buffer(uint8_t *buffer, size_t size) -{ - transcode_frame *frame; - int samples; - int ret; - - if (streaming_not_supported) - { - DPRINTF(E_LOG, L_STREAMING, "Streaming unsupported\n"); - return -1; - } - - if (streaming_quality_in.channels == 0) - { - DPRINTF(E_LOG, L_STREAMING, "Streaming quality is zero (%d/%d/%d)\n", streaming_quality_in.sample_rate, streaming_quality_in.bits_per_sample, streaming_quality_in.channels); - return -1; - } - - samples = BTOS(size, streaming_quality_in.bits_per_sample, streaming_quality_in.channels); - - frame = transcode_frame_new(buffer, size, samples, &streaming_quality_in); - if (!frame) - { - DPRINTF(E_LOG, L_STREAMING, "Could not convert raw PCM to frame\n"); - return -1; - } - - ret = transcode_encode(streaming_encoded_data, streaming_encode_ctx, frame, 0); - transcode_frame_free(frame); - - return ret; -} - -/* We know that the icymeta is limited to 1+255*16 (ie 4081) bytes so caller must - * provide a buf of this size to avoid needless mallocs - * - * The icy meta block is defined by a single byte indicating how many double byte - * words used for the actual meta. Unused bytes are null padded - * - * https://stackoverflow.com/questions/4911062/pulling-track-info-from-an-audio-stream-using-php/4914538#4914538 - * http://www.smackfu.com/stuff/programming/shoutcast.html - */ +// We know that the icymeta is limited to 1+255*16 (ie 4081) bytes so caller must +// provide a buf of this size to avoid needless mallocs +// +// The icy meta block is defined by a single byte indicating how many double byte +// words used for the actual meta. Unused bytes are null padded +// +// https://stackoverflow.com/questions/4911062/pulling-track-info-from-an-audio-stream-using-php/4914538#4914538 +// http://www.smackfu.com/stuff/programming/shoutcast.html static uint8_t * -streaming_icy_meta_create(uint8_t buf[STREAMING_ICY_METALEN_MAX+1], const char *title, unsigned *buflen) +icy_meta_create(uint8_t buf[STREAMING_ICY_METALEN_MAX+1], unsigned *buflen, const char *title) { unsigned titlelen; unsigned metalen; @@ -322,317 +123,203 @@ streaming_icy_meta_create(uint8_t buf[STREAMING_ICY_METALEN_MAX+1], const char * return buf; } -static uint8_t * -streaming_icy_meta_splice(const uint8_t *data, size_t datalen, off_t offset, size_t *len) +static void +icy_meta_splice(struct evbuffer *out, struct evbuffer *in, size_t *icy_remaining) { - uint8_t meta[STREAMING_ICY_METALEN_MAX+1]; // Buffer, of max sz, for the created icymeta - unsigned metalen; // How much of the buffer is in use - uint8_t *buf; // Client returned buffer; contains the audio (from data) spliced w/meta (from meta) + uint8_t meta[STREAMING_ICY_METALEN_MAX + 1]; + unsigned metalen; + size_t buf_remaining; + size_t consume; - if (data == NULL || datalen == 0) - return NULL; + for (buf_remaining = evbuffer_get_length(in); buf_remaining > 0; buf_remaining -= consume) + { + consume = MIN(*icy_remaining, buf_remaining); + evbuffer_remove_buffer(in, out, consume); + *icy_remaining -= consume; + if (*icy_remaining == 0) + { + pthread_mutex_lock(&streaming_metadata_lck); + icy_meta_create(meta, &metalen, streaming_icy_title); + pthread_mutex_unlock(&streaming_metadata_lck); - memset(meta, 0, sizeof(meta)); - streaming_icy_meta_create(meta, streaming_icy_title, &metalen); - - *len = datalen + metalen; - // DPRINTF(E_DBG, L_STREAMING, "splicing meta, audio block=%d bytes, offset=%d, metalen=%d new buflen=%d\n", datalen, offset, metalen, *len); - buf = malloc(*len); - memcpy(buf, data, offset); - memcpy(buf+offset, &meta[0], metalen); - memcpy(buf+offset+metalen, data+offset, datalen-offset); - - return buf; + evbuffer_add(out, meta, metalen); + *icy_remaining = streaming_icy_metaint; + } + } } +// Thread: player. TODO Would be nice to avoid the lock. Consider moving all the +// ICY tag stuff to streaming.c and make a STREAMING_FORMAT_MP3_ICY? static void -streaming_player_status_update(void) +icy_metadata_cb(char *metadata) { - struct db_queue_item *queue_item; - uint32_t prev_id; - - prev_id = streaming_player_status.id; - player_get_status(&streaming_player_status); - - if (prev_id == streaming_player_status.id || !streaming_icy_clients) - { - return; - } - - queue_item = db_queue_fetch_byfileid(streaming_player_status.id); - if (!queue_item) - { - streaming_icy_title[0] = '\0'; - return; - } - - snprintf(streaming_icy_title, sizeof(streaming_icy_title), "%s - %s", queue_item->title, queue_item->artist); - free_queue_item(queue_item, 0); + pthread_mutex_lock(&streaming_metadata_lck); + snprintf(streaming_icy_title, sizeof(streaming_icy_title), "%s", metadata); + pthread_mutex_unlock(&streaming_metadata_lck); } + +/* ----------------------------- Session helpers ---------------------------- */ + static void -streaming_send_cb(evutil_socket_t fd, short event, void *arg) +session_free(struct streaming_session *session) +{ + if (!session) + return; + + if (session->readev) + { + streaming_session_deregister(session->fd); + event_free(session->readev); + } + + evbuffer_free(session->readbuf); + free(session); +} + +static struct streaming_session * +session_new(struct httpd_request *hreq, bool icy_is_requested) { struct streaming_session *session; - struct evbuffer *evbuf; - uint8_t rawbuf[STREAMING_READ_SIZE]; - uint8_t *buf; - uint8_t *splice_buf = NULL; - size_t splice_len; - size_t count; - int overflow; + + CHECK_NULL(L_STREAMING, session = calloc(1, sizeof(struct streaming_session))); + CHECK_NULL(L_STREAMING, session->readbuf = evbuffer_new()); + + session->hreq = hreq; + session->icy_is_requested = icy_is_requested; + session->icy_remaining = streaming_icy_metaint; + + return session; +} + + +/* ----------------------------- Event callbacks ---------------------------- */ + +static void +conn_close_cb(void *arg) +{ + struct streaming_session *session = arg; + + session_free(session); +} + +static void +read_cb(evutil_socket_t fd, short event, void *arg) +{ + struct streaming_session *session = arg; + struct httpd_request *hreq; int len; - int ret; - // Player wrote data to the pipe (EV_READ) - if (event & EV_READ) + CHECK_NULL(L_STREAMING, hreq = session->hreq); + + len = evbuffer_read(session->readbuf, fd, -1); + if (len < 0 && errno != EAGAIN) { - while (1) - { - ret = read(fd, &rawbuf, sizeof(rawbuf)); - if (ret <= 0) - break; + DPRINTF(E_INFO, L_STREAMING, "Stopping mp3 streaming to %s:%d\n", session->hreq->peer_address, (int)session->hreq->peer_port); - if (streaming_player_changed) - { - streaming_player_changed = 0; - streaming_player_status_update(); - } - - ret = encode_buffer(rawbuf, ret); - if (ret < 0) - return; - } + httpd_send_reply_end(session->hreq); + session_free(session); + return; } - // Event timed out, let's see what the player is doing and send silence if it is paused + + if (session->icy_is_requested) + icy_meta_splice(hreq->out_body, session->readbuf, &session->icy_remaining); else - { - if (streaming_player_changed) - { - streaming_player_changed = 0; - streaming_player_status_update(); - } + evbuffer_add_buffer(hreq->out_body, session->readbuf); - if (streaming_player_status.status != PLAY_PAUSED) - return; + httpd_send_reply_chunk(hreq, NULL, NULL); - memset(&rawbuf, 0, sizeof(rawbuf)); - ret = encode_buffer(rawbuf, sizeof(rawbuf)); - if (ret < 0) - return; - } - - len = evbuffer_get_length(streaming_encoded_data); - if (len == 0) - return; - - // Send data - evbuf = evbuffer_new(); - pthread_mutex_lock(&streaming_sessions_lck); - for (session = streaming_sessions; session; session = session->next) - { - // Does this session want ICY meta data and is it time to send? - count = session->bytes_sent+len; - if (session->require_icy && count > streaming_icy_metaint) - { - overflow = count%streaming_icy_metaint; - buf = evbuffer_pullup(streaming_encoded_data, -1); - - // DPRINTF(E_DBG, L_STREAMING, "session=%x sent=%ld len=%ld overflow=%ld\n", session, session->bytes_sent, len, overflow); - - // Splice the 'icy title' in with the encoded audio data - splice_len = 0; - splice_buf = streaming_icy_meta_splice(buf, len, len-overflow, &splice_len); - - evbuffer_add(evbuf, splice_buf, splice_len); - - free(splice_buf); - splice_buf = NULL; - - evhttp_send_reply_chunk(session->req, evbuf); - - if (session->next == NULL) - { - // We're the last session, drop the contents of the encoded buffer - evbuffer_drain(streaming_encoded_data, len); - } - session->bytes_sent = overflow; - } - else - { - if (session->next) - { - buf = evbuffer_pullup(streaming_encoded_data, -1); - evbuffer_add(evbuf, buf, len); - evhttp_send_reply_chunk(session->req, evbuf); - } - else - { - evhttp_send_reply_chunk(session->req, streaming_encoded_data); - } - session->bytes_sent += len; - } - } - pthread_mutex_unlock(&streaming_sessions_lck); - - evbuffer_free(evbuf); + session->bytes_sent += len; } -// Thread: player (not fully thread safe, but hey...) -static void -player_change_cb(short event_mask) -{ - streaming_player_changed = 1; -} -// Thread: player (also prone to race conditions, mostly during deinit) -void -streaming_write(struct output_buffer *obuf) -{ - int ret; +/* -------------------------- Module implementation ------------------------- */ - // Explicit no-lock - let the write to pipes fail if during deinit - if (!streaming_sessions) - return; - - if (!quality_is_equal(&obuf->data[0].quality, &streaming_quality_in)) - { - ret = write(streaming_meta[1], &obuf->data[0].quality, sizeof(struct media_quality)); - if (ret < 0) - { - if (errno == EBADF) - DPRINTF(E_LOG, L_STREAMING, "streaming pipe already closed\n"); - else - DPRINTF(E_LOG, L_STREAMING, "Error writing to streaming pipe: %s\n", strerror(errno)); - return; - } - } - - ret = write(streaming_pipe[1], obuf->data[0].buffer, obuf->data[0].bufsize); - if (ret < 0) - { - if (errno == EAGAIN) - DPRINTF(E_WARN, L_STREAMING, "Streaming pipe full, skipping write\n"); - else - { - if (errno == EBADF) - DPRINTF(E_LOG, L_STREAMING, "Streaming pipe already closed\n"); - else - DPRINTF(E_LOG, L_STREAMING, "Error writing to streaming pipe: %s\n", strerror(errno)); - } - } -} - -int -streaming_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed) +static int +streaming_mp3_handler(struct httpd_request *hreq) { struct streaming_session *session; - struct evhttp_connection *evcon; - struct evkeyvalq *output_headers; - cfg_t *lib; - const char *name; - const char *address; - ev_uint16_t port; + const char *name = cfg_getstr(cfg_getsec(cfg, "library"), "name"); const char *param; - bool require_icy = false; + bool icy_is_requested; char buf[9]; - if (streaming_not_supported) + param = httpd_header_find(hreq->in_headers, "Icy-MetaData"); + icy_is_requested = (param && strcmp(param, "1") == 0); + if (icy_is_requested) { - DPRINTF(E_LOG, L_STREAMING, "Got MP3 streaming request, but cannot encode to MP3\n"); - - evhttp_send_error(req, HTTP_NOTFOUND, "Not Found"); - return -1; + httpd_header_add(hreq->out_headers, "icy-name", name); + snprintf(buf, sizeof(buf), "%d", streaming_icy_metaint); + httpd_header_add(hreq->out_headers, "icy-metaint", buf); } - evcon = evhttp_request_get_connection(req); - httpd_peer_get(&address, &port, evcon); - param = evhttp_find_header( evhttp_request_get_input_headers(req), "Icy-MetaData"); - if (param && strcmp(param, "1") == 0) - require_icy = true; - - DPRINTF(E_INFO, L_STREAMING, "Beginning mp3 streaming (with icy=%d, icy_metaint=%d) to %s:%d\n", require_icy, streaming_icy_metaint, address, (int)port); - - lib = cfg_getsec(cfg, "library"); - name = cfg_getstr(lib, "name"); - - output_headers = evhttp_request_get_output_headers(req); - evhttp_add_header(output_headers, "Content-Type", "audio/mpeg"); - evhttp_add_header(output_headers, "Server", PACKAGE_NAME "/" VERSION); - evhttp_add_header(output_headers, "Cache-Control", "no-cache"); - evhttp_add_header(output_headers, "Pragma", "no-cache"); - evhttp_add_header(output_headers, "Expires", "Mon, 31 Aug 2015 06:00:00 GMT"); - if (require_icy) - { - ++streaming_icy_clients; - evhttp_add_header(output_headers, "icy-name", name); - snprintf(buf, sizeof(buf)-1, "%d", streaming_icy_metaint); - evhttp_add_header(output_headers, "icy-metaint", buf); - } - evhttp_add_header(output_headers, "Access-Control-Allow-Origin", "*"); - evhttp_add_header(output_headers, "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); - - evhttp_send_reply_start(req, HTTP_OK, "OK"); - - session = calloc(1, sizeof(struct streaming_session)); + session = session_new(hreq, icy_is_requested); if (!session) - { - DPRINTF(E_LOG, L_STREAMING, "Out of memory for streaming request\n"); + return -1; - evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); - return -1; - } + // Ask streaming output module for a fd to read mp3 from + session->fd = streaming_session_register(STREAMING_FORMAT_MP3, streaming_default_quality); - pthread_mutex_lock(&streaming_sessions_lck); + CHECK_NULL(L_STREAMING, session->readev = event_new(hreq->evbase, session->fd, EV_READ | EV_PERSIST, read_cb, session)); + event_add(session->readev, NULL); - if (!streaming_sessions) - { - event_add(streamingev, &streaming_silence_tv); - event_add(metaev, NULL); - } + httpd_request_close_cb_set(hreq, conn_close_cb, session); - session->req = req; - session->next = streaming_sessions; - session->require_icy = require_icy; - session->bytes_sent = 0; - streaming_sessions = session; + httpd_header_add(hreq->out_headers, "Content-Type", "audio/mpeg"); + httpd_header_add(hreq->out_headers, "Server", PACKAGE_NAME "/" VERSION); + httpd_header_add(hreq->out_headers, "Cache-Control", "no-cache"); + httpd_header_add(hreq->out_headers, "Pragma", "no-cache"); + httpd_header_add(hreq->out_headers, "Expires", "Mon, 31 Aug 2015 06:00:00 GMT"); - pthread_mutex_unlock(&streaming_sessions_lck); - - evhttp_connection_set_closecb(evcon, streaming_close_cb, session); + httpd_send_reply_start(hreq, HTTP_OK, "OK"); return 0; } -int -streaming_is_request(const char *path) -{ - char *ptr; +static struct httpd_uri_map streaming_handlers[] = + { + { + .regexp = "^/stream.mp3$", + .handler = streaming_mp3_handler, + .flags = HTTPD_HANDLER_REALTIME, + }, + { + .regexp = NULL, + .handler = NULL + } + }; - ptr = strrchr(path, '/'); - if (ptr && (strcasecmp(ptr, "/stream.mp3") == 0)) - return 1; - - return 0; -} - -int -streaming_init(void) +static void +streaming_request(struct httpd_request *hreq) { int ret; - cfg_t *cfgsec; + + if (!hreq->handler) + { + DPRINTF(E_LOG, L_STREAMING, "Unrecognized path in streaming request: '%s'\n", hreq->uri); + + httpd_send_error(hreq, HTTP_NOTFOUND, NULL); + return; + } + + ret = hreq->handler(hreq); + if (ret < 0) + httpd_send_error(hreq, HTTP_INTERNAL, NULL); +} + +static int +streaming_init(void) +{ int val; - cfgsec = cfg_getsec(cfg, "streaming"); - - val = cfg_getint(cfgsec, "sample_rate"); + val = cfg_getint(cfg_getsec(cfg, "streaming"), "sample_rate"); // Validate against the variations of libmp3lame's supported sample rates: 32000/44100/48000 if (val % 11025 > 0 && val % 12000 > 0 && val % 8000 > 0) - DPRINTF(E_LOG, L_STREAMING, "Non standard streaming sample_rate=%d, defaulting\n", val); + DPRINTF(E_LOG, L_STREAMING, "Unsupported streaming sample_rate=%d, defaulting\n", val); else - streaming_quality_out.sample_rate = val; + streaming_default_quality.sample_rate = val; - val = cfg_getint(cfgsec, "bit_rate"); + val = cfg_getint(cfg_getsec(cfg, "streaming"), "bit_rate"); switch (val) { case 64: @@ -640,107 +327,37 @@ streaming_init(void) case 128: case 192: case 320: - streaming_quality_out.bit_rate = val*1000; + streaming_default_quality.bit_rate = val*1000; break; default: DPRINTF(E_LOG, L_STREAMING, "Unsuppported streaming bit_rate=%d, supports: 64/96/128/192/320, defaulting\n", val); } - DPRINTF(E_INFO, L_STREAMING, "Streaming quality: %d/%d/%d @ %dkbps\n", streaming_quality_out.sample_rate, streaming_quality_out.bits_per_sample, streaming_quality_out.channels, streaming_quality_out.bit_rate/1000); + DPRINTF(E_INFO, L_STREAMING, "Streaming quality: %d/%d/%d @ %dkbps\n", + streaming_default_quality.sample_rate, streaming_default_quality.bits_per_sample, + streaming_default_quality.channels, streaming_default_quality.bit_rate/1000); - val = cfg_getint(cfgsec, "icy_metaint"); + val = cfg_getint(cfg_getsec(cfg, "streaming"), "icy_metaint"); // Too low a value forces server to send more meta than data if (val >= 4096 && val <= 131072) streaming_icy_metaint = val; else DPRINTF(E_INFO, L_STREAMING, "Unsupported icy_metaint=%d, supported range: 4096..131072, defaulting to %d\n", val, streaming_icy_metaint); - ret = mutex_init(&streaming_sessions_lck); - if (ret < 0) - { - DPRINTF(E_FATAL, L_STREAMING, "Could not initialize mutex (%d): %s\n", ret, strerror(ret)); - goto error; - } - - // Non-blocking because otherwise httpd and player thread may deadlock -#ifdef HAVE_PIPE2 - ret = pipe2(streaming_pipe, O_CLOEXEC | O_NONBLOCK); -#else - if ( pipe(streaming_pipe) < 0 || - fcntl(streaming_pipe[0], F_SETFL, O_CLOEXEC | O_NONBLOCK) < 0 || - fcntl(streaming_pipe[1], F_SETFL, O_CLOEXEC | O_NONBLOCK) < 0 ) - ret = -1; - else - ret = 0; -#endif - if (ret < 0) - { - DPRINTF(E_FATAL, L_STREAMING, "Could not create pipe: %s\n", strerror(errno)); - goto error; - } - -#ifdef HAVE_PIPE2 - ret = pipe2(streaming_meta, O_CLOEXEC | O_NONBLOCK); -#else - if ( pipe(streaming_meta) < 0 || - fcntl(streaming_meta[0], F_SETFL, O_CLOEXEC | O_NONBLOCK) < 0 || - fcntl(streaming_meta[1], F_SETFL, O_CLOEXEC | O_NONBLOCK) < 0 ) - ret = -1; - else - ret = 0; -#endif - if (ret < 0) - { - DPRINTF(E_FATAL, L_STREAMING, "Could not create pipe: %s\n", strerror(errno)); - goto error; - } - - // Listen to playback changes so we don't have to poll to check for pausing - ret = listener_add(player_change_cb, LISTENER_PLAYER); - if (ret < 0) - { - DPRINTF(E_FATAL, L_STREAMING, "Could not add listener\n"); - goto error; - } - - // Initialize buffer for encoded mp3 audio and event for pipe reading - CHECK_NULL(L_STREAMING, streaming_encoded_data = evbuffer_new()); - - CHECK_NULL(L_STREAMING, streamingev = event_new(evbase_httpd, streaming_pipe[0], EV_TIMEOUT | EV_READ | EV_PERSIST, streaming_send_cb, NULL)); - CHECK_NULL(L_STREAMING, metaev = event_new(evbase_httpd, streaming_meta[0], EV_READ | EV_PERSIST, streaming_meta_cb, NULL)); - - streaming_icy_clients = 0; + CHECK_ERR(L_STREAMING, mutex_init(&streaming_metadata_lck)); + streaming_metadatacb_register(icy_metadata_cb); return 0; - - error: - close(streaming_pipe[0]); - close(streaming_pipe[1]); - close(streaming_meta[0]); - close(streaming_meta[1]); - - return -1; } -void -streaming_deinit(void) +struct httpd_module httpd_streaming = { - streaming_end(); - - event_free(metaev); - event_free(streamingev); - streamingev = NULL; - - listener_remove(player_change_cb); - - close(streaming_pipe[0]); - close(streaming_pipe[1]); - close(streaming_meta[0]); - close(streaming_meta[1]); - - transcode_encode_cleanup(&streaming_encode_ctx); - evbuffer_free(streaming_encoded_data); - - pthread_mutex_destroy(&streaming_sessions_lck); -} + .name = "Streaming", + .type = MODULE_STREAMING, + .logdomain = L_STREAMING, + .fullpaths = { "/stream.mp3", NULL }, + .handlers = streaming_handlers, + .init = streaming_init, + .request = streaming_request, +}; diff --git a/src/httpd_streaming.h b/src/httpd_streaming.h deleted file mode 100644 index 3df82023..00000000 --- a/src/httpd_streaming.h +++ /dev/null @@ -1,29 +0,0 @@ - -#ifndef __HTTPD_STREAMING_H__ -#define __HTTPD_STREAMING_H__ - -#include "httpd.h" -#include "outputs.h" - -/* httpd_streaming takes care of incoming requests to /stream.mp3 - * It will receive decoded audio from the player, and encode it, and - * stream it to one or more clients. It will not be available - * if a suitable ffmpeg/libav encoder is not present at runtime. - */ - -void -streaming_write(struct output_buffer *obuf); - -int -streaming_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed); - -int -streaming_is_request(const char *path); - -int -streaming_init(void); - -void -streaming_deinit(void); - -#endif /* !__HTTPD_STREAMING_H__ */ diff --git a/src/main.c b/src/main.c index 51e2d0ac..d611384e 100644 --- a/src/main.c +++ b/src/main.c @@ -47,9 +47,7 @@ #include #include -#ifdef HAVE_LIBEVENT_PTHREADS -# include -#endif +#include #include #include #include @@ -730,9 +728,7 @@ main(int argc, char **argv) /* Initialize event base (after forking) */ CHECK_NULL(L_MAIN, evbase_main = event_base_new()); -#ifdef HAVE_LIBEVENT_PTHREADS CHECK_ERR(L_MAIN, evthread_use_pthreads()); -#endif DPRINTF(E_LOG, L_MAIN, "mDNS init\n"); ret = mdns_init(); diff --git a/src/misc.c b/src/misc.c index 2a145988..0f7c7d78 100644 --- a/src/misc.c +++ b/src/misc.c @@ -52,6 +52,8 @@ #include #include // getifaddrs +#include // evhttp_bind + #include #include @@ -276,8 +278,8 @@ net_connect(const char *addr, unsigned short port, int type, const char *log_ser // If *port is 0 then a random port will be assigned, and *port will be updated // with the port number -int -net_bind(short unsigned *port, int type, const char *log_service_name) +static int +net_bind_impl(short unsigned *port, int type, const char *log_service_name, bool reuseport) { struct addrinfo hints = { 0 }; struct addrinfo *servinfo; @@ -315,6 +317,14 @@ net_bind(short unsigned *port, int type, const char *log_service_name) if (fd < 0) continue; + // Makes us able to attach multiple threads to the same port + if (reuseport) + ret = setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes)); + else + ret = setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &no, sizeof(no)); + if (ret < 0) + continue; + // TODO libevent sets this, we do the same? ret = setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &yes, sizeof(yes)); if (ret < 0) @@ -375,7 +385,19 @@ net_bind(short unsigned *port, int type, const char *log_service_name) } int -net_evhttp_bind(struct evhttp *evhttp, short unsigned port, const char *log_service_name) +net_bind(short unsigned *port, int type, const char *log_service_name) +{ + return net_bind_impl(port, type, log_service_name, false); +} + +int +net_bind_with_reuseport(short unsigned *port, int type, const char *log_service_name) +{ + return net_bind_impl(port, type, log_service_name, true); +} + +int +net_evhttp_bind(struct evhttp *evhttp, unsigned short port, const char *log_service_name) { const char *bind_address; bool v6_enabled; diff --git a/src/misc.h b/src/misc.h index 982462f8..d2716e73 100644 --- a/src/misc.h +++ b/src/misc.h @@ -14,7 +14,6 @@ #include #include -#include #ifndef SOCK_NONBLOCK #include @@ -55,7 +54,13 @@ int net_bind(short unsigned *port, int type, const char *log_service_name); int -net_evhttp_bind(struct evhttp *evhttp, short unsigned port, const char *log_service_name); +net_bind_with_reuseport(short unsigned *port, int type, const char *log_service_name); + +// To avoid polluting namespace too much we don't include event2/http.h here +struct evhttp; + +int +net_evhttp_bind(struct evhttp *evhttp, unsigned short port, const char *log_service_name); // Just checks if the protocol is http or https bool diff --git a/src/mpd.c b/src/mpd.c index e31dc392..289d6a16 100644 --- a/src/mpd.c +++ b/src/mpd.c @@ -47,7 +47,6 @@ #include "commands.h" #include "conffile.h" #include "db.h" -#include "httpd.h" #include "library.h" #include "listener.h" #include "logger.h" @@ -4651,7 +4650,7 @@ artwork_cb(struct evhttp_request *req, void *arg) if (evhttp_request_get_command(req) != EVHTTP_REQ_GET) { DPRINTF(E_LOG, L_MPD, "Unsupported request type for artwork\n"); - httpd_send_error(req, HTTP_BADMETHOD, "Method not allowed"); + evhttp_send_error(req, HTTP_BADMETHOD, "Method not allowed"); return; } @@ -4662,7 +4661,7 @@ artwork_cb(struct evhttp_request *req, void *arg) if (!decoded) { DPRINTF(E_LOG, L_MPD, "Bad artwork request with uri '%s'\n", uri); - httpd_send_error(req, HTTP_BADREQUEST, 0); + evhttp_send_error(req, HTTP_BADREQUEST, 0); return; } @@ -4670,7 +4669,7 @@ artwork_cb(struct evhttp_request *req, void *arg) if (!path) { DPRINTF(E_LOG, L_MPD, "Invalid path from artwork request with uri '%s'\n", uri); - httpd_send_error(req, HTTP_BADREQUEST, 0); + evhttp_send_error(req, HTTP_BADREQUEST, 0); evhttp_uri_free(decoded); return; } @@ -4679,7 +4678,7 @@ artwork_cb(struct evhttp_request *req, void *arg) if (!decoded_path) { DPRINTF(E_LOG, L_MPD, "Error decoding path from artwork request with uri '%s'\n", uri); - httpd_send_error(req, HTTP_BADREQUEST, 0); + evhttp_send_error(req, HTTP_BADREQUEST, 0); evhttp_uri_free(decoded); return; } @@ -4694,7 +4693,7 @@ artwork_cb(struct evhttp_request *req, void *arg) if (!itemid) { DPRINTF(E_WARN, L_MPD, "No item found for path '%s' from request uri '%s'\n", decoded_path, uri); - httpd_send_error(req, HTTP_NOTFOUND, "Document was not found"); + evhttp_send_error(req, HTTP_NOTFOUND, "Document was not found"); evhttp_uri_free(decoded); free(decoded_path); return; @@ -4704,7 +4703,7 @@ artwork_cb(struct evhttp_request *req, void *arg) if (!evbuffer) { DPRINTF(E_LOG, L_MPD, "Could not allocate an evbuffer for artwork request\n"); - httpd_send_error(req, HTTP_INTERNAL, "Document was not found"); + evhttp_send_error(req, HTTP_INTERNAL, "Document was not found"); evhttp_uri_free(decoded); free(decoded_path); return; @@ -4713,7 +4712,7 @@ artwork_cb(struct evhttp_request *req, void *arg) format = artwork_get_item(evbuffer, itemid, ART_DEFAULT_WIDTH, ART_DEFAULT_HEIGHT, 0); if (format < 0) { - httpd_send_error(req, HTTP_NOTFOUND, "Document was not found"); + evhttp_send_error(req, HTTP_NOTFOUND, "Document was not found"); } else { @@ -4728,7 +4727,7 @@ artwork_cb(struct evhttp_request *req, void *arg) break; } - httpd_send_reply(req, HTTP_OK, "OK", evbuffer, HTTPD_SEND_NO_GZIP); + evhttp_send_reply(req, HTTP_OK, "OK", evbuffer); } evbuffer_free(evbuffer); diff --git a/src/outputs.c b/src/outputs.c index b3786e01..c4e5ba4b 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -467,6 +467,16 @@ metadata_cb_prepare(void *arg) event_active(metadata->ev, 0, 0); } +static void +metadata_free(struct output_metadata *metadata) +{ + if (!metadata) + return; + if (metadata->ev) + event_free(metadata->ev); + free(metadata); +} + static void metadata_send(enum output_types type, uint32_t item_id, bool startup, output_metadata_finalize_cb cb) { @@ -689,6 +699,11 @@ outputs_cb(int callback_id, uint64_t device_id, enum output_device_state state) event_active(outputs_deferredev, 0, 0); } +void +outputs_metadata_free(struct output_metadata *metadata) +{ + metadata_free(metadata); +} /* ---------------------------- Called by player ---------------------------- */ diff --git a/src/outputs.h b/src/outputs.h index 7997a4e7..434d88ad 100644 --- a/src/outputs.h +++ b/src/outputs.h @@ -252,7 +252,7 @@ struct output_definition // Called from worker thread for async preparation of metadata (e.g. getting // artwork, which might involce downloading image data). The prepared data is - // saved to metadata->data, which metadata_send() can use. + // saved to metadata->priv, which metadata_send() can use. void *(*metadata_prepare)(struct output_metadata *metadata); // Send metadata to outputs. Ownership of *metadata is transferred. @@ -284,6 +284,9 @@ outputs_quality_unsubscribe(struct media_quality *quality); void outputs_cb(int callback_id, uint64_t device_id, enum output_device_state); +void +outputs_metadata_free(struct output_metadata *metadata); + /* ---------------------------- Called by player ---------------------------- */ // Ownership of *add is transferred, so don't address after calling. Instead you diff --git a/src/outputs/streaming.c b/src/outputs/streaming.c index a6d02413..d1b34d00 100644 --- a/src/outputs/streaming.c +++ b/src/outputs/streaming.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 Espen Jürgensen + * Copyright (C) 2023 Espen Jürgensen * * 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 @@ -16,13 +16,593 @@ * 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 "streaming.h" #include "outputs.h" -#include "httpd_streaming.h" +#include "misc.h" +#include "worker.h" +#include "transcode.h" +#include "logger.h" +#include "db.h" + +/* About + * + * This output takes the writes from the player thread, gives them to a worker + * thread for mp3 encoding, and then the mp3 is written to a fd for the httpd + * request handler to read and pass to clients. If there is no writing from the + * player, but there are clients, it instead writes silence to the fd. + */ + +// Seconds between sending silence when player is idle +// (to prevent client from hanging up) +#define STREAMING_SILENCE_INTERVAL 1 + +// How many bytes of silence we encode with the above interval. There is no +// particular reason for using this size, just that it seems to have worked for +// a while. +#define SILENCE_BUF_SIZE STOB(352, 16, 2) + +// The wanted structure represents a particular format and quality that should +// be produced for one or more sessions. A pipe pair is created for each session +// for the i/o. +#define WANTED_PIPES_MAX 8 + +struct pipepair +{ + int writefd; + int readfd; +}; + +struct streaming_wanted +{ + int refcount; + struct pipepair pipes[WANTED_PIPES_MAX]; + + enum streaming_format format; + struct media_quality quality_in; + struct media_quality quality_out; + + struct encode_ctx *xcode_ctx; + struct evbuffer *encoded_data; + + struct streaming_wanted *next; +}; + +struct streaming_ctx +{ + struct streaming_wanted *wanted; + struct event *silenceev; + struct timeval silencetv; + struct media_quality last_quality; + + // seqnum may wrap around so must be unsigned + unsigned int seqnum; + unsigned int seqnum_encode_next; + + // callback with new metadata, e.g. for ICY tags + void (*metadatacb)(char *metadata); +}; + +struct encode_cmdarg +{ + uint8_t *buf; + size_t bufsize; + int samples; + unsigned int seqnum; + struct media_quality quality; +}; + +static pthread_mutex_t streaming_wanted_lck; +static pthread_cond_t streaming_sequence_cond; + +static struct streaming_ctx streaming = +{ + .silencetv = { STREAMING_SILENCE_INTERVAL, 0 }, +}; + +extern struct event_base *evbase_player; + + +/* ------------------------------- Helpers ---------------------------------- */ + +static int +pipe_open(struct pipepair *p) +{ + int fd[2]; + int ret; + +#ifdef HAVE_PIPE2 + ret = pipe2(fd, O_CLOEXEC | O_NONBLOCK); +#else + if ( pipe(fd) < 0 || + fcntl(fd[0], F_SETFL, O_CLOEXEC | O_NONBLOCK) < 0 || + fcntl(fd[1], F_SETFL, O_CLOEXEC | O_NONBLOCK) < 0 ) + ret = -1; + else + ret = 0; +#endif + if (ret < 0) + { + DPRINTF(E_LOG, L_STREAMING, "Could not create pipe: %s\n", strerror(errno)); + return -1; + } + + p->writefd = fd[1]; + p->readfd = fd[0]; + return 0; +} + +static void +pipe_close(struct pipepair *p) +{ + if (p->readfd >= 0) + close(p->readfd); + if (p->writefd >= 0) + close(p->writefd); + + p->writefd = -1; + p->readfd = -1; +} + +static void +wanted_free(struct streaming_wanted *w) +{ + if (!w) + return; + + for (int i = 0; i < WANTED_PIPES_MAX; i++) + pipe_close(&w->pipes[i]); + + transcode_encode_cleanup(&w->xcode_ctx); + evbuffer_free(w->encoded_data); + free(w); +} + +static struct streaming_wanted * +wanted_new(enum streaming_format format, struct media_quality quality) +{ + struct streaming_wanted *w; + + CHECK_NULL(L_STREAMING, w = calloc(1, sizeof(struct streaming_wanted))); + CHECK_NULL(L_STREAMING, w->encoded_data = evbuffer_new()); + + w->quality_out = quality; + w->format = format; + + for (int i = 0; i < WANTED_PIPES_MAX; i++) + { + w->pipes[i].writefd = -1; + w->pipes[i].readfd = -1; + } + + return w; +} + +static void +wanted_remove(struct streaming_wanted **wanted, struct streaming_wanted *remove) +{ + struct streaming_wanted *prev = NULL; + struct streaming_wanted *w; + + for (w = *wanted; w; w = w->next) + { + if (w == remove) + break; + + prev = w; + } + + if (!w) + return; + + if (!prev) + *wanted = remove->next; + else + prev->next = remove->next; + + wanted_free(remove); +} + +static struct streaming_wanted * +wanted_add(struct streaming_wanted **wanted, enum streaming_format format, struct media_quality quality) +{ + struct streaming_wanted *w; + + w = wanted_new(format, quality); + w->next = *wanted; + *wanted = w; + + return w; +} + +static struct streaming_wanted * +wanted_find_byformat(struct streaming_wanted *wanted, enum streaming_format format, struct media_quality quality) +{ + struct streaming_wanted *w; + + for (w = wanted; w; w = w->next) + { + if (w->format == format && quality_is_equal(&w->quality_out, &quality)) + return w; + } + + return NULL; +} + +static struct streaming_wanted * +wanted_find_byreadfd(struct streaming_wanted *wanted, int readfd) +{ + struct streaming_wanted *w; + int i; + + for (w = wanted; w; w = w->next) + for (i = 0; i < WANTED_PIPES_MAX; i++) + { + if (w->pipes[i].readfd == readfd) + return w; + } + + return NULL; +} + +static int +wanted_session_add(struct pipepair *p, struct streaming_wanted *w) +{ + int ret; + int i; + + for (i = 0; i < WANTED_PIPES_MAX; i++) + { + if (w->pipes[i].writefd != -1) // In use + continue; + + ret = pipe_open(&w->pipes[i]); + if (ret < 0) + return -1; + + memcpy(p, &w->pipes[i], sizeof(struct pipepair)); + break; + } + + if (i == WANTED_PIPES_MAX) + { + DPRINTF(E_LOG, L_STREAMING, "Cannot add streaming session, max pipe limit reached\n"); + return -1; + } + + w->refcount++; + DPRINTF(E_DBG, L_STREAMING, "Session register readfd %d, wanted->refcount=%d\n", p->readfd, w->refcount); + return 0; +} + + +static void +wanted_session_remove(struct streaming_wanted *w, int readfd) +{ + int i; + + for (i = 0; i < WANTED_PIPES_MAX; i++) + { + if (w->pipes[i].readfd != readfd) + continue; + + pipe_close(&w->pipes[i]); + break; + } + + if (i == WANTED_PIPES_MAX) + { + DPRINTF(E_LOG, L_STREAMING, "Cannot remove streaming session, readfd %d not found\n", readfd); + return; + } + + w->refcount--; + DPRINTF(E_DBG, L_STREAMING, "Session deregister readfd %d, wanted->refcount=%d\n", readfd, w->refcount); +} + + +/* ----------------------------- Thread: Worker ----------------------------- */ + +static int +encode_reset(struct streaming_wanted *w, struct media_quality quality_in) +{ + struct media_quality quality_out = w->quality_out; + struct decode_ctx *decode_ctx = NULL; + + transcode_encode_cleanup(&w->xcode_ctx); + + if (quality_in.bits_per_sample == 16) + decode_ctx = transcode_decode_setup_raw(XCODE_PCM16, &quality_in); + else if (quality_in.bits_per_sample == 24) + decode_ctx = transcode_decode_setup_raw(XCODE_PCM24, &quality_in); + else if (quality_in.bits_per_sample == 32) + decode_ctx = transcode_decode_setup_raw(XCODE_PCM32, &quality_in); + + if (!decode_ctx) + { + DPRINTF(E_LOG, L_STREAMING, "Error setting up decoder for input quality sr %d, bps %d, ch %d, cannot MP3 encode\n", + quality_in.sample_rate, quality_in.bits_per_sample, quality_in.channels); + goto error; + } + + w->quality_in = quality_in; + w->xcode_ctx = transcode_encode_setup(XCODE_MP3, &quality_out, decode_ctx, NULL, 0, 0); + if (!w->xcode_ctx) + { + DPRINTF(E_LOG, L_STREAMING, "Error setting up encoder for output quality sr %d, bps %d, ch %d, cannot MP3 encode\n", + quality_out.sample_rate, quality_out.bits_per_sample, quality_out.channels); + goto error; + } + + transcode_decode_cleanup(&decode_ctx); + return 0; + + error: + transcode_decode_cleanup(&decode_ctx); + return -1; +} + +static int +encode_frame(struct streaming_wanted *w, struct media_quality quality_in, transcode_frame *frame) +{ + int ret; + + if (!w->xcode_ctx || !quality_is_equal(&quality_in, &w->quality_in)) + { + DPRINTF(E_DBG, L_STREAMING, "Resetting transcode context\n"); + if (encode_reset(w, quality_in) < 0) + return -1; + } + + ret = transcode_encode(w->encoded_data, w->xcode_ctx, frame, 0); + if (ret < 0) + { + return -1; + } + + return 0; +} + +static void +encode_write(uint8_t *buf, size_t buflen, struct streaming_wanted *w, struct pipepair *p) +{ + int ret; + + if (p->writefd < 0) + return; + + ret = write(p->writefd, buf, buflen); + if (ret < 0) + { + DPRINTF(E_LOG, L_STREAMING, "Error writing to stream pipe %d (format %d): %s\n", p->writefd, w->format, strerror(errno)); + wanted_session_remove(w, p->readfd); + } +} + +static void +encode_data_cb(void *arg) +{ + struct encode_cmdarg *ctx = arg; + transcode_frame *frame; + struct streaming_wanted *w; + struct streaming_wanted *next; + uint8_t *buf; + size_t len; + int ret; + int i; + + frame = transcode_frame_new(ctx->buf, ctx->bufsize, ctx->samples, &ctx->quality); + if (!frame) + { + DPRINTF(E_LOG, L_STREAMING, "Could not convert raw PCM to frame\n"); + goto out; + } + + pthread_mutex_lock(&streaming_wanted_lck); + + // To make sure we process the frames in order + while (ctx->seqnum != streaming.seqnum_encode_next) + pthread_cond_wait(&streaming_sequence_cond, &streaming_wanted_lck); + + for (w = streaming.wanted; w; w = next) + { + next = w->next; + + ret = encode_frame(w, ctx->quality, frame); + if (ret < 0) + wanted_remove(&streaming.wanted, w); // This will close all the fds, so readers get an error + + len = evbuffer_get_length(w->encoded_data); + if (len == 0) + continue; + + buf = evbuffer_pullup(w->encoded_data, -1); + + for (i = 0; i < WANTED_PIPES_MAX; i++) + encode_write(buf, len, w, &w->pipes[i]); + + evbuffer_drain(w->encoded_data, -1); + + if (w->refcount == 0) + wanted_remove(&streaming.wanted, w); + } + + streaming.seqnum_encode_next++; + pthread_cond_broadcast(&streaming_sequence_cond); + pthread_mutex_unlock(&streaming_wanted_lck); + + out: + transcode_frame_free(frame); + free(ctx->buf); +} + +static void * +streaming_metadata_prepare(struct output_metadata *metadata) +{ + struct db_queue_item *queue_item; + char *title; + + queue_item = db_queue_fetch_byitemid(metadata->item_id); + if (!queue_item) + { + DPRINTF(E_LOG, L_STREAMING, "Could not fetch queue item id %d for new metadata\n", metadata->item_id); + return NULL; + } + + title = safe_asprintf("%s - %s", queue_item->title, queue_item->artist); + free_queue_item(queue_item, 0); + + return title; +} + + +/* ----------------------------- Thread: httpd ------------------------------ */ + +int +streaming_session_register(enum streaming_format format, struct media_quality quality) +{ + struct streaming_wanted *w; + struct pipepair pipe; + int ret; + + pthread_mutex_lock(&streaming_wanted_lck); + w = wanted_find_byformat(streaming.wanted, format, quality); + if (!w) + w = wanted_add(&streaming.wanted, format, quality); + + ret = wanted_session_add(&pipe, w); + if (ret < 0) + pipe.readfd = -1; + + pthread_mutex_unlock(&streaming_wanted_lck); + + return pipe.readfd; +} + +void +streaming_session_deregister(int readfd) +{ + struct streaming_wanted *w; + + pthread_mutex_lock(&streaming_wanted_lck); + w = wanted_find_byreadfd(streaming.wanted, readfd); + if (!w) + goto out; + + wanted_session_remove(w, readfd); + + if (w->refcount == 0) + wanted_remove(&streaming.wanted, w); + + out: + pthread_mutex_unlock(&streaming_wanted_lck); +} + +// Not thread safe, but only called once during httpd init +void +streaming_metadatacb_register(streaming_metadatacb cb) +{ + streaming.metadatacb = cb; +} + +/* ----------------------------- Thread: Player ----------------------------- */ + +static void +encode_worker_invoke(uint8_t *buf, size_t bufsize, int samples, struct media_quality quality) +{ + struct encode_cmdarg ctx; + + if (quality.channels == 0) + { + DPRINTF(E_LOG, L_STREAMING, "Streaming quality is zero (%d/%d/%d)\n", + quality.sample_rate, quality.bits_per_sample, quality.channels); + return; + } + + CHECK_NULL(L_STREAMING, ctx.buf = malloc(bufsize)); + memcpy(ctx.buf, buf, bufsize); + ctx.bufsize = bufsize; + ctx.samples = samples; + ctx.quality = quality; + ctx.seqnum = streaming.seqnum; + + streaming.seqnum++; + + worker_execute(encode_data_cb, &ctx, sizeof(struct encode_cmdarg), 0); +} + +static void +silenceev_cb(evutil_socket_t fd, short event, void *arg) +{ + uint8_t silence[SILENCE_BUF_SIZE] = { 0 }; + int samples; + + // No lock since this is just an early exit, it doesn't need to be accurate + if (!streaming.wanted) + return; + + samples = BTOS(SILENCE_BUF_SIZE, streaming.last_quality.bits_per_sample, streaming.last_quality.channels); + + encode_worker_invoke(silence, SILENCE_BUF_SIZE, samples, streaming.last_quality); + + evtimer_add(streaming.silenceev, &streaming.silencetv); +} + +static void +streaming_write(struct output_buffer *obuf) +{ + // No lock since this is just an early exit, it doesn't need to be accurate + if (!streaming.wanted) + return; + + encode_worker_invoke(obuf->data[0].buffer, obuf->data[0].bufsize, obuf->data[0].samples, obuf->data[0].quality); + + streaming.last_quality = obuf->data[0].quality; + + // In case this is the last player write() we want to start streaming silence + evtimer_add(streaming.silenceev, &streaming.silencetv); +} + +static void +streaming_metadata_send(struct output_metadata *metadata) +{ + char *title = metadata->priv; + + // Calls back to httpd_streaming to update the title + if (streaming.metadatacb) + streaming.metadatacb(title); + + free(title); + outputs_metadata_free(metadata); +} + +static int +streaming_init(void) +{ + CHECK_NULL(L_STREAMING, streaming.silenceev = event_new(evbase_player, -1, 0, silenceev_cb, NULL)); + CHECK_ERR(L_STREAMING, mutex_init(&streaming_wanted_lck)); + CHECK_ERR(L_STREAMING, pthread_cond_init(&streaming_sequence_cond, NULL)); + + return 0; +} + +static void +streaming_deinit(void) +{ + event_free(streaming.silenceev); +} + struct output_definition output_streaming = { @@ -30,5 +610,9 @@ struct output_definition output_streaming = .type = OUTPUT_TYPE_STREAMING, .priority = 0, .disabled = 0, + .init = streaming_init, + .deinit = streaming_deinit, .write = streaming_write, + .metadata_prepare = streaming_metadata_prepare, + .metadata_send = streaming_metadata_send, }; diff --git a/src/outputs/streaming.h b/src/outputs/streaming.h new file mode 100644 index 00000000..34673345 --- /dev/null +++ b/src/outputs/streaming.h @@ -0,0 +1,23 @@ + +#ifndef __STREAMING_H__ +#define __STREAMING_H__ + +#include "misc.h" // struct media_quality + +typedef void (*streaming_metadatacb)(char *metadata); + +enum streaming_format +{ + STREAMING_FORMAT_MP3, +}; + +int +streaming_session_register(enum streaming_format format, struct media_quality quality); + +void +streaming_session_deregister(int readfd); + +void +streaming_metadatacb_register(streaming_metadatacb cb); + +#endif /* !__STREAMING_H__ */ diff --git a/src/worker.c b/src/worker.c index 23ad95c4..ecbc9836 100644 --- a/src/worker.c +++ b/src/worker.c @@ -29,6 +29,10 @@ #include #include #include +#include + +#include + #include #include @@ -36,9 +40,17 @@ #include "db.h" #include "logger.h" #include "worker.h" -#include "commands.h" +#include "evthr.h" #include "misc.h" +#define THREADPOOL_NTHREADS 4 + +static struct evthr_pool *worker_threadpool; +static __thread struct evthr *worker_thr; + + +/* ----------------------------- CALLBACK EXECUTION ------------------------- */ +/* Worker threads */ struct worker_arg { @@ -49,19 +61,6 @@ struct worker_arg }; -/* --- Globals --- */ -// worker thread -static pthread_t tid_worker; - -// Event base, pipes and events -struct event_base *evbase_worker; -static int g_initialized; -static struct commands_base *cmdbase; - - -/* ---------------------------- CALLBACK EXECUTION ------------------------- */ -/* Thread: worker */ - static void execute_cb(int fd, short what, void *arg) { @@ -74,64 +73,47 @@ execute_cb(int fd, short what, void *arg) free(cmdarg); } - -static enum command_state -execute(void *arg, int *retval) +static void +execute(struct evthr *thr, void *arg, void *shared) { struct worker_arg *cmdarg = arg; struct timeval tv = { cmdarg->delay, 0 }; + struct event_base *evbase; if (cmdarg->delay) { - cmdarg->timer = evtimer_new(evbase_worker, execute_cb, cmdarg); + evbase = evthr_get_base(thr); + cmdarg->timer = evtimer_new(evbase, execute_cb, cmdarg); evtimer_add(cmdarg->timer, &tv); - - *retval = 0; - return COMMAND_PENDING; // Not done yet, ask caller not to free cmd + return; } cmdarg->cb(cmdarg->cb_arg); free(cmdarg->cb_arg); - - *retval = 0; - return COMMAND_END; + free(cmdarg); } - -/* --------------------------------- MAIN --------------------------------- */ -/* Thread: worker */ - -static void * -worker(void *arg) +static void +init_cb(struct evthr *thr, void *shared) { - int ret; + CHECK_ERR(L_MAIN, db_perthread_init()); - ret = db_perthread_init(); - if (ret < 0) - { - DPRINTF(E_LOG, L_MAIN, "Error: DB init failed (worker thread)\n"); - pthread_exit(NULL); - } + worker_thr = thr; - g_initialized = 1; + thread_setname(pthread_self(), "worker"); +} - event_base_dispatch(evbase_worker); - - if (g_initialized) - { - DPRINTF(E_LOG, L_MAIN, "Worker event loop terminated ahead of time!\n"); - g_initialized = 0; - } +static void +exit_cb(struct evthr *thr, void *shared) +{ + worker_thr = NULL; db_perthread_deinit(); - - pthread_exit(NULL); } /* ---------------------------- Our worker API --------------------------- */ -/* Thread: player */ void worker_execute(void (*cb)(void *), void *cb_arg, size_t arg_size, int delay) { @@ -164,7 +146,13 @@ worker_execute(void (*cb)(void *), void *cb_arg, size_t arg_size, int delay) cmdarg->cb_arg = argcpy; cmdarg->delay = delay; - commands_exec_async(cmdbase, execute, cmdarg); + evthr_pool_defer(worker_threadpool, execute, cmdarg); +} + +struct event_base * +worker_evbase_get(void) +{ + return evthr_get_base(worker_thr); } int @@ -172,51 +160,30 @@ worker_init(void) { int ret; - evbase_worker = event_base_new(); - if (!evbase_worker) + worker_threadpool = evthr_pool_wexit_new(THREADPOOL_NTHREADS, init_cb, exit_cb, NULL); + if (!worker_threadpool) { - DPRINTF(E_LOG, L_MAIN, "Could not create an event base\n"); - goto evbase_fail; + DPRINTF(E_LOG, L_MAIN, "Could not create worker thread pool\n"); + goto error; } - cmdbase = commands_base_new(evbase_worker, NULL); - - ret = pthread_create(&tid_worker, NULL, worker, NULL); + ret = evthr_pool_start(worker_threadpool); if (ret < 0) { - DPRINTF(E_LOG, L_MAIN, "Could not spawn worker thread: %s\n", strerror(errno)); - - goto thread_fail; + DPRINTF(E_LOG, L_MAIN, "Could not spawn worker threads\n"); + goto error; } - thread_setname(tid_worker, "worker"); - return 0; - thread_fail: - commands_base_free(cmdbase); - event_base_free(evbase_worker); - evbase_worker = NULL; - - evbase_fail: + error: + worker_deinit(); return -1; } void worker_deinit(void) { - int ret; - - g_initialized = 0; - commands_base_destroy(cmdbase); - - ret = pthread_join(tid_worker, NULL); - if (ret != 0) - { - DPRINTF(E_FATAL, L_MAIN, "Could not join worker thread: %s\n", strerror(errno)); - return; - } - - // Free event base (should free events too) - event_base_free(evbase_worker); + evthr_pool_stop(worker_threadpool); + evthr_pool_free(worker_threadpool); } diff --git a/src/worker.h b/src/worker.h index 125e9730..32afea7c 100644 --- a/src/worker.h +++ b/src/worker.h @@ -2,8 +2,10 @@ #ifndef __WORKER_H__ #define __WORKER_H__ +#include + /* The worker thread is made for running asyncronous tasks from a real time - * thread, mainly the player thread. + * thread. * The worker_execute() function will trigger a callback from the worker thread. * Before returning the function will copy the argument given, so the caller @@ -19,6 +21,11 @@ void worker_execute(void (*cb)(void *), void *cb_arg, size_t arg_size, int delay); +/* Can be called within a callback to get the worker thread's event base + */ +struct event_base * +worker_evbase_get(void); + int worker_init(void);