From bf317987c3662b97b54a3567836557b586641e63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lepin?= Date: Sat, 12 Nov 2016 03:59:07 +0100 Subject: [PATCH] Settings dialog (fixes #4) and WIP authentication (Buggy and needs testing) --- CMakeLists.txt | 23 ++++++-- Config.cpp | 113 ++++++++++++++++++++++++++++++++------ Config.h | 13 ++++- WSRequestHandler.cpp | 12 ++-- data/locale/en-US.ini | 4 ++ data/locale/fr-FR.ini | 4 ++ forms/settings-dialog.cpp | 70 +++++++++++++++++++++++ forms/settings-dialog.h | 28 ++++++++++ forms/settings-dialog.ui | 101 ++++++++++++++++++++++++++++++++++ obs-websocket.cpp | 16 +++++- 10 files changed, 355 insertions(+), 29 deletions(-) create mode 100644 data/locale/en-US.ini create mode 100644 data/locale/fr-FR.ini create mode 100644 forms/settings-dialog.cpp create mode 100644 forms/settings-dialog.h create mode 100644 forms/settings-dialog.ui diff --git a/CMakeLists.txt b/CMakeLists.txt index cc4d9079..a2b83365 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,11 +4,16 @@ project(obs-websocket) set(CMAKE_PREFIX_PATH "${QTDIR}") set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) include(external/FindLibObs.cmake) find_package(LibObs REQUIRED) find_package(Qt5Core REQUIRED) find_package(Qt5WebSockets REQUIRED) +find_package(Qt5Widgets REQUIRED) + +add_subdirectory(deps/mbedtls EXCLUDE_FROM_ALL) +set(ENABLED_PROGRAMS false) set(OBS_FRONTEND_LIB "OBS_FRONTEND_LIB-NOTFOUND" CACHE FILEPATH "OBS frontend library") if(OBS_FRONTEND_LIB EQUAL "OBS_FRONTEND_LIB-NOTFOUND") @@ -21,7 +26,8 @@ set(obs-websocket_SOURCES WSRequestHandler.cpp WSEvents.cpp Config.cpp - Utils.cpp) + Utils.cpp + forms/settings-dialog.cpp) set(obs-websocket_HEADERS obs-websocket.h @@ -29,19 +35,24 @@ set(obs-websocket_HEADERS WSRequestHandler.h WSEvents.h Config.h - Utils.h) + Utils.h + forms/settings-dialog.h) add_library(obs-websocket MODULE ${obs-websocket_SOURCES} ${obs-websocket_HEADERS}) -qt5_use_modules(obs-websocket - Core WebSockets) +add_dependencies(obs-websocket mbedcrypto) include_directories( "${LIBOBS_INCLUDE_DIR}/../UI/obs-frontend-api" ${Qt5Core_INCLUDES} - ${Qt5WebSockets_INCLUDES}) + ${Qt5WebSockets_INCLUDES} + ${Qt5Widgets_INCLUDES} + ${mbedcrypto_INCLUDES} + "${CMAKE_SOURCE_DIR}/deps/mbedtls/include") target_link_libraries(obs-websocket libobs ${OBS_FRONTEND_LIB} Qt5::Core - Qt5::WebSockets) \ No newline at end of file + Qt5::WebSockets + Qt5::Widgets + mbedcrypto) diff --git a/Config.cpp b/Config.cpp index d79f944a..c841da00 100644 --- a/Config.cpp +++ b/Config.cpp @@ -1,16 +1,102 @@ +#include +#include +#include #include "Config.h" +#define CONFIG_SECTION_NAME "obs-websocket" +#define CONFIG_PARAM_CHALLENGE "auth_hash" +#define CONFIG_PARAM_SALT "auth_salt" +#define CONFIG_PARAM_AUTHREQUIRED "auth_required" + Config *Config::_instance = new Config(); Config::Config() { + // Default settings AuthRequired = false; Challenge = ""; Salt = ""; SettingsLoaded = false; + + mbedtls_entropy_init(&entropy); + mbedtls_ctr_drbg_init(&rng); + mbedtls_ctr_drbg_seed(&rng, mbedtls_entropy_func, &entropy, nullptr, 0); + //mbedtls_ctr_drbg_set_prediction_resistance(&rng, MBEDTLS_CTR_DRBG_PR_ON); +} + +Config::~Config() { + mbedtls_ctr_drbg_free(&rng); + mbedtls_entropy_free(&entropy); +} + +const char* Config::GenerateSalt(mbedtls_ctr_drbg_context *rng) { + // Generate 32 random chars + unsigned char *random_chars = (unsigned char *)bzalloc(32); + mbedtls_ctr_drbg_random(rng, random_chars, 32); + + // Convert the 32 random chars to a base64 string + unsigned char *salt = (unsigned char*)bzalloc(64); + size_t salt_bytes; + mbedtls_base64_encode(salt, 64, &salt_bytes, random_chars, 32); + salt[salt_bytes] = 0; // Null-terminate the string + + bfree(random_chars); + return (char *)salt; +} + +const char* Config::GenerateChallenge(const char *password, const char *salt) { + size_t passwordLength = strlen(password); + size_t saltLength = strlen(salt); + + // Concatenate the password and the salt + unsigned char *passAndSalt = (unsigned char*)bzalloc(passwordLength + saltLength); + memcpy(passAndSalt, password, passwordLength); + memcpy(passAndSalt + passwordLength, salt, saltLength); + passAndSalt[passwordLength + saltLength] = 0; // Null-terminate the string + + // Generate a SHA256 hash of the password + unsigned char *challengeHash = (unsigned char *)bzalloc(32); + mbedtls_sha256(passAndSalt, passwordLength + saltLength, challengeHash, 0); + + // Encode SHA256 hash to Base64 + unsigned char *challenge = (unsigned char*)bzalloc(64); + size_t challenge_bytes = 0; + mbedtls_base64_encode(challenge, 64, &challenge_bytes, challengeHash, 32); + challenge[64] = 0; // Null-terminate the string + + bfree(passAndSalt); + bfree(challengeHash); + return (char*)challenge; } void Config::SetPassword(const char *password) { + const char *new_salt = GenerateSalt(&rng); + const char *new_challenge = GenerateChallenge(password, new_salt); + this->Salt = new_salt; + this->Challenge = new_challenge; +} + +bool Config::CheckAuth(const char *response) { + size_t challengeLength = strlen(this->Challenge); + size_t responseLength = strlen(response); + + // Concatenate challenge and auth response + char *challengeAndResponse = (char*)bzalloc(challengeLength + responseLength); + memcpy(challengeAndResponse, this->Challenge, challengeLength); + memcpy(challengeAndResponse + challengeLength, response, responseLength); + challengeAndResponse[challengeLength + responseLength] = 0; // Null-terminate the string + + // Generate a SHA256 hash of challengeAndResponse + unsigned char *hash = (unsigned char*)bzalloc(32); + mbedtls_sha256((unsigned char*)challengeAndResponse, challengeLength + responseLength, hash, 0); + + // Encode the SHA256 hash to Base64 + unsigned char *expected_response = (unsigned char*)bzalloc(64); + size_t base64_size = 0; + mbedtls_base64_encode(expected_response, 64, &base64_size, hash, 32); + expected_response[64] = 0; // Null-terminate the string + + return (strcmp((char*)expected_response, response) == 0); } void Config::OBSSaveCallback(obs_data_t *save_data, bool saving, void *private_data) { @@ -18,28 +104,23 @@ void Config::OBSSaveCallback(obs_data_t *save_data, bool saving, void *private_d if (saving) { obs_data_t *settings = obs_data_create(); - obs_data_set_bool(settings, "auth_required", conf->AuthRequired); - obs_data_set_string(settings, "auth_hash", conf->Challenge); - obs_data_set_string(settings, "auth_salt", conf->Salt); + obs_data_set_bool(settings, CONFIG_PARAM_AUTHREQUIRED, conf->AuthRequired); + obs_data_set_string(settings, CONFIG_PARAM_CHALLENGE, conf->Challenge); + obs_data_set_string(settings, CONFIG_PARAM_SALT, conf->Salt); - obs_data_set_obj(save_data, "obs-websocket", settings); + obs_data_set_obj(save_data, CONFIG_SECTION_NAME, settings); obs_data_release(settings); } else { - obs_data_t *settings = obs_data_get_obj(save_data, "obs-websocket"); - if (!settings) { - settings = obs_data_create(); - obs_data_set_bool(settings, "auth_required", conf->AuthRequired); - obs_data_set_string(settings, "auth_hash", conf->Challenge); - obs_data_set_string(settings, "auth_salt", conf->Salt); + obs_data_t *settings = obs_data_get_obj(save_data, CONFIG_SECTION_NAME); + if (settings) { + conf->AuthRequired = obs_data_get_bool(settings, CONFIG_PARAM_AUTHREQUIRED); + conf->Challenge = obs_data_get_string(settings, CONFIG_PARAM_CHALLENGE); + conf->Salt = obs_data_get_string(settings, CONFIG_PARAM_SALT); + + conf->SettingsLoaded = true; } - - conf->AuthRequired = obs_data_get_bool(settings, "auth_required"); - conf->Challenge = obs_data_get_string(settings, "auth_hash"); - conf->Salt = obs_data_get_string(settings, "auth_salt"); - conf->SettingsLoaded = true; - obs_data_release(settings); } } diff --git a/Config.h b/Config.h index 31d4f76c..b6d70b5a 100644 --- a/Config.h +++ b/Config.h @@ -2,21 +2,30 @@ #define CONFIG_H #include +#include +#include class Config { public: Config(); + ~Config(); void SetPassword(const char *password); + bool CheckAuth(const char *userChallenge); + static const char* GenerateSalt(mbedtls_ctr_drbg_context *rng); + static const char* GenerateChallenge(const char *password, const char *salt); + static void OBSSaveCallback(obs_data_t *save_data, bool saving, void *); + bool AuthRequired; const char *Challenge; const char *Salt; bool SettingsLoaded; - static void OBSSaveCallback(obs_data_t *save_data, bool saving, void *); - + static Config* Current(); private: static Config *_instance; + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context rng; }; #endif // CONFIG_H \ No newline at end of file diff --git a/WSRequestHandler.cpp b/WSRequestHandler.cpp index 4133bf74..57872527 100644 --- a/WSRequestHandler.cpp +++ b/WSRequestHandler.cpp @@ -77,6 +77,7 @@ void WSRequestHandler::processTextMessage(QString textMessage) { void WSRequestHandler::socketDisconnected() { blog(LOG_INFO, "[obs-websockets] client %s:%d disconnected", _client->peerAddress().toString().toStdString(), _client->peerPort()); + _authenticated = false; _client->deleteLater(); emit disconnected(); } @@ -147,10 +148,13 @@ void WSRequestHandler::HandleAuthenticate(WSRequestHandler *owner) { return; } - // TODO : Implement auth here - - owner->_authenticated = true; - owner->SendOKResponse(); + if (!(owner->_authenticated) && Config::Current()->CheckAuth(auth)) { + owner->_authenticated = true; + owner->SendOKResponse(); + } + else { + owner->SendErrorResponse("Authentication Failed."); + } } void WSRequestHandler::HandleSetCurrentScene(WSRequestHandler *owner) { diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini new file mode 100644 index 00000000..35e950a8 --- /dev/null +++ b/data/locale/en-US.ini @@ -0,0 +1,4 @@ +Menu.SettingsItem="Websocket server settings" +Settings.DialogTitle="obs-websocket" +Settings.AuthRequired="Enable authentication" +Settings.Password="Password" \ No newline at end of file diff --git a/data/locale/fr-FR.ini b/data/locale/fr-FR.ini new file mode 100644 index 00000000..e44dde5c --- /dev/null +++ b/data/locale/fr-FR.ini @@ -0,0 +1,4 @@ +Menu.SettingsItem="Paramètres du serveur Websocket" +Settings.DialogTitle="obs-websocket" +Settings.AuthRequired="Activer l'authentification" +Settings.Password="Mot de passe" \ No newline at end of file diff --git a/forms/settings-dialog.cpp b/forms/settings-dialog.cpp new file mode 100644 index 00000000..c80c45cf --- /dev/null +++ b/forms/settings-dialog.cpp @@ -0,0 +1,70 @@ +#include +#include "settings-dialog.h" +#include "ui_settings-dialog.h" +#include "Config.h" + +#define CHANGE_ME "changeme" + +SettingsDialog::SettingsDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::SettingsDialog) +{ + ui->setupUi(this); + + connect(ui->authRequired, &QCheckBox::stateChanged, this, &SettingsDialog::AuthCheckboxChanged); + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SettingsDialog::FormAccepted); + + AuthCheckboxChanged(); +} + +void SettingsDialog::showEvent(QShowEvent *event) { + ui->authRequired->setChecked(Config::Current()->AuthRequired); + ui->password->setText(CHANGE_ME); +} + +void SettingsDialog::ToggleShowHide() { + if (!isVisible()) { + setVisible(true); + } + else { + setVisible(false); + } +} + +void SettingsDialog::AuthCheckboxChanged() { + if (ui->authRequired->isChecked()) { + ui->password->setEnabled(true); + } + else { + ui->password->setEnabled(false); + } +} + +void SettingsDialog::FormAccepted() { + if (ui->authRequired->isChecked()) { + if (ui->password->text() != CHANGE_ME) { + QByteArray pwd = ui->password->text().toLocal8Bit(); + const char *new_password = pwd; + + blog(LOG_INFO, "new password : %s", new_password); + Config::Current()->SetPassword(new_password); + } + + if (strcmp(Config::Current()->Challenge, "") != 0) { + Config::Current()->AuthRequired = true; + } + else { + Config::Current()->AuthRequired = false; + } + } + else { + Config::Current()->AuthRequired = false; + } + + obs_frontend_save(); +} + +SettingsDialog::~SettingsDialog() +{ + delete ui; +} diff --git a/forms/settings-dialog.h b/forms/settings-dialog.h new file mode 100644 index 00000000..3d0c4506 --- /dev/null +++ b/forms/settings-dialog.h @@ -0,0 +1,28 @@ +#ifndef SETTINGSDIALOG_H +#define SETTINGSDIALOG_H + +#include + +namespace Ui { +class SettingsDialog; +} + +class SettingsDialog : public QDialog +{ + Q_OBJECT + +public: + explicit SettingsDialog(QWidget *parent = 0); + ~SettingsDialog(); + void showEvent(QShowEvent *event); + void ToggleShowHide(); + +private Q_SLOTS: + void AuthCheckboxChanged(); + void FormAccepted(); + +private: + Ui::SettingsDialog *ui; +}; + +#endif // SETTINGSDIALOG_H diff --git a/forms/settings-dialog.ui b/forms/settings-dialog.ui new file mode 100644 index 00000000..e905a1ec --- /dev/null +++ b/forms/settings-dialog.ui @@ -0,0 +1,101 @@ + + + SettingsDialog + + + + 0 + 0 + 354 + 110 + + + + + 0 + 0 + + + + Settings.DialogTitle + + + false + + + + QLayout::SetDefaultConstraint + + + + + + + Settings.Password + + + + + + + QLineEdit::Password + + + + + + + Settings.AuthRequired + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + SettingsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SettingsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/obs-websocket.cpp b/obs-websocket.cpp index 78e51c4f..570b4156 100644 --- a/obs-websocket.cpp +++ b/obs-websocket.cpp @@ -1,26 +1,40 @@ #include #include +#include #include "obs-websocket.h" #include "WSEvents.h" #include "WSServer.h" #include "Config.h" +#include "forms/settings-dialog.h" OBS_DECLARE_MODULE() OBS_MODULE_USE_DEFAULT_LOCALE("obs-websocket", "en-US") WSEvents *eventHandler; WSServer *server; +SettingsDialog *settings_dialog; bool obs_module_load(void) { - blog(LOG_INFO, "[obs-websockets] you can haz websockets (version %.2f)", OBS_WEBSOCKET_VERSION); + blog(LOG_INFO, "[obs-websockets] you can haz websockets (version %.1f)", OBS_WEBSOCKET_VERSION); server = new WSServer(4444); eventHandler = new WSEvents(server); obs_frontend_add_save_callback(Config::OBSSaveCallback, Config::Current()); + QAction *menu_action = (QAction*)obs_frontend_add_tools_menu_qaction(obs_module_text("Menu.SettingsItem")); + + obs_frontend_push_ui_translation(obs_module_get_string); + settings_dialog = new SettingsDialog(); + obs_frontend_pop_ui_translation(); + + auto menu_cb = [] { + settings_dialog->ToggleShowHide(); + }; + menu_action->connect(menu_action, &QAction::triggered, menu_cb); + return true; }