owntone-server/src/httpd.c

1664 lines
43 KiB
C

/*
* Copyright (C) 2009-2010 Julien BLACHE <jb@jblache.org>
*
* 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 <config.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <limits.h>
#include <errno.h>
#include <time.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdint.h>
#include <inttypes.h>
#include <event2/event.h>
#include <regex.h>
#include <zlib.h>
#include "logger.h"
#include "db.h"
#include "conffile.h"
#include "misc.h"
#include "worker.h"
#include "evthr.h"
#include "httpd.h"
#include "httpd_internal.h"
#include "transcode.h"
#include "cache.h"
#include "listener.h"
#include "player.h"
#ifdef LASTFM
# include "lastfm.h"
#endif
#ifdef HAVE_LIBWEBSOCKETS
# include "websocket.h"
#endif
#define STREAM_CHUNK_SIZE (64 * 1024)
#define ERR_PAGE "<html>\n<head>\n" \
"<title>%d %s</title>\n" \
"</head>\n<body>\n" \
"<h1>%s</h1>\n" \
"</body>\n</html>\n"
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;
enum transcode_profile profile;
char *ctype;
};
struct stream_ctx {
struct httpd_request *hreq;
struct event *ev;
int id;
int fd;
off_t size;
off_t stream_size;
off_t offset;
off_t start_offset;
off_t end_offset;
bool no_register_playback;
struct transcode_ctx *xcode;
};
static const struct content_type_map ext2ctype[] =
{
{ ".html", XCODE_NONE, "text/html; charset=utf-8" },
{ ".xml", XCODE_NONE, "text/xml; charset=utf-8" },
{ ".css", XCODE_NONE, "text/css; charset=utf-8" },
{ ".txt", XCODE_NONE, "text/plain; charset=utf-8" },
{ ".js", XCODE_NONE, "application/javascript; charset=utf-8" },
{ ".gif", XCODE_NONE, "image/gif" },
{ ".ico", XCODE_NONE, "image/x-ico" },
{ ".png", XCODE_PNG, "image/png" },
{ ".jpg", XCODE_JPEG, "image/jpeg" },
{ ".mp3", XCODE_MP3, "audio/mpeg" },
{ ".m4a", XCODE_MP4_ALAC, "audio/mp4" },
{ ".wav", XCODE_WAV, "audio/wav" },
{ NULL, XCODE_NONE, NULL }
};
static char webroot_directory[PATH_MAX];
static const char *httpd_allow_origin;
static int httpd_port;
// 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 --------------------------------- */
static int
path_is_legal(const char *path)
{
return strncmp(webroot_directory, path, strlen(webroot_directory));
}
/* Callback from the worker thread (async operation as it may block) */
static void
playcount_inc_cb(void *arg)
{
int *id = arg;
db_file_inc_playcount(*id);
}
#ifdef LASTFM
/* Callback from the worker thread (async operation as it may block) */
static void
scrobble_cb(void *arg)
{
int *id = arg;
lastfm_scrobble(*id);
}
#endif
static const char *
content_type_from_ext(const char *ext)
{
int i;
if (!ext)
return NULL;
for (i = 0; ext2ctype[i].ext; i++)
{
if (strcmp(ext, ext2ctype[i].ext) == 0)
return ext2ctype[i].ctype;
}
return NULL;
}
static const char *
content_type_from_profile(enum transcode_profile profile)
{
int i;
if (profile == XCODE_NONE)
return NULL;
for (i = 0; ext2ctype[i].ext; i++)
{
if (profile == ext2ctype[i].profile)
return ext2ctype[i].ctype;
}
return NULL;
}
static int
basic_auth_cred_extract(char **user, char **pwd, const char *auth)
{
char *decoded = NULL;
regex_t preg = { 0 };
regmatch_t matchptr[3]; // Room for entire string, username substring and password substring
int ret;
decoded = (char *)b64_decode(NULL, auth);
if (!decoded)
goto error;
// Apple Music gives is "(dt:1):password", which we need to support even if it
// isn't according to the basic auth RFC that says the username cannot include
// a colon
ret = regcomp(&preg, "(\\(.*?\\)|[^:]*):(.*)", REG_EXTENDED);
if (ret != 0)
goto error;
ret = regexec(&preg, decoded, ARRAY_SIZE(matchptr), matchptr, 0);
if (ret != 0 || matchptr[1].rm_so == -1 || matchptr[2].rm_so == -1)
goto error;
*user = strndup(decoded + matchptr[1].rm_so, matchptr[1].rm_eo - matchptr[1].rm_so);
*pwd = strndup(decoded + matchptr[2].rm_so, matchptr[2].rm_eo - matchptr[2].rm_so);
free(decoded);
return 0;
error:
free(decoded);
return -1;
}
/* --------------------------- MODULES INTERFACE ---------------------------- */
static void
modules_handlers_unset(struct httpd_uri_map *uri_map)
{
struct httpd_uri_map *uri;
for (uri = uri_map; uri->preg; uri++)
{
regfree(uri->preg); // Frees allocation by regcomp
free(uri->preg); // Frees our own calloc
}
}
static int
modules_handlers_set(struct httpd_uri_map *uri_map)
{
struct httpd_uri_map *uri;
char buf[64];
int ret;
for (uri = uri_map; uri->handler; uri++)
{
uri->preg = calloc(1, sizeof(regex_t));
if (!uri->preg)
{
DPRINTF(E_LOG, L_HTTPD, "Error setting URI handler, out of memory");
goto error;
}
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;
}
}
return 0;
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->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_request_handler_set(struct httpd_request *hreq)
{
struct httpd_uri_map *map;
int ret;
// Path with e.g. /api -> JSON module
hreq->module = modules_search(hreq->path);
if (!hreq->module)
{
return;
}
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);
}
/*
* Checks if the given ETag matches the "If-None-Match" request header
*
* If the request does not contains a "If-None-Match" header value or the value
* does not match the given ETag, it returns false (modified) and adds the
* "Cache-Control" and "ETag" headers to the response header.
*
* @param req The request with request and response headers
* @param etag The valid ETag for the requested resource
* @return True if the given ETag matches the request-header-value "If-None-Match", otherwise false
*/
bool
httpd_request_etag_matches(struct httpd_request *hreq, const char *etag)
{
const char *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
httpd_header_add(hreq->out_headers, "Cache-Control", "private,no-cache,max-age=0");
httpd_header_add(hreq->out_headers, "ETag", etag);
return false;
}
/*
* Checks if the given timestamp matches the "If-Modified-Since" request header
*
* If the request does not contains a "If-Modified-Since" header value or the value
* does not match the given timestamp, it returns false (modified) and adds the
* "Cache-Control" and "Last-Modified" headers to the response header.
*
* @param req The request with request and response headers
* @param mtime The last modified timestamp for the requested resource
* @return True if the given timestamp matches the request-header-value "If-Modified-Since", otherwise false
*/
bool
httpd_request_not_modified_since(struct httpd_request *hreq, time_t mtime)
{
char last_modified[1000];
const char *modified_since;
struct tm timebuf;
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));
// Return not modified, if given timestamp matches "If-Modified-Since" request header
if (modified_since && (strcasecmp(last_modified, modified_since) == 0))
return true;
// Add cache headers to allow client side caching
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 httpd_request *hreq)
{
// Remove potentially set cache control headers
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
httpd_header_add(hreq->out_headers, "Cache-Control", "no-store");
}
static void
serve_file(struct httpd_request *hreq)
{
char path[PATH_MAX];
char deref[PATH_MAX];
const char *ctype;
struct stat sb;
int fd;
uint8_t buf[4096];
bool slashed;
int ret;
if (!httpd_request_is_authorized(hreq))
return;
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", hreq->uri);
httpd_send_error(hreq, HTTP_NOTFOUND, "Not Found");
return;
}
if (!realpath(path, deref))
{
DPRINTF(E_LOG, L_HTTPD, "Could not dereference %s: %s\n", path, strerror(errno));
httpd_send_error(hreq, HTTP_NOTFOUND, "Not Found");
return;
}
if (strlen(deref) >= PATH_MAX)
{
DPRINTF(E_LOG, L_HTTPD, "Dereferenced path exceeds PATH_MAX: %s\n", path);
httpd_send_error(hreq, HTTP_NOTFOUND, "Not Found");
return;
}
ret = lstat(deref, &sb);
if (ret < 0)
{
DPRINTF(E_WARN, L_HTTPD, "Could not lstat() %s: %s\n", deref, strerror(errno));
httpd_send_error(hreq, HTTP_NOTFOUND, "Not Found");
return;
}
if (S_ISDIR(sb.st_mode))
{
slashed = (path[strlen(path) - 1] == '/');
strncat(path, ((slashed) ? "index.html" : "/index.html"), sizeof(path) - strlen(path) - 1);
if (!realpath(path, deref))
{
DPRINTF(E_LOG, L_HTTPD, "Could not dereference %s: %s\n", path, strerror(errno));
httpd_send_error(hreq, HTTP_NOTFOUND, "Not Found");
return;
}
if (strlen(deref) >= PATH_MAX)
{
DPRINTF(E_LOG, L_HTTPD, "Dereferenced path exceeds PATH_MAX: %s\n", path);
httpd_send_error(hreq, HTTP_NOTFOUND, "Not Found");
return;
}
ret = stat(deref, &sb);
if (ret < 0)
{
DPRINTF(E_LOG, L_HTTPD, "Could not stat() %s: %s\n", path, strerror(errno));
httpd_send_error(hreq, HTTP_NOTFOUND, "Not Found");
return;
}
}
if (path_is_legal(deref) != 0)
{
DPRINTF(E_WARN, L_HTTPD, "Access to file outside the web root dir forbidden: %s\n", deref);
httpd_send_error(hreq, HTTP_FORBIDDEN, "Forbidden");
return;
}
if (httpd_request_not_modified_since(hreq, sb.st_mtime))
{
httpd_send_reply(hreq, HTTP_NOTMODIFIED, NULL, HTTPD_SEND_NO_GZIP);
return;
}
fd = open(deref, O_RDONLY);
if (fd < 0)
{
DPRINTF(E_LOG, L_HTTPD, "Could not open %s: %s\n", deref, strerror(errno));
httpd_send_error(hreq, HTTP_NOTFOUND, "Not Found");
return;
}
ret = evbuffer_expand(hreq->out_body, sb.st_size);
if (ret < 0)
{
DPRINTF(E_LOG, L_HTTPD, "Out of memory for htdocs-file\n");
goto out_fail;
}
while ((ret = read(fd, buf, sizeof(buf))) > 0)
evbuffer_add(hreq->out_body, buf, ret);
if (ret < 0)
{
DPRINTF(E_LOG, L_HTTPD, "Could not read file into evbuffer\n");
goto out_fail;
}
ctype = content_type_from_ext(strrchr(path, '.'));
if (!ctype)
ctype = "application/octet-stream";
httpd_header_add(hreq->out_headers, "Content-Type", ctype);
httpd_send_reply(hreq, HTTP_OK, "OK", HTTPD_SEND_NO_GZIP);
close(fd);
return;
out_fail:
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_chunk_resched_cb(httpd_connection *conn, void *arg)
{
struct stream_ctx *st = arg;
// TODO not thread safe if st was freed in worker thread, but maybe not possible?
event_active(st->ev, 0, 0);
}
static void
stream_free(struct stream_ctx *st)
{
if (!st)
return;
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)
{
if (!st->no_register_playback
&& (st->stream_size > ((st->size * 50) / 100))
&& (st->offset > ((st->size * 80) / 100)))
{
st->no_register_playback = true;
worker_execute(playcount_inc_cb, &st->id, sizeof(int), 0);
#ifdef LASTFM
worker_execute(scrobble_cb, &st->id, sizeof(int), 1);
#endif
}
}
static struct stream_ctx *
stream_new(struct media_file_info *mfi, struct httpd_request *hreq, event_callback_fn stream_cb)
{
struct stream_ctx *st;
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);
if (httpd_query_value_find(hreq->query, "no_register_playback"))
st->no_register_playback = true;
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, enum transcode_profile profile, struct httpd_request *hreq,
int64_t offset, int64_t end_offset, event_callback_fn stream_cb)
{
struct transcode_decode_setup_args decode_args = { 0 };
struct transcode_encode_setup_args encode_args = { 0 };
struct media_quality quality = { 0 };
struct evbuffer *prepared_header = NULL;
struct stream_ctx *st;
int cached;
int ret;
// We use source sample rate etc, but for MP3 we must set a bit rate
quality.bit_rate = 1000 * cfg_getint(cfg_getsec(cfg, "streaming"), "bit_rate");
st = stream_new(mfi, hreq, stream_cb);
if (!st)
{
goto error;
}
if (profile == XCODE_MP4_ALAC)
{
CHECK_NULL(L_HTTPD, prepared_header = evbuffer_new());
ret = cache_xcode_header_get(prepared_header, &cached, mfi->id, "mp4");
if (ret < 0 || !cached) // Error or not found
{
evbuffer_free(prepared_header);
prepared_header = NULL;
}
}
decode_args.profile = profile;
decode_args.is_http = (mfi->data_kind == DATA_KIND_HTTP);
decode_args.path = mfi->path;
decode_args.len_ms = mfi->song_length;
encode_args.profile = profile;
encode_args.quality = &quality;
encode_args.prepared_header = prepared_header;
st->xcode = transcode_setup(decode_args, encode_args);
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->size = transcode_encode_query(st->xcode->encode_ctx, "estimated_size");
if (st->size < 0)
{
DPRINTF(E_WARN, L_HTTPD, "Transcoding setup failed, could not determine estimated size\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;
if (prepared_header)
evbuffer_free(prepared_header);
return st;
error:
if (prepared_header)
evbuffer_free(prepared_header);
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 = stream_new(mfi, hreq, stream_cb);
if (!st)
{
goto error;
}
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 stat() %s: %s\n", mfi->path, strerror(errno));
httpd_send_error(hreq, HTTP_NOTFOUND, "Not Found");
goto error;
}
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;
}
static void
stream_chunk_xcode_cb(int fd, short event, void *arg)
{
struct stream_ctx *st = arg;
int xcoded;
int ret;
xcoded = transcode(st->hreq->out_body, NULL, st->xcode, STREAM_CHUNK_SIZE);
if (xcoded <= 0)
{
if (xcoded == 0)
DPRINTF(E_INFO, L_HTTPD, "Done streaming transcoded file id %d\n", st->id);
else
DPRINTF(E_LOG, L_HTTPD, "Transcoding error, file id %d\n", st->id);
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
if (st->start_offset > st->offset)
{
ret = st->start_offset - st->offset;
if (ret < xcoded)
{
evbuffer_drain(st->hreq->out_body, ret);
st->offset += ret;
ret = xcoded - ret;
}
else
{
evbuffer_drain(st->hreq->out_body, xcoded);
st->offset += xcoded;
// Reschedule immediately - consume up to start_offset
event_active(st->ev, 0, 0);
return;
}
}
else
ret = xcoded;
httpd_send_reply_chunk(st->hreq, stream_chunk_resched_cb, st);
st->offset += ret;
stream_end_register(st);
}
static void
stream_chunk_raw_cb(int fd, short event, void *arg)
{
struct stream_ctx *st = arg;
size_t chunk_size;
int ret;
if (st->end_offset && (st->offset > st->end_offset))
{
stream_end(st);
return;
}
if (st->end_offset && ((st->offset + STREAM_CHUNK_SIZE) > (st->end_offset + 1)))
chunk_size = st->end_offset + 1 - st->offset;
else
chunk_size = STREAM_CHUNK_SIZE;
ret = evbuffer_read(st->hreq->out_body, st->fd, chunk_size);
if (ret <= 0)
{
if (ret == 0)
DPRINTF(E_INFO, L_HTTPD, "Done streaming file id %d\n", st->id);
else
DPRINTF(E_LOG, L_HTTPD, "Streaming error, file id %d\n", st->id);
stream_end(st);
return;
}
DPRINTF(E_DBG, L_HTTPD, "Read %d bytes; streaming file id %d\n", ret, st->id);
httpd_send_reply_chunk(st->hreq, stream_chunk_resched_cb, st);
st->offset += ret;
stream_end_register(st);
}
static void
stream_fail_cb(void *arg)
{
struct stream_ctx *st = arg;
stream_free(st);
}
/* -------------------------- SPEAKER/CACHE HANDLING ------------------------ */
// Thread: player (must not block)
static void
speaker_enum_cb(struct player_speaker_info *spk, void *arg)
{
bool *want_mp4 = arg;
*want_mp4 = *want_mp4 || (spk->format == MEDIA_FORMAT_ALAC && strcmp(spk->output_type, "RCP/SoundBridge") == 0);
}
// Thread: worker
static void
speaker_update_handler_cb(void *arg)
{
const char *prefer_format = cfg_getstr(cfg_getsec(cfg, "library"), "prefer_format");
bool want_mp4;
want_mp4 = (prefer_format && (strcmp(prefer_format, "alac") == 0));
if (!want_mp4)
player_speaker_enumerate(speaker_enum_cb, &want_mp4);
cache_xcode_toggle(want_mp4);
}
// Thread: player (must not block)
static void
httpd_speaker_update_handler(short event_mask)
{
worker_execute(speaker_update_handler_cb, NULL, 0, 0);
}
/* ---------------------------- REQUEST CALLBACKS --------------------------- */
// Worker thread, invoked by request_cb() below
static void
request_async_cb(void *arg)
{
struct httpd_request *hreq = *(struct httpd_request **)arg;
#ifdef HAVE_GETTID
DPRINTF(E_DBG, hreq->module->logdomain, "%s request '%s' in worker thread %d\n", hreq->module->name, hreq->uri, (int)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))
{
httpd_send_reply(hreq, HTTP_OK, "OK", HTTPD_SEND_NO_GZIP);
return;
}
else if (!hreq->uri || !hreq->uri_parsed)
{
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;
}
httpd_request_handler_set(hreq);
if (hreq->module && hreq->is_async)
{
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);
}
// Don't touch hreq here, if async it has been passed to a worker thread
}
/* ------------------------------- HTTPD API -------------------------------- */
void
httpd_stream_file(struct httpd_request *hreq, int id)
{
struct media_file_info *mfi = NULL;
struct stream_ctx *st = NULL;
enum transcode_profile profile;
enum transcode_profile spk_profile;
const char *param;
const char *param_end;
const char *ctype;
char buf[64];
int64_t offset = 0;
int64_t end_offset = 0;
int ret;
param = httpd_header_find(hreq->in_headers, "Range");
if (param)
{
DPRINTF(E_DBG, L_HTTPD, "Found Range header: %s\n", param);
/* Start offset */
ret = safe_atoi64(param + strlen("bytes="), &offset);
if (ret < 0)
{
DPRINTF(E_LOG, L_HTTPD, "Invalid start offset, will stream whole file (%s)\n", param);
offset = 0;
}
/* End offset, if any */
else
{
param_end = strchr(param, '-');
if (param_end && (strlen(param_end) > 1))
{
ret = safe_atoi64(param_end + 1, &end_offset);
if (ret < 0)
{
DPRINTF(E_LOG, L_HTTPD, "Invalid end offset, will stream to end of file (%s)\n", param);
end_offset = 0;
}
if (end_offset < offset)
{
DPRINTF(E_LOG, L_HTTPD, "End offset < start offset, will stream to end of file (%" PRIi64 " < %" PRIi64 ")\n", end_offset, offset);
end_offset = 0;
}
}
}
}
mfi = db_file_fetch_byid(id);
if (!mfi)
{
DPRINTF(E_LOG, L_HTTPD, "Item %d not found\n", id);
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);
httpd_send_error(hreq, HTTP_INTERNAL, "Cannot stream non-file content");
goto error;
}
param = httpd_header_find(hreq->in_headers, "Accept-Codecs");
profile = transcode_needed(hreq->user_agent, param, mfi->codectype);
if (profile == XCODE_UNKNOWN)
{
DPRINTF(E_LOG, L_HTTPD, "Could not serve '%s' to client, unable to determine output format\n", mfi->path);
httpd_send_error(hreq, HTTP_INTERNAL, "Cannot stream, unable to determine output format");
goto error;
}
if (profile != XCODE_NONE)
{
DPRINTF(E_INFO, L_HTTPD, "Preparing to transcode %s\n", mfi->path);
spk_profile = httpd_xcode_profile_get(hreq);
if (spk_profile != XCODE_NONE)
profile = spk_profile;
st = stream_new_transcode(mfi, profile, hreq, offset, end_offset, stream_chunk_xcode_cb);
if (!st)
goto error;
ctype = content_type_from_profile(profile);
if (!ctype)
goto error;
if (!httpd_header_find(hreq->out_headers, "Content-Type"))
httpd_header_add(hreq->out_headers, "Content-Type", ctype);
}
else
{
DPRINTF(E_INFO, L_HTTPD, "Preparing to stream %s\n", mfi->path);
st = stream_new_raw(mfi, hreq, offset, end_offset, stream_chunk_raw_cb);
if (!st)
goto error;
// 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/<type>
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
{
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 (!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
httpd_header_add(hreq->out_headers, "Content-Type", buf);
}
}
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 (profile == XCODE_NONE)
{
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
httpd_header_add(hreq->out_headers, "Content-Length", buf);
}
httpd_send_reply_start(hreq, HTTP_OK, "OK");
}
else
{
DPRINTF(E_DBG, L_HTTPD, "Stream request with range %" PRIi64 "-%" PRIi64 "\n", offset, end_offset);
ret = snprintf(buf, sizeof(buf), "bytes %" PRIi64 "-%" PRIi64 "/%" PRIi64,
offset, (end_offset) ? end_offset : (int64_t)st->size, (int64_t)st->size);
if ((ret < 0) || (ret >= sizeof(buf)))
DPRINTF(E_LOG, L_HTTPD, "Content-Range too large for buffer, dropping\n");
else
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
httpd_header_add(hreq->out_headers, "Content-Length", buf);
httpd_send_reply_start(hreq, 206, "Partial Content");
}
#ifdef HAVE_POSIX_FADVISE
if (profile == XCODE_NONE)
{
// 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 )
DPRINTF(E_DBG, L_HTTPD, "posix_fadvise() failed with error %d\n", ret);
}
#endif
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;
error:
stream_free(st);
free_mfi(mfi, 0);
}
// Returns enum transcode_profile, but is just declared with int so we don't
// need to include transcode.h in httpd_internal.h
int
httpd_xcode_profile_get(struct httpd_request *hreq)
{
struct player_speaker_info spk;
int ret;
// No peer address if the function is called from httpd_daap.c when the DAAP
// cache is being updated
if (!hreq->peer_address || !hreq->user_agent)
return XCODE_NONE;
// A Roku Soundbridge may also be RCP device/speaker for which the user may
// have set a prefered streaming format, but in all other cases we don't use
// speaker configuration (so caller will let transcode_needed decide)
if (strncmp(hreq->user_agent, "Roku", strlen("Roku")) != 0)
return XCODE_NONE;
ret = player_speaker_get_byaddress(&spk, hreq->peer_address);
if (ret < 0)
return XCODE_NONE;
if (spk.format == MEDIA_FORMAT_WAV)
return XCODE_WAV;
if (spk.format == MEDIA_FORMAT_MP3)
return XCODE_MP3;
if (spk.format == MEDIA_FORMAT_ALAC)
return XCODE_MP4_ALAC;
return XCODE_NONE;
}
struct evbuffer *
httpd_gzip_deflate(struct evbuffer *in)
{
struct evbuffer *out;
struct evbuffer_iovec iovec[1];
z_stream strm;
int ret;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
// Just to keep Coverity from complaining about uninitialized values
strm.total_in = 0;
strm.total_out = 0;
// Set up a gzip stream (the "+ 16" in 15 + 16), instead of a zlib stream (default)
ret = deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY);
if (ret != Z_OK)
{
DPRINTF(E_LOG, L_HTTPD, "zlib setup failed: %s\n", zError(ret));
return NULL;
}
strm.next_in = evbuffer_pullup(in, -1);
strm.avail_in = evbuffer_get_length(in);
out = evbuffer_new();
if (!out)
{
DPRINTF(E_LOG, L_HTTPD, "Could not allocate evbuffer for gzipped reply\n");
goto out_deflate_end;
}
// We use this to avoid a memcpy. The 512 is an arbitrary padding to make sure
// there is enough space, even if the compressed output should be slightly
// larger than input (could happen with small inputs).
ret = evbuffer_reserve_space(out, strm.avail_in + 512, iovec, 1);
if (ret < 0)
{
DPRINTF(E_LOG, L_HTTPD, "Could not reserve memory for gzipped reply\n");
goto out_evbuf_free;
}
strm.next_out = iovec[0].iov_base;
strm.avail_out = iovec[0].iov_len;
ret = deflate(&strm, Z_FINISH);
if (ret != Z_STREAM_END)
goto out_evbuf_free;
iovec[0].iov_len -= strm.avail_out;
evbuffer_commit_space(out, iovec, 1);
deflateEnd(&strm);
return out;
out_evbuf_free:
evbuffer_free(out);
out_deflate_end:
deflateEnd(&strm);
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 httpd_request *hreq, int code, const char *reason, enum httpd_send_flags flags)
{
struct evbuffer *gzbuf;
struct evbuffer *save;
const char *param;
int do_gzip;
if (!hreq->backend)
return;
do_gzip = ( (!(flags & HTTPD_SEND_NO_GZIP)) &&
(evbuffer_get_length(hreq->out_body) > 512) &&
(param = httpd_header_find(hreq->in_headers, "Accept-Encoding")) &&
(strstr(param, "gzip") || strstr(param, "*"))
);
cors_headers_add(hreq, httpd_allow_origin);
if (do_gzip && (gzbuf = httpd_gzip_deflate(hreq->out_body)))
{
DPRINTF(E_DBG, L_HTTPD, "Gzipping response\n");
httpd_header_add(hreq->out_headers, "Content-Encoding", "gzip");
save = hreq->out_body;
hreq->out_body = gzbuf;
evbuffer_free(save);
}
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 httpd_request *hreq, int error, const char *reason)
{
evbuffer_drain(hreq->out_body, -1);
httpd_headers_clear(hreq->out_headers);
cors_headers_add(hreq, httpd_allow_origin);
httpd_header_add(hreq->out_headers, "Content-Type", "text/html");
httpd_header_add(hreq->out_headers, "Connection", "close");
evbuffer_add_printf(hreq->out_body, ERR_PAGE, error, reason, reason);
httpd_send(hreq, HTTPD_REPLY_COMPLETE, error, reason, NULL, NULL);
}
bool
httpd_request_is_trusted(struct httpd_request *hreq)
{
return httpd_backend_peer_is_trusted(hreq->backend);
}
bool
httpd_request_is_authorized(struct httpd_request *hreq)
{
const char *passwd;
int ret;
if (httpd_request_is_trusted(hreq))
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", hreq->uri);
httpd_send_error(hreq, HTTP_FORBIDDEN, "Forbidden");
return false;
}
DPRINTF(E_DBG, L_HTTPD, "Checking web interface authentication\n");
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", hreq->uri);
// httpd_basic_auth has sent a reply
return false;
}
DPRINTF(E_DBG, L_HTTPD, "Authentication successful\n");
return true;
}
int
httpd_basic_auth(struct httpd_request *hreq, const char *user, const char *passwd, const char *realm)
{
char header[256];
const char *auth;
char *authuser;
char *authpwd;
int ret;
auth = httpd_header_find(hreq->in_headers, "Authorization");
if (!auth)
{
DPRINTF(E_DBG, L_HTTPD, "No Authorization header\n");
goto need_auth;
}
if (strncmp(auth, "Basic ", strlen("Basic ")) != 0)
{
DPRINTF(E_LOG, L_HTTPD, "Bad Authentication header\n");
goto need_auth;
}
auth += strlen("Basic ");
ret = basic_auth_cred_extract(&authuser, &authpwd, auth);
if (ret < 0)
{
DPRINTF(E_LOG, L_HTTPD, "Malformed Authentication header\n");
goto need_auth;
}
if (user)
{
if (strcmp(user, authuser) != 0)
{
DPRINTF(E_LOG, L_HTTPD, "Username mismatch\n");
free(authuser);
free(authpwd);
goto need_auth;
}
}
if (strcmp(passwd, authpwd) != 0)
{
DPRINTF(E_LOG, L_HTTPD, "Bad password\n");
free(authuser);
free(authpwd);
goto need_auth;
}
free(authuser);
free(authpwd);
return 0;
need_auth:
ret = snprintf(header, sizeof(header), "Basic realm=\"%s\"", realm);
if ((ret < 0) || (ret >= sizeof(header)))
{
httpd_send_error(hreq, HTTP_SERVUNAVAIL, "Internal Server Error");
return -1;
}
httpd_header_add(hreq->out_headers, "WWW-Authenticate", header);
evbuffer_add_printf(hreq->out_body, ERR_PAGE, HTTP_UNAUTHORIZED, "Unauthorized", "Authorization required");
httpd_send_reply(hreq, HTTP_UNAUTHORIZED, "Unauthorized", HTTPD_SEND_NO_GZIP);
return -1;
}
static int
bind_test(short unsigned port)
{
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 */
int
httpd_init(const char *webroot)
{
struct stat sb;
int ret;
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;
}
// 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;
}
// Prepare modules, e.g. httpd_daap
ret = modules_init();
if (ret < 0)
{
DPRINTF(E_FATAL, L_HTTPD, "Modules init failed\n");
goto error;
}
#ifdef HAVE_LIBWEBSOCKETS
ret = websocket_init();
if (ret < 0)
{
DPRINTF(E_FATAL, L_HTTPD, "Websocket init failed\n");
goto error;
}
#endif
httpd_threadpool = evthr_pool_wexit_new(THREADPOOL_NTHREADS, thread_init_cb, thread_exit_cb, NULL);
if (!httpd_threadpool)
{
DPRINTF(E_LOG, L_HTTPD, "Could not create httpd thread pool\n");
goto error;
}
ret = evthr_pool_start(httpd_threadpool);
if (ret < 0)
{
DPRINTF(E_LOG, L_HTTPD, "Could not spawn worker threads\n");
goto error;
}
// We need to know about speaker format changes so we can ask the cache to
// start preparing headers for mp4/alac if selected
listener_add(httpd_speaker_update_handler, LISTENER_SPEAKER);
return 0;
error:
httpd_deinit();
return -1;
}
/* Thread: main */
void
httpd_deinit(void)
{
listener_remove(httpd_speaker_update_handler);
// Give modules a chance to hang up connections nicely
modules_deinit();
#ifdef HAVE_LIBWEBSOCKETS
websocket_deinit();
#endif
evthr_pool_stop(httpd_threadpool);
evthr_pool_free(httpd_threadpool);
}