From d4b05e98ae16cf8eb2f912fd65fdf07a58f8225f Mon Sep 17 00:00:00 2001 From: chme Date: Fri, 9 Feb 2018 18:26:37 +0100 Subject: [PATCH 1/8] [httpd] Add PUT and DELETE to the allowed http request methods --- src/httpd.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/httpd.c b/src/httpd.c index a0b28c6b..3a3852e8 100644 --- a/src/httpd.c +++ b/src/httpd.c @@ -1687,7 +1687,7 @@ httpd_init(const char *webroot) if (allow_origin) { if (strlen(allow_origin) != 0) - evhttp_set_allowed_methods(evhttpd, EVHTTP_REQ_GET | EVHTTP_REQ_POST | EVHTTP_REQ_HEAD | EVHTTP_REQ_OPTIONS); + evhttp_set_allowed_methods(evhttpd, EVHTTP_REQ_GET | EVHTTP_REQ_POST | EVHTTP_REQ_PUT | EVHTTP_REQ_DELETE | EVHTTP_REQ_HEAD | EVHTTP_REQ_OPTIONS); else allow_origin = NULL; } From a98713ba49e36f345310a219aa65ad8278802b7f Mon Sep 17 00:00:00 2001 From: chme Date: Fri, 9 Feb 2018 18:30:31 +0100 Subject: [PATCH 2/8] [jsonapi] Return proper HTTP status codes --- src/httpd_jsonapi.c | 132 +++++++++++++++++++++++++------------------- 1 file changed, 75 insertions(+), 57 deletions(-) diff --git a/src/httpd_jsonapi.c b/src/httpd_jsonapi.c index f98830b7..2d418b63 100644 --- a/src/httpd_jsonapi.c +++ b/src/httpd_jsonapi.c @@ -75,7 +75,7 @@ pairing_kickoff(struct evhttp_request *req) if (!request) { DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n"); - return -1; + return HTTP_BADREQUEST; } DPRINTF(E_DBG, L_WEB, "Received pairing post request: %s\n", json_object_to_json_string(request)); @@ -88,7 +88,7 @@ pairing_kickoff(struct evhttp_request *req) jparse_free(request); - return 0; + return HTTP_NOCONTENT; } /* @@ -126,7 +126,7 @@ pairing_get(struct evbuffer *evbuf) jparse_free(jreply); free(remote_name); - return 0; + return HTTP_OK; } @@ -177,7 +177,7 @@ jsonapi_reply_config(struct httpd_request *hreq) jparse_free(jreply); - return 0; + return HTTP_OK; } /* @@ -212,7 +212,7 @@ jsonapi_reply_library(struct httpd_request *hreq) if (ret < 0) { DPRINTF(E_LOG, L_WEB, "library: failed to get file count info\n"); - return -1; + return HTTP_INTERNAL; } artists = db_files_get_artist_count(); @@ -233,7 +233,7 @@ jsonapi_reply_library(struct httpd_request *hreq) jparse_free(jreply); - return 0; + return HTTP_OK; } /* @@ -243,7 +243,7 @@ static int jsonapi_reply_update(struct httpd_request *hreq) { library_rescan(); - return 0; + return HTTP_NOCONTENT; } /* @@ -279,7 +279,7 @@ jsonapi_reply_spotify(struct httpd_request *hreq) { DPRINTF(E_LOG, L_WEB, "Cannot display Spotify oauth interface (http_form_uriencode() failed)\n"); jparse_free(jreply); - return -1; + return HTTP_INTERNAL; } json_object_object_add(jreply, "oauth_uri", json_object_new_string(oauth_uri)); @@ -300,7 +300,7 @@ jsonapi_reply_spotify(struct httpd_request *hreq) jparse_free(jreply); - return 0; + return HTTP_OK; } static int @@ -324,7 +324,7 @@ jsonapi_reply_spotify_login(struct httpd_request *hreq) if (!request) { DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n"); - return -1; + return HTTP_BADREQUEST; } CHECK_NULL(L_WEB, jreply = json_object_new_object()); @@ -368,7 +368,7 @@ jsonapi_reply_spotify_login(struct httpd_request *hreq) DPRINTF(E_LOG, L_WEB, "Received spotify login request but was not compiled with enable-spotify\n"); #endif - return 0; + return HTTP_OK; } /* @@ -409,7 +409,7 @@ jsonapi_reply_lastfm(struct httpd_request *hreq) jparse_free(jreply); - return 0; + return HTTP_OK; } /* @@ -435,7 +435,7 @@ jsonapi_reply_lastfm_login(struct httpd_request *hreq) if (!request) { DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n"); - return -1; + return HTTP_BADREQUEST; } CHECK_NULL(L_WEB, jreply = json_object_new_object()); @@ -482,7 +482,7 @@ jsonapi_reply_lastfm_login(struct httpd_request *hreq) DPRINTF(E_LOG, L_WEB, "Received LastFM login request but was not compiled with enable-lastfm\n"); #endif - return 0; + return HTTP_OK; } static int @@ -491,7 +491,7 @@ jsonapi_reply_lastfm_logout(struct httpd_request *hreq) #ifdef LASTFM lastfm_logout(); #endif - return 0; + return HTTP_NOCONTENT; } static void @@ -534,7 +534,7 @@ jsonapi_reply_outputs(struct httpd_request *hreq) jparse_free(jreply); - return 0; + return HTTP_OK; } static int @@ -547,7 +547,7 @@ jsonapi_reply_verification(struct httpd_request *hreq) if (evhttp_request_get_command(hreq->req) != EVHTTP_REQ_POST) { DPRINTF(E_LOG, L_WEB, "Verification: request is not a POST request\n"); - return -1; + return HTTP_BADREQUEST; } in_evbuf = evhttp_request_get_input_buffer(hreq->req); @@ -555,7 +555,7 @@ jsonapi_reply_verification(struct httpd_request *hreq) if (!request) { DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n"); - return -1; + return HTTP_BADREQUEST; } DPRINTF(E_DBG, L_WEB, "Received verification post request: %s\n", json_object_to_json_string(request)); @@ -568,7 +568,7 @@ jsonapi_reply_verification(struct httpd_request *hreq) jparse_free(request); - return 0; + return HTTP_NOCONTENT; } static int @@ -584,7 +584,7 @@ jsonapi_reply_select_outputs(struct httpd_request *hreq) if (evhttp_request_get_command(hreq->req) != EVHTTP_REQ_POST) { DPRINTF(E_LOG, L_WEB, "Select outputs: request is not a POST request\n"); - return -1; + return HTTP_BADREQUEST; } in_evbuf = evhttp_request_get_input_buffer(hreq->req); @@ -592,7 +592,7 @@ jsonapi_reply_select_outputs(struct httpd_request *hreq) if (!request) { DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n"); - return -1; + return HTTP_BADREQUEST; } DPRINTF(E_DBG, L_WEB, "Received select-outputs post request: %s\n", json_object_to_json_string(request)); @@ -627,7 +627,7 @@ jsonapi_reply_select_outputs(struct httpd_request *hreq) jparse_free(request); - return 0; + return HTTP_NOCONTENT; } static int @@ -639,10 +639,10 @@ jsonapi_reply_player_play(struct httpd_request *hreq) if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Error starting playback.\n"); - return -1; + return HTTP_INTERNAL; } - return 0; + return HTTP_NOCONTENT; } static int @@ -654,10 +654,10 @@ jsonapi_reply_player_pause(struct httpd_request *hreq) if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Error pausing playback.\n"); - return -1; + return HTTP_INTERNAL; } - return 0; + return HTTP_NOCONTENT; } static int @@ -669,10 +669,10 @@ jsonapi_reply_player_stop(struct httpd_request *hreq) if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Error stopping playback.\n"); - return -1; + return HTTP_INTERNAL; } - return 0; + return HTTP_NOCONTENT; } static int @@ -684,17 +684,17 @@ jsonapi_reply_player_next(struct httpd_request *hreq) if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Error switching to next item.\n"); - return -1; + return HTTP_INTERNAL; } ret = player_playback_start(); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Error starting playback after switching to next item.\n"); - return -1; + return HTTP_INTERNAL; } - return 0; + return HTTP_NOCONTENT; } static int @@ -706,17 +706,17 @@ jsonapi_reply_player_previous(struct httpd_request *hreq) if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Error switching to previous item.\n"); - return -1; + return HTTP_INTERNAL; } ret = player_playback_start(); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Error starting playback after switching to previous item.\n"); - return -1; + return HTTP_INTERNAL; } - return 0; + return HTTP_NOCONTENT; } static int @@ -771,7 +771,7 @@ jsonapi_reply_player(struct httpd_request *hreq) jparse_free(reply); - return 0; + return HTTP_OK; } static json_object * @@ -882,7 +882,10 @@ jsonapi_reply_queue(struct httpd_request *hreq) jparse_free(reply); free(query_params.filter); - return ret; + if (ret < 0) + return HTTP_INTERNAL; + + return HTTP_OK; } static int @@ -892,7 +895,7 @@ jsonapi_reply_player_repeat(struct httpd_request *hreq) param = evhttp_find_header(hreq->query, "state"); if (!param) - return -1; + return HTTP_BADREQUEST; if (strcmp(param, "single") == 0) { @@ -907,7 +910,7 @@ jsonapi_reply_player_repeat(struct httpd_request *hreq) player_repeat_set(REPEAT_OFF); } - return 0; + return HTTP_NOCONTENT; } static int @@ -918,12 +921,12 @@ jsonapi_reply_player_shuffle(struct httpd_request *hreq) param = evhttp_find_header(hreq->query, "state"); if (!param) - return -1; + return HTTP_BADREQUEST; shuffle = (strcmp(param, "true") == 0); player_shuffle_set(shuffle); - return 0; + return HTTP_NOCONTENT; } static int @@ -934,12 +937,12 @@ jsonapi_reply_player_consume(struct httpd_request *hreq) param = evhttp_find_header(hreq->query, "state"); if (!param) - return -1; + return HTTP_BADREQUEST; consume = (strcmp(param, "true") == 0); player_consume_set(consume); - return 0; + return HTTP_NOCONTENT; } static int @@ -952,21 +955,21 @@ jsonapi_reply_player_volume(struct httpd_request *hreq) param = evhttp_find_header(hreq->query, "volume"); if (!param) - return -1; + return HTTP_BADREQUEST; ret = safe_atoi32(param, &volume); if (ret < 0) - return -1; + return HTTP_BADREQUEST; if (volume < 0 || volume > 100) - return -1; + return HTTP_BADREQUEST; param = evhttp_find_header(hreq->query, "output_id"); if (param) { ret = safe_atou64(param, &output_id); if (ret < 0) - return -1; + return HTTP_BADREQUEST; ret = player_volume_setabs_speaker(output_id, volume); } @@ -975,7 +978,10 @@ jsonapi_reply_player_volume(struct httpd_request *hreq) ret = player_volume_set(volume); } - return ret; + if (ret < 0) + return HTTP_INTERNAL; + + return HTTP_NOCONTENT; } static struct httpd_uri_map adm_handlers[] = @@ -1014,7 +1020,7 @@ jsonapi_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed) { struct httpd_request *hreq; struct evkeyvalq *headers; - int ret; + int status_code; DPRINTF(E_DBG, L_WEB, "JSON api request: '%s'\n", uri_parsed->uri); @@ -1035,18 +1041,30 @@ jsonapi_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed) CHECK_NULL(L_WEB, hreq->reply = evbuffer_new()); - ret = hreq->handler(hreq); - if (ret < 0) + status_code = hreq->handler(hreq); + + switch (status_code) { - httpd_send_error(req, 500, "Internal Server Error"); - goto error; + case HTTP_OK: /* 200 OK */ + evhttp_add_header(headers, "Content-Type", "application/json"); + httpd_send_reply(req, status_code, "OK", hreq->reply, 0); + break; + case HTTP_NOCONTENT: /* 204 No Content */ + httpd_send_reply(req, status_code, "No Content", hreq->reply, 0); + break; + + case HTTP_BADREQUEST: /* 400 Bad Request */ + httpd_send_error(req, status_code, "Bad Request"); + break; + case HTTP_NOTFOUND: /* 404 Not Found */ + httpd_send_error(req, status_code, "Not Found"); + break; + case HTTP_INTERNAL: /* 500 Internal Server Error */ + default: + httpd_send_error(req, HTTP_INTERNAL, "Internal Server Error"); + break; } - evhttp_add_header(headers, "Content-Type", "application/json"); - - httpd_send_reply(req, HTTP_OK, "OK", hreq->reply, 0); - - error: evbuffer_free(hreq->reply); free(hreq); } From 1379ef235cd035982d4c55cdbca8f099379b05dd Mon Sep 17 00:00:00 2001 From: chme Date: Sun, 11 Feb 2018 08:52:58 +0100 Subject: [PATCH 3/8] [httpd] Support assigning request methods to uri handlers --- src/httpd.c | 8 ++++++++ src/httpd.h | 1 + 2 files changed, 9 insertions(+) diff --git a/src/httpd.c b/src/httpd.c index 3a3852e8..5b3ec74b 100644 --- a/src/httpd.c +++ b/src/httpd.c @@ -880,6 +880,7 @@ httpd_request_parse(struct evhttp_request *req, struct httpd_uri_parsed *uri_par struct httpd_request *hreq; struct evhttp_connection *evcon; struct evkeyvalq *headers; + int req_method; int i; int ret; @@ -889,6 +890,7 @@ httpd_request_parse(struct evhttp_request *req, struct httpd_uri_parsed *uri_par hreq->req = req; hreq->uri_parsed = uri_parsed; hreq->query = &(uri_parsed->ev_query); + req_method = 0; if (req) { @@ -900,6 +902,8 @@ httpd_request_parse(struct evhttp_request *req, struct httpd_uri_parsed *uri_par evhttp_connection_get_peer(evcon, &hreq->peer_address, &hreq->peer_port); else DPRINTF(E_LOG, L_HTTPD, "Connection to client lost or missing\n"); + + req_method = evhttp_request_get_command(req); } if (user_agent) @@ -908,6 +912,10 @@ httpd_request_parse(struct evhttp_request *req, struct httpd_uri_parsed *uri_par // Find a handler for the path for (i = 0; uri_map[i].handler; i++) { + // Check if handler supports the current http request method + if (uri_map[i].method && req_method && !(req_method & uri_map[i].method)) + continue; + ret = regexec(&uri_map[i].preg, uri_parsed->path, 0, NULL, 0); if (ret == 0) { diff --git a/src/httpd.h b/src/httpd.h index 74ddef15..bf071d27 100644 --- a/src/httpd.h +++ b/src/httpd.h @@ -69,6 +69,7 @@ struct httpd_request { */ struct httpd_uri_map { + int method; char *regexp; int (*handler)(struct httpd_request *hreq); regex_t preg; From 83bb06225b49d0e0205c7f554c5a43b25a5a5716 Mon Sep 17 00:00:00 2001 From: chme Date: Sun, 11 Feb 2018 10:26:27 +0100 Subject: [PATCH 4/8] [misc] Add helper function to check if a key with a given type exists --- src/misc_json.c | 8 ++++++++ src/misc_json.h | 3 +++ 2 files changed, 11 insertions(+) diff --git a/src/misc_json.c b/src/misc_json.c index 05616fa6..b6eb18d1 100644 --- a/src/misc_json.c +++ b/src/misc_json.c @@ -46,6 +46,14 @@ jparse_free(json_object* haystack) } } +bool +jparse_contains_key(json_object *haystack, const char *key, json_type type) +{ + json_object *needle; + + return json_object_object_get_ex(haystack, key, &needle) && json_object_get_type(needle) == type; +} + int jparse_array_from_obj(json_object *haystack, const char *key, json_object **needle) { diff --git a/src/misc_json.h b/src/misc_json.h index a19e371b..2a8410fe 100644 --- a/src/misc_json.h +++ b/src/misc_json.h @@ -36,6 +36,9 @@ void jparse_free(json_object *haystack); +bool +jparse_contains_key(json_object *haystack, const char *key, json_type type); + int jparse_array_from_obj(json_object *haystack, const char *key, json_object **needle); From 0e9feac829c1dfc5fe29fc980ab3eac96809fff7 Mon Sep 17 00:00:00 2001 From: chme Date: Sun, 11 Feb 2018 08:53:51 +0100 Subject: [PATCH 5/8] [jsonapi] Rework outputs endpoint and strictly match uris by request method --- src/httpd_jsonapi.c | 227 ++++++++++++++++++++++++++++++++------------ 1 file changed, 164 insertions(+), 63 deletions(-) diff --git a/src/httpd_jsonapi.c b/src/httpd_jsonapi.c index 2d418b63..17cf4e78 100644 --- a/src/httpd_jsonapi.c +++ b/src/httpd_jsonapi.c @@ -64,13 +64,13 @@ * } */ static int -pairing_kickoff(struct evhttp_request *req) +jsonapi_reply_pairing_kickoff(struct httpd_request *hreq) { struct evbuffer *evbuf; json_object* request; const char* message; - evbuf = evhttp_request_get_input_buffer(req); + evbuf = evhttp_request_get_input_buffer(hreq->req); request = jparse_obj_from_evbuffer(evbuf); if (!request) { @@ -102,7 +102,7 @@ pairing_kickoff(struct evhttp_request *req) * } */ static int -pairing_get(struct evbuffer *evbuf) +jsonapi_reply_pairing_get(struct httpd_request *hreq) { char *remote_name; json_object *jreply; @@ -121,7 +121,7 @@ pairing_get(struct evbuffer *evbuf) json_object_object_add(jreply, "active", json_object_new_boolean(false)); } - CHECK_ERRNO(L_WEB, evbuffer_add_printf(evbuf, "%s", json_object_to_json_string(jreply))); + CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); jparse_free(jreply); free(remote_name); @@ -371,23 +371,6 @@ jsonapi_reply_spotify_login(struct httpd_request *hreq) return HTTP_OK; } -/* - * 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 httpd_request *hreq) -{ - if (evhttp_request_get_command(hreq->req) == EVHTTP_REQ_POST) - { - return pairing_kickoff(hreq->req); - } - - return pairing_get(hreq->reply); -} - static int jsonapi_reply_lastfm(struct httpd_request *hreq) { @@ -494,14 +477,18 @@ jsonapi_reply_lastfm_logout(struct httpd_request *hreq) return HTTP_NOCONTENT; } -static void -speaker_enum_cb(struct spk_info *spk, void *arg) +struct outputs_param +{ + json_object *output; + uint64_t output_id; +}; + +static json_object * +speaker_to_json(struct spk_info *spk) { - json_object *outputs; json_object *output; char output_id[21]; - outputs = arg; output = json_object_new_object(); snprintf(output_id, sizeof(output_id), "%" PRIu64, spk->id); @@ -514,14 +501,132 @@ speaker_enum_cb(struct spk_info *spk, void *arg) json_object_object_add(output, "needs_auth_key", json_object_new_boolean(spk->needs_auth_key)); json_object_object_add(output, "volume", json_object_new_int(spk->absvol)); + return output; +} + +static void +speaker_enum_cb(struct spk_info *spk, void *arg) +{ + json_object *outputs; + json_object *output; + + outputs = arg; + + output = speaker_to_json(spk); json_object_array_add(outputs, output); } +static void +speaker_get_cb(struct spk_info *spk, void *arg) +{ + struct outputs_param *outputs_param = arg; + + if (outputs_param->output_id == spk->id) + { + outputs_param->output = speaker_to_json(spk); + } +} + +/* + * GET /api/outputs/[output_id] + */ +static int +jsonapi_reply_outputs_get_byid(struct httpd_request *hreq) +{ + struct outputs_param outputs_param; + uint64_t output_id; + int ret; + + ret = safe_atou64(hreq->uri_parsed->path_parts[2], &output_id); + if (ret < 0) + { + DPRINTF(E_LOG, L_WEB, "No valid output id given to outputs endpoint '%s'\n", hreq->uri_parsed->path); + + return HTTP_BADREQUEST; + } + + outputs_param.output_id = output_id; + outputs_param.output = NULL; + + player_speaker_enumerate(speaker_get_cb, &outputs_param); + + if (!outputs_param.output) + { + DPRINTF(E_LOG, L_WEB, "No output found for '%s'\n", hreq->uri_parsed->path); + + return HTTP_BADREQUEST; + } + + CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(outputs_param.output))); + + jparse_free(outputs_param.output); + + return HTTP_OK; +} + +/* + * PUT /api/outputs/[output_id] + */ +static int +jsonapi_reply_outputs_put_byid(struct httpd_request *hreq) +{ + uint64_t output_id; + struct evbuffer *in_evbuf; + json_object* request; + bool selected; + int volume; + int ret; + + ret = safe_atou64(hreq->uri_parsed->path_parts[2], &output_id); + if (ret < 0) + { + DPRINTF(E_LOG, L_WEB, "No valid output id given to outputs endpoint '%s'\n", hreq->uri_parsed->path); + + return HTTP_BADREQUEST; + } + + in_evbuf = evhttp_request_get_input_buffer(hreq->req); + request = jparse_obj_from_evbuffer(in_evbuf); + if (!request) + { + DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n"); + + return HTTP_BADREQUEST; + } + + ret = 0; + + if (jparse_contains_key(request, "selected", json_type_boolean)) + { + selected = jparse_bool_from_obj(request, "selected"); + if (selected) + ret = player_speaker_enable(output_id); + else + ret = player_speaker_disable(output_id); + } + + if (ret == 0 && jparse_contains_key(request, "volume", json_type_int)) + { + volume = jparse_int_from_obj(request, "volume"); + ret = player_volume_setabs_speaker(output_id, volume); + } + + jparse_free(request); + + if (ret < 0) + return HTTP_INTERNAL; + + return HTTP_NOCONTENT; +} + +/* + * Endpoint "/api/outputs" + */ static int jsonapi_reply_outputs(struct httpd_request *hreq) { - json_object *jreply; json_object *outputs; + json_object *jreply; outputs = json_object_new_array(); @@ -544,12 +649,6 @@ jsonapi_reply_verification(struct httpd_request *hreq) json_object* request; const char* message; - if (evhttp_request_get_command(hreq->req) != EVHTTP_REQ_POST) - { - DPRINTF(E_LOG, L_WEB, "Verification: request is not a POST request\n"); - return HTTP_BADREQUEST; - } - in_evbuf = evhttp_request_get_input_buffer(hreq->req); request = jparse_obj_from_evbuffer(in_evbuf); if (!request) @@ -572,7 +671,7 @@ jsonapi_reply_verification(struct httpd_request *hreq) } static int -jsonapi_reply_select_outputs(struct httpd_request *hreq) +jsonapi_reply_outputs_set(struct httpd_request *hreq) { struct evbuffer *in_evbuf; json_object *request; @@ -581,12 +680,6 @@ jsonapi_reply_select_outputs(struct httpd_request *hreq) int nspk, i, ret; uint64_t *ids; - if (evhttp_request_get_command(hreq->req) != EVHTTP_REQ_POST) - { - DPRINTF(E_LOG, L_WEB, "Select outputs: request is not a POST request\n"); - return HTTP_BADREQUEST; - } - in_evbuf = evhttp_request_get_input_buffer(hreq->req); request = jparse_obj_from_evbuffer(in_evbuf); if (!request) @@ -986,30 +1079,38 @@ jsonapi_reply_player_volume(struct httpd_request *hreq) static struct httpd_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 = "^/api/lastfm-login", .handler = jsonapi_reply_lastfm_login }, - { .regexp = "^/api/lastfm-logout", .handler = jsonapi_reply_lastfm_logout }, - { .regexp = "^/api/lastfm", .handler = jsonapi_reply_lastfm }, - { .regexp = "^/api/outputs", .handler = jsonapi_reply_outputs }, - { .regexp = "^/api/select-outputs", .handler = jsonapi_reply_select_outputs }, - { .regexp = "^/api/verification", .handler = jsonapi_reply_verification }, - { .regexp = "^/api/player/play", .handler = jsonapi_reply_player_play }, - { .regexp = "^/api/player/pause", .handler = jsonapi_reply_player_pause }, - { .regexp = "^/api/player/stop", .handler = jsonapi_reply_player_stop }, - { .regexp = "^/api/player/next", .handler = jsonapi_reply_player_next }, - { .regexp = "^/api/player/previous", .handler = jsonapi_reply_player_previous }, - { .regexp = "^/api/player/shuffle", .handler = jsonapi_reply_player_shuffle }, - { .regexp = "^/api/player/repeat", .handler = jsonapi_reply_player_repeat }, - { .regexp = "^/api/player/consume", .handler = jsonapi_reply_player_consume }, - { .regexp = "^/api/player/volume", .handler = jsonapi_reply_player_volume }, - { .regexp = "^/api/player", .handler = jsonapi_reply_player }, - { .regexp = "^/api/queue", .handler = jsonapi_reply_queue }, - { .regexp = NULL, .handler = NULL } + { EVHTTP_REQ_GET, "^/api/config$", jsonapi_reply_config }, + { EVHTTP_REQ_GET, "^/api/library$", jsonapi_reply_library }, + { EVHTTP_REQ_GET, "^/api/update$", jsonapi_reply_update }, + { EVHTTP_REQ_POST, "^/api/spotify-login$", jsonapi_reply_spotify_login }, + { EVHTTP_REQ_GET, "^/api/spotify$", jsonapi_reply_spotify }, + { EVHTTP_REQ_GET, "^/api/pairing$", jsonapi_reply_pairing_get }, + { EVHTTP_REQ_POST, "^/api/pairing$", jsonapi_reply_pairing_kickoff }, + { EVHTTP_REQ_POST, "^/api/lastfm-login$", jsonapi_reply_lastfm_login }, + { EVHTTP_REQ_GET, "^/api/lastfm-logout$", jsonapi_reply_lastfm_logout }, + { EVHTTP_REQ_GET, "^/api/lastfm$", jsonapi_reply_lastfm }, + { EVHTTP_REQ_POST, "^/api/verification$", jsonapi_reply_verification }, + + { EVHTTP_REQ_GET, "^/api/outputs$", jsonapi_reply_outputs }, + { EVHTTP_REQ_PUT, "^/api/outputs/set$", jsonapi_reply_outputs_set }, + { EVHTTP_REQ_POST, "^/api/select-outputs$", jsonapi_reply_outputs_set }, // deprecated: use "/api/outputs/set" + { EVHTTP_REQ_GET, "^/api/outputs/[[:digit:]]+$", jsonapi_reply_outputs_get_byid }, + { EVHTTP_REQ_PUT, "^/api/outputs/[[:digit:]]+$", jsonapi_reply_outputs_put_byid }, + + { EVHTTP_REQ_PUT, "^/api/player/play$", jsonapi_reply_player_play }, + { EVHTTP_REQ_PUT, "^/api/player/pause$", jsonapi_reply_player_pause }, + { EVHTTP_REQ_PUT, "^/api/player/stop$", jsonapi_reply_player_stop }, + { EVHTTP_REQ_PUT, "^/api/player/next$", jsonapi_reply_player_next }, + { EVHTTP_REQ_PUT, "^/api/player/previous$", jsonapi_reply_player_previous }, + { EVHTTP_REQ_PUT, "^/api/player/shuffle$", jsonapi_reply_player_shuffle }, + { EVHTTP_REQ_PUT, "^/api/player/repeat$", jsonapi_reply_player_repeat }, + { EVHTTP_REQ_PUT, "^/api/player/consume$", jsonapi_reply_player_consume }, + { EVHTTP_REQ_PUT, "^/api/player/volume$", jsonapi_reply_player_volume }, + { EVHTTP_REQ_GET, "^/api/player$", jsonapi_reply_player }, + + { EVHTTP_REQ_GET, "^/api/queue$", jsonapi_reply_queue }, + + { 0, NULL, NULL } }; From 12127230729a5cfcdd4373b7dfc59e971b35b4a9 Mon Sep 17 00:00:00 2001 From: chme Date: Tue, 13 Feb 2018 20:03:55 +0100 Subject: [PATCH 6/8] [jsonapi] Do not set "DAAP-Server" header in json api response --- src/httpd_jsonapi.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/httpd_jsonapi.c b/src/httpd_jsonapi.c index 17cf4e78..b0e561be 100644 --- a/src/httpd_jsonapi.c +++ b/src/httpd_jsonapi.c @@ -1137,9 +1137,6 @@ jsonapi_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed) return; } - headers = evhttp_request_get_output_headers(req); - evhttp_add_header(headers, "DAAP-Server", "forked-daapd/" VERSION); - CHECK_NULL(L_WEB, hreq->reply = evbuffer_new()); status_code = hreq->handler(hreq); @@ -1147,6 +1144,7 @@ jsonapi_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed) switch (status_code) { case HTTP_OK: /* 200 OK */ + headers = evhttp_request_get_output_headers(req); evhttp_add_header(headers, "Content-Type", "application/json"); httpd_send_reply(req, status_code, "OK", hreq->reply, 0); break; From bd5bf5624520a12f0ca36b18250b8bcc16df13d0 Mon Sep 17 00:00:00 2001 From: chme Date: Sat, 10 Feb 2018 09:21:45 +0100 Subject: [PATCH 7/8] [htdocs] Select outputs with /api/outputs/set endpoint --- htdocs/js/forked-daapd.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/js/forked-daapd.js b/htdocs/js/forked-daapd.js index 3003ea00..687b5d5e 100644 --- a/htdocs/js/forked-daapd.js +++ b/htdocs/js/forked-daapd.js @@ -80,7 +80,7 @@ var app = new Vue({ } } - axios.post('/api/select-outputs', { outputs: selected_outputs }).then(response => { + axios.put('/api/outputs/set', { outputs: selected_outputs }).then(response => { if (!this.config.websocket_port) { this.loadOutputs(); } From b17fb1c1017aa76d2671abe80a9cd6598a266545 Mon Sep 17 00:00:00 2001 From: chme Date: Thu, 8 Feb 2018 21:19:55 +0100 Subject: [PATCH 8/8] [README] JSON API Endpoint reference documentation --- README_JSON_API.md | 527 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 527 insertions(+) create mode 100644 README_JSON_API.md diff --git a/README_JSON_API.md b/README_JSON_API.md new file mode 100644 index 00000000..581b4712 --- /dev/null +++ b/README_JSON_API.md @@ -0,0 +1,527 @@ +# forked-daapd API Endpoint Reference + +Available API endpoints: + +* [Player](#player): control playback, volume, shuffle/repeat modes +* [Outputs / Speakers](#outputs--speakers): list available outputs and enable/disable outputs +* [Server info](#server-info): get server information +* [Push notifications](#push-notifications): receive push notifications + +## Player + +| Method | Endpoint | Description | +| --------- | ------------------------------------------------ | ------------------------------------ | +| GET | [/api/player](#get-player-status) | Get player status | +| PUT | [/api/player/play, /api/player/pause, /api/player/stop](#control-playback) | Start, pause or stop playback | +| PUT | [/api/player/next, /api/player/prev](#skip-tracks) | Skip forward or backward | +| PUT | [/api/player/shuffle](#set-shuffle-mode) | Set shuffle mode | +| PUT | [/api/player/consume](#set-consume-mode) | Set consume mode | +| PUT | [/api/player/repeat](#set-repeat-mode) | Set repeat mode | +| PUT | [/api/player/volume](#set-volume) | Set master volume or volume for a specific output | + + + +### Get player status + +**Endpoint** + +``` +GET /api/player +``` + +**Response** + +| Key | Type | Value | +| ----------------- | -------- | ----------------------------------------- | +| state | string | `play`, `pause` or `stop` | +| repeat | string | `off`, `all` or `single` | +| consume | boolean | `true` if consume mode is enabled | +| shuffle | boolean | `true` if shuffle mode is enabled | +| volume | integer | Master volume in percent (0 - 100) | +| item_id | integer | The current playing queue item `id` | +| item_length_ms | integer | Total length in milliseconds of the current queue item | +| item_progress_ms | integer | Progress into the current queue item in milliseconds | + + +**Example** + +``` +curl -X GET "http://localhost:3689/api/player" +``` + +``` +{ + "state": "pause", + "repeat": "off", + "consume": false, + "shuffle": false, + "volume": 50, + "item_id": 269, + "item_length_ms": 278093, + "item_progress_ms": 3674 +} +``` + + +### Control playback + +Start or resume, pause, stop playback. + +**Endpoint** + +``` +PUT /api/player/play +``` + +``` +PUT /api/player/pause +``` + +``` +PUT /api/player/stop +``` + +**Response** + +On success returns the HTTP `204 No Content` success status response code. + +**Example** + +``` +curl -X PUT "http://localhost:3689/api/player/play" +``` + +``` +curl -X PUT "http://localhost:3689/api/player/pause" +``` + +``` +curl -X PUT "http://localhost:3689/api/player/stop" +``` + + +### Skip tracks + +Skip forward or backward + +**Endpoint** + +``` +PUT /api/player/next +``` + +``` +PUT /api/player/prev +``` + +**Response** + +On success returns the HTTP `204 No Content` success status response code. + +**Example** + +``` +curl -X PUT "http://localhost:3689/api/player/next" +``` + +``` +curl -X PUT "http://localhost:3689/api/player/prev" +``` + + +### Set shuffle mode + +Enable or disable shuffle mode + +**Endpoint** + +``` +PUT /api/player/shuffle +``` + +**Query parameters** + +| Parameter | Value | +| --------------- | ----------------------------------------------------------- | +| state | The new shuffle state, should be either `true` or `false` | + + +**Response** + +On success returns the HTTP `204 No Content` success status response code. + +**Example** + +``` +curl -X PUT "http://localhost:3689/api/player/shuffle?state=true" +``` + + +### Set consume mode + +Enable or disable consume mode + +**Endpoint** + +``` +PUT /api/player/consume +``` + +**Query parameters** + +| Parameter | Value | +| --------------- | ----------------------------------------------------------- | +| state | The new consume state, should be either `true` or `false` | + + +**Response** + +On success returns the HTTP `204 No Content` success status response code. + +**Example** + +``` +curl -X PUT "http://localhost:3689/api/player/consume?state=true" +``` + + +### Set repeat mode + +Change repeat mode + +**Endpoint** + +``` +PUT /api/player/repeat +``` + +**Query parameters** + +| Parameter | Value | +| --------------- | ----------------------------------------------------------- | +| state | The new repeat mode, should be either `off`, `all` or `single` | + + +**Response** + +On success returns the HTTP `204 No Content` success status response code. + +**Example** + +``` +curl -X PUT "http://localhost:3689/api/player/repeat?state=all" +``` + + +### Set volume + +Change master volume or volume of a specific output. + +**Endpoint** + +``` +PUT /api/player/volume +``` + +**Query parameters** + +| Parameter | Value | +| --------------- | ----------------------------------------------------------- | +| volume | The new volume (0 - 100) | +| output_id | *(Optional)* If an output id is given, only the volume of this output will be changed. If parameter is omited, the master volume will be changed. | + + +**Response** + +On success returns the HTTP `204 No Content` success status response code. + +**Example** + +``` +curl -X PUT "http://localhost:3689/api/player/volume?volume=50" +``` + +``` +curl -X PUT "http://localhost:3689/api/player/volume?volume=50&output_id=0" +``` + + + +## Outputs / Speakers + +| Method | Endpoint | Description | +| --------- | ------------------------------------------------ | ------------------------------------ | +| GET | [/api/outputs](#get-a-list-of-available-outputs) | Get a list of available outputs | +| PUT | [/api/outputs/set](#set-enabled-outputs) | Set enabled outputs | +| GET | [/api/outputs/{id}](#get-an-output) | Get an output | +| PUT | [/api/outputs/{id}](#change-an-output) | Change an output (enable/disable or volume) | + + + +### Get a list of available outputs + +**Endpoint** + +``` +GET /api/outputs +``` + +**Response** + +| Key | Type | Value | +| --------------- | -------- | ----------------------------------------- | +| outputs | array | Array of `output` objects | + +**`output` object** + +| Key | Type | Value | +| --------------- | -------- | ----------------------------------------- | +| id | string | Output id | +| name | string | Output name | +| type | string | Type of the output: `AirPlay`, `Chromecast`, `ALSA`, `Pulseaudio`, `fifo` | +| selected | boolean | `true` if output is enabled | +| has_password | boolean | `true` if output is password protected | +| requires_auth | boolean | `true` if output requires authentication | +| needs_auth_key | boolean | `true` if output requires an authorization key (device verification) | +| volume | integer | Volume in percent (0 - 100) | + + +**Example** + +``` +curl -X GET "http://localhost:3689/api/outputs" +``` + +``` +{ + "outputs": [ + { + "id": "123456789012345", + "name": "kitchen", + "type": "AirPlay", + "selected": true, + "has_password": false, + "requires_auth": false, + "needs_auth_key": false, + "volume": 0 + }, + { + "id": "0", + "name": "Computer", + "type": "ALSA", + "selected": true, + "has_password": false, + "requires_auth": false, + "needs_auth_key": false, + "volume": 19 + }, + { + "id": "100", + "name": "daapd-fifo", + "type": "fifo", + "selected": false, + "has_password": false, + "requires_auth": false, + "needs_auth_key": false, + "volume": 0 + } + ] +} +``` + +### Set enabled outputs + +Set the enabled outputs by passing an array of output ids. forked-daapd enables all outputs +with the given ids and disables the remaining outputs. + +**Endpoint** + +``` +PUT /api/outputs/set +``` + +**Body parameters** + +| Parameter | Type | Value | +| --------------- | -------- | -------------------- | +| outputs | array | Array of output ids | + +**Response** + +On success returns the HTTP `204 No Content` success status response code. + +**Example** + +``` +curl -X PUT "http://localhost:3689/api/outputs/set" --data "{\"outputs\":[\"198018693182577\",\"0\"]}" +``` + + +### Get an output + +Get an output + +**Endpoint** + +``` +GET /api/outputs/{id} +``` + +**Path parameters** + +| Parameter | Value | +| --------------- | -------------------- | +| id | Output id | + +**Response** + +On success returns the HTTP `200 OK` success status response code. With the response body holding the **`output` object**. + +**Example** + +``` +curl -X GET "http://localhost:3689/api/outputs/0" +``` + +``` +{ + "id": "0", + "name": "Computer", + "type": "ALSA", + "selected": true, + "has_password": false, + "requires_auth": false, + "needs_auth_key": false, + "volume": 3 +} +``` + +### Change an output + +Enable or disable an output and change its volume. + +**Endpoint** + +``` +PUT /api/outputs/{id} +``` + +**Path parameters** + +| Parameter | Value | +| --------------- | -------------------- | +| id | Output id | + +**Body parameters** + +| Parameter | Type | Value | +| --------------- | --------- | -------------------- | +| selected | boolean | *(Optional)* `true` to enable and `false` to disable the output | +| volume | integer | *(Optional)* Volume in percent (0 - 100) | + +**Response** + +On success returns the HTTP `204 No Content` success status response code. + +**Example** + +``` +curl -X PUT "http://localhost:3689/api/outputs/0" --data "{\"selected\":true, \"volume\": 50}" +``` + +## Server info + +| Method | Endpoint | Description | +| --------- | ------------------------------------------------ | ------------------------------------ | +| GET | [/api/config](#config) | Get configuration information | + + + +### Config + +**Endpoint** + +``` +GET /api/config +``` + +**Response** + +| Key | Type | Value | +| --------------- | -------- | ----------------------------------------- | +| version | string | forked-daapd server version | +| websocket_port | integer | Port number for the [websocket](#push-notifications) (or `0` if websocket is disabled) | +| buildoptions | array | Array of strings indicating which features are supported by the forked-daapd server | + + +**Example** + +``` +curl -X GET "http://localhost:3689/api/config" +``` + +``` +{ + "websocket_port": 3688, + "version": "25.0", + "buildoptions": [ + "ffmpeg", + "iTunes XML", + "Spotify", + "LastFM", + "MPD", + "Device verification", + "Websockets", + "ALSA" + ] +} +``` + +## Push notifications + +If forked-daapd was built with websocket support, forked-daapd exposes a websocket at `localhost:3688` to inform clients of changes (e. g. player state or library updates). +The port depends on the forked-daapd configuration and can be read using the [`/api/config`](#config) endpoint. + +After connecting to the websocket, the client should send a message containing the event types it is interested in. After that forked-daapd +will send a message each time one of the events occurred. + +**Message** + +| Key | Type | Value | +| --------------- | -------- | ----------------------------------------- | +| notify | array | Array of event types | + +**Event types** + +| Type | Description | +| --------------- | ----------------------------------------- | +| update | Library update started or finished | +| outputs | An output was enabled or disabled | +| player | Player state changes | +| options | Playback option changes (shuffle, repeat, consume mode) | +| volume | Volume changes | +| queue | Queue changes | + +**Example** + +``` +curl --include \ + --no-buffer \ + --header "Connection: Upgrade" \ + --header "Upgrade: websocket" \ + --header "Host: localhost:3688" \ + --header "Origin: http://localhost:3688" \ + --header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \ + --header "Sec-WebSocket-Version: 13" \ + --header "Sec-WebSocket-Protocol: notify" \ + http://localhost:3688/ \ + --data "{ \"notify\": [ \"player\" ] }" +``` + +``` +{ + "notify": [ + "player" + ] +} +```