Merge branch 'spwebapi2'

This commit is contained in:
ejurgensen 2016-11-27 22:54:06 +01:00
commit 3921cf5732
15 changed files with 1260 additions and 385 deletions

View File

@ -435,13 +435,14 @@ curl "http://localhost:3689/logout?session-id=50"
## Spotify
forked-daapd has *some* support for Spotify. It must be compiled with the
`--enable-spotify option` (see
forked-daapd has support for playback of the tracks in your Spotify library. It
must have been compiled with the `--enable-spotify` option (see
[INSTALL](https://github.com/ejurgensen/forked-daapd/blob/master/INSTALL)).
You must have also have libspotify installed, otherwise the Spotify integration
will not be available. You can get libspotify here:
You must also have libspotify installed, otherwise Spotify integration will not
be available. Unfortunately the library is no longer available from Spotify, and
at the time of writing they have not provided an alternative. You can, however,
still get libspotify here:
- Original (binary) tar.gz, see https://developer.spotify.com
- Debian package (libspotify-dev), see https://apt.mopidy.com
You must also have a Spotify premium account. If you normally log into Spotify
@ -449,26 +450,31 @@ with your Facebook account you must first go to Spotify's web site where you can
get the Spotify username and password that matches your account. With
forked-daapd you cannot login into Spotify with your Facebook username/password.
The procedure for logging in to Spotify is very much like the Remote pairing
procedure. You must prepare a file, which should have the ending ".spotify".
The file must have two lines: The first is your Spotify user name, and the
second is your password. Move the file to your forked-daapd library.
Forked-daapd will then log in and add all the music in your Spotify playlists
to its database.
The procedure for logging in to Spotify is a two-step procedure due to the
current state of libspotify:
1. Put a file in your forked-daapd library containing two lines, the first being
your Spotify user name, and the second your password. The filename must have
the ending ".spotify"
2. Delete the file again - forked-daapd will have read it.
3. forked-daapd will log in and add all music in your Spotify playlists to its
database. Wait until completed (follow progress in the log file).
4. In a browser, go to http://forked-daapd.local:3689/oauth and click the link
to authorize forked-daapd with Spotify.
Spotify will automatically notify forked-daapd about playlist updates, so you
should not need to restart forked-daapd to syncronize with Spotify.
should not need to restart forked-daapd to syncronize with Spotify. However,
Spotify only notifies about playlist updates, not new saved tracks/albums, so
you need to repeat step 4 above to load those.
For safety you should delete the ".spotify" file after first login. Forked-daapd
will not store your password, but will still be able to log you in automatically
afterwards, because libspotify saves a login token. You can configure the
location of your Spotify user data in the configuration file.
Forked-daapd will not store your password, but will still be able to log you in
automatically afterwards, because libspotify saves a login token. You can
configure the location of your Spotify user data in the configuration file.
To permanently logout and remove credentials, delete the contents of
`/var/cache/forked-daapd/libspotify` (while forked-daapd is stopped).
Limitations: You will only be able to play tracks from your Spotify playlists,
so you can't search and listen to music from the rest of the Spotify catalogue.
Limitations:
You will not be able to do any playlist management through forked-daapd - use
a Spotify client for that. You also can only listen to your music by letting
forked-daapd do the playback - so that means you can't stream from forked-daapd

View File

@ -159,63 +159,6 @@ AC_CHECK_HEADERS(getopt.h,,)
AC_CHECK_HEADERS(stdint.h,,)
dnl --- Begin configuring the options ---
dnl iTunes playlists with libplist
AC_ARG_ENABLE(itunes, AS_HELP_STRING([--enable-itunes], [enable iTunes Music Library XML support (default=no)]))
AS_IF([test "x$enable_itunes" = "xyes"], [
AC_DEFINE(ITUNES, 1, [Define to 1 to enable iTunes XML support])
PKG_CHECK_MODULES(LIBPLIST, [ libplist >= 0.16 ])
])
AM_CONDITIONAL(COND_ITUNES, [test "x$enable_itunes" = "xyes"])
dnl Spotify with dynamic linking to libspotify
AC_ARG_ENABLE(spotify, AS_HELP_STRING([--enable-spotify], [enable Spotify support (default=no)]))
AS_IF([test "x$enable_spotify" = "xyes"], [
AC_DEFINE(SPOTIFY, 1, [Define to 1 to enable Spotify support])
AC_CHECK_HEADER(libspotify/api.h, , AC_MSG_ERROR([libspotify/api.h not found]))
AC_DEFINE(HAVE_SPOTIFY_H, 1, [Define to 1 if you have the <libspotify/api.h> header file.])
dnl Don't link to libspotify, but instead enable dynamic linking
SPOTIFY_CFLAGS="-rdynamic"
SPOTIFY_LIBS="-ldl"
AC_SUBST(SPOTIFY_CFLAGS)
AC_SUBST(SPOTIFY_LIBS)
])
AM_CONDITIONAL(COND_SPOTIFY, [test "x$enable_spotify" = "xyes"])
dnl LastFM support with libcurl
AC_ARG_ENABLE(lastfm, AS_HELP_STRING([--enable-lastfm], [enable LastFM support (default=no)]))
AS_IF([test "x$enable_lastfm" = "xyes"], [
AC_DEFINE(LASTFM, 1, [Define to 1 to enable LastFM support])
PKG_CHECK_MODULES(LIBCURL, [ libcurl ])
AC_CHECK_LIB([mxml], [mxmlGetOpaque], AC_DEFINE(HAVE_MXML_GETOPAQUE, 1, [Define to 1 if your mxml has mxmlGetOpaque.]))
])
AM_CONDITIONAL(COND_LASTFM, [test "x$enable_lastfm" = "xyes"])
dnl ChromeCast support with libprotobuf-c
AC_ARG_ENABLE(chromecast, AS_HELP_STRING([--enable-chromecast], [enable ChromeCast support (default=no)]))
AS_IF([test "x$enable_chromecast" = "xyes"], [
AC_DEFINE(CHROMECAST, 1, [Define to 1 to enable Chromecast support])
PKG_CHECK_MODULES(LIBPROTOBUF_C, [ libprotobuf-c >= 1.0.0 ], , [ protobuf_old="yes" ])
PKG_CHECK_MODULES(GNUTLS, [ gnutls ])
PKG_CHECK_EXISTS([ json-c >= 0.11 ],
[ PKG_CHECK_MODULES(JSON_C, [ json-c ]) ],
[ PKG_CHECK_MODULES(JSON_C, [ json ], AC_DEFINE(HAVE_JSON_C_OLD, 1, [Define 1 to if you have json-c < 0.11])) ]
)
])
AS_IF([test "x$protobuf_old" = "xyes"], [
AC_DEFINE(HAVE_PROTOBUF_OLD, 1, [Define to 1 if you have libprotobuf < 1.0.0])
LDFLAGS="${LDFLAGS} -lprotobuf-c"
])
AM_CONDITIONAL(COND_CHROMECAST, [test "x$enable_chromecast" = "xyes"])
AM_CONDITIONAL(COND_PROTOBUF_OLD, [test "x$protobuf_old" = "xyes"])
dnl MPD support
AC_ARG_ENABLE(mpd, AS_HELP_STRING([--disable-mpd], [disable MPD client protocol support (default=no)]))
AS_IF([test "x$enable_mpd" != "xno"], [
AC_DEFINE(MPD, 1, [Define to 1 to enable MPD support])
])
AM_CONDITIONAL(COND_MPD, [test "x$enable_mpd" != "xno"])
dnl ALSA
AC_ARG_WITH(alsa, AS_HELP_STRING([--without-alsa], [without ALSA support (default=no)]))
AS_IF([test "x$with_alsa" != "xno"], [
@ -234,6 +177,78 @@ AS_IF([test "x$with_pulseaudio" = "xyes"], [
)
])
AM_CONDITIONAL(COND_PULSEAUDIO, [test "x$with_pulseaudio" = "xyes"])
dnl Build with libcurl
AC_ARG_WITH([libcurl], AS_HELP_STRING([--without-libcurl], [without libcurl (default=no)]))
AS_IF([test "x$with_libcurl" != "xno"], [
AC_DEFINE(HAVE_LIBCURL, 1, [Define to 1 to build with libcurl])
PKG_CHECK_MODULES(LIBCURL, [ libcurl ])
])
dnl Build with json-c
AC_ARG_WITH([json], AS_HELP_STRING([--without-json-c], [without json-c (default=no)]))
AS_IF([test "x$with_json" != "xno"], [
AC_DEFINE(HAVE_JSON, 1, [Define to 1 to build with json-c])
PKG_CHECK_EXISTS([ json-c >= 0.11 ],
[ PKG_CHECK_MODULES(JSON_C, [ json-c ]) ],
[ PKG_CHECK_MODULES(JSON_C, [ json ], AC_DEFINE(HAVE_JSON_C_OLD, 1, [Define 1 to if you have json-c < 0.11])) ]
)
])
dnl iTunes playlists with libplist
AC_ARG_ENABLE(itunes, AS_HELP_STRING([--enable-itunes], [enable iTunes Music Library XML support (default=no)]))
AS_IF([test "x$enable_itunes" = "xyes"], [
AC_DEFINE(ITUNES, 1, [Define to 1 to enable iTunes XML support])
PKG_CHECK_MODULES(LIBPLIST, [ libplist >= 0.16 ])
])
AM_CONDITIONAL(COND_ITUNES, [test "x$enable_itunes" = "xyes"])
dnl Spotify with dynamic linking to libspotify
AC_ARG_ENABLE(spotify, AS_HELP_STRING([--enable-spotify], [enable Spotify support (default=no)]))
AS_IF([test "x$enable_spotify" = "xyes"], [
AC_DEFINE(SPOTIFY, 1, [Define to 1 to enable Spotify support])
AS_IF([test "x$with_json" = "xno"], AC_MSG_ERROR([Spotify support requires json-c]))
AC_CHECK_HEADER(libspotify/api.h, , AC_MSG_ERROR([libspotify/api.h not found]))
AC_DEFINE(HAVE_SPOTIFY_H, 1, [Define to 1 if you have the <libspotify/api.h> header file.])
dnl Don't link to libspotify, but instead enable dynamic linking
SPOTIFY_CFLAGS="-rdynamic"
SPOTIFY_LIBS="-ldl"
AC_SUBST(SPOTIFY_CFLAGS)
AC_SUBST(SPOTIFY_LIBS)
])
AM_CONDITIONAL(COND_SPOTIFY, [test "x$enable_spotify" = "xyes"])
dnl LastFM support with libcurl
AC_ARG_ENABLE(lastfm, AS_HELP_STRING([--enable-lastfm], [enable LastFM support (default=no)]))
AS_IF([test "x$enable_lastfm" = "xyes"], [
AC_DEFINE(LASTFM, 1, [Define to 1 to enable LastFM support])
AS_IF([test "x$with_libcurl" = "xno"], AC_MSG_ERROR([LastFM support requires libcurl]))
AC_CHECK_LIB([mxml], [mxmlGetOpaque], AC_DEFINE(HAVE_MXML_GETOPAQUE, 1, [Define to 1 if your mxml has mxmlGetOpaque.]))
])
AM_CONDITIONAL(COND_LASTFM, [test "x$enable_lastfm" = "xyes"])
dnl ChromeCast support with libprotobuf-c
AC_ARG_ENABLE(chromecast, AS_HELP_STRING([--enable-chromecast], [enable ChromeCast support (default=no)]))
AS_IF([test "x$enable_chromecast" = "xyes"], [
AC_DEFINE(CHROMECAST, 1, [Define to 1 to enable Chromecast support])
AS_IF([test "x$with_json" = "xno"], AC_MSG_ERROR([Chromecast support requires json-c]))
PKG_CHECK_MODULES(LIBPROTOBUF_C, [ libprotobuf-c >= 1.0.0 ], , [ protobuf_old="yes" ])
PKG_CHECK_MODULES(GNUTLS, [ gnutls ])
])
AS_IF([test "x$protobuf_old" = "xyes"], [
AC_DEFINE(HAVE_PROTOBUF_OLD, 1, [Define to 1 if you have libprotobuf < 1.0.0])
LDFLAGS="${LDFLAGS} -lprotobuf-c"
])
AM_CONDITIONAL(COND_CHROMECAST, [test "x$enable_chromecast" = "xyes"])
AM_CONDITIONAL(COND_PROTOBUF_OLD, [test "x$protobuf_old" = "xyes"])
dnl MPD support
AC_ARG_ENABLE(mpd, AS_HELP_STRING([--disable-mpd], [disable MPD client protocol support (default=no)]))
AS_IF([test "x$enable_mpd" != "xno"], [
AC_DEFINE(MPD, 1, [Define to 1 to enable MPD support])
])
AM_CONDITIONAL(COND_MPD, [test "x$enable_mpd" != "xno"])
dnl --- End options ---
dnl Checks for header files.

View File

@ -1129,8 +1129,8 @@ source_item_stream_get(struct artwork_ctx *ctx)
memset(&client, 0, sizeof(struct http_client_ctx));
client.url = url;
client.headers = kv;
client.body = ctx->evbuf;
client.input_headers = kv;
client.input_body = ctx->evbuf;
if (http_client_request(&client) < 0)
goto out_kv;

View File

@ -3906,7 +3906,7 @@ db_spotify_purge(void)
ret = db_query_run(queries[i], 0, 1);
if (ret == 0)
DPRINTF(E_DBG, L_DB, "Purged %d rows\n", sqlite3_changes(hdl));
DPRINTF(E_DBG, L_DB, "Processed %d rows\n", sqlite3_changes(hdl));
}
// Disable the spotify directory by setting 'disabled' to INOTIFY_FAKE_COOKIE value
@ -3950,7 +3950,7 @@ db_spotify_pl_delete(int id)
/* Spotify */
void
db_spotify_files_delete()
db_spotify_files_delete(void)
{
#define Q_TMPL "DELETE FROM files WHERE path LIKE 'spotify:%%' AND NOT path IN (SELECT filepath FROM playlistitems);"
char *query;

View File

@ -620,7 +620,7 @@ void
db_spotify_pl_delete(int id);
void
db_spotify_files_delete();
db_spotify_files_delete(void);
#endif
/* Admin */

View File

@ -1222,11 +1222,8 @@ bulk_scan(int flags)
else
{
/* Protect spotify from the imminent purge if rescanning */
if (flags & F_SCAN_RESCAN)
{
db_file_ping_bymatch("spotify:", 0);
db_pl_ping_bymatch("spotify:", 0);
}
DPRINTF(E_DBG, L_SCAN, "Purging old database content\n");
db_purge_cruft(start);

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2015 Espen Jürgensen <espenjurgensen@gmail.com>
* Copyright (C) 2016 Espen Jürgensen <espenjurgensen@gmail.com>
*
* 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
@ -36,6 +36,10 @@
#include <event2/event.h>
#ifdef HAVE_LIBCURL
#include <curl/curl.h>
#endif
#include "http.h"
#include "logger.h"
#include "misc.h"
@ -128,10 +132,10 @@ request_cb(struct evhttp_request *req, void *arg)
ctx->ret = 0;
if (ctx->headers)
headers_save(ctx->headers, evhttp_request_get_input_headers(req));
if (ctx->body)
evbuffer_add_buffer(ctx->body, evhttp_request_get_input_buffer(req));
if (ctx->input_headers)
headers_save(ctx->input_headers, evhttp_request_get_input_headers(req));
if (ctx->input_body)
evbuffer_add_buffer(ctx->input_body, evhttp_request_get_input_buffer(req));
event_base_loopbreak(ctx->evbase);
@ -171,8 +175,8 @@ request_header_cb(struct evhttp_request *req, void *arg)
}
#endif
int
http_client_request(struct http_client_ctx *ctx)
static int
http_client_request_impl(struct http_client_ctx *ctx)
{
struct evhttp_connection *evcon;
struct evhttp_request *req;
@ -269,6 +273,144 @@ http_client_request(struct http_client_ctx *ctx)
return ctx->ret;
}
#ifdef HAVE_LIBCURL
static size_t
curl_request_cb(char *ptr, size_t size, size_t nmemb, void *userdata)
{
size_t realsize;
struct http_client_ctx *ctx;
int ret;
realsize = size * nmemb;
ctx = (struct http_client_ctx *)userdata;
if (!ctx->input_body)
return realsize;
ret = evbuffer_add(ctx->input_body, ptr, realsize);
if (ret < 0)
{
DPRINTF(E_LOG, L_HTTP, "Error adding reply from %s to input buffer\n", ctx->url);
return 0;
}
return realsize;
}
static int
https_client_request_impl(struct http_client_ctx *ctx)
{
CURL *curl;
CURLcode res;
struct curl_slist *headers;
struct onekeyval *okv;
char header[1024];
curl = curl_easy_init();
if (!curl)
{
DPRINTF(E_LOG, L_HTTP, "Error: Could not get curl handle\n");
return -1;
}
curl_easy_setopt(curl, CURLOPT_URL, ctx->url);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "forked-daapd/" VERSION);
if (ctx->output_headers)
{
headers = NULL;
for (okv = ctx->output_headers->head; okv; okv = okv->next)
{
snprintf(header, sizeof(header), "%s: %s", okv->name, okv->value);
headers = curl_slist_append(headers, header);
}
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
}
if (ctx->output_body)
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, ctx->output_body);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, HTTP_CLIENT_TIMEOUT);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_request_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, ctx);
/* Make request */
DPRINTF(E_INFO, L_HTTP, "Making request for %s\n", ctx->url);
res = curl_easy_perform(curl);
if (res != CURLE_OK)
{
DPRINTF(E_LOG, L_HTTP, "Request to %s failed: %s\n", ctx->url, curl_easy_strerror(res));
curl_easy_cleanup(curl);
return -1;
}
curl_easy_cleanup(curl);
return 0;
}
#endif /* HAVE_LIBCURL */
int
http_client_request(struct http_client_ctx *ctx)
{
if (strncmp(ctx->url, "http:", strlen("http:")) == 0)
return http_client_request_impl(ctx);
#ifdef HAVE_LIBCURL
if (strncmp(ctx->url, "https:", strlen("https:")) == 0)
return https_client_request_impl(ctx);
#endif
DPRINTF(E_LOG, L_HTTP, "Request for %s is not supported (not built with libcurl?)\n", ctx->url);
return -1;
}
char *
http_form_urlencode(struct keyval *kv)
{
struct evbuffer *evbuf;
struct onekeyval *okv;
char *body;
char *k;
char *v;
evbuf = evbuffer_new();
for (okv = kv->head; okv; okv = okv->next)
{
k = evhttp_encode_uri(okv->name);
if (!k)
continue;
v = evhttp_encode_uri(okv->value);
if (!v)
{
free(k);
continue;
}
evbuffer_add_printf(evbuf, "%s=%s", k, v);
if (okv->next)
evbuffer_add_printf(evbuf, "&");
free(k);
free(v);
}
evbuffer_add(evbuf, "\n", 1);
body = evbuffer_readln(evbuf, NULL, EVBUFFER_EOL_ANY);
evbuffer_free(evbuf);
DPRINTF(E_DBG, L_HTTP, "Parameters in request are: %s\n", body);
return body;
}
int
http_stream_setup(char **stream, const char *url)
{
@ -296,7 +438,7 @@ http_stream_setup(char **stream, const char *url)
return -1;
ctx.url = url;
ctx.body = evbuf;
ctx.input_body = evbuf;
ret = http_client_request(&ctx);
if (ret < 0)
@ -308,13 +450,13 @@ http_stream_setup(char **stream, const char *url)
}
// Pad with CRLF because evbuffer_readln() might not read the last line otherwise
evbuffer_add(ctx.body, "\r\n", 2);
evbuffer_add(ctx.input_body, "\r\n", 2);
/* Read the playlist until the first stream link is found, but give up if
* nothing is found in the first 10 lines
*/
n = 0;
while ((line = evbuffer_readln(ctx.body, NULL, EVBUFFER_EOL_ANY)) && (n < 10))
while ((line = evbuffer_readln(ctx.input_body, NULL, EVBUFFER_EOL_ANY)) && (n < 10))
{
n++;
if (strncasecmp(line, "http://", strlen("http://")) == 0)
@ -328,7 +470,7 @@ http_stream_setup(char **stream, const char *url)
free(line);
}
evbuffer_free(ctx.body);
evbuffer_free(ctx.input_body);
if (n != -1)
{

View File

@ -10,13 +10,18 @@
struct http_client_ctx
{
/* Destination URL, header and body of outgoing request body (headers and
* body is currently only supported for https)
*/
const char *url;
struct keyval *output_headers;
char *output_body;
/* A keyval/evbuf to store response headers and body.
* Can be set to NULL to ignore that part of the response.
*/
struct keyval *headers;
struct evbuffer *body;
struct keyval *input_headers;
struct evbuffer *input_body;
/* Cut the connection after the headers have been received
* Used for getting Shoutcast/ICY headers for old versions of libav/ffmpeg
@ -47,15 +52,27 @@ struct http_icy_metadata
};
/* Generic HTTP client. No support for https.
/* Make a http(s) request. We use libcurl to make https requests. We could use
* libevent and avoid the dependency, but for SSL, libevent needs to be v2.1
* or better, which is still a bit too new to be in the major distros.
*
* @param ctx HTTP request params, see above
* @return 0 if successful, -1 if an error occurred
* @return 0 if successful, -1 if an error occurred (e.g. no libcurl)
*/
int
http_client_request(struct http_client_ctx *ctx);
/* Converts the keyval dictionary to a application/x-www-form-urlencoded string.
* The values will be uri_encoded. Example output: "key1=foo%20bar&key2=123".
*
* @param kv is the struct containing the parameters
* @return encoded string if succesful, NULL if an error occurred
*/
char *
http_form_urlencode(struct keyval *kv);
/* Returns a newly allocated string with the first stream in the m3u given in
* url. If url is not a m3u, the string will be a copy of url.
*

View File

@ -64,6 +64,9 @@
#ifdef LASTFM
# include "lastfm.h"
#endif
#ifdef HAVE_SPOTIFY_H
# include "spotify.h"
#endif
/*
* HTTP client quirks by User-Agent, from mt-daapd
@ -140,6 +143,7 @@ static struct evhttp *evhttpd;
static pthread_t tid_httpd;
static char *allow_origin;
static int httpd_port;
#ifdef HAVE_LIBEVENT2_OLD
struct stream_ctx *g_st;
@ -198,6 +202,61 @@ scrobble_cb(void *arg)
}
#endif
static void
oauth_interface(struct evhttp_request *req, const char *uri)
{
struct evbuffer *evbuf;
struct evkeyvalq query;
const char *req_uri;
const char *ptr;
char redirect_uri[256];
int ret;
req_uri = evhttp_request_get_uri(req);
evbuf = evbuffer_new();
if (!evbuf)
{
DPRINTF(E_LOG, L_HTTPD, "Could not alloc evbuf for oauth\n");
return;
}
evbuffer_add_printf(evbuf, "<H1>forked-daapd oauth</H1>\n\n");
memset(&query, 0, sizeof(struct evkeyvalq));
ptr = strchr(req_uri, '?');
if (ptr)
{
ret = evhttp_parse_query_str(ptr + 1, &query);
if (ret < 0)
{
evbuffer_add_printf(evbuf, "OAuth error: Could not parse parameters in callback (%s)\n", req_uri);
httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0);
evbuffer_free(evbuf);
return;
}
}
#ifdef HAVE_SPOTIFY_H
snprintf(redirect_uri, sizeof(redirect_uri), "http://forked-daapd.local:%d/oauth/spotify", httpd_port);
if (strncmp(uri, "/oauth/spotify", strlen("/oauth/spotify")) == 0)
spotify_oauth_callback(evbuf, &query, redirect_uri);
else
spotify_oauth_interface(evbuf, redirect_uri);
#endif
evbuffer_add_printf(evbuf, "<p><i>(sorry about this ugly interface)</i></p>\n");
evhttp_clear_headers(&query);
httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0);
evbuffer_free(evbuf);
}
static void
stream_end_register(struct stream_ctx *st)
{
@ -912,6 +971,12 @@ serve_file(struct evhttp_request *req, char *uri)
}
}
if (strncmp(uri, "/oauth", strlen("/oauth")) == 0)
{
oauth_interface(req, uri);
return;
}
ret = snprintf(path, sizeof(path), "%s%s", WEBFACE_ROOT, uri + 1); /* skip starting '/' */
if ((ret < 0) || (ret >= sizeof(path)))
{
@ -1347,7 +1412,6 @@ int
httpd_init(void)
{
int v6enabled;
unsigned short port;
int ret;
httpd_exit = 0;
@ -1428,7 +1492,7 @@ httpd_init(void)
}
v6enabled = cfg_getbool(cfg_getsec(cfg, "general"), "ipv6");
port = cfg_getint(cfg_getsec(cfg, "library"), "port");
httpd_port = cfg_getint(cfg_getsec(cfg, "library"), "port");
// For CORS headers
allow_origin = cfg_getstr(cfg_getsec(cfg, "general"), "allow_origin");
@ -1442,20 +1506,20 @@ httpd_init(void)
if (v6enabled)
{
ret = evhttp_bind_socket(evhttpd, "::", port);
ret = evhttp_bind_socket(evhttpd, "::", httpd_port);
if (ret < 0)
{
DPRINTF(E_LOG, L_HTTPD, "Could not bind to port %d with IPv6, falling back to IPv4\n", port);
DPRINTF(E_LOG, L_HTTPD, "Could not bind to port %d with IPv6, falling back to IPv4\n", httpd_port);
v6enabled = 0;
}
}
if (!v6enabled)
{
ret = evhttp_bind_socket(evhttpd, "0.0.0.0", port);
ret = evhttp_bind_socket(evhttpd, "0.0.0.0", httpd_port);
if (ret < 0)
{
DPRINTF(E_FATAL, L_HTTPD, "Could not bind to port %d (forked-daapd already running?)\n", port);
DPRINTF(E_FATAL, L_HTTPD, "Could not bind to port %d (forked-daapd already running?)\n", httpd_port);
goto bind_fail;
}
}

View File

@ -39,14 +39,7 @@
#include "lastfm.h"
#include "logger.h"
#include "misc.h"
struct https_client_ctx
{
const char *url;
const char *body;
struct evbuffer *data;
};
#include "http.h"
// LastFM becomes disabled if we get a scrobble, try initialising session,
// but can't (probably no session key in db because user does not use LastFM)
@ -172,51 +165,6 @@ credentials_read(char *path, char **username, char **password)
return 0;
}
/* Converts parameters to a string in application/x-www-form-urlencoded format */
static int
body_print(char **body, struct keyval *kv)
{
struct evbuffer *evbuf;
struct onekeyval *okv;
char *k;
char *v;
evbuf = evbuffer_new();
for (okv = kv->head; okv; okv = okv->next)
{
k = evhttp_encode_uri(okv->name);
if (!k)
continue;
v = evhttp_encode_uri(okv->value);
if (!v)
{
free(k);
continue;
}
evbuffer_add(evbuf, k, strlen(k));
evbuffer_add(evbuf, "=", 1);
evbuffer_add(evbuf, v, strlen(v));
if (okv->next)
evbuffer_add(evbuf, "&", 1);
free(k);
free(v);
}
evbuffer_add(evbuf, "\n", 1);
*body = evbuffer_readln(evbuf, NULL, EVBUFFER_EOL_ANY);
evbuffer_free(evbuf);
DPRINTF(E_DBG, L_LASTFM, "Parameters in request are: %s\n", *body);
return 0;
}
/* Creates an md5 signature of the concatenated parameters and adds it to keyval */
static int
param_sign(struct keyval *kv)
@ -288,28 +236,8 @@ mxmlGetOpaque(mxml_node_t *node) /* I - Node to get */
/* --------------------------------- MAIN --------------------------------- */
static size_t
request_cb(char *ptr, size_t size, size_t nmemb, void *userdata)
{
size_t realsize;
struct https_client_ctx *ctx;
int ret;
realsize = size * nmemb;
ctx = (struct https_client_ctx *)userdata;
ret = evbuffer_add(ctx->data, ptr, realsize);
if (ret < 0)
{
DPRINTF(E_LOG, L_LASTFM, "Error adding reply from LastFM to data buffer\n");
return 0;
}
return realsize;
}
static void
response_proces(struct https_client_ctx *ctx)
response_proces(struct http_client_ctx *ctx)
{
mxml_node_t *tree;
mxml_node_t *s_node;
@ -319,9 +247,9 @@ response_proces(struct https_client_ctx *ctx)
char *sk;
// NULL-terminate the buffer
evbuffer_add(ctx->data, "", 1);
evbuffer_add(ctx->input_body, "", 1);
body = (char *)evbuffer_pullup(ctx->data, -1);
body = (char *)evbuffer_pullup(ctx->input_body, -1);
if (!body || (strlen(body) == 0))
{
DPRINTF(E_LOG, L_LASTFM, "Empty response\n");
@ -380,59 +308,10 @@ response_proces(struct https_client_ctx *ctx)
mxmlDelete(tree);
}
// We use libcurl to make the request. We could use libevent and avoid the
// dependency, but for SSL, libevent needs to be v2.1 or better, which is still
// a bit too new to be in the major distros
static int
https_client_request(struct https_client_ctx *ctx)
{
CURL *curl;
CURLcode res;
curl = curl_easy_init();
if (!curl)
{
DPRINTF(E_LOG, L_LASTFM, "Error: Could not get curl handle\n");
return -1;
}
curl_easy_setopt(curl, CURLOPT_USERAGENT, "libcurl-agent/1.0");
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, ctx->body);
curl_easy_setopt(curl, CURLOPT_URL, ctx->url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, request_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, ctx);
ctx->data = evbuffer_new();
if (!ctx->data)
{
DPRINTF(E_LOG, L_LASTFM, "Could not create evbuffer for LastFM response\n");
curl_easy_cleanup(curl);
return -1;
}
res = curl_easy_perform(curl);
if (res != CURLE_OK)
{
DPRINTF(E_LOG, L_LASTFM, "Request to %s failed: %s\n", ctx->url, curl_easy_strerror(res));
curl_easy_cleanup(curl);
return -1;
}
response_proces(ctx);
evbuffer_free(ctx->data);
curl_easy_cleanup(curl);
return 0;
}
static int
request_post(char *method, struct keyval *kv, int auth)
{
struct https_client_ctx ctx;
char *body;
struct http_client_ctx ctx;
int ret;
ret = keyval_add(kv, "method", method);
@ -453,18 +332,27 @@ request_post(char *method, struct keyval *kv, int auth)
return -1;
}
ret = body_print(&body, kv);
memset(&ctx, 0, sizeof(struct http_client_ctx));
ctx.output_body = http_form_urlencode(kv);
if (ret < 0)
{
DPRINTF(E_LOG, L_LASTFM, "Aborting request, body_print failed\n");
DPRINTF(E_LOG, L_LASTFM, "Aborting request, http_form_urlencode failed\n");
return -1;
}
memset(&ctx, 0, sizeof(struct https_client_ctx));
ctx.url = auth ? auth_url : api_url;
ctx.body = body;
ctx.input_body = evbuffer_new();
ret = https_client_request(&ctx);
ret = http_client_request(&ctx);
if (ret < 0)
goto out_free_ctx;
response_proces(&ctx);
out_free_ctx:
free(ctx.output_body);
evbuffer_free(ctx.input_body);
return ret;
}

View File

@ -68,7 +68,7 @@ GCRY_THREAD_OPTION_PTHREAD_IMPL;
#include "player.h"
#include "worker.h"
#ifdef LASTFM
#ifdef HAVE_LIBCURL
# include <curl/curl.h>
#endif
#ifdef HAVE_SPOTIFY_H
@ -632,7 +632,7 @@ main(int argc, char **argv)
#endif
av_log_set_callback(logger_ffmpeg);
#ifdef LASTFM
#ifdef HAVE_LIBCURL
/* Initialize libcurl */
curl_global_init(CURL_GLOBAL_DEFAULT);
#endif
@ -810,6 +810,9 @@ main(int argc, char **argv)
goto mdns_reg_fail;
}
/* Register this CNAME with mDNS for OAuth */
mdns_cname("forked-daapd.local");
#if defined(__linux__)
/* Set up signal fd */
sigfd = signalfd(-1, &sigs, SFD_NONBLOCK | SFD_CLOEXEC);
@ -936,7 +939,7 @@ main(int argc, char **argv)
signal_block_fail:
gcrypt_init_fail:
#ifdef LASTFM
#ifdef HAVE_LIBCURL
curl_global_cleanup();
#endif
#if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 13)

View File

@ -37,6 +37,16 @@ mdns_deinit(void);
int
mdns_register(char *name, char *type, int port, char **txt);
/*
* Register a CNAME record, it will be an alias for hostname
* Call only from the main thread!
*
* @in name The CNAME alias, e.g. "forked-daapd.local"
* @return 0 on success, -1 on error
*/
int
mdns_cname(char *name);
/*
* Start a service browser, a callback will be made when the service changes state
* Call only from the main thread!

View File

@ -33,6 +33,7 @@
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <unistd.h>
#include <event2/event.h>
@ -351,8 +352,15 @@ struct mdns_record_browser {
int port;
};
enum publish
{
MDNS_PUBLISH_SERVICE,
MDNS_PUBLISH_CNAME,
};
struct mdns_group_entry
{
enum publish publish;
char *name;
char *type;
int port;
@ -622,13 +630,100 @@ entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, AVAHI_GCC_U
}
}
static void
_create_services(void)
static int
create_group_entry(struct mdns_group_entry *ge, int commit)
{
struct mdns_group_entry *pentry;
char hostname[HOST_NAME_MAX + 1];
char rdata[HOST_NAME_MAX + 6 + 1]; // Includes room for ".local" and 0-terminator
int count;
int i;
int ret;
DPRINTF(E_DBG, L_MDNS, "Creating service group\n");
if (!mdns_group)
{
mdns_group = avahi_entry_group_new(mdns_client, entry_group_callback, NULL);
if (!mdns_group)
{
DPRINTF(E_WARN, L_MDNS, "Could not create Avahi EntryGroup: %s\n", MDNSERR);
return -1;
}
}
if (ge->publish == MDNS_PUBLISH_SERVICE)
{
DPRINTF(E_DBG, L_MDNS, "Adding service %s/%s\n", ge->name, ge->type);
ret = avahi_entry_group_add_service_strlst(mdns_group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, 0,
ge->name, ge->type,
NULL, NULL, ge->port, ge->txt);
if (ret < 0)
{
DPRINTF(E_LOG, L_MDNS, "Could not add mDNS service %s/%s: %s\n", ge->name, ge->type, avahi_strerror(ret));
return -1;
}
}
else if (ge->publish == MDNS_PUBLISH_CNAME)
{
DPRINTF(E_DBG, L_MDNS, "Adding CNAME record %s\n", ge->name);
ret = gethostname(hostname, HOST_NAME_MAX);
if (ret < 0)
{
DPRINTF(E_LOG, L_MDNS, "Could not add CNAME %s, gethostname failed\n", ge->name);
return -1;
}
// Note, gethostname does not guarantee 0-termination
ret = snprintf(rdata, sizeof(rdata), ".%s.local", hostname);
if (!(ret > 0 && ret < sizeof(rdata)))
{
DPRINTF(E_LOG, L_MDNS, "Could not add CNAME %s, hostname is invalid\n", ge->name);
return -1;
}
// Convert to dns string: .forked-daapd.local -> \12forked-daapd\6local
count = 0;
for (i = ret - 1; i >= 0; i--)
{
if (rdata[i] == '.')
{
rdata[i] = count;
count = 0;
}
else
count++;
}
// ret + 1 should be the string length of rdata incl. 0-terminator
ret = avahi_entry_group_add_record(mdns_group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC,
AVAHI_PUBLISH_USE_MULTICAST | AVAHI_PUBLISH_ALLOW_MULTIPLE,
ge->name, AVAHI_DNS_CLASS_IN, AVAHI_DNS_TYPE_CNAME,
AVAHI_DEFAULT_TTL, rdata, ret + 1);
if (ret < 0)
{
DPRINTF(E_LOG, L_MDNS, "Could not add CNAME record %s: %s\n", ge->name, avahi_strerror(ret));
return -1;
}
}
if (!commit)
return 0;
ret = avahi_entry_group_commit(mdns_group);
if (ret < 0)
{
DPRINTF(E_LOG, L_MDNS, "Could not commit mDNS services: %s\n", MDNSERR);
return -1;
}
return 0;
}
static void
create_all_group_entries(void)
{
struct mdns_group_entry *ge;
int ret;
if (!group_entries)
{
@ -636,32 +731,17 @@ _create_services(void)
return;
}
if (mdns_group == NULL)
if (mdns_group)
avahi_entry_group_reset(mdns_group);
DPRINTF(E_INFO, L_MDNS, "Re-registering mDNS groups (services and records)\n");
for (ge = group_entries; ge; ge = ge->next)
{
mdns_group = avahi_entry_group_new(mdns_client, entry_group_callback, NULL);
create_group_entry(ge, 0);
if (!mdns_group)
{
DPRINTF(E_WARN, L_MDNS, "Could not create Avahi EntryGroup: %s\n", MDNSERR);
return;
}
}
pentry = group_entries;
while (pentry)
{
DPRINTF(E_DBG, L_MDNS, "Re-registering %s/%s\n", pentry->name, pentry->type);
ret = avahi_entry_group_add_service_strlst(mdns_group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, 0,
pentry->name, pentry->type,
NULL, NULL, pentry->port, pentry->txt);
if (ret < 0)
{
DPRINTF(E_WARN, L_MDNS, "Could not add mDNS services: %s\n", avahi_strerror(ret));
return;
}
pentry = pentry->next;
}
ret = avahi_entry_group_commit(mdns_group);
if (ret < 0)
@ -680,7 +760,7 @@ client_callback(AvahiClient *c, AvahiClientState state, AVAHI_GCC_UNUSED void *
case AVAHI_CLIENT_S_RUNNING:
DPRINTF(E_LOG, L_MDNS, "Avahi state change: Client running\n");
if (!mdns_group)
_create_services();
create_all_group_entries();
for (mb = browser_list; mb; mb = mb->next)
{
@ -808,8 +888,6 @@ mdns_register(char *name, char *type, int port, char **txt)
AvahiStringList *txt_sl;
int i;
DPRINTF(E_DBG, L_MDNS, "Adding mDNS service %s/%s\n", name, type);
ge = calloc(1, sizeof(struct mdns_group_entry));
if (!ge)
{
@ -817,6 +895,7 @@ mdns_register(char *name, char *type, int port, char **txt)
return -1;
}
ge->publish = MDNS_PUBLISH_SERVICE;
ge->name = strdup(name);
ge->type = strdup(type);
ge->port = port;
@ -837,14 +916,30 @@ mdns_register(char *name, char *type, int port, char **txt)
ge->next = group_entries;
group_entries = ge;
if (mdns_group)
{
DPRINTF(E_DBG, L_MDNS, "Resetting mDNS group\n");
avahi_entry_group_reset(mdns_group);
create_all_group_entries(); // TODO why is this required?
return 0;
}
DPRINTF(E_DBG, L_MDNS, "Creating service group\n");
_create_services();
int
mdns_cname(char *name)
{
struct mdns_group_entry *ge;
ge = calloc(1, sizeof(struct mdns_group_entry));
if (!ge)
{
DPRINTF(E_LOG, L_MDNS, "Out of memory for mDNS CNAME\n");
return -1;
}
ge->publish = MDNS_PUBLISH_CNAME;
ge->name = strdup(name);
ge->next = group_entries;
group_entries = ge;
create_all_group_entries();
return 0;
}

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@
#include <event2/event.h>
#include <event2/buffer.h>
#include <event2/http.h>
int
spotify_playback_setup(const char *path);
@ -32,6 +33,12 @@ spotify_audio_get(struct evbuffer *evbuf, int wanted);
int
spotify_artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h);
void
spotify_oauth_interface(struct evbuffer *evbuf, const char *redirect_uri);
void
spotify_oauth_callback(struct evbuffer *evbuf, struct evkeyvalq *param, const char *redirect_uri);
void
spotify_login(char *path);