From 16bc68f2f94d1bcfd21e5124537081d76859ea89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lepin?= Date: Mon, 31 Dec 2018 17:05:23 +0100 Subject: [PATCH] server: import websockets++ server from master --- .gitmodules | 6 ++ CMakeLists.txt | 34 ++++--- src/WSServer.cpp | 249 +++++++++++++++++++++++++++-------------------- src/WSServer.h | 51 ++++++---- 4 files changed, 203 insertions(+), 137 deletions(-) create mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..3e9749f0 --- /dev/null +++ b/.gitmodules @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 50d81520..e8897f28 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 $==1 ("${CMAKE_COMMAND}" -E copy "$" - "${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 $==1 ("${CMAKE_COMMAND}" -E copy "$" - "${QTDIR}/bin/Qt5WebSockets.dll" - "${QTDIR}/bin/Qt5Network.dll" "${RELEASE_DIR}/obs-plugins/${ARCH_NAME}") COMMAND if $==1 ("${CMAKE_COMMAND}" -E copy @@ -130,8 +141,6 @@ if(WIN32) COMMAND if $==1 ( "${CMAKE_COMMAND}" -E copy "$" - "${QTDIR}/bin/Qt5WebSocketsd.dll" - "${QTDIR}/bin/Qt5Networkd.dll" "${LIBOBS_INCLUDE_DIR}/../${OBS_BUILDDIR_ARCH}/rundir/$/obs-plugins/${ARCH_NAME}") COMMAND if $==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) diff --git a/src/WSServer.cpp b/src/WSServer.cpp index 4ca45823..d7f52cc3 100644 --- a/src/WSServer.cpp +++ b/src/WSServer.cpp @@ -16,11 +16,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see */ -#include #include #include #include #include +#include #include #include "WSServer.h" @@ -30,139 +30,178 @@ with this program. If not, see 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(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(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); } diff --git a/src/WSServer.h b/src/WSServer.h index 3d33532a..c1e7265a 100644 --- a/src/WSServer.h +++ b/src/WSServer.h @@ -20,33 +20,48 @@ with this program. If not, see #define WSSERVER_H #include -#include #include +#include +#include +#include + #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 server; +typedef std::set> con_list; - private: - QWebSocketServer* _wsServer; - QList _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