diff --git a/src/Makefile.am b/src/Makefile.am
index b1d4ac05..f465e12a 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -114,6 +114,7 @@ forked_daapd_SOURCES = main.c \
httpd_dacp.c httpd_dacp.h \
httpd_jsonapi.c httpd_jsonapi.h \
httpd_streaming.c httpd_streaming.h \
+ httpd_oauth.c httpd_oauth.h \
http.c http.h \
dmap_common.c dmap_common.h \
$(FFMPEG_SRC) \
diff --git a/src/httpd.c b/src/httpd.c
index 0c99446a..1766f6ce 100644
--- a/src/httpd.c
+++ b/src/httpd.c
@@ -62,36 +62,16 @@
#include "httpd_dacp.h"
#include "httpd_jsonapi.h"
#include "httpd_streaming.h"
+#include "httpd_oauth.h"
#include "transcode.h"
#ifdef LASTFM
# include "lastfm.h"
#endif
-#ifdef HAVE_SPOTIFY_H
-# include "spotify.h"
-#endif
#ifdef HAVE_LIBWEBSOCKETS
# include "websocket.h"
#endif
-/*
- * HTTP client quirks by User-Agent, from mt-daapd
- *
- * - iTunes:
- * + Connection: Keep-Alive on HTTP error 401
- * - Hifidelio:
- * + Connection: Keep-Alive for streaming (Connection: close not honoured)
- *
- * These quirks are not implemented. Implement as needed.
- *
- * Implemented quirks:
- *
- * - Roku:
- * + Does not encode space as + in query string
- * - iTunes:
- * + Does not encode space as + in query string
- */
-
#define WEB_ROOT DATADIR "/htdocs"
#define STREAM_CHUNK_SIZE (64 * 1024)
@@ -122,7 +102,6 @@ struct stream_ctx {
struct transcode_ctx *xcode;
};
-
static const struct content_type_map ext2ctype[] =
{
{ ".html", "text/html; charset=utf-8" },
@@ -136,6 +115,8 @@ static const struct content_type_map ext2ctype[] =
{ NULL, NULL }
};
+static const char *http_reply_401 = "
401 UnauthorizedAuthorization required";
+
struct event_base *evbase_httpd;
#ifdef HAVE_EVENTFD
@@ -155,8 +136,309 @@ static int httpd_port;
struct stream_ctx *g_st;
#endif
+
+/* -------------------------------- HELPERS --------------------------------- */
+
+static int
+path_is_legal(const char *path)
+{
+ return strncmp(WEB_ROOT, path, strlen(WEB_ROOT));
+}
+
+/* Callback from the worker thread (async operation as it may block) */
static void
-redirect_to_admin(struct evhttp_request *req);
+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
+
+/*
+ * 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)
+{
+ struct evkeyvalq *headers;
+ const char *ua;
+ const char *uri;
+ const char *u;
+ const char *q;
+ char *fixed;
+ char *f;
+ int len;
+
+ 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)
+ {
+ if (*u == '+')
+ len += 2;
+
+ u++;
+ }
+
+ fixed = (char *)malloc(len + 1);
+ if (!fixed)
+ return NULL;
+
+ strncpy(fixed, uri, q - uri);
+
+ f = fixed + (q - uri);
+ while (*q)
+ {
+ switch (*q)
+ {
+ case '+':
+ *f = '%';
+ f++;
+ *f = '2';
+ f++;
+ *f = 'B';
+ break;
+
+ case ' ':
+ *f = '+';
+ break;
+
+ default:
+ *f = *q;
+ break;
+ }
+
+ q++;
+ f++;
+ }
+
+ *f = '\0';
+
+ return fixed;
+}
+*/
+
+/* --------------------------- REQUEST HELPERS ------------------------------ */
+
+static void
+serve_file(struct evhttp_request *req, const char *uri)
+{
+ char *ext;
+ char path[PATH_MAX];
+ char *deref;
+ char *ctype;
+ struct evbuffer *evbuf;
+ struct evkeyvalq *input_headers;
+ struct evkeyvalq *output_headers;
+ struct stat sb;
+ int fd;
+ int i;
+ uint8_t buf[4096];
+ const char *modified_since;
+ char last_modified[1000];
+ struct tm *tm_modified;
+ int ret;
+
+ /* Check authentication */
+ if (!httpd_admin_check_auth(req))
+ {
+ DPRINTF(E_DBG, L_HTTPD, "Remote web interface request denied;\n");
+ return;
+ }
+
+ ret = snprintf(path, sizeof(path), "%s%s", WEB_ROOT, uri);
+ 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");
+
+ return;
+ }
+
+ ret = lstat(path, &sb);
+ if (ret < 0)
+ {
+ DPRINTF(E_LOG, L_HTTPD, "Could not lstat() %s: %s\n", path, strerror(errno));
+
+ httpd_send_error(req, HTTP_NOTFOUND, "Not Found");
+
+ return;
+ }
+
+ if (S_ISDIR(sb.st_mode))
+ {
+ httpd_redirect_to_index(req, uri);
+
+ return;
+ }
+ else if (S_ISLNK(sb.st_mode))
+ {
+ deref = realpath(path, NULL);
+ if (!deref)
+ {
+ DPRINTF(E_LOG, L_HTTPD, "Could not dereference %s: %s\n", path, strerror(errno));
+
+ httpd_send_error(req, HTTP_NOTFOUND, "Not Found");
+
+ return;
+ }
+
+ if (strlen(deref) + 1 > PATH_MAX)
+ {
+ DPRINTF(E_LOG, L_HTTPD, "Dereferenced path exceeds PATH_MAX: %s\n", path);
+
+ httpd_send_error(req, HTTP_NOTFOUND, "Not Found");
+
+ free(deref);
+ return;
+ }
+
+ strcpy(path, deref);
+ free(deref);
+
+ ret = stat(path, &sb);
+ if (ret < 0)
+ {
+ DPRINTF(E_LOG, L_HTTPD, "Could not stat() %s: %s\n", path, strerror(errno));
+
+ httpd_send_error(req, HTTP_NOTFOUND, "Not Found");
+
+ return;
+ }
+
+ if (S_ISDIR(sb.st_mode))
+ {
+ httpd_redirect_to_index(req, uri);
+
+ return;
+ }
+ }
+
+ if (path_is_legal(path) != 0)
+ {
+ httpd_send_error(req, 403, "Forbidden");
+
+ return;
+ }
+
+ tm_modified = gmtime(&sb.st_mtime);
+ strftime(last_modified, sizeof(last_modified), "%a, %d %b %Y %H:%M:%S %Z", tm_modified);
+
+ input_headers = evhttp_request_get_input_headers(req);
+ modified_since = evhttp_find_header(input_headers, "If-Modified-Since");
+
+ if (modified_since && strcasecmp(last_modified, modified_since) == 0)
+ {
+ 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");
+ return;
+ }
+
+ fd = open(path, O_RDONLY);
+ if (fd < 0)
+ {
+ DPRINTF(E_LOG, L_HTTPD, "Could not open %s: %s\n", path, strerror(errno));
+
+ httpd_send_error(req, HTTP_NOTFOUND, "Not Found");
+ evbuffer_free(evbuf);
+ return;
+ }
+
+ ret = evbuffer_expand(evbuf, 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(evbuf, buf, ret);
+
+ if (ret < 0)
+ {
+ DPRINTF(E_LOG, L_HTTPD, "Could not read file into evbuffer\n");
+ goto out_fail;
+ }
+
+ ctype = "application/octet-stream";
+ ext = strrchr(path, '.');
+ if (ext)
+ {
+ for (i = 0; ext2ctype[i].ext; i++)
+ {
+ if (strcmp(ext, ext2ctype[i].ext) == 0)
+ {
+ ctype = ext2ctype[i].ctype;
+ break;
+ }
+ }
+ }
+
+ output_headers = evhttp_request_get_output_headers(req);
+ evhttp_add_header(output_headers, "Content-Type", ctype);
+
+ // Allow browsers to cache the file
+ evhttp_add_header(output_headers, "Cache-Control", "private");
+ evhttp_add_header(output_headers, "Last-Modified", last_modified);
+
+ httpd_send_reply(req, HTTP_OK, "OK", evbuf, HTTPD_SEND_NO_GZIP);
+
+ evbuffer_free(evbuf);
+ close(fd);
+ return;
+
+ out_fail:
+ httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal error");
+ evbuffer_free(evbuf);
+ close(fd);
+}
+
+
+/* ---------------------------- STREAM HANDLING ----------------------------- */
static void
stream_end(struct stream_ctx *st, int failed)
@@ -190,81 +472,6 @@ stream_end(struct stream_ctx *st, int failed)
free(st);
}
-/* 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 void
-oauth_interface(struct evhttp_request *req, const char *uri)
-{
-#ifdef HAVE_SPOTIFY_H
- struct evkeyvalq query;
- const char *req_uri;
- const char *ptr;
- char redirect_uri[256];
- char *errmsg;
- int ret;
-
- req_uri = evhttp_request_get_uri(req);
-
- memset(&query, 0, sizeof(struct evkeyvalq));
-
- ptr = strchr(req_uri, '?');
- if (ptr)
- {
- ret = evhttp_parse_query_str(ptr + 1, &query);
- if (ret < 0)
- {
- DPRINTF(E_LOG, L_HTTPD, "OAuth error: Could not parse parameters in callback (%s)\n", req_uri);
- httpd_send_error(req, HTTP_BADREQUEST, "Could not parse parameters in callback");
- return;
- }
- }
-
-
- if (strncmp(uri, "/oauth/spotify", strlen("/oauth/spotify")) == 0)
- {
- snprintf(redirect_uri, sizeof(redirect_uri), "http://forked-daapd.local:%d/oauth/spotify", httpd_port);
- ret = spotify_oauth_callback(&query, redirect_uri, &errmsg);
- if (ret < 0)
- {
- DPRINTF(E_LOG, L_HTTPD, "OAuth error: Could not parse parameters in callback (%s)\n", req_uri);
- httpd_send_error(req, HTTP_INTERNAL, errmsg);
- }
- else
- {
- redirect_to_admin(req);
- }
- evhttp_clear_headers(&query);
- free(errmsg);
- }
- else
- {
- httpd_send_error(req, HTTP_NOTFOUND, NULL);
- }
-
-#else
- DPRINTF(E_LOG, L_HTTPD, "This version was built without modules requiring OAuth support\n");
- httpd_send_error(req, HTTP_NOTFOUND, "No modules with OAuth support");
-#endif
-}
-
static void
stream_end_register(struct stream_ctx *st)
{
@@ -454,6 +661,130 @@ stream_fail_cb(struct evhttp_connection *evcon, void *arg)
}
+/* ---------------------------- MAIN HTTPD THREAD --------------------------- */
+
+static void *
+httpd(void *arg)
+{
+ int ret;
+
+ ret = db_perthread_init();
+ if (ret < 0)
+ {
+ DPRINTF(E_LOG, L_HTTPD, "Error: DB init failed\n");
+
+ pthread_exit(NULL);
+ }
+
+ 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") )
+ {
+ output_headers = evhttp_request_get_output_headers(req);
+
+ evhttp_add_header(output_headers, "Access-Control-Allow-Origin", allow_origin);
+
+ // Allow only GET method and authorization header in cross origin requests
+ evhttp_add_header(output_headers, "Access-Control-Allow-Method", "GET");
+ 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);
+ return;
+ }
+
+ uri = evhttp_request_get_uri(req);
+ if (!uri)
+ {
+ DPRINTF(E_WARN, L_HTTPD, "No URI in request\n");
+ httpd_redirect_to_admin(req);
+ return;
+ }
+
+ parsed = httpd_uri_parse(uri);
+ if (!parsed || !parsed->path || (strcmp(parsed->path, "/") == 0))
+ {
+ httpd_redirect_to_admin(req);
+ goto out;
+ }
+
+ /* 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 (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(req, parsed->path);
+
+ out:
+ httpd_uri_free(parsed);
+}
+
+
+/* ------------------------------- HTTPD API -------------------------------- */
+
void
httpd_uri_free(struct httpd_uri_parsed *parsed)
{
@@ -546,7 +877,6 @@ httpd_uri_parse(const char *uri)
return NULL;
}
-
/* Thread: httpd */
void
httpd_stream_file(struct evhttp_request *req, int id)
@@ -996,16 +1326,8 @@ httpd_send_error(struct evhttp_request* req, int error, const char* reason)
evbuffer_free(evbuf);
}
-/* Thread: httpd */
-static int
-path_is_legal(char *path)
-{
- return strncmp(WEB_ROOT, path, strlen(WEB_ROOT));
-}
-
-/* Thread: httpd */
-static void
-redirect_to_admin(struct evhttp_request *req)
+void
+httpd_redirect_to_admin(struct evhttp_request *req)
{
struct evkeyvalq *headers;
@@ -1015,9 +1337,8 @@ redirect_to_admin(struct evhttp_request *req)
httpd_send_reply(req, HTTP_MOVETEMP, "Moved", NULL, HTTPD_SEND_NO_GZIP);
}
-/* Thread: httpd */
-static void
-redirect_to_index(struct evhttp_request *req, const char *uri)
+void
+httpd_redirect_to_index(struct evhttp_request *req, const char *uri)
{
struct evkeyvalq *headers;
char buf[256];
@@ -1078,395 +1399,6 @@ httpd_admin_check_auth(struct evhttp_request *req)
return true;
}
-/* Thread: httpd */
-static void
-serve_file(struct evhttp_request *req, const char *uri)
-{
- char *ext;
- char path[PATH_MAX];
- char *deref;
- char *ctype;
- struct evbuffer *evbuf;
- struct evkeyvalq *input_headers;
- struct evkeyvalq *output_headers;
- struct stat sb;
- int fd;
- int i;
- uint8_t buf[4096];
- const char *modified_since;
- char last_modified[1000];
- struct tm *tm_modified;
- int ret;
-
- /* Check authentication */
- if (!httpd_admin_check_auth(req))
- {
- DPRINTF(E_DBG, L_HTTPD, "Remote web interface request denied;\n");
- return;
- }
-
- if (strncmp(uri, "/oauth", strlen("/oauth")) == 0)
- {
- oauth_interface(req, uri);
- return;
- }
-
- ret = snprintf(path, sizeof(path), "%s%s", WEB_ROOT, uri);
- 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");
-
- return;
- }
-
- ret = lstat(path, &sb);
- if (ret < 0)
- {
- DPRINTF(E_LOG, L_HTTPD, "Could not lstat() %s: %s\n", path, strerror(errno));
-
- httpd_send_error(req, HTTP_NOTFOUND, "Not Found");
-
- return;
- }
-
- if (S_ISDIR(sb.st_mode))
- {
- redirect_to_index(req, uri);
-
- return;
- }
- else if (S_ISLNK(sb.st_mode))
- {
- deref = realpath(path, NULL);
- if (!deref)
- {
- DPRINTF(E_LOG, L_HTTPD, "Could not dereference %s: %s\n", path, strerror(errno));
-
- httpd_send_error(req, HTTP_NOTFOUND, "Not Found");
-
- return;
- }
-
- if (strlen(deref) + 1 > PATH_MAX)
- {
- DPRINTF(E_LOG, L_HTTPD, "Dereferenced path exceeds PATH_MAX: %s\n", path);
-
- httpd_send_error(req, HTTP_NOTFOUND, "Not Found");
-
- free(deref);
- return;
- }
-
- strcpy(path, deref);
- free(deref);
-
- ret = stat(path, &sb);
- if (ret < 0)
- {
- DPRINTF(E_LOG, L_HTTPD, "Could not stat() %s: %s\n", path, strerror(errno));
-
- httpd_send_error(req, HTTP_NOTFOUND, "Not Found");
-
- return;
- }
-
- if (S_ISDIR(sb.st_mode))
- {
- redirect_to_index(req, uri);
-
- return;
- }
- }
-
- if (path_is_legal(path) != 0)
- {
- httpd_send_error(req, 403, "Forbidden");
-
- return;
- }
-
- tm_modified = gmtime(&sb.st_mtime);
- strftime(last_modified, sizeof(last_modified), "%a, %d %b %Y %H:%M:%S %Z", tm_modified);
-
- input_headers = evhttp_request_get_input_headers(req);
- modified_since = evhttp_find_header(input_headers, "If-Modified-Since");
-
- if (modified_since && strcasecmp(last_modified, modified_since) == 0)
- {
- 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");
- return;
- }
-
- fd = open(path, O_RDONLY);
- if (fd < 0)
- {
- DPRINTF(E_LOG, L_HTTPD, "Could not open %s: %s\n", path, strerror(errno));
-
- httpd_send_error(req, HTTP_NOTFOUND, "Not Found");
- evbuffer_free(evbuf);
- return;
- }
-
- ret = evbuffer_expand(evbuf, 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(evbuf, buf, ret);
-
- if (ret < 0)
- {
- DPRINTF(E_LOG, L_HTTPD, "Could not read file into evbuffer\n");
- goto out_fail;
- }
-
- ctype = "application/octet-stream";
- ext = strrchr(path, '.');
- if (ext)
- {
- for (i = 0; ext2ctype[i].ext; i++)
- {
- if (strcmp(ext, ext2ctype[i].ext) == 0)
- {
- ctype = ext2ctype[i].ctype;
- break;
- }
- }
- }
-
- output_headers = evhttp_request_get_output_headers(req);
- evhttp_add_header(output_headers, "Content-Type", ctype);
-
- // Allow browsers to cache the file
- evhttp_add_header(output_headers, "Cache-Control", "private");
- evhttp_add_header(output_headers, "Last-Modified", last_modified);
-
- httpd_send_reply(req, HTTP_OK, "OK", evbuf, HTTPD_SEND_NO_GZIP);
-
- evbuffer_free(evbuf);
- close(fd);
- return;
-
- out_fail:
- httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal error");
- evbuffer_free(evbuf);
- close(fd);
-}
-
-/* Thread: httpd */
-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") )
- {
- output_headers = evhttp_request_get_output_headers(req);
-
- evhttp_add_header(output_headers, "Access-Control-Allow-Origin", allow_origin);
-
- // Allow only GET method and authorization header in cross origin requests
- evhttp_add_header(output_headers, "Access-Control-Allow-Method", "GET");
- 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);
- return;
- }
-
- uri = evhttp_request_get_uri(req);
- if (!uri)
- {
- DPRINTF(E_WARN, L_HTTPD, "No URI in request\n");
- redirect_to_admin(req);
- return;
- }
-
- parsed = httpd_uri_parse(uri);
- if (!parsed || !parsed->path || (strcmp(parsed->path, "/") == 0))
- {
- redirect_to_admin(req);
- goto out;
- }
-
- /* 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 (streaming_is_request(parsed->path))
- {
- streaming_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(req, parsed->path);
-
- out:
- httpd_uri_free(parsed);
-}
-
-/* Thread: httpd */
-static void *
-httpd(void *arg)
-{
- int ret;
-
- ret = db_perthread_init();
- if (ret < 0)
- {
- DPRINTF(E_LOG, L_HTTPD, "Error: DB init failed\n");
-
- pthread_exit(NULL);
- }
-
- 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);
-}
-
-/* Thread: httpd */
-static void
-exit_cb(int fd, short event, void *arg)
-{
- event_base_loopbreak(evbase_httpd);
-
- httpd_exit = 1;
-}
-
-/*static char *
-httpd_fixup_uri(struct evhttp_request *req)
-{
- struct evkeyvalq *headers;
- const char *ua;
- const char *uri;
- const char *u;
- const char *q;
- char *fixed;
- char *f;
- int len;
-
- 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)
- {
- if (*u == '+')
- len += 2;
-
- u++;
- }
-
- fixed = (char *)malloc(len + 1);
- if (!fixed)
- return NULL;
-
- strncpy(fixed, uri, q - uri);
-
- f = fixed + (q - uri);
- while (*q)
- {
- switch (*q)
- {
- case '+':
- *f = '%';
- f++;
- *f = '2';
- f++;
- *f = 'B';
- break;
-
- case ' ':
- *f = '+';
- break;
-
- default:
- *f = *q;
- break;
- }
-
- q++;
- f++;
- }
-
- *f = '\0';
-
- return fixed;
-}*/
-
-static const char *http_reply_401 = "401 UnauthorizedAuthorization required";
-
int
httpd_basic_auth(struct evhttp_request *req, const char *user, const char *passwd, const char *realm)
{
@@ -1615,6 +1547,14 @@ httpd_init(void)
goto jsonapi_fail;
}
+ ret = oauth_init();
+ if (ret < 0)
+ {
+ DPRINTF(E_FATAL, L_HTTPD, "OAuth init failed\n");
+
+ goto oauth_fail;
+ }
+
#ifdef HAVE_LIBWEBSOCKETS
ret = websocket_init();
if (ret < 0)
@@ -1740,6 +1680,8 @@ httpd_init(void)
websocket_deinit();
websocket_fail:
#endif
+ oauth_deinit();
+ oauth_fail:
jsonapi_deinit();
jsonapi_fail:
dacp_deinit();
@@ -1791,6 +1733,7 @@ httpd_deinit(void)
#ifdef HAVE_LIBWEBSOCKETS
websocket_deinit();
#endif
+ oauth_deinit();
jsonapi_deinit();
rsp_deinit();
dacp_deinit();
diff --git a/src/httpd.h b/src/httpd.h
index 3bce64b9..c666efeb 100644
--- a/src/httpd.h
+++ b/src/httpd.h
@@ -91,6 +91,18 @@ httpd_send_reply(struct evhttp_request *req, int code, const char *reason, struc
void
httpd_send_error(struct evhttp_request *req, int error, const char *reason);
+/*
+ * Redirects to /admin.html
+ */
+void
+httpd_redirect_to_admin(struct evhttp_request *req);
+
+/*
+ * Redirects to [uri]/index.html
+ */
+void
+httpd_redirect_to_index(struct evhttp_request *req, const char *uri);
+
int
httpd_basic_auth(struct evhttp_request *req, const char *user, const char *passwd, const char *realm);
diff --git a/src/httpd_daap.c b/src/httpd_daap.c
index 09f029ed..5ae42de2 100644
--- a/src/httpd_daap.c
+++ b/src/httpd_daap.c
@@ -1630,7 +1630,7 @@ daap_reply_playlists(struct evbuffer *reply, struct daap_request *dreq)
if (plid == 1)
dmap_add_char(playlist, "abpl", 1);
- DPRINTF(E_DBG, L_DAAP, "Done with playlist\n");
+ DPRINTF(E_SPAM, L_DAAP, "Done with playlist\n");
len = evbuffer_get_length(playlist);
dmap_add_container(playlistlist, "mlit", len);
diff --git a/src/httpd_oauth.c b/src/httpd_oauth.c
new file mode 100644
index 00000000..931694ad
--- /dev/null
+++ b/src/httpd_oauth.c
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2017 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
+#include
+#include
+#include
+#include
+
+#include "httpd_oauth.h"
+#include "logger.h"
+#include "misc.h"
+#include "conffile.h"
+#ifdef HAVE_SPOTIFY_H
+# include "spotify.h"
+#endif
+
+struct oauth_request {
+ // The parsed request URI given to us by httpd.c
+ struct httpd_uri_parsed *uri_parsed;
+ // Shortcut to &uri_parsed->ev_query
+ struct evkeyvalq *query;
+ // http request struct
+ struct evhttp_request *req;
+ // A pointer to the handler that will process the request
+ void (*handler)(struct oauth_request *oreq);
+};
+
+struct uri_map {
+ regex_t preg;
+ char *regexp;
+ void (*handler)(struct oauth_request *oreq);
+};
+
+/* Forward declaration of handlers */
+static struct uri_map oauth_handlers[];
+
+
+/* ------------------------------- HELPERS ---------------------------------- */
+
+static struct oauth_request *
+oauth_request_parse(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed)
+{
+ struct oauth_request *oreq;
+ int i;
+ int ret;
+
+ CHECK_NULL(L_WEB, oreq = calloc(1, sizeof(struct oauth_request)));
+
+ oreq->req = req;
+ oreq->uri_parsed = uri_parsed;
+ oreq->query = &(uri_parsed->ev_query);
+
+ for (i = 0; oauth_handlers[i].handler; i++)
+ {
+ ret = regexec(&oauth_handlers[i].preg, uri_parsed->path, 0, NULL, 0);
+ if (ret == 0)
+ {
+ oreq->handler = oauth_handlers[i].handler;
+ break;
+ }
+ }
+
+ if (!oreq->handler)
+ {
+ DPRINTF(E_LOG, L_WEB, "Unrecognized path '%s' in OAuth request: '%s'\n", uri_parsed->path, uri_parsed->uri);
+ goto error;
+ }
+
+ return oreq;
+
+ error:
+ free(oreq);
+
+ return NULL;
+}
+
+
+/* --------------------------- REPLY HANDLERS ------------------------------- */
+
+#ifdef HAVE_SPOTIFY_H
+static void
+oauth_reply_spotify(struct oauth_request *oreq)
+{
+ char redirect_uri[256];
+ char *errmsg;
+ int httpd_port;
+ int ret;
+
+ httpd_port = cfg_getint(cfg_getsec(cfg, "library"), "port");
+
+ snprintf(redirect_uri, sizeof(redirect_uri), "http://forked-daapd.local:%d/oauth/spotify", httpd_port);
+ ret = spotify_oauth_callback(oreq->query, redirect_uri, &errmsg);
+ if (ret < 0)
+ {
+ DPRINTF(E_LOG, L_WEB, "Could not parse Spotify OAuth callback: '%s'\n", oreq->uri_parsed->uri);
+ httpd_send_error(oreq->req, HTTP_INTERNAL, errmsg);
+ free(errmsg);
+ return;
+ }
+
+ httpd_redirect_to_admin(oreq->req);
+}
+#else
+static void
+oauth_reply_spotify(struct oauth_request *oreq)
+{
+ DPRINTF(E_LOG, L_HTTPD, "This version of forked-daapd was built without support for Spotify\n");
+
+ httpd_send_error(oreq->req, HTTP_NOTFOUND, "This version of forked-daapd was built without support for Spotify");
+}
+#endif
+
+static struct uri_map oauth_handlers[] =
+ {
+ {
+ .regexp = "^/oauth/spotify$",
+ .handler = oauth_reply_spotify
+ },
+ {
+ .regexp = NULL,
+ .handler = NULL
+ }
+ };
+
+
+/* ------------------------------- OAUTH API -------------------------------- */
+
+void
+oauth_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed)
+{
+ struct oauth_request *oreq;
+
+ DPRINTF(E_LOG, L_WEB, "OAuth request: '%s'\n", uri_parsed->uri);
+
+ oreq = oauth_request_parse(req, uri_parsed);
+ if (!oreq)
+ {
+ httpd_send_error(req, HTTP_NOTFOUND, NULL);
+ return;
+ }
+
+ oreq->handler(oreq);
+
+ free(oreq);
+}
+
+int
+oauth_is_request(const char *path)
+{
+ 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);
+}
diff --git a/src/httpd_oauth.h b/src/httpd_oauth.h
new file mode 100644
index 00000000..71ff1495
--- /dev/null
+++ b/src/httpd_oauth.h
@@ -0,0 +1,18 @@
+#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__ */