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__ */