/* * Copyright (C) 2010 Julien BLACHE * * 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 #include #include "evhttp/evhttp.h" #include "logger.h" #include "misc.h" #include "conffile.h" #include "httpd.h" #include "httpd_dacp.h" #include "dmap_helpers.h" #include "player.h" /* From httpd_daap.c */ struct daap_session; struct daap_session * daap_session_find(struct evhttp_request *req, struct evkeyvalq *query, struct evbuffer *evbuf); struct uri_map { regex_t preg; char *regexp; void (*handler)(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query); }; struct dacp_update_request { struct evhttp_request *req; struct dacp_update_request *next; }; /* Play status update requests */ static struct dacp_update_request *update_requests; /* Update requests helpers */ static void update_fail_cb(struct evhttp_connection *evcon, void *arg) { struct dacp_update_request *ur; struct dacp_update_request *p; ur = (struct dacp_update_request *)arg; DPRINTF(E_DBG, L_DACP, "Update request: client closed connection\n"); evhttp_connection_set_closecb(ur->req->evcon, NULL, NULL); if (ur == update_requests) update_requests = ur->next; else { for (p = update_requests; p && (p->next != ur); p = p->next) ; if (!p) { DPRINTF(E_LOG, L_DACP, "WARNING: struct dacp_update_request not found in list; BUG!\n"); return; } p->next = ur->next; } free(ur); } static void dacp_reply_ctrlint(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { dmap_add_container(evbuf, "caci", 127); /* 8 + len */ dmap_add_int(evbuf, "mstt", 200); /* 12 */ dmap_add_char(evbuf, "muty", 0); /* 9 */ dmap_add_int(evbuf, "mtco", 1); /* 12 */ dmap_add_int(evbuf, "mrco", 1); /* 12 */ dmap_add_container(evbuf, "mlcl", 74); /* 8 + len */ dmap_add_container(evbuf, "mlit", 66); /* 8 + len */ dmap_add_int(evbuf, "miid", 1); /* 12 */ /* Database ID */ dmap_add_char(evbuf, "cmik", 1); /* 9 */ dmap_add_char(evbuf, "cmsp", 1); /* 9 */ dmap_add_char(evbuf, "cmsv", 1); /* 9 */ dmap_add_char(evbuf, "cass", 1); /* 9 */ dmap_add_char(evbuf, "casu", 1); /* 9 */ dmap_add_char(evbuf, "ceSG", 1); /* 9 */ evhttp_send_reply(req, HTTP_OK, "OK", evbuf); } static void dacp_reply_cue_play(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { const char *sort; const char *cuequery; const char *param; struct player_source *ps; uint32_t id; int ret; /* /cue?command=play&query=...&sort=...&index=N */ cuequery = evhttp_find_header(query, "query"); if (!cuequery) { DPRINTF(E_LOG, L_DACP, "No query given for cue play command\n"); dmap_send_error(req, "cacr", "No query given"); return; } sort = evhttp_find_header(query, "sort"); ps = player_queue_make(cuequery, sort); if (!ps) { DPRINTF(E_LOG, L_DACP, "Could not build song queue\n"); dmap_send_error(req, "cacr", "Could not build song queue"); return; } player_queue_add(ps); id = 0; param = evhttp_find_header(query, "index"); if (param) { ret = safe_atou32(param, &id); if (ret < 0) DPRINTF(E_LOG, L_DACP, "Invalid index (%s) in cue request\n", param); } ret = player_playback_start(&id); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not start playback\n"); dmap_send_error(req, "cacr", "Playback failed to start"); return; } dmap_add_container(evbuf, "cacr", 24); /* 8 + len */ dmap_add_int(evbuf, "mstt", 200); /* 12 */ dmap_add_int(evbuf, "miid", id); /* 12 */ evhttp_send_reply(req, HTTP_OK, "OK", evbuf); } static void dacp_reply_cue_clear(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { /* /cue?command=clear */ player_playback_stop(); player_queue_clear(); dmap_add_container(evbuf, "cacr", 24); /* 8 + len */ dmap_add_int(evbuf, "mstt", 200); /* 12 */ dmap_add_int(evbuf, "miid", 0); /* 12 */ evhttp_send_reply(req, HTTP_OK, "OK", evbuf); } static void dacp_reply_cue(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; const char *param; s = daap_session_find(req, query, evbuf); if (!s) return; param = evhttp_find_header(query, "command"); if (!param) { DPRINTF(E_DBG, L_DACP, "No command in cue request\n"); dmap_send_error(req, "cacr", "No command in cue request"); return; } if (strcmp(param, "clear") == 0) dacp_reply_cue_clear(req, evbuf, uri, query); else if (strcmp(param, "play") == 0) dacp_reply_cue_play(req, evbuf, uri, query); else { DPRINTF(E_LOG, L_DACP, "Unknown cue command %s\n", param); dmap_send_error(req, "cacr", "Unknown command in cue request"); return; } } static void dacp_reply_pause(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; s = daap_session_find(req, query, evbuf); if (!s) return; /* TODO */ /* 204 No Content is the canonical reply */ evhttp_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf); } static void dacp_reply_playpause(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; s = daap_session_find(req, query, evbuf); if (!s) return; /* TODO */ /* 204 No Content is the canonical reply */ evhttp_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf); } static void dacp_reply_nextitem(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; s = daap_session_find(req, query, evbuf); if (!s) return; /* TODO */ /* 204 No Content is the canonical reply */ evhttp_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf); } static void dacp_reply_previtem(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; s = daap_session_find(req, query, evbuf); if (!s) return; /* TODO */ /* 204 No Content is the canonical reply */ evhttp_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf); } static void dacp_reply_beginff(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; s = daap_session_find(req, query, evbuf); if (!s) return; /* TODO */ /* 204 No Content is the canonical reply */ evhttp_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf); } static void dacp_reply_beginrew(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; s = daap_session_find(req, query, evbuf); if (!s) return; /* TODO */ /* 204 No Content is the canonical reply */ evhttp_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf); } static void dacp_reply_playresume(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; s = daap_session_find(req, query, evbuf); if (!s) return; /* TODO */ /* 204 No Content is the canonical reply */ evhttp_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf); } static void dacp_reply_playstatusupdate(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; struct dacp_update_request *ur; const char *param; int current_rev = 2; int reqd_rev; int ret; s = daap_session_find(req, query, evbuf); if (!s) return; param = evhttp_find_header(query, "revision-number"); if (!param) { DPRINTF(E_LOG, L_DACP, "Missing revision-number in update request\n"); dmap_send_error(req, "cmst", "Invalid request"); return; } ret = safe_atoi32(param, &reqd_rev); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Parameter revision-number not an integer\n"); dmap_send_error(req, "cmst", "Invalid request"); return; } if (reqd_rev == 1) { dmap_add_container(evbuf, "cmst", 84); /* 8 + len */ dmap_add_int(evbuf, "mstt", 200); /* 12 */ dmap_add_int(evbuf, "cmsr", current_rev); /* 12 */ dmap_add_char(evbuf, "cavc", 1); /* 9 */ /* volume controllable */ dmap_add_char(evbuf, "caps", 2); /* 9 */ /* play status, 2 = stopped, 3 = paused, 4 = playing */ dmap_add_char(evbuf, "cash", 0); /* 9 */ /* shuffle, true/false */ dmap_add_char(evbuf, "carp", 0); /* 9 */ /* repeat, 0 = off, 1 = repeat song, 2 = repeat (playlist) */ dmap_add_int(evbuf, "caas", 2); /* 12 */ /* album shuffle */ dmap_add_int(evbuf, "caar", 6); /* 12 */ /* album repeat */ evhttp_send_reply(req, HTTP_OK, "OK", evbuf); return; } /* Else, just let the request hang until we have changes to push back */ ur = (struct dacp_update_request *)malloc(sizeof(struct dacp_update_request)); if (!ur) { DPRINTF(E_LOG, L_DACP, "Out of memory for update request\n"); dmap_send_error(req, "cmst", "Out of memory"); return; } ur->req = req; ur->next = update_requests; update_requests = ur; /* If the connection fails before we have an update to push out * to the client, we need to know. */ evhttp_connection_set_closecb(req->evcon, update_fail_cb, ur); } static void dacp_reply_nowplayingartwork(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; s = daap_session_find(req, query, evbuf); if (!s) return; /* We don't have any support for artwork at the moment... */ /* No artwork -> 404 Not Found */ evhttp_send_error(req, HTTP_NOTFOUND, "Not Found"); } static void dacp_reply_getproperty(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; const char *param; s = daap_session_find(req, query, evbuf); if (!s) return; param = evhttp_find_header(query, "properties"); if (!param) { DPRINTF(E_WARN, L_DACP, "Invalid DACP getproperty request, no properties\n"); dmap_send_error(req, "cmgt", "Invalid request"); return; } /* The client can request multiple properties at once (hence the parameter * name), similar to DAAP meta tags. Should be treated likewise. */ if (strcmp(param, "dmcp.volume") == 0) { dmap_add_container(evbuf, "cmgt", 24); /* 8 + len */ dmap_add_int(evbuf, "mstt", 200); /* 12 */ dmap_add_int(evbuf, "cmvo", 42); /* 12 */ /* Volume, 0-100 */ evhttp_send_reply(req, HTTP_OK, "OK", evbuf); } else dmap_send_error(req, "cmgt", "Not implemented"); } static void dacp_reply_setproperty(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; s = daap_session_find(req, query, evbuf); if (!s) return; /* Known properties: * dacp.shufflestate 0/1 * dacp.repeatstate 0/1/2 * dacp.playingtime seek to time in ms * dmcp.volume 0-100, float */ /* /ctrl-int/1/setproperty?dacp.shufflestate=1&session-id=100 */ /* TODO */ /* 204 No Content is the canonical reply */ evhttp_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf); } static void speaker_enum_cb(uint64_t id, const char *name, int selected, int has_password, void *arg) { struct evbuffer *evbuf; int len; evbuf = (struct evbuffer *)arg; len = 8 + strlen(name) + 16; if (selected) len += 9; if (has_password) len += 9; dmap_add_container(evbuf, "mdcl", len); /* 8 + len */ if (selected) dmap_add_char(evbuf, "caia", 1); /* 9 */ if (has_password) dmap_add_char(evbuf, "cahp", 1); /* 9 */ dmap_add_string(evbuf, "minm", name); /* 8 + len */ dmap_add_long(evbuf, "msma", id); /* 16 */ } static void dacp_reply_getspeakers(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; struct evbuffer *spklist; s = daap_session_find(req, query, evbuf); if (!s) return; spklist = evbuffer_new(); if (!spklist) { DPRINTF(E_LOG, L_DACP, "Could not create evbuffer for speaker list\n"); dmap_send_error(req, "casp", "Out of memory"); return; } player_speaker_enumerate(speaker_enum_cb, spklist); dmap_add_container(evbuf, "casp", 12 + EVBUFFER_LENGTH(spklist)); /* 8 + len */ dmap_add_int(evbuf, "mstt", 200); /* 12 */ evbuffer_add_buffer(evbuf, spklist); evbuffer_free(spklist); evhttp_send_reply(req, HTTP_OK, "OK", evbuf); } static void dacp_reply_setspeakers(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { struct daap_session *s; const char *param; const char *ptr; uint64_t *ids; int nspk; int i; int ret; s = daap_session_find(req, query, evbuf); if (!s) return; param = evhttp_find_header(query, "speaker-id"); if (!param) { DPRINTF(E_LOG, L_DACP, "Missing speaker-id parameter in DACP setspeakers request\n"); evhttp_send_error(req, HTTP_BADREQUEST, "Bad Request"); return; } if (strlen(param) == 0) { ids = NULL; goto fastpath; } nspk = 1; ptr = param; while ((ptr = strchr(ptr + 1, ','))) nspk++; ids = (uint64_t *)malloc((nspk + 1) * sizeof(uint64_t)); if (!ids) { DPRINTF(E_LOG, L_DACP, "Out of memory for speaker ids\n"); evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); return; } param--; i = 1; do { param++; ret = safe_hextou64(param, &ids[i]); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Invalid speaker id in request: %s\n", param); nspk--; continue; } i++; } while ((param = strchr(param + 1, ','))); ids[0] = nspk; fastpath: ret = player_speaker_set(ids); if (ids) free(ids); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Speakers de/activation failed!\n"); /* Password problem */ if (ret == -2) evhttp_send_error(req, 902, ""); else evhttp_send_error(req, 500, "Internal Server Error"); return; } /* 204 No Content is the canonical reply */ evhttp_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf); } static struct uri_map dacp_handlers[] = { { .regexp = "^/ctrl-int$", .handler = dacp_reply_ctrlint }, { .regexp = "^/ctrl-int/[[:digit:]]+/cue$", .handler = dacp_reply_cue }, { .regexp = "^/ctrl-int/[[:digit:]]+/pause$", .handler = dacp_reply_pause }, { .regexp = "^/ctrl-int/[[:digit:]]+/playpause$", .handler = dacp_reply_playpause }, { .regexp = "^/ctrl-int/[[:digit:]]+/nextitem$", .handler = dacp_reply_nextitem }, { .regexp = "^/ctrl-int/[[:digit:]]+/previtem$", .handler = dacp_reply_previtem }, { .regexp = "^/ctrl-int/[[:digit:]]+/beginff$", .handler = dacp_reply_beginff }, { .regexp = "^/ctrl-int/[[:digit:]]+/beginrew$", .handler = dacp_reply_beginrew }, { .regexp = "^/ctrl-int/[[:digit:]]+/playresume$", .handler = dacp_reply_playresume }, { .regexp = "^/ctrl-int/[[:digit:]]+/playstatusupdate$", .handler = dacp_reply_playstatusupdate }, { .regexp = "^/ctrl-int/[[:digit:]]+/nowplayingartwork$", .handler = dacp_reply_nowplayingartwork }, { .regexp = "^/ctrl-int/[[:digit:]]+/getproperty$", .handler = dacp_reply_getproperty }, { .regexp = "^/ctrl-int/[[:digit:]]+/setproperty$", .handler = dacp_reply_setproperty }, { .regexp = "^/ctrl-int/[[:digit:]]+/getspeakers$", .handler = dacp_reply_getspeakers }, { .regexp = "^/ctrl-int/[[:digit:]]+/setspeakers$", .handler = dacp_reply_setspeakers }, { .regexp = NULL, .handler = NULL } }; void dacp_request(struct evhttp_request *req) { char *full_uri; char *uri; char *ptr; char *uri_parts[7]; struct evbuffer *evbuf; struct evkeyvalq query; int handler; int ret; int i; 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) { 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_DACP, "DACP request: %s\n", full_uri); handler = -1; for (i = 0; dacp_handlers[i].handler; i++) { ret = regexec(&dacp_handlers[i].preg, uri, 0, NULL, 0); if (ret == 0) { handler = i; break; } } if (handler < 0) { DPRINTF(E_LOG, L_DACP, "Unrecognized DACP request\n"); evhttp_send_error(req, HTTP_BADREQUEST, "Bad Request"); free(uri); free(full_uri); return; } /* DACP has no HTTP authentication - Remote is identified by its pairing-guid */ memset(uri_parts, 0, sizeof(uri_parts)); uri_parts[0] = strtok_r(uri, "/", &ptr); for (i = 1; (i < sizeof(uri_parts) / sizeof(uri_parts[0])) && uri_parts[i - 1]; i++) { uri_parts[i] = strtok_r(NULL, "/", &ptr); } if (!uri_parts[0] || uri_parts[i - 1] || (i < 2)) { DPRINTF(E_LOG, L_DACP, "DACP URI has too many/few components (%d)\n", (uri_parts[0]) ? i : 0); evhttp_send_error(req, HTTP_BADREQUEST, "Bad Request"); free(uri); free(full_uri); return; } evbuf = evbuffer_new(); if (!evbuf) { DPRINTF(E_LOG, L_DACP, "Could not allocate evbuffer for DACP reply\n"); evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); free(uri); free(full_uri); return; } evhttp_parse_query(full_uri, &query); evhttp_add_header(req->output_headers, "DAAP-Server", "forked-daapd/" VERSION); /* Content-Type for all DACP replies; can be overriden as needed */ evhttp_add_header(req->output_headers, "Content-Type", "application/x-dmap-tagged"); dacp_handlers[handler].handler(req, evbuf, uri_parts, &query); evbuffer_free(evbuf); evhttp_clear_headers(&query); free(uri); free(full_uri); } int dacp_is_request(struct evhttp_request *req, char *uri) { if (strncmp(uri, "/ctrl-int/", strlen("/ctrl-int/")) == 0) return 1; if (strcmp(uri, "/ctrl-int") == 0) return 1; return 0; } int dacp_init(void) { char buf[64]; int i; int ret; update_requests = NULL; for (i = 0; dacp_handlers[i].handler; i++) { ret = regcomp(&dacp_handlers[i].preg, dacp_handlers[i].regexp, REG_EXTENDED | REG_NOSUB); if (ret != 0) { regerror(ret, &dacp_handlers[i].preg, buf, sizeof(buf)); DPRINTF(E_FATAL, L_DACP, "DACP init failed; regexp error: %s\n", buf); return -1; } } return 0; } void dacp_deinit(void) { struct dacp_update_request *ur; int i; for (i = 0; dacp_handlers[i].handler; i++) regfree(&dacp_handlers[i].preg); for (ur = update_requests; update_requests; ur = update_requests) { update_requests = ur->next; evhttp_connection_set_closecb(ur->req->evcon, NULL, NULL); free(ur); } }