[web] Support libevent as WS server instead of libwebsockets
If libevent >= 2.2 is detected during configure and "websocket_port" == 0 in the config file, the libwebsocket implementation is disabled and instead the libevent http server offers the websocket connection. The connection to the websocket is then done with the path "/ws".
This commit is contained in:
parent
81cf713a83
commit
61d6945108
|
@ -143,7 +143,13 @@ OWNTONE_MODULES_CHECK([COMMON], [SQLITE3], [sqlite3 >= 3.5.0],
|
||||||
])
|
])
|
||||||
|
|
||||||
OWNTONE_MODULES_CHECK([OWNTONE], [LIBEVENT], [libevent >= 2.1.4],
|
OWNTONE_MODULES_CHECK([OWNTONE], [LIBEVENT], [libevent >= 2.1.4],
|
||||||
[event_base_new], [event2/event.h])
|
[event_base_new], [event2/event.h],
|
||||||
|
[dnl check for version 2.2 (with websocket server support)
|
||||||
|
PKG_CHECK_EXISTS([libevent >= 2.2.1],
|
||||||
|
[AC_DEFINE([HAVE_LIBEVENT22], 1,
|
||||||
|
[Define to 1 if you have libevent > 2.2])],
|
||||||
|
[])
|
||||||
|
])
|
||||||
OWNTONE_MODULES_CHECK([OWNTONE], [LIBEVENT_PTHREADS], [libevent_pthreads],
|
OWNTONE_MODULES_CHECK([OWNTONE], [LIBEVENT_PTHREADS], [libevent_pthreads],
|
||||||
[evthread_use_pthreads], [event2/thread.h])
|
[evthread_use_pthreads], [event2/thread.h])
|
||||||
|
|
||||||
|
|
|
@ -130,6 +130,7 @@ owntone_SOURCES = main.c \
|
||||||
listener.c listener.h \
|
listener.c listener.h \
|
||||||
commands.c commands.h \
|
commands.c commands.h \
|
||||||
outputs/plist_wrap.h \
|
outputs/plist_wrap.h \
|
||||||
|
websocket_ev.c websocket_ev.h \
|
||||||
$(LIBWEBSOCKETS_SRC) \
|
$(LIBWEBSOCKETS_SRC) \
|
||||||
$(GPERF_SRC) \
|
$(GPERF_SRC) \
|
||||||
$(LEXER_SRC) $(PARSER_SRC)
|
$(LEXER_SRC) $(PARSER_SRC)
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "commands.h"
|
#include "commands.h"
|
||||||
#include "httpd_internal.h"
|
#include "httpd_internal.h"
|
||||||
|
#include "websocket_ev.h"
|
||||||
|
|
||||||
// #define DEBUG_ALLOC 1
|
// #define DEBUG_ALLOC 1
|
||||||
|
|
||||||
|
@ -341,7 +342,10 @@ httpd_server_free(httpd_server *server)
|
||||||
close(server->fd);
|
close(server->fd);
|
||||||
|
|
||||||
if (server->evhttp)
|
if (server->evhttp)
|
||||||
evhttp_free(server->evhttp);
|
{
|
||||||
|
websocketev_deinit();
|
||||||
|
evhttp_free(server->evhttp);
|
||||||
|
}
|
||||||
|
|
||||||
commands_base_free(server->cmdbase);
|
commands_base_free(server->cmdbase);
|
||||||
free(server);
|
free(server);
|
||||||
|
@ -374,6 +378,7 @@ httpd_server_new(struct event_base *evbase, unsigned short port, httpd_request_c
|
||||||
goto error;
|
goto error;
|
||||||
|
|
||||||
evhttp_set_gencb(server->evhttp, gencb_httpd, server);
|
evhttp_set_gencb(server->evhttp, gencb_httpd, server);
|
||||||
|
websocketev_init(server->evhttp);
|
||||||
|
|
||||||
return server;
|
return server;
|
||||||
|
|
||||||
|
|
|
@ -478,7 +478,11 @@ websocket_init(void)
|
||||||
|
|
||||||
if (websocket_port <= 0)
|
if (websocket_port <= 0)
|
||||||
{
|
{
|
||||||
|
#ifdef HAVE_LIBEVENT22
|
||||||
|
DPRINTF(E_DBG, L_WEB, "Libwebsocket disabled, using libevent websocket instead. To enable it, set websocket_port in config to a valid port number.\n");
|
||||||
|
#else
|
||||||
DPRINTF(E_LOG, L_WEB, "Websocket disabled. To enable it, set websocket_port in config to a valid port number.\n");
|
DPRINTF(E_LOG, L_WEB, "Websocket disabled. To enable it, set websocket_port in config to a valid port number.\n");
|
||||||
|
#endif
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,331 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2024 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 <event2/buffer.h>
|
||||||
|
#include <event2/bufferevent.h>
|
||||||
|
#include <event2/event.h>
|
||||||
|
#include <event2/http.h>
|
||||||
|
#include <json.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#ifdef HAVE_LIBEVENT22
|
||||||
|
#include <event2/ws.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "conffile.h"
|
||||||
|
#include "listener.h"
|
||||||
|
#include "logger.h"
|
||||||
|
#include "misc.h"
|
||||||
|
|
||||||
|
#ifdef HAVE_LIBEVENT22
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 client
|
||||||
|
{
|
||||||
|
struct evws_connection *evws;
|
||||||
|
char name[INET6_ADDRSTRLEN];
|
||||||
|
short requested_events;
|
||||||
|
struct client *next;
|
||||||
|
};
|
||||||
|
static struct client *clients = NULL;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Notify clients of the notify-protocol about occurred events
|
||||||
|
*
|
||||||
|
* Sends a JSON message of the form:
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* "notify": [ "update" ]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
static char *
|
||||||
|
create_notify_reply(short events, short *requested_events)
|
||||||
|
{
|
||||||
|
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 = strdup(json_object_to_json_string(reply));
|
||||||
|
|
||||||
|
json_object_put(reply);
|
||||||
|
|
||||||
|
return json_response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Thread: library (the thread the event occurred) */
|
||||||
|
static void
|
||||||
|
listener_cb(short event_mask)
|
||||||
|
{
|
||||||
|
struct client *client = NULL;
|
||||||
|
char *reply = NULL;
|
||||||
|
|
||||||
|
for (client = clients; client; client = clients->next)
|
||||||
|
{
|
||||||
|
reply = create_notify_reply(event_mask, &client->requested_events);
|
||||||
|
evws_send_text(client->evws, reply);
|
||||||
|
free(reply);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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, const char *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;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
on_msg_cb(struct evws_connection *evws, int type, const unsigned char *data, size_t len, void *arg)
|
||||||
|
{
|
||||||
|
struct client *self = arg;
|
||||||
|
const char *msg = (const char *)data;
|
||||||
|
|
||||||
|
process_notify_request(&self->requested_events, msg, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
on_close_cb(struct evws_connection *evws, void *arg)
|
||||||
|
{
|
||||||
|
struct client *client = NULL;
|
||||||
|
struct client *prev = NULL;
|
||||||
|
|
||||||
|
for (client = clients; client && client != arg; client = clients->next)
|
||||||
|
{
|
||||||
|
prev = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client)
|
||||||
|
{
|
||||||
|
if (prev)
|
||||||
|
prev->next = client->next;
|
||||||
|
else
|
||||||
|
clients = client->next;
|
||||||
|
|
||||||
|
free(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
gencb_ws(struct evhttp_request *req, void *arg)
|
||||||
|
{
|
||||||
|
struct client *client;
|
||||||
|
|
||||||
|
client = calloc(1, sizeof(*client));
|
||||||
|
|
||||||
|
client->evws = evws_new_session(req, on_msg_cb, client, 0);
|
||||||
|
if (!client->evws)
|
||||||
|
{
|
||||||
|
free(client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
evws_connection_set_closecb(client->evws, on_close_cb, client);
|
||||||
|
client->next = clients;
|
||||||
|
clients = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
websocketev_init(struct evhttp *evhttp)
|
||||||
|
{
|
||||||
|
int websocket_port = cfg_getint(cfg_getsec(cfg, "general"), "websocket_port");
|
||||||
|
|
||||||
|
if (websocket_port > 0)
|
||||||
|
{
|
||||||
|
DPRINTF(E_DBG, L_WEB,
|
||||||
|
"Libevent websocket disabled, using libwebsockets instead. Set "
|
||||||
|
"websocket_port to 0 to enable it.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
evhttp_set_cb(evhttp, "/ws", gencb_ws, NULL);
|
||||||
|
|
||||||
|
listener_add(listener_cb, LISTENER_UPDATE | LISTENER_DATABASE | LISTENER_PAIRING | LISTENER_SPOTIFY | LISTENER_LASTFM
|
||||||
|
| LISTENER_SPEAKER | LISTENER_PLAYER | LISTENER_OPTIONS | LISTENER_VOLUME
|
||||||
|
| LISTENER_QUEUE);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
websocketev_deinit(void)
|
||||||
|
{
|
||||||
|
listener_remove(listener_cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
int
|
||||||
|
websocketev_init(struct evhttp *evhttp)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
websocketev_deinit(void)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
#ifndef SRC_WEBSOCKET_EV_H_
|
||||||
|
#define SRC_WEBSOCKET_EV_H_
|
||||||
|
|
||||||
|
#include <event2/http.h>
|
||||||
|
|
||||||
|
int
|
||||||
|
websocketev_init(struct evhttp *evhttp);
|
||||||
|
|
||||||
|
void
|
||||||
|
websocketev_deinit(void);
|
||||||
|
|
||||||
|
#endif /* SRC_WEBSOCKET_EV_H_ */
|
|
@ -156,34 +156,8 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
open_ws() {
|
open_ws() {
|
||||||
if (this.configurationStore.websocket_port <= 0) {
|
const wsPort = this.configurationStore.websocket_port
|
||||||
this.notificationsStore.add({
|
const socket = wsPort <= 0 ? this.create_websocket() : this.create_websocket_with_port(wsPort)
|
||||||
text: this.$t('server.missing-port'),
|
|
||||||
type: 'danger'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let protocol = 'ws://'
|
|
||||||
if (window.location.protocol === 'https:') {
|
|
||||||
protocol = 'wss://'
|
|
||||||
}
|
|
||||||
|
|
||||||
let wsUrl = `${protocol}${window.location.hostname}:${this.configurationStore.websocket_port}`
|
|
||||||
|
|
||||||
if (import.meta.env.DEV && import.meta.env.VITE_OWNTONE_URL) {
|
|
||||||
/*
|
|
||||||
* If we are running in development mode, construct the websocket
|
|
||||||
* url from the host of the environment variable VITE_OWNTONE_URL
|
|
||||||
*/
|
|
||||||
const url = new URL(import.meta.env.VITE_OWNTONE_URL)
|
|
||||||
wsUrl = `${protocol}${url.hostname}:${this.configurationStore.websocket_port}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const socket = new ReconnectingWebSocket(wsUrl, 'notify', {
|
|
||||||
maxReconnectInterval: 2000,
|
|
||||||
reconnectInterval: 1000
|
|
||||||
})
|
|
||||||
|
|
||||||
const vm = this
|
const vm = this
|
||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
|
@ -283,6 +257,49 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
create_websocket() {
|
||||||
|
// Create websocket connection on the same http connection
|
||||||
|
const wsHost = window.location.hostname
|
||||||
|
const wsPath = '/ws'
|
||||||
|
const wsPort = window.location.port
|
||||||
|
|
||||||
|
let wsProtocol = 'ws://'
|
||||||
|
if (window.location.protocol === 'https:') {
|
||||||
|
wsProtocol = 'wss://'
|
||||||
|
}
|
||||||
|
|
||||||
|
const wsUrl = `${wsProtocol}${wsHost}:${wsPort}${wsPath}`
|
||||||
|
|
||||||
|
const socket = new ReconnectingWebSocket(wsUrl, 'notify', {
|
||||||
|
maxReconnectInterval: 2000,
|
||||||
|
reconnectInterval: 1000
|
||||||
|
})
|
||||||
|
return socket
|
||||||
|
},
|
||||||
|
create_websocket_with_port(port) {
|
||||||
|
// Create websocket connection on a separate http port
|
||||||
|
let protocol = 'ws://'
|
||||||
|
if (window.location.protocol === 'https:') {
|
||||||
|
protocol = 'wss://'
|
||||||
|
}
|
||||||
|
|
||||||
|
let wsUrl = `${protocol}${window.location.hostname}:${port}`
|
||||||
|
|
||||||
|
if (import.meta.env.DEV && import.meta.env.VITE_OWNTONE_URL) {
|
||||||
|
/*
|
||||||
|
* If we are running in development mode, construct the websocket
|
||||||
|
* url from the host of the environment variable VITE_OWNTONE_URL
|
||||||
|
*/
|
||||||
|
const url = new URL(import.meta.env.VITE_OWNTONE_URL)
|
||||||
|
wsUrl = `${protocol}${url.hostname}:${port}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const socket = new ReconnectingWebSocket(wsUrl, 'notify', {
|
||||||
|
maxReconnectInterval: 2000,
|
||||||
|
reconnectInterval: 1000
|
||||||
|
})
|
||||||
|
return socket
|
||||||
|
},
|
||||||
update_is_clipped() {
|
update_is_clipped() {
|
||||||
if (this.show_burger_menu || this.show_player_menu) {
|
if (this.show_burger_menu || this.show_player_menu) {
|
||||||
document.querySelector('html').classList.add('is-clipped')
|
document.querySelector('html').classList.add('is-clipped')
|
||||||
|
|
|
@ -32,6 +32,7 @@ export default defineConfig({
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': { target },
|
'/api': { target },
|
||||||
|
'/ws': { target: target, ws: true },
|
||||||
'/artwork': { target },
|
'/artwork': { target },
|
||||||
'/stream.mp3': { target }
|
'/stream.mp3': { target }
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue