diff --git a/src/Makefile.am b/src/Makefile.am index 45b1d8ac..1dd15f2c 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -108,6 +108,7 @@ forked_daapd_SOURCES = main.c \ httpd_rsp.c httpd_rsp.h \ httpd_daap.c httpd_daap.h \ httpd_dacp.c httpd_dacp.h \ + httpd_jsonapi.c httpd_jsonapi.h \ httpd_streaming.c httpd_streaming.h \ http.c http.h \ dmap_common.c dmap_common.h \ diff --git a/src/httpd.c b/src/httpd.c index 2db284d4..178dc941 100644 --- a/src/httpd.c +++ b/src/httpd.c @@ -59,6 +59,7 @@ #include "httpd_rsp.h" #include "httpd_daap.h" #include "httpd_dacp.h" +#include "httpd_jsonapi.h" #include "httpd_streaming.h" #include "transcode.h" #ifdef LASTFM @@ -1221,6 +1222,12 @@ httpd_gen_cb(struct evhttp_request *req, void *arg) { dacp_request(req); + goto out; + } + else if (jsonapi_is_request(req, uri)) + { + jsonapi_request(req); + goto out; } else if (streaming_is_request(req, uri)) @@ -1495,6 +1502,14 @@ httpd_init(void) goto dacp_fail; } + ret = jsonapi_init(); + if (ret < 0) + { + DPRINTF(E_FATAL, L_HTTPD, "JSON api init failed\n"); + + goto jsonapi_fail; + } + streaming_init(); #ifdef HAVE_EVENTFD @@ -1601,6 +1616,8 @@ httpd_init(void) #endif pipe_fail: streaming_deinit(); + jsonapi_deinit(); + jsonapi_fail: dacp_deinit(); dacp_fail: daap_deinit(); @@ -1647,6 +1664,7 @@ httpd_deinit(void) } streaming_deinit(); + jsonapi_deinit(); rsp_deinit(); dacp_deinit(); daap_deinit(); diff --git a/src/httpd_jsonapi.c b/src/httpd_jsonapi.c new file mode 100644 index 00000000..cbf22044 --- /dev/null +++ b/src/httpd_jsonapi.c @@ -0,0 +1,577 @@ +/* + * Copyright (C) 2017 Christian Meffert + * + * Adapted from httpd_adm.c: + * Copyright (C) 2015 Stuart NAIFEH + * + * Adapted from httpd_daap.c and httpd.c: + * Copyright (C) 2009-2011 Julien BLACHE + * Copyright (C) 2010 Kai Elwert + * + * Adapted from mt-daapd: + * Copyright (C) 2003-2007 Ron Pedde + * + * 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 + */ + +#include "httpd_jsonapi.h" + +#ifdef HAVE_CONFIG_H +# include +#endif +#include +#include +#include +#include +#include +#include + +#include "conffile.h" +#include "db.h" +#include "httpd.h" +#include "library.h" +#include "logger.h" +#include "misc_json.h" +#include "remote_pairing.h" +#ifdef HAVE_SPOTIFY_H +# include "spotify_webapi.h" +# include "spotify.h" +#endif + +struct uri_map +{ + regex_t preg; + char *regexp; + int (*handler)(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query); +}; + + + +/* + * Endpoint to retrieve configuration values + * + * Example response: + * + * { + * "websocket_port": 6603, + * "version": "25.0" + * } + */ +static int +jsonapi_reply_config(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query) +{ + json_object *reply; + json_object *buildopts; + int websocket_port; + char **buildoptions; + int i; + int ret; + + reply = json_object_new_object(); + + // Websocket port +#ifdef WEBSOCKET + websocket_port = cfg_getint(cfg_getsec(cfg, "general"), "websocket_port"); +#else + websocket_port = 0; +#endif + json_object_object_add(reply, "websocket_port", json_object_new_int(websocket_port)); + + // forked-daapd version + json_object_object_add(reply, "version", json_object_new_string(VERSION)); + + // enabled build options + buildopts = json_object_new_array(); + buildoptions = buildopts_get(); + for (i = 0; buildoptions[i]; i++) + { + json_object_array_add(buildopts, json_object_new_string(buildoptions[i])); + } + json_object_object_add(reply, "buildoptions", buildopts); + + ret = evbuffer_add_printf(evbuf, "%s", json_object_to_json_string(reply)); + jparse_free(reply); + if (ret < 0) + { + DPRINTF(E_LOG, L_WEB, "config: Couldn't add config data to response buffer.\n"); + return -1; + } + + return 0; +} + +/* + * Endpoint to retrieve informations about the library + * + * Example response: + * + * { + * "artists": 84, + * "albums": 151, + * "songs": 3085, + * "db_playtime": 687824, + * "updating": false + *} + */ +static int +jsonapi_reply_library(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query) +{ + struct query_params qp; + struct filecount_info fci; + int artists; + int albums; + bool is_scanning; + json_object *reply; + int ret; + + // Fetch values for response + + memset(&qp, 0, sizeof(struct query_params)); + qp.type = Q_COUNT_ITEMS; + ret = db_filecount_get(&fci, &qp); + if (ret < 0) + { + DPRINTF(E_LOG, L_WEB, "library: failed to get file count info\n"); + return -1; + } + + artists = db_files_get_artist_count(); + albums = db_files_get_album_count(); + + is_scanning = library_is_scanning(); + + + // Build json response + + reply = json_object_new_object(); + + json_object_object_add(reply, "artists", json_object_new_int(artists)); + json_object_object_add(reply, "albums", json_object_new_int(albums)); + json_object_object_add(reply, "songs", json_object_new_int(fci.count)); + json_object_object_add(reply, "db_playtime", json_object_new_int64((fci.length / 1000))); + json_object_object_add(reply, "updating", json_object_new_boolean(is_scanning)); + + ret = evbuffer_add_printf(evbuf, "%s", json_object_to_json_string(reply)); + jparse_free(reply); + if (ret < 0) + { + DPRINTF(E_LOG, L_WEB, "library: Couldn't add library information data to response buffer.\n"); + return -1; + } + + return 0; +} + +/* + * Endpoint to trigger a library rescan + */ +static int +jsonapi_reply_update(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query) +{ + library_rescan(); + return 0; +} + +/* + * Endpoint to retrieve information about the spotify integration + * + * Exampe response: + * + * { + * "enabled": true, + * "oauth_uri": "https://accounts.spotify.com/authorize/?client_id=... + * } + */ +static int +jsonapi_reply_spotify(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query) +{ + int httpd_port; + char __attribute__((unused)) redirect_uri[256]; + char *oauth_uri; + json_object *reply; + int ret; + + reply = json_object_new_object(); + +#ifdef HAVE_SPOTIFY_H + struct spotify_status_info info; + + json_object_object_add(reply, "enabled", json_object_new_boolean(true)); + + httpd_port = cfg_getint(cfg_getsec(cfg, "library"), "port"); + snprintf(redirect_uri, sizeof(redirect_uri), "http://forked-daapd.local:%d/oauth/spotify", httpd_port); + + oauth_uri = spotifywebapi_oauth_uri_get(redirect_uri); + if (!uri) + { + DPRINTF(E_LOG, L_WEB, "Cannot display Spotify oauth interface (http_form_uriencode() failed)\n"); + } + else + { + json_object_object_add(reply, "oauth_uri", json_object_new_string(oauth_uri)); + free(oauth_uri); + } + + spotify_status_info_get(&info); + json_object_object_add(reply, "libspotify_installed", json_object_new_boolean(info.libspotify_installed)); + json_object_object_add(reply, "libspotify_logged_in", json_object_new_boolean(info.libspotify_logged_in)); + json_object_object_add(reply, "libspotify_user", json_object_new_string(info.libspotify_user)); + json_object_object_add(reply, "webapi_token_valid", json_object_new_boolean(info.webapi_token_valid)); + json_object_object_add(reply, "webapi_user", json_object_new_string(info.webapi_user)); + +#else + json_object_object_add(reply, "enabled", json_object_new_boolean(false)); +#endif + + ret = evbuffer_add_printf(evbuf, "%s", json_object_to_json_string(reply)); + jparse_free(reply); + if (ret < 0) + { + DPRINTF(E_LOG, L_WEB, "spotify: Couldn't add spotify information data to response buffer.\n"); + return -1; + } + + return 0; +} + +static int +jsonapi_reply_spotify_login(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query) +{ + struct evbuffer *in_evbuf; + json_object* request; + const char *user; + const char *password; + char *errmsg = NULL; + json_object* reply; + json_object* errors; + int ret; + + DPRINTF(E_DBG, L_WEB, "Received spotify login request\n"); + +#ifdef HAVE_SPOTIFY_H + in_evbuf = evhttp_request_get_input_buffer(req); + request = jparse_obj_from_evbuffer(in_evbuf); + if (!request) + { + DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n"); + return -1; + } + + reply = json_object_new_object(); + + user = jparse_str_from_obj(request, "user"); + password = jparse_str_from_obj(request, "password"); + if (user && strlen(user) > 0 && password && strlen(password) > 0) + { + ret = spotify_login_user(user, password, &errmsg); + if (ret < 0) + { + json_object_object_add(reply, "success", json_object_new_boolean(false)); + errors = json_object_new_object(); + json_object_object_add(errors, "error", json_object_new_string(errmsg)); + json_object_object_add(reply, "errors", errors); + } + else + { + json_object_object_add(reply, "success", json_object_new_boolean(true)); + } + free(errmsg); + } + else + { + DPRINTF(E_LOG, L_WEB, "No user or password in spotify login post request\n"); + + json_object_object_add(reply, "success", json_object_new_boolean(false)); + errors = json_object_new_object(); + if (!user || strlen(user) == 0) + json_object_object_add(errors, "user", json_object_new_string("Username is required")); + if (!password || strlen(password) == 0) + json_object_object_add(errors, "password", json_object_new_string("Password is required")); + json_object_object_add(reply, "errors", errors); + } + + ret = evbuffer_add_printf(evbuf, "%s", json_object_to_json_string(reply)); + jparse_free(reply); + if (ret < 0) + { + DPRINTF(E_LOG, L_WEB, "spotify: Couldn't add spotify login data to response buffer.\n"); + return -1; + } + +#else + DPRINTF(E_LOG, L_WEB, "Received spotify login request but was not compiled with enable-spotify\n"); +#endif + + return 0; +} + +/* + * Endpoint to kickoff pairing of a daap/dacp client + * + * Expects the paring pin to be present in the post request body, e. g.: + * + * { + * "pin": "1234" + * } + */ +static int +pairing_kickoff(struct evhttp_request* req) +{ + struct evbuffer *evbuf; + json_object* request; + const char* message; + + evbuf = evhttp_request_get_input_buffer(req); + request = jparse_obj_from_evbuffer(evbuf); + if (!request) + { + DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n"); + return -1; + } + + DPRINTF(E_DBG, L_WEB, "Received pairing post request: %s\n", json_object_to_json_string(request)); + + message = jparse_str_from_obj(request, "pin"); + if (message) + remote_pairing_kickoff((char **)&message); + else + DPRINTF(E_LOG, L_WEB, "Missing pin in request body: %s\n", json_object_to_json_string(request)); + + jparse_free(request); + + return 0; +} + +/* + * Endpoint to retrieve pairing information + * + * Example response: + * + * { + * "active": true, + * "remote": "remote name" + * } + */ +static int +pairing_get(struct evbuffer *evbuf) +{ + char *remote_name; + json_object *reply; + int ret; + + remote_name = remote_pairing_get_name(); + + reply = json_object_new_object(); + + if (remote_name) + { + json_object_object_add(reply, "active", json_object_new_boolean(true)); + json_object_object_add(reply, "remote", json_object_new_string(remote_name)); + } + else + { + json_object_object_add(reply, "active", json_object_new_boolean(false)); + } + + ret = evbuffer_add_printf(evbuf, "%s", json_object_to_json_string(reply)); + jparse_free(reply); + free(remote_name); + + if (ret < 0) + { + DPRINTF(E_LOG, L_WEB, "pairing: Couldn't add pairing information data to response buffer.\n"); + return -1; + } + + return 0; +} + +/* + * Endpoint to pair daap/dacp client + * + * If request is a GET request, returns information about active pairing remote. + * If request is a POST request, tries to pair the active remote with the given pin. + */ +static int +jsonapi_reply_pairing(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query) +{ + if (evhttp_request_get_command(req) == EVHTTP_REQ_POST) + { + return pairing_kickoff(req); + } + + return pairing_get(evbuf); +} + +static struct uri_map adm_handlers[] = + { + { .regexp = "^/api/config", .handler = jsonapi_reply_config }, + { .regexp = "^/api/library", .handler = jsonapi_reply_library }, + { .regexp = "^/api/update", .handler = jsonapi_reply_update }, + { .regexp = "^/api/spotify-login", .handler = jsonapi_reply_spotify_login }, + { .regexp = "^/api/spotify", .handler = jsonapi_reply_spotify }, + { .regexp = "^/api/pairing", .handler = jsonapi_reply_pairing }, + { .regexp = NULL, .handler = NULL } + }; + +void +jsonapi_request(struct evhttp_request *req) +{ + char *full_uri; + char *uri; + char *ptr; + struct evbuffer *evbuf; + struct evkeyvalq query; + struct evkeyvalq *headers; + int handler; + int ret; + int i; + + /* Check authentication */ + if (!httpd_admin_check_auth(req)) + { + DPRINTF(E_DBG, L_WEB, "JSON api request denied;\n"); + return; + } + + memset(&query, 0, sizeof(struct evkeyvalq)); + + full_uri = httpd_fixup_uri(req); + if (!full_uri) + { + evhttp_send_error(req, HTTP_BADREQUEST, "Bad Request"); + return; + } + + ptr = strchr(full_uri, '?'); + if (ptr) + *ptr = '\0'; + + uri = strdup(full_uri); + if (!uri) + { + free(full_uri); + evhttp_send_error(req, HTTP_BADREQUEST, "Bad Request"); + return; + } + + if (ptr) + *ptr = '?'; + + ptr = uri; + uri = evhttp_decode_uri(uri); + free(ptr); + + DPRINTF(E_DBG, L_WEB, "Web admin request: %s\n", full_uri); + + handler = -1; + for (i = 0; adm_handlers[i].handler; i++) + { + ret = regexec(&adm_handlers[i].preg, uri, 0, NULL, 0); + if (ret == 0) + { + handler = i; + break; + } + } + + if (handler < 0) + { + DPRINTF(E_LOG, L_WEB, "Unrecognized web admin request\n"); + + evhttp_send_error(req, HTTP_BADREQUEST, "Bad Request"); + + free(uri); + free(full_uri); + return; + } + + evbuf = evbuffer_new(); + if (!evbuf) + { + DPRINTF(E_LOG, L_WEB, "Could not allocate evbuffer for Web Admin reply\n"); + + evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); + + free(uri); + free(full_uri); + return; + } + + evhttp_parse_query(full_uri, &query); + + headers = evhttp_request_get_output_headers(req); + evhttp_add_header(headers, "DAAP-Server", "forked-daapd/" VERSION); + + ret = adm_handlers[handler].handler(req, evbuf, uri, &query); + if (ret < 0) + { + evhttp_send_error(req, 500, "Internal Server Error"); + } + else + { + headers = evhttp_request_get_output_headers(req); + evhttp_add_header(headers, "Content-Type", "application/json"); + httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); + } + + evbuffer_free(evbuf); + evhttp_clear_headers(&query); + free(uri); + free(full_uri); +} + +int +jsonapi_is_request(struct evhttp_request *req, char *uri) +{ + if (strncmp(uri, "/api/", strlen("/api/")) == 0) + return 1; + if (strcmp(uri, "/api") == 0) + return 1; + + return 0; +} + +int +jsonapi_init(void) +{ + char buf[64]; + int i; + int ret; + + for (i = 0; adm_handlers[i].handler; i++) + { + ret = regcomp(&adm_handlers[i].preg, adm_handlers[i].regexp, REG_EXTENDED | REG_NOSUB); + if (ret != 0) + { + regerror(ret, &adm_handlers[i].preg, buf, sizeof(buf)); + + DPRINTF(E_FATAL, L_WEB, "Admin web interface init failed; regexp error: %s\n", buf); + return -1; + } + } + + return 0; +} + +void +jsonapi_deinit(void) +{ + int i; + + for (i = 0; adm_handlers[i].handler; i++) + regfree(&adm_handlers[i].preg); +} + diff --git a/src/httpd_jsonapi.h b/src/httpd_jsonapi.h new file mode 100644 index 00000000..2a305343 --- /dev/null +++ b/src/httpd_jsonapi.h @@ -0,0 +1,19 @@ + +#ifndef __HTTPD_JSONAPI_H__ +#define __HTTPD_JSONAPI_H__ + +#include + +int +jsonapi_init(void); + +void +jsonapi_deinit(void); + +void +jsonapi_request(struct evhttp_request *req); + +int +jsonapi_is_request(struct evhttp_request *req, char *uri); + +#endif /* !__HTTPD_JSONAPI_H__ */