server: import websockets++ server from master

This commit is contained in:
Stéphane Lepin 2018-12-31 17:05:23 +01:00
parent 974d6b48b2
commit 16bc68f2f9
4 changed files with 203 additions and 137 deletions

6
.gitmodules vendored Normal file
View File

@ -0,0 +1,6 @@
[submodule "deps/websocketpp"]
path = deps/websocketpp
url = https://github.com/zaphoyd/websocketpp.git
[submodule "deps/asio"]
path = deps/asio
url = https://github.com/chriskohlhoff/asio.git

View File

@ -6,14 +6,26 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if (WIN32 OR APPLE)
include(external/FindLibObs.cmake)
endif()
add_definitions(-DASIO_STANDALONE)
if (UNIX)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
endif()
if (WIN32 OR APPLE)
include(external/FindLibObs.cmake)
endif()
find_package(LibObs REQUIRED)
find_package(Qt5Core REQUIRED)
find_package(Qt5WebSockets REQUIRED)
find_package(Qt5Widgets REQUIRED)
find_package(Boost REQUIRED)
set(obs-websocket_SOURCES
src/obs-websocket.cpp
@ -52,13 +64,14 @@ add_library(obs-websocket MODULE
include_directories(
"${LIBOBS_INCLUDE_DIR}/../UI/obs-frontend-api"
${Qt5Core_INCLUDES}
${Qt5WebSockets_INCLUDES}
${Qt5Widgets_INCLUDES})
${Qt5Widgets_INCLUDES}
${Boost_INCLUDE_DIRS}
"${CMAKE_SOURCE_DIR}/deps/asio/asio/include"
"${CMAKE_SOURCE_DIR}/deps/websocketpp")
target_link_libraries(obs-websocket
libobs
Qt5::Core
Qt5::WebSockets
Qt5::Widgets)
# --- End of section ---
@ -70,6 +83,8 @@ if(WIN32)
message(FATAL_ERROR "Could not find OBS Frontend API's library !")
endif()
add_definitions(-D_WEBSOCKETPP_CPP11_STL_)
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(ARCH_NAME "64bit")
set(OBS_BUILDDIR_ARCH "build64")
@ -102,8 +117,6 @@ if(WIN32)
COMMAND if $<CONFIG:Release>==1 ("${CMAKE_COMMAND}" -E copy
"$<TARGET_FILE:obs-websocket>"
"${QTDIR}/bin/Qt5WebSockets.dll"
"${QTDIR}/bin/Qt5Network.dll"
"${RELEASE_DIR}/obs-plugins/${ARCH_NAME}")
# If config is RelWithDebInfo, package release files
@ -118,8 +131,6 @@ if(WIN32)
COMMAND if $<CONFIG:RelWithDebInfo>==1 ("${CMAKE_COMMAND}" -E copy
"$<TARGET_FILE:obs-websocket>"
"${QTDIR}/bin/Qt5WebSockets.dll"
"${QTDIR}/bin/Qt5Network.dll"
"${RELEASE_DIR}/obs-plugins/${ARCH_NAME}")
COMMAND if $<CONFIG:RelWithDebInfo>==1 ("${CMAKE_COMMAND}" -E copy
@ -130,8 +141,6 @@ if(WIN32)
COMMAND if $<CONFIG:Debug>==1 (
"${CMAKE_COMMAND}" -E copy
"$<TARGET_FILE:obs-websocket>"
"${QTDIR}/bin/Qt5WebSocketsd.dll"
"${QTDIR}/bin/Qt5Networkd.dll"
"${LIBOBS_INCLUDE_DIR}/../${OBS_BUILDDIR_ARCH}/rundir/$<CONFIG>/obs-plugins/${ARCH_NAME}")
COMMAND if $<CONFIG:Debug>==1 (
@ -155,11 +164,8 @@ endif()
# --- Linux-specific build settings and tasks ---
if(UNIX AND NOT APPLE)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
set_target_properties(obs-websocket PROPERTIES PREFIX "")
target_link_libraries(obs-websocket
obs-frontend-api)
target_link_libraries(obs-websocket obs-frontend-api)
file(GLOB locale_files data/locale/*.ini)

View File

@ -16,11 +16,11 @@ You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include <QtWebSockets/QWebSocket>
#include <QtCore/QThread>
#include <QtCore/QByteArray>
#include <QMainWindow>
#include <QMessageBox>
#include <QtConcurrent/QtConcurrent>
#include <obs-frontend-api.h>
#include "WSServer.h"
@ -30,139 +30,178 @@ with this program. If not, see <https://www.gnu.org/licenses/>
QT_USE_NAMESPACE
using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;
using websocketpp::lib::bind;
WSServer* WSServer::Instance = nullptr;
QString decodeBase64(const QString& source)
{
return QString::fromUtf8(
QByteArray::fromBase64(
source.toUtf8()
)
);
}
WSServer::WSServer(QObject* parent)
: QObject(parent),
_wsServer(Q_NULLPTR),
_clients(),
_connections(),
_clMutex(QMutex::Recursive)
{
_wsServer = new QWebSocketServer(
QStringLiteral("obs-websocket"),
QWebSocketServer::NonSecureMode);
_server.init_asio();
_server.set_validate_handler(bind(&WSServer::validateConnection, this, ::_1));
_server.set_open_handler(bind(&WSServer::onOpen, this, ::_1));
_server.set_close_handler(bind(&WSServer::onClose, this, ::_1));
_server.set_message_handler(bind(&WSServer::onMessage, this, ::_1, ::_2));
}
WSServer::~WSServer() {
Stop();
WSServer::~WSServer()
{
stop();
}
void WSServer::Start(quint16 port) {
if (port == _wsServer->serverPort())
void WSServer::start(quint16 port)
{
if (_server.is_listening() && port == _serverPort) {
blog(LOG_INFO, "WebSocketsServer::start: server already on this port. no restart needed");
return;
if(_wsServer->isListening())
Stop();
bool serverStarted = _wsServer->listen(QHostAddress::Any, port);
if (serverStarted) {
blog(LOG_INFO, "server started successfully on TCP port %d", port);
connect(_wsServer, SIGNAL(newConnection()),
this, SLOT(onNewConnection()));
}
else {
QString errorString = _wsServer->errorString();
blog(LOG_ERROR,
"error: failed to start server on TCP port %d: %s",
port, errorString.toUtf8().constData());
QMainWindow* mainWindow = (QMainWindow*)obs_frontend_get_main_window();
obs_frontend_push_ui_translation(obs_module_get_string);
QString title = tr("OBSWebsocket.Server.StartFailed.Title");
QString msg = tr("OBSWebsocket.Server.StartFailed.Message").arg(port);
obs_frontend_pop_ui_translation();
QMessageBox::warning(mainWindow, title, msg);
if (_server.is_listening()) {
stop();
}
_serverPort = port;
_server.listen(_serverPort);
_server.start_accept();
QtConcurrent::run([=]() {
_server.run();
});
blog(LOG_INFO, "server started successfully on port %d", _serverPort);
}
void WSServer::Stop() {
QMutexLocker locker(&_clMutex);
for(QWebSocket* pClient : _clients) {
pClient->close();
}
locker.unlock();
_wsServer->close();
void WSServer::stop()
{
_server.stop();
_server.stop_listening();
blog(LOG_INFO, "server stopped successfully");
}
void WSServer::broadcast(QString message) {
void WSServer::broadcast(QString message)
{
QMutexLocker locker(&_clMutex);
for(QWebSocket* pClient : _clients) {
if (Config::Current()->AuthRequired
&& (pClient->property(PROP_AUTHENTICATED).toBool() == false)) {
// Skip this client if unauthenticated
continue;
for (connection_hdl hdl : _connections) {
_server.send(hdl, message.toStdString(), websocketpp::frame::opcode::text);
}
}
bool WSServer::validateConnection(connection_hdl hdl)
{
// TODO enforce subprotocol
Config* config = Config::Current();
if (config->AuthRequired) {
auto conn = _server.get_con_from_hdl(hdl);
QString authorization =
QString::fromStdString(conn->get_request_header("Authorization"));
if (!authorization.isNull() && !authorization.isEmpty()) {
const QStringList& parts = authorization.split(" ", QString::SplitBehavior::SkipEmptyParts);
if (parts.length() >= 2) {
const QString& authType = parts.at(0);
const QString& authValue = parts.at(1);
if (authType == "Basic") {
const QStringList& decodedParts =
decodeBase64(authValue).split(":", QString::SplitBehavior::SkipEmptyParts);
if (decodedParts.length() >= 2) {
const QString& username = decodedParts.at(0);
const QString& password = decodedParts.at(1);
// TODO time-constant string comparison
if (password == config->AuthPassword) {
return true;
}
}
}
}
}
pClient->sendTextMessage(message);
conn->set_status(websocketpp::http::status_code::unauthorized);
conn->append_header("WWW-Authenticate", "Basic charset=\"UTF-8\"");
return false;
}
return true;
}
void WSServer::onNewConnection() {
QWebSocket* pSocket = _wsServer->nextPendingConnection();
if (pSocket) {
connect(pSocket, SIGNAL(textMessageReceived(const QString&)),
this, SLOT(onTextMessageReceived(QString)));
connect(pSocket, SIGNAL(disconnected()),
this, SLOT(onSocketDisconnected()));
void WSServer::onOpen(connection_hdl hdl)
{
QMutexLocker locker(&_clMutex);
_connections.insert(hdl);
locker.unlock();
pSocket->setProperty(PROP_AUTHENTICATED, false);
QMutexLocker locker(&_clMutex);
_clients << pSocket;
locker.unlock();
QHostAddress clientAddr = pSocket->peerAddress();
QString clientIp = Utils::FormatIPAddress(clientAddr);
blog(LOG_INFO, "new client connection from %s:%d",
clientIp.toUtf8().constData(), pSocket->peerPort());
obs_frontend_push_ui_translation(obs_module_get_string);
QString title = tr("OBSWebsocket.NotifyConnect.Title");
QString msg = tr("OBSWebsocket.NotifyConnect.Message")
.arg(Utils::FormatIPAddress(clientAddr));
obs_frontend_pop_ui_translation();
Utils::SysTrayNotify(msg, QSystemTrayIcon::Information, title);
}
QString clientIp = getRemoteEndpoint(hdl);
notifyConnection(clientIp);
blog(LOG_INFO, "new client connection from %s", clientIp.toUtf8().constData());
}
void WSServer::onTextMessageReceived(QString message) {
QWebSocket* pSocket = qobject_cast<QWebSocket*>(sender());
if (pSocket) {
WSRequestHandler handler(pSocket);
handler.processIncomingMessage(message);
void WSServer::onMessage(connection_hdl hdl, server::message_ptr message)
{
auto opcode = message->get_opcode();
if (opcode != websocketpp::frame::opcode::text) {
return;
}
QString payload = QString::fromStdString(message->get_payload());
// TODO refactor handler
WSRequestHandler handler;
handler.processIncomingMessage(payload);
std::string response = handler.getResponse().toStdString();
_server.send(hdl, response, websocketpp::frame::opcode::text);
}
void WSServer::onSocketDisconnected() {
QWebSocket* pSocket = qobject_cast<QWebSocket*>(sender());
if (pSocket) {
pSocket->setProperty(PROP_AUTHENTICATED, false);
void WSServer::onClose(connection_hdl hdl)
{
QMutexLocker locker(&_clMutex);
_connections.erase(hdl);
locker.unlock();
QMutexLocker locker(&_clMutex);
_clients.removeAll(pSocket);
locker.unlock();
pSocket->deleteLater();
QHostAddress clientAddr = pSocket->peerAddress();
QString clientIp = Utils::FormatIPAddress(clientAddr);
blog(LOG_INFO, "client %s:%d disconnected",
clientIp.toUtf8().constData(), pSocket->peerPort());
obs_frontend_push_ui_translation(obs_module_get_string);
QString title = tr("OBSWebsocket.NotifyDisconnect.Title");
QString msg = tr("OBSWebsocket.NotifyDisconnect.Message")
.arg(Utils::FormatIPAddress(clientAddr));
obs_frontend_pop_ui_translation();
Utils::SysTrayNotify(msg, QSystemTrayIcon::Information, title);
}
QString clientIp = getRemoteEndpoint(hdl);
notifyDisconnection(clientIp);
blog(LOG_INFO, "client %s disconnected", clientIp.toUtf8().constData());
}
QString WSServer::getRemoteEndpoint(connection_hdl hdl)
{
auto conn = _server.get_con_from_hdl(hdl);
return QString::fromStdString(conn->get_remote_endpoint());
}
void WSServer::notifyConnection(QString clientIp)
{
obs_frontend_push_ui_translation(obs_module_get_string);
QString title = tr("OBSWebsocket.NotifyConnect.Title");
QString msg = tr("OBSWebsocket.NotifyConnect.Message").arg(clientIp);
obs_frontend_pop_ui_translation();
Utils::SysTrayNotify(msg, QSystemTrayIcon::Information, title);
}
void WSServer::notifyDisconnection(QString clientIp)
{
obs_frontend_push_ui_translation(obs_module_get_string);
QString title = tr("OBSWebsocket.NotifyDisconnect.Title");
QString msg = tr("OBSWebsocket.NotifyDisconnect.Message").arg(clientIp);
obs_frontend_pop_ui_translation();
Utils::SysTrayNotify(msg, QSystemTrayIcon::Information, title);
}

View File

@ -20,33 +20,48 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#define WSSERVER_H
#include <QObject>
#include <QList>
#include <QMutex>
#include <set>
#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>
#include "WSRequestHandler.h"
QT_FORWARD_DECLARE_CLASS(QWebSocketServer)
QT_FORWARD_DECLARE_CLASS(QWebSocket)
class WSServer : public QObject {
Q_OBJECT
public:
explicit WSServer(QObject* parent = Q_NULLPTR);
virtual ~WSServer();
void Start(quint16 port);
void Stop();
void broadcast(QString message);
static WSServer* Instance;
using websocketpp::connection_hdl;
private slots:
void onNewConnection();
void onTextMessageReceived(QString message);
void onSocketDisconnected();
typedef websocketpp::server<websocketpp::config::asio> server;
typedef std::set<connection_hdl,std::owner_less<connection_hdl>> con_list;
private:
QWebSocketServer* _wsServer;
QList<QWebSocket*> _clients;
QMutex _clMutex;
class WSServer : public QObject
{
Q_OBJECT
public:
explicit WSServer(QObject* parent = Q_NULLPTR);
virtual ~WSServer();
void start(quint16 port);
void stop();
void broadcast(QString message);
static WSServer* Instance;
private:
bool validateConnection(connection_hdl hdl);
void onOpen(connection_hdl hdl);
void onMessage(connection_hdl hdl, server::message_ptr message);
void onClose(connection_hdl hdl);
QString getRemoteEndpoint(connection_hdl hdl);
void notifyConnection(QString clientIp);
void notifyDisconnection(QString clientIp);
server _server;
quint16 _serverPort;
con_list _connections;
QMutex _clMutex;
};
#endif // WSSERVER_H