mirror of
https://github.com/owntone/owntone-server.git
synced 2024-12-28 08:05:56 -05:00
e03120c944
The player may emit a listener event when shutting down, and since websockets didn't remove it's listener callback it would receive an event despite being deinitialized. This would lead to an invalid read by lws_cancel_service in listener_cb().
553 lines
14 KiB
C
553 lines
14 KiB
C
/*
|
|
* Copyright (C) 2017 Christian Meffert <christian.meffert@googlemail.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
|
|
* 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 <config.h>
|
|
#endif
|
|
|
|
#include <json.h>
|
|
#include <libwebsockets.h>
|
|
#include <pthread.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
|
|
#include "conffile.h"
|
|
#include "listener.h"
|
|
#include "logger.h"
|
|
#include "misc.h"
|
|
|
|
|
|
static struct lws_context *context;
|
|
static pthread_t tid_websocket;
|
|
|
|
static const char *websocket_interface;
|
|
static int websocket_port;
|
|
static bool websocket_exit = false;
|
|
|
|
// Lock the event mask of events processed by the writeable callback
|
|
static pthread_mutex_t websocket_write_event_lock;
|
|
// Event mask of events processed by the writeable callback
|
|
static short websocket_write_events;
|
|
|
|
|
|
/* Thread: library (the thread the event occurred) */
|
|
static void
|
|
listener_cb(short event_mask)
|
|
{
|
|
pthread_mutex_lock(&websocket_write_event_lock);
|
|
websocket_write_events |= event_mask;
|
|
pthread_mutex_unlock(&websocket_write_event_lock);
|
|
|
|
lws_cancel_service(context);
|
|
}
|
|
|
|
/*
|
|
* Libwebsocket requires the HTTP protocol to be the first supported protocol
|
|
*
|
|
* This adds an empty implementation, because we are serving HTTP over libevent in httpd.h.
|
|
*/
|
|
static int
|
|
callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Each session of the "notify" protocol holds this event mask
|
|
*
|
|
* The client sends the events it wants to be notified of and the event mask is
|
|
* set accordingly translating them to the LISTENER enum (see listener.h)
|
|
*/
|
|
struct per_session_data {
|
|
struct per_session_data *pss_list;
|
|
struct lws *wsi;
|
|
short requested_events;
|
|
short write_events;
|
|
};
|
|
|
|
/* one of these is created for each vhost our protocol is used with */
|
|
struct per_vhost_data {
|
|
struct per_session_data *pss_list; /* linked-list of live pss*/
|
|
};
|
|
|
|
/*
|
|
* Processes client requests to the notify-protocol
|
|
*
|
|
* Expects the message in "in" to be a JSON string of the form:
|
|
*
|
|
* {
|
|
* "notify": [ "update" ]
|
|
* }
|
|
*/
|
|
static int
|
|
process_notify_request(short *requested_events, void *in, size_t len)
|
|
{
|
|
json_tokener *tokener;
|
|
json_object *request;
|
|
json_object *item;
|
|
int count, i;
|
|
enum json_tokener_error jerr;
|
|
json_object *needle;
|
|
const char *event_type;
|
|
|
|
*requested_events = 0;
|
|
|
|
tokener = json_tokener_new();
|
|
request = json_tokener_parse_ex(tokener, in, len);
|
|
jerr = json_tokener_get_error(tokener);
|
|
|
|
if (jerr != json_tokener_success)
|
|
{
|
|
DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request: %s\n", json_tokener_error_desc(jerr));
|
|
json_tokener_free(tokener);
|
|
return -1;
|
|
}
|
|
|
|
DPRINTF(E_DBG, L_WEB, "notify callback request: %s\n", json_object_to_json_string(request));
|
|
|
|
if (json_object_object_get_ex(request, "notify", &needle) && json_object_get_type(needle) == json_type_array)
|
|
{
|
|
count = json_object_array_length(needle);
|
|
for (i = 0; i < count; i++)
|
|
{
|
|
item = json_object_array_get_idx(needle, i);
|
|
|
|
if (json_object_get_type(item) == json_type_string)
|
|
{
|
|
event_type = json_object_get_string(item);
|
|
DPRINTF(E_DBG, L_WEB, "notify callback event received: %s\n", event_type);
|
|
|
|
if (0 == strcmp(event_type, "update"))
|
|
{
|
|
*requested_events |= LISTENER_UPDATE;
|
|
}
|
|
else if (0 == strcmp(event_type, "database"))
|
|
{
|
|
*requested_events |= LISTENER_DATABASE;
|
|
}
|
|
else if (0 == strcmp(event_type, "pairing"))
|
|
{
|
|
*requested_events |= LISTENER_PAIRING;
|
|
}
|
|
else if (0 == strcmp(event_type, "spotify"))
|
|
{
|
|
*requested_events |= LISTENER_SPOTIFY;
|
|
}
|
|
else if (0 == strcmp(event_type, "lastfm"))
|
|
{
|
|
*requested_events |= LISTENER_LASTFM;
|
|
}
|
|
else if (0 == strcmp(event_type, "outputs"))
|
|
{
|
|
*requested_events |= LISTENER_SPEAKER;
|
|
}
|
|
else if (0 == strcmp(event_type, "player"))
|
|
{
|
|
*requested_events |= LISTENER_PLAYER;
|
|
}
|
|
else if (0 == strcmp(event_type, "options"))
|
|
{
|
|
*requested_events |= LISTENER_OPTIONS;
|
|
}
|
|
else if (0 == strcmp(event_type, "volume"))
|
|
{
|
|
*requested_events |= LISTENER_VOLUME;
|
|
}
|
|
else if (0 == strcmp(event_type, "queue"))
|
|
{
|
|
*requested_events |= LISTENER_QUEUE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
json_tokener_free(tokener);
|
|
json_object_put(request);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Notify clients of the notify-protocol about occurred events
|
|
*
|
|
* Sends a JSON message of the form:
|
|
*
|
|
* {
|
|
* "notify": [ "update" ]
|
|
* }
|
|
*/
|
|
static void
|
|
send_notify_reply(short events, struct lws* wsi)
|
|
{
|
|
unsigned char* buf;
|
|
const char* json_response;
|
|
json_object* reply;
|
|
json_object* notify;
|
|
|
|
DPRINTF(E_DBG, L_WEB, "notify callback reply: %d\n", events);
|
|
|
|
notify = json_object_new_array();
|
|
if (events & LISTENER_UPDATE)
|
|
{
|
|
json_object_array_add(notify, json_object_new_string("update"));
|
|
}
|
|
if (events & LISTENER_DATABASE)
|
|
{
|
|
json_object_array_add(notify, json_object_new_string("database"));
|
|
}
|
|
if (events & LISTENER_PAIRING)
|
|
{
|
|
json_object_array_add(notify, json_object_new_string("pairing"));
|
|
}
|
|
if (events & LISTENER_SPOTIFY)
|
|
{
|
|
json_object_array_add(notify, json_object_new_string("spotify"));
|
|
}
|
|
if (events & LISTENER_LASTFM)
|
|
{
|
|
json_object_array_add(notify, json_object_new_string("lastfm"));
|
|
}
|
|
if (events & LISTENER_SPEAKER)
|
|
{
|
|
json_object_array_add(notify, json_object_new_string("outputs"));
|
|
}
|
|
if (events & LISTENER_PLAYER)
|
|
{
|
|
json_object_array_add(notify, json_object_new_string("player"));
|
|
}
|
|
if (events & LISTENER_OPTIONS)
|
|
{
|
|
json_object_array_add(notify, json_object_new_string("options"));
|
|
}
|
|
if (events & LISTENER_VOLUME)
|
|
{
|
|
json_object_array_add(notify, json_object_new_string("volume"));
|
|
}
|
|
if (events & LISTENER_QUEUE)
|
|
{
|
|
json_object_array_add(notify, json_object_new_string("queue"));
|
|
}
|
|
|
|
reply = json_object_new_object();
|
|
json_object_object_add(reply, "notify", notify);
|
|
|
|
json_response = json_object_to_json_string(reply);
|
|
|
|
buf = malloc(LWS_PRE + strlen(json_response));
|
|
memcpy(&buf[LWS_PRE], json_response, strlen(json_response));
|
|
lws_write(wsi, &buf[LWS_PRE], strlen(json_response), LWS_WRITE_TEXT);
|
|
|
|
free(buf);
|
|
json_object_put(reply);
|
|
}
|
|
|
|
/*
|
|
* Callback for the "notify" protocol
|
|
*/
|
|
static int
|
|
callback_notify(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len)
|
|
{
|
|
struct per_session_data *pss = user;
|
|
#if LWS_LIBRARY_VERSION_MAJOR < 3
|
|
struct per_session_data **ppss = NULL;
|
|
#endif
|
|
struct per_vhost_data *vhd = lws_protocol_vh_priv_get(
|
|
#if LWS_LIBRARY_VERSION_MAJOR >= 3
|
|
lws_get_vhost(wsi),
|
|
#else
|
|
lws_vhost_get(wsi),
|
|
#endif
|
|
lws_get_protocol(wsi));
|
|
short events = 0;
|
|
int ret = 0;
|
|
|
|
DPRINTF(E_DBG, L_WEB, "notify callback reason: %d\n", reason);
|
|
|
|
switch (reason)
|
|
{
|
|
case LWS_CALLBACK_PROTOCOL_INIT:
|
|
vhd = lws_protocol_vh_priv_zalloc(
|
|
#if LWS_LIBRARY_VERSION_MAJOR >= 3
|
|
lws_get_vhost(wsi),
|
|
#else
|
|
lws_vhost_get(wsi),
|
|
#endif
|
|
lws_get_protocol(wsi),
|
|
sizeof(struct per_vhost_data));
|
|
if (!vhd)
|
|
{
|
|
DPRINTF(E_LOG, L_WEB, "Failed to allocate websocket per-vhoststorage\n");
|
|
return 1;
|
|
}
|
|
break;
|
|
|
|
case LWS_CALLBACK_ESTABLISHED:
|
|
/* add ourselves to the list of live pss held in the vhd */
|
|
if (vhd)
|
|
{
|
|
#if LWS_LIBRARY_VERSION_MAJOR >= 3
|
|
lws_ll_fwd_insert(pss, pss_list, vhd->pss_list);
|
|
#else
|
|
pss->pss_list = vhd->pss_list;
|
|
vhd->pss_list = pss;
|
|
#endif
|
|
pss->wsi = wsi;
|
|
}
|
|
break;
|
|
|
|
case LWS_CALLBACK_CLOSED:
|
|
/* remove our closing pss from the list of live pss */
|
|
if (vhd)
|
|
{
|
|
#if LWS_LIBRARY_VERSION_MAJOR >= 3
|
|
lws_ll_fwd_remove(struct per_session_data, pss_list, pss, vhd->pss_list);
|
|
#else
|
|
ppss = &(vhd->pss_list);
|
|
while (*ppss) {
|
|
if (*ppss == pss) {
|
|
*ppss = pss->pss_list;
|
|
break;
|
|
}
|
|
ppss = &(*ppss)->pss_list;
|
|
}
|
|
#endif
|
|
}
|
|
break;
|
|
|
|
case LWS_CALLBACK_SERVER_WRITEABLE:
|
|
#if LWS_LIBRARY_VERSION_MAJOR < 3
|
|
pthread_mutex_lock(&websocket_write_event_lock);
|
|
events = websocket_write_events;
|
|
websocket_write_events = 0;
|
|
pthread_mutex_unlock(&websocket_write_event_lock);
|
|
if (vhd && events)
|
|
{
|
|
ppss = &(vhd->pss_list);
|
|
while (*ppss) {
|
|
(*ppss)->write_events |= events;
|
|
ppss = &(*ppss)->pss_list;
|
|
}
|
|
}
|
|
#endif
|
|
if (pss->requested_events & pss->write_events)
|
|
{
|
|
events = pss->requested_events & pss->write_events;
|
|
send_notify_reply(events, wsi);
|
|
pss->write_events = 0;
|
|
}
|
|
break;
|
|
|
|
case LWS_CALLBACK_RECEIVE:
|
|
ret = process_notify_request(&pss->requested_events, in, len);
|
|
break;
|
|
|
|
#if LWS_LIBRARY_VERSION_MAJOR >= 3
|
|
case LWS_CALLBACK_EVENT_WAIT_CANCELLED:
|
|
if (vhd)
|
|
{
|
|
pthread_mutex_lock(&websocket_write_event_lock);
|
|
events = websocket_write_events;
|
|
websocket_write_events = 0;
|
|
pthread_mutex_unlock(&websocket_write_event_lock);
|
|
lws_start_foreach_llp(struct per_session_data **, ppss, vhd->pss_list)
|
|
{
|
|
(*ppss)->write_events |= events;
|
|
lws_callback_on_writable((*ppss)->wsi);
|
|
} lws_end_foreach_llp(ppss, pss_list);
|
|
}
|
|
break;
|
|
#endif
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Supported protocols of the websocket, needs to be in line with the protocols array
|
|
*/
|
|
enum ws_protocols
|
|
{
|
|
WS_PROTOCOL_HTTP = 0,
|
|
WS_PROTOCOL_NOTIFY,
|
|
};
|
|
|
|
static struct lws_protocols protocols[] =
|
|
{
|
|
// The first protocol must always be the HTTP handler
|
|
{
|
|
"http-only", // Protocol name
|
|
callback_http, // Callback function
|
|
0, // Size of per session data
|
|
0, // Frame size / rx buffer (0 = max frame size)
|
|
},
|
|
{
|
|
"notify",
|
|
callback_notify,
|
|
sizeof(struct per_session_data),
|
|
0,
|
|
},
|
|
{ NULL, NULL, 0, 0 } // terminator
|
|
};
|
|
|
|
|
|
/* Thread: websocket */
|
|
static void *
|
|
websocket(void *arg)
|
|
{
|
|
listener_add(listener_cb, LISTENER_UPDATE | LISTENER_DATABASE | LISTENER_PAIRING | LISTENER_SPOTIFY | LISTENER_LASTFM | LISTENER_SPEAKER
|
|
| LISTENER_PLAYER | LISTENER_OPTIONS | LISTENER_VOLUME | LISTENER_QUEUE);
|
|
|
|
while(!websocket_exit)
|
|
{
|
|
#if LWS_LIBRARY_VERSION_MAJOR >= 3
|
|
if (lws_service(context, 0))
|
|
websocket_exit = true;
|
|
#else
|
|
lws_service(context, 10000);
|
|
if (websocket_write_events)
|
|
lws_callback_on_writable_all_protocol(context, &protocols[WS_PROTOCOL_NOTIFY]);
|
|
#endif
|
|
}
|
|
|
|
listener_remove(listener_cb);
|
|
|
|
pthread_exit(NULL);
|
|
}
|
|
|
|
static void
|
|
logger_libwebsockets(int level, const char *line)
|
|
{
|
|
int severity;
|
|
|
|
switch (level)
|
|
{
|
|
case LLL_ERR:
|
|
severity = E_LOG;
|
|
break;
|
|
|
|
case LLL_WARN:
|
|
severity = E_WARN;
|
|
break;
|
|
|
|
case LLL_NOTICE:
|
|
severity = E_DBG;
|
|
break;
|
|
|
|
case LLL_INFO:
|
|
case LLL_DEBUG:
|
|
severity = E_SPAM;
|
|
break;
|
|
|
|
default:
|
|
severity = E_LOG;
|
|
break;
|
|
}
|
|
|
|
DPRINTF(severity, L_WEB, "LWS %s", line);
|
|
}
|
|
|
|
int
|
|
websocket_init(void)
|
|
{
|
|
struct lws_context_creation_info info;
|
|
int ret;
|
|
|
|
websocket_interface = cfg_getstr(cfg_getsec(cfg, "general"), "websocket_interface");
|
|
websocket_port = cfg_getint(cfg_getsec(cfg, "general"), "websocket_port");
|
|
|
|
if (websocket_port <= 0)
|
|
{
|
|
DPRINTF(E_LOG, L_WEB, "Websocket disabled. To enable it, set websocket_port in config to a valid port number.\n");
|
|
return 0;
|
|
}
|
|
|
|
memset(&info, 0, sizeof(info));
|
|
info.port = websocket_port;
|
|
info.iface = websocket_interface;
|
|
info.protocols = protocols;
|
|
#ifdef LWS_SERVER_OPTION_IPV6_V6ONLY_MODIFY // Debian Buster's libwebsockets does not have this flag
|
|
if (cfg_getbool(cfg_getsec(cfg, "general"), "ipv6"))
|
|
info.options |= LWS_SERVER_OPTION_IPV6_V6ONLY_MODIFY; // Assures dual stack is enabled by switching off IPV6_V6ONLY
|
|
else
|
|
info.options |= LWS_SERVER_OPTION_DISABLE_IPV6;
|
|
#else
|
|
if (!cfg_getbool(cfg_getsec(cfg, "general"), "ipv6"))
|
|
info.options |= LWS_SERVER_OPTION_DISABLE_IPV6;
|
|
#endif
|
|
info.gid = -1;
|
|
info.uid = -1;
|
|
|
|
// Set header space to avoid "LWS Ran out of header data space" error
|
|
info.max_http_header_data = 4096;
|
|
|
|
// Log levels below NOTICE are only emmited if libwebsockets was built with DEBUG defined
|
|
lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE | LLL_INFO | LLL_DEBUG,
|
|
logger_libwebsockets);
|
|
|
|
context = lws_create_context(&info);
|
|
if (context == NULL)
|
|
{
|
|
DPRINTF(E_LOG, L_WEB, "Failed to create websocket context\n");
|
|
return -1;
|
|
}
|
|
|
|
ret = mutex_init(&websocket_write_event_lock);
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_WEB, "Failed to initialize mutex: %s\n", strerror(ret));
|
|
lws_context_destroy(context);
|
|
return -1;
|
|
}
|
|
|
|
ret = pthread_create(&tid_websocket, NULL, websocket, NULL);
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_WEB, "Could not spawn websocket thread (%d): %s\n", ret, strerror(ret));
|
|
pthread_mutex_destroy(&websocket_write_event_lock);
|
|
lws_context_destroy(context);
|
|
return -1;
|
|
}
|
|
|
|
thread_setname(tid_websocket, "websocket");
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
websocket_deinit(void)
|
|
{
|
|
int ret;
|
|
|
|
if (websocket_port <= 0)
|
|
return;
|
|
|
|
websocket_exit = true;
|
|
lws_cancel_service(context);
|
|
ret = pthread_join(tid_websocket, NULL);
|
|
if (ret < 0)
|
|
DPRINTF(E_LOG, L_WEB, "Error joining websocket thread (%d): %s\n", ret, strerror(ret));
|
|
|
|
lws_context_destroy(context);
|
|
pthread_mutex_destroy(&websocket_write_event_lock);
|
|
}
|