Settings dialog (fixes #4) and WIP authentication (Buggy and needs testing)

This commit is contained in:
Stéphane Lepin 2016-11-12 03:59:07 +01:00
parent 5f7061c586
commit bf317987c3
10 changed files with 355 additions and 29 deletions

View File

@ -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)
Qt5::WebSockets
Qt5::Widgets
mbedcrypto)

View File

@ -1,16 +1,102 @@
#include <mbedtls/base64.h>
#include <mbedtls/sha256.h>
#include <obs-frontend-api.h>
#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);
}
}

View File

@ -2,21 +2,30 @@
#define CONFIG_H
#include <obs-module.h>
#include <mbedtls/entropy.h>
#include <mbedtls/ctr_drbg.h>
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

View File

@ -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) {

4
data/locale/en-US.ini Normal file
View File

@ -0,0 +1,4 @@
Menu.SettingsItem="Websocket server settings"
Settings.DialogTitle="obs-websocket"
Settings.AuthRequired="Enable authentication"
Settings.Password="Password"

4
data/locale/fr-FR.ini Normal file
View File

@ -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"

70
forms/settings-dialog.cpp Normal file
View File

@ -0,0 +1,70 @@
#include <obs-frontend-api.h>
#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;
}

28
forms/settings-dialog.h Normal file
View File

@ -0,0 +1,28 @@
#ifndef SETTINGSDIALOG_H
#define SETTINGSDIALOG_H
#include <QDialog>
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

101
forms/settings-dialog.ui Normal file
View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SettingsDialog</class>
<widget class="QDialog" name="SettingsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>354</width>
<height>110</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Settings.DialogTitle</string>
</property>
<property name="sizeGripEnabled">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="3" column="0">
<widget class="QLabel" name="lbl_password">
<property name="text">
<string>Settings.Password</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="password">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="authRequired">
<property name="text">
<string>Settings.AuthRequired</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>SettingsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>SettingsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -1,26 +1,40 @@
#include <obs-module.h>
#include <obs-frontend-api.h>
#include <QAction>
#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;
}