/* * Copyright (C) 2009-2011 Julien BLACHE * * Adapted from mt-daapd: * Copyright (C) 2006-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 */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include "mxml-compat.h" #include "httpd_rsp.h" #include "logger.h" #include "db.h" #include "conffile.h" #include "misc.h" #include "httpd.h" #include "transcode.h" #include "rsp_query.h" #define RSP_VERSION "1.0" #define RSP_XML_ROOT "?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?" #define F_FULL (1 << 0) #define F_BROWSE (1 << 1) #define F_ID (1 << 2) #define F_DETAILED (1 << 3) #define F_ALWAYS (F_FULL | F_BROWSE | F_ID | F_DETAILED) struct field_map { char *field; size_t offset; int flags; }; static const struct field_map pl_fields[] = { { "id", dbpli_offsetof(id), F_ALWAYS }, { "title", dbpli_offsetof(title), F_FULL | F_BROWSE | F_DETAILED }, { "type", dbpli_offsetof(type), F_DETAILED }, { "items", dbpli_offsetof(items), F_FULL | F_BROWSE | F_DETAILED }, { "query", dbpli_offsetof(query), F_DETAILED }, { "db_timestamp", dbpli_offsetof(db_timestamp), F_DETAILED }, { "path", dbpli_offsetof(path), F_DETAILED }, { "index", dbpli_offsetof(index), F_DETAILED }, { NULL, 0, 0 } }; static const struct field_map rsp_fields[] = { { "id", dbmfi_offsetof(id), F_ALWAYS }, { "path", dbmfi_offsetof(path), F_DETAILED }, { "fname", dbmfi_offsetof(fname), F_DETAILED }, { "title", dbmfi_offsetof(title), F_ALWAYS }, { "artist", dbmfi_offsetof(artist), F_DETAILED | F_FULL | F_BROWSE }, { "album", dbmfi_offsetof(album), F_DETAILED | F_FULL | F_BROWSE }, { "genre", dbmfi_offsetof(genre), F_DETAILED | F_FULL }, { "comment", dbmfi_offsetof(comment), F_DETAILED | F_FULL }, { "type", dbmfi_offsetof(type), F_ALWAYS }, { "composer", dbmfi_offsetof(composer), F_DETAILED | F_FULL }, { "orchestra", dbmfi_offsetof(orchestra), F_DETAILED | F_FULL }, { "conductor", dbmfi_offsetof(conductor), F_DETAILED | F_FULL }, { "url", dbmfi_offsetof(url), F_DETAILED | F_FULL }, { "bitrate", dbmfi_offsetof(bitrate), F_DETAILED | F_FULL }, { "samplerate", dbmfi_offsetof(samplerate), F_DETAILED | F_FULL }, { "song_length", dbmfi_offsetof(song_length), F_DETAILED | F_FULL }, { "file_size", dbmfi_offsetof(file_size), F_DETAILED | F_FULL }, { "year", dbmfi_offsetof(year), F_DETAILED | F_FULL }, { "track", dbmfi_offsetof(track), F_DETAILED | F_FULL | F_BROWSE }, { "total_tracks", dbmfi_offsetof(total_tracks), F_DETAILED | F_FULL }, { "disc", dbmfi_offsetof(disc), F_DETAILED | F_FULL | F_BROWSE }, { "total_discs", dbmfi_offsetof(total_discs), F_DETAILED | F_FULL }, { "bpm", dbmfi_offsetof(bpm), F_DETAILED | F_FULL }, { "compilation", dbmfi_offsetof(compilation), F_DETAILED | F_FULL }, { "rating", dbmfi_offsetof(rating), F_DETAILED | F_FULL }, { "play_count", dbmfi_offsetof(play_count), F_DETAILED | F_FULL }, { "skip_count", dbmfi_offsetof(skip_count), F_DETAILED | F_FULL }, { "data_kind", dbmfi_offsetof(data_kind), F_DETAILED }, { "item_kind", dbmfi_offsetof(item_kind), F_DETAILED }, { "description", dbmfi_offsetof(description), F_DETAILED | F_FULL }, { "time_added", dbmfi_offsetof(time_added), F_DETAILED | F_FULL }, { "time_modified", dbmfi_offsetof(time_modified), F_DETAILED | F_FULL }, { "time_played", dbmfi_offsetof(time_played), F_DETAILED | F_FULL }, { "time_skipped", dbmfi_offsetof(time_skipped), F_DETAILED | F_FULL }, { "db_timestamp", dbmfi_offsetof(db_timestamp), F_DETAILED }, { "disabled", dbmfi_offsetof(disabled), F_ALWAYS }, { "sample_count", dbmfi_offsetof(sample_count), F_DETAILED }, { "codectype", dbmfi_offsetof(codectype), F_ALWAYS }, { "idx", dbmfi_offsetof(idx), F_DETAILED }, { "has_video", dbmfi_offsetof(has_video), F_DETAILED }, { "contentrating", dbmfi_offsetof(contentrating), F_DETAILED }, { NULL, 0, 0 } }; /* -------------------------------- HELPERS --------------------------------- */ static struct evbuffer * mxml_to_evbuf(mxml_node_t *tree) { struct evbuffer *evbuf; char *xml; int ret; evbuf = evbuffer_new(); if (!evbuf) { DPRINTF(E_LOG, L_RSP, "Could not create evbuffer for RSP reply\n"); return NULL; } xml = mxmlSaveAllocString(tree, MXML_NO_CALLBACK); if (!xml) { DPRINTF(E_LOG, L_RSP, "Could not finalize RSP reply\n"); evbuffer_free(evbuf); return NULL; } ret = evbuffer_add(evbuf, xml, strlen(xml)); free(xml); if (ret < 0) { DPRINTF(E_LOG, L_RSP, "Could not load evbuffer for RSP reply\n"); evbuffer_free(evbuf); return NULL; } return evbuf; } static void rsp_send_error(struct evhttp_request *req, char *errmsg) { struct evbuffer *evbuf; struct evkeyvalq *headers; mxml_node_t *reply; mxml_node_t *status; mxml_node_t *node; /* We'd use mxmlNewXML(), but then we can't put any attributes * on the root node and we need some. */ reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT); node = mxmlNewElement(reply, "response"); status = mxmlNewElement(node, "status"); /* Status block */ node = mxmlNewElement(status, "errorcode"); mxmlNewText(node, 0, "1"); node = mxmlNewElement(status, "errorstring"); mxmlNewText(node, 0, errmsg); node = mxmlNewElement(status, "records"); mxmlNewText(node, 0, "0"); node = mxmlNewElement(status, "totalrecords"); mxmlNewText(node, 0, "0"); evbuf = mxml_to_evbuf(reply); mxmlDelete(reply); if (!evbuf) { httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error"); return; } headers = evhttp_request_get_output_headers(req); evhttp_add_header(headers, "Content-Type", "text/xml; charset=utf-8"); evhttp_add_header(headers, "Connection", "close"); httpd_send_reply(req, HTTP_OK, "OK", evbuf, HTTPD_SEND_NO_GZIP); evbuffer_free(evbuf); } static int query_params_set(struct query_params *qp, struct httpd_request *hreq) { const char *param; int ret; qp->offset = 0; param = evhttp_find_header(hreq->query, "offset"); if (param) { ret = safe_atoi32(param, &qp->offset); if (ret < 0) { rsp_send_error(hreq->req, "Invalid offset"); return -1; } } qp->limit = 0; param = evhttp_find_header(hreq->query, "limit"); if (param) { ret = safe_atoi32(param, &qp->limit); if (ret < 0) { rsp_send_error(hreq->req, "Invalid limit"); return -1; } } if (qp->offset || qp->limit) qp->idx_type = I_SUB; else qp->idx_type = I_NONE; param = evhttp_find_header(hreq->query, "query"); if (param) { DPRINTF(E_DBG, L_RSP, "RSP browse query filter: %s\n", param); qp->filter = rsp_query_parse_sql(param); if (!qp->filter) DPRINTF(E_LOG, L_RSP, "Ignoring improper RSP query\n"); } return 0; } static void rsp_send_reply(struct evhttp_request *req, mxml_node_t *reply) { struct evbuffer *evbuf; struct evkeyvalq *headers; evbuf = mxml_to_evbuf(reply); mxmlDelete(reply); if (!evbuf) { rsp_send_error(req, "Could not finalize reply"); return; } headers = evhttp_request_get_output_headers(req); evhttp_add_header(headers, "Content-Type", "text/xml; charset=utf-8"); evhttp_add_header(headers, "Connection", "close"); httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0); evbuffer_free(evbuf); } static int rsp_request_authorize(struct httpd_request *hreq) { char *passwd; int ret; if (peer_address_is_trusted(hreq->peer_address)) return 0; passwd = cfg_getstr(cfg_getsec(cfg, "library"), "password"); if (!passwd) return 0; DPRINTF(E_DBG, L_RSP, "Checking authentication for library\n"); // We don't care about the username ret = httpd_basic_auth(hreq->req, NULL, passwd, cfg_getstr(cfg_getsec(cfg, "library"), "name")); if (ret != 0) { DPRINTF(E_LOG, L_RSP, "Unsuccessful library authorization attempt from '%s'\n", hreq->peer_address); return -1; } return 0; } /* --------------------------- REPLY HANDLERS ------------------------------- */ static int rsp_reply_info(struct httpd_request *hreq) { mxml_node_t *reply; mxml_node_t *status; mxml_node_t *info; mxml_node_t *node; cfg_t *lib; char *library; uint32_t songcount; db_files_get_count(&songcount, NULL, NULL); lib = cfg_getsec(cfg, "library"); library = cfg_getstr(lib, "name"); /* We'd use mxmlNewXML(), but then we can't put any attributes * on the root node and we need some. */ reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT); node = mxmlNewElement(reply, "response"); status = mxmlNewElement(node, "status"); info = mxmlNewElement(node, "info"); /* Status block */ node = mxmlNewElement(status, "errorcode"); mxmlNewText(node, 0, "0"); node = mxmlNewElement(status, "errorstring"); mxmlNewText(node, 0, ""); node = mxmlNewElement(status, "records"); mxmlNewText(node, 0, "0"); node = mxmlNewElement(status, "totalrecords"); mxmlNewText(node, 0, "0"); /* Info block */ node = mxmlNewElement(info, "count"); mxmlNewTextf(node, 0, "%d", (int)songcount); node = mxmlNewElement(info, "rsp-version"); mxmlNewText(node, 0, RSP_VERSION); node = mxmlNewElement(info, "server-version"); mxmlNewText(node, 0, VERSION); node = mxmlNewElement(info, "name"); mxmlNewText(node, 0, library); rsp_send_reply(hreq->req, reply); return 0; } static int rsp_reply_db(struct httpd_request *hreq) { struct query_params qp; struct db_playlist_info dbpli; char **strval; mxml_node_t *reply; mxml_node_t *status; mxml_node_t *pls; mxml_node_t *pl; mxml_node_t *node; int i; int ret; memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_PL; qp.idx_type = I_NONE; ret = db_query_start(&qp); if (ret < 0) { DPRINTF(E_LOG, L_RSP, "Could not start query\n"); rsp_send_error(hreq->req, "Could not start query"); return -1; } /* We'd use mxmlNewXML(), but then we can't put any attributes * on the root node and we need some. */ reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT); node = mxmlNewElement(reply, "response"); status = mxmlNewElement(node, "status"); pls = mxmlNewElement(node, "playlists"); /* Status block */ node = mxmlNewElement(status, "errorcode"); mxmlNewText(node, 0, "0"); node = mxmlNewElement(status, "errorstring"); mxmlNewText(node, 0, ""); node = mxmlNewElement(status, "records"); mxmlNewTextf(node, 0, "%d", qp.results); node = mxmlNewElement(status, "totalrecords"); mxmlNewTextf(node, 0, "%d", qp.results); /* Playlists block (all playlists) */ while (((ret = db_query_fetch_pl(&qp, &dbpli)) == 0) && (dbpli.id)) { /* Playlist block (one playlist) */ pl = mxmlNewElement(pls, "playlist"); for (i = 0; pl_fields[i].field; i++) { if (pl_fields[i].flags & F_FULL) { strval = (char **) ((char *)&dbpli + pl_fields[i].offset); node = mxmlNewElement(pl, pl_fields[i].field); mxmlNewText(node, 0, *strval); } } } if (ret < 0) { DPRINTF(E_LOG, L_RSP, "Error fetching results\n"); mxmlDelete(reply); db_query_end(&qp); rsp_send_error(hreq->req, "Error fetching query results"); return -1; } /* HACK * Add a dummy empty string to the playlists element if there is no data * to return - this prevents mxml from sending out an empty * tag that the SoundBridge does not handle. It's hackish, but it works. */ if (qp.results == 0) mxmlNewText(pls, 0, ""); db_query_end(&qp); rsp_send_reply(hreq->req, reply); return 0; } static int rsp_reply_playlist(struct httpd_request *hreq) { struct query_params qp; struct db_media_file_info dbmfi; struct evkeyvalq *headers; const char *param; const char *ua; const char *client_codecs; char **strval; mxml_node_t *reply; mxml_node_t *status; mxml_node_t *items; mxml_node_t *item; mxml_node_t *node; int mode; int records; int transcode; int32_t bitrate; int i; int ret; memset(&qp, 0, sizeof(struct query_params)); ret = safe_atoi32(hreq->uri_parsed->path_parts[2], &qp.id); if (ret < 0) { rsp_send_error(hreq->req, "Invalid playlist ID"); return -1; } if (qp.id == 0) qp.type = Q_ITEMS; else qp.type = Q_PLITEMS; qp.sort = S_NAME; mode = F_FULL; param = evhttp_find_header(hreq->query, "type"); if (param) { if (strcasecmp(param, "full") == 0) mode = F_FULL; else if (strcasecmp(param, "browse") == 0) mode = F_BROWSE; else if (strcasecmp(param, "id") == 0) mode = F_ID; else if (strcasecmp(param, "detailed") == 0) mode = F_DETAILED; else DPRINTF(E_LOG, L_RSP, "Unknown browse mode %s\n", param); } ret = query_params_set(&qp, hreq); if (ret < 0) return -1; ret = db_query_start(&qp); if (ret < 0) { DPRINTF(E_LOG, L_RSP, "Could not start query\n"); rsp_send_error(hreq->req, "Could not start query"); if (qp.filter) free(qp.filter); return -1; } if (qp.offset > qp.results) records = 0; else records = qp.results - qp.offset; if (qp.limit && (records > qp.limit)) records = qp.limit; /* We'd use mxmlNewXML(), but then we can't put any attributes * on the root node and we need some. */ reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT); node = mxmlNewElement(reply, "response"); status = mxmlNewElement(node, "status"); items = mxmlNewElement(node, "items"); /* Status block */ node = mxmlNewElement(status, "errorcode"); mxmlNewText(node, 0, "0"); node = mxmlNewElement(status, "errorstring"); mxmlNewText(node, 0, ""); node = mxmlNewElement(status, "records"); mxmlNewTextf(node, 0, "%d", records); node = mxmlNewElement(status, "totalrecords"); mxmlNewTextf(node, 0, "%d", qp.results); /* Items block (all items) */ while (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id)) { headers = evhttp_request_get_input_headers(hreq->req); ua = evhttp_find_header(headers, "User-Agent"); client_codecs = evhttp_find_header(headers, "Accept-Codecs"); transcode = transcode_needed(ua, client_codecs, dbmfi.codectype); /* Item block (one item) */ item = mxmlNewElement(items, "item"); for (i = 0; rsp_fields[i].field; i++) { if (!(rsp_fields[i].flags & mode)) continue; strval = (char **) ((char *)&dbmfi + rsp_fields[i].offset); if (!(*strval) || (strlen(*strval) == 0)) continue; node = mxmlNewElement(item, rsp_fields[i].field); if (!transcode) mxmlNewText(node, 0, *strval); else { switch (rsp_fields[i].offset) { case dbmfi_offsetof(type): mxmlNewText(node, 0, "wav"); break; case dbmfi_offsetof(bitrate): bitrate = 0; ret = safe_atoi32(dbmfi.samplerate, &bitrate); if ((ret < 0) || (bitrate == 0)) bitrate = 1411; else bitrate = (bitrate * 8) / 250; mxmlNewTextf(node, 0, "%d", bitrate); break; case dbmfi_offsetof(description): mxmlNewText(node, 0, "wav audio file"); break; case dbmfi_offsetof(codectype): mxmlNewText(node, 0, "wav"); node = mxmlNewElement(item, "original_codec"); mxmlNewText(node, 0, *strval); break; default: mxmlNewText(node, 0, *strval); break; } } } } if (qp.filter) free(qp.filter); if (ret < 0) { DPRINTF(E_LOG, L_RSP, "Error fetching results\n"); mxmlDelete(reply); db_query_end(&qp); rsp_send_error(hreq->req, "Error fetching query results"); return -1; } /* HACK * Add a dummy empty string to the items element if there is no data * to return - this prevents mxml from sending out an empty * tag that the SoundBridge does not handle. It's hackish, but it works. */ if (qp.results == 0) mxmlNewText(items, 0, ""); db_query_end(&qp); rsp_send_reply(hreq->req, reply); return 0; } static int rsp_reply_browse(struct httpd_request *hreq) { struct query_params qp; char *browse_item; mxml_node_t *reply; mxml_node_t *status; mxml_node_t *items; mxml_node_t *node; int records; int ret; memset(&qp, 0, sizeof(struct query_params)); if (strcmp(hreq->uri_parsed->path_parts[3], "artist") == 0) { qp.type = Q_BROWSE_ARTISTS; } else if (strcmp(hreq->uri_parsed->path_parts[3], "genre") == 0) { qp.type = Q_BROWSE_GENRES; } else if (strcmp(hreq->uri_parsed->path_parts[3], "album") == 0) { qp.type = Q_BROWSE_ALBUMS; } else if (strcmp(hreq->uri_parsed->path_parts[3], "composer") == 0) { qp.type = Q_BROWSE_COMPOSERS; } else { DPRINTF(E_LOG, L_RSP, "Unsupported browse type '%s'\n", hreq->uri_parsed->path_parts[3]); rsp_send_error(hreq->req, "Unsupported browse type"); return -1; } ret = safe_atoi32(hreq->uri_parsed->path_parts[2], &qp.id); if (ret < 0) { rsp_send_error(hreq->req, "Invalid playlist ID"); return -1; } ret = query_params_set(&qp, hreq); if (ret < 0) return -1; ret = db_query_start(&qp); if (ret < 0) { DPRINTF(E_LOG, L_RSP, "Could not start query\n"); rsp_send_error(hreq->req, "Could not start query"); if (qp.filter) free(qp.filter); return -1; } if (qp.offset > qp.results) records = 0; else records = qp.results - qp.offset; if (qp.limit && (records > qp.limit)) records = qp.limit; /* We'd use mxmlNewXML(), but then we can't put any attributes * on the root node and we need some. */ reply = mxmlNewElement(MXML_NO_PARENT, RSP_XML_ROOT); node = mxmlNewElement(reply, "response"); status = mxmlNewElement(node, "status"); items = mxmlNewElement(node, "items"); /* Status block */ node = mxmlNewElement(status, "errorcode"); mxmlNewText(node, 0, "0"); node = mxmlNewElement(status, "errorstring"); mxmlNewText(node, 0, ""); node = mxmlNewElement(status, "records"); mxmlNewTextf(node, 0, "%d", records); node = mxmlNewElement(status, "totalrecords"); mxmlNewTextf(node, 0, "%d", qp.results); /* Items block (all items) */ while (((ret = db_query_fetch_string(&qp, &browse_item)) == 0) && (browse_item)) { node = mxmlNewElement(items, "item"); mxmlNewText(node, 0, browse_item); } if (qp.filter) free(qp.filter); if (ret < 0) { DPRINTF(E_LOG, L_RSP, "Error fetching results\n"); mxmlDelete(reply); db_query_end(&qp); rsp_send_error(hreq->req, "Error fetching query results"); return -1; } /* HACK * Add a dummy empty string to the items element if there is no data * to return - this prevents mxml from sending out an empty * tag that the SoundBridge does not handle. It's hackish, but it works. */ if (qp.results == 0) mxmlNewText(items, 0, ""); db_query_end(&qp); rsp_send_reply(hreq->req, reply); return 0; } static int rsp_stream(struct httpd_request *hreq) { int id; int ret; ret = safe_atoi32(hreq->uri_parsed->path_parts[2], &id); if (ret < 0) { httpd_send_error(hreq->req, HTTP_BADREQUEST, "Bad Request"); return -1; } httpd_stream_file(hreq->req, id); return 0; } static struct httpd_uri_map rsp_handlers[] = { { .regexp = "^/rsp/info$", .handler = rsp_reply_info }, { .regexp = "^/rsp/db$", .handler = rsp_reply_db }, { .regexp = "^/rsp/db/[[:digit:]]+$", .handler = rsp_reply_playlist }, { .regexp = "^/rsp/db/[[:digit:]]+/[^/]+$", .handler = rsp_reply_browse }, { .regexp = "^/rsp/stream/[[:digit:]]+$", .handler = rsp_stream }, { .regexp = NULL, .handler = NULL } }; /* -------------------------------- RSP API --------------------------------- */ void rsp_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed) { struct httpd_request *hreq; int ret; DPRINTF(E_DBG, L_RSP, "RSP request: '%s'\n", uri_parsed->uri); hreq = httpd_request_parse(req, uri_parsed, NULL, rsp_handlers); if (!hreq) { DPRINTF(E_LOG, L_RSP, "Unrecognized path '%s' in RSP request: '%s'\n", uri_parsed->path, uri_parsed->uri); rsp_send_error(req, "Server error"); return; } ret = rsp_request_authorize(hreq); if (ret < 0) { rsp_send_error(req, "Access denied"); free(hreq); return; } hreq->handler(hreq); free(hreq); } int rsp_is_request(const char *path) { if (strncmp(path, "/rsp/", strlen("/rsp/")) == 0) return 1; return 0; } int rsp_init(void) { char buf[64]; int i; int ret; for (i = 0; rsp_handlers[i].handler; i++) { ret = regcomp(&rsp_handlers[i].preg, rsp_handlers[i].regexp, REG_EXTENDED | REG_NOSUB); if (ret != 0) { regerror(ret, &rsp_handlers[i].preg, buf, sizeof(buf)); DPRINTF(E_FATAL, L_RSP, "RSP init failed; regexp error: %s\n", buf); return -1; } } return 0; } void rsp_deinit(void) { int i; for (i = 0; rsp_handlers[i].handler; i++) regfree(&rsp_handlers[i].preg); }