diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini
index 69fbed16..0382082f 100644
--- a/data/locale/en-US.ini
+++ b/data/locale/en-US.ini
@@ -6,6 +6,7 @@ OBSWebsocket.Settings.Password="Password"
OBSWebsocket.Settings.LockToIPv4="Lock server to only using IPv4"
OBSWebsocket.Settings.DebugEnable="Enable debug logging"
OBSWebsocket.Settings.AlertsEnable="Enable System Tray Alerts"
+OBSWebsocket.Settings.AuthDisabledWarning="Running obs-websocket with authentication disabled is not recommended, as it allows attackers to easily collect sensetive data. Are you sure you want to proceed?"
OBSWebsocket.NotifyConnect.Title="New WebSocket connection"
OBSWebsocket.NotifyConnect.Message="Client %1 connected"
OBSWebsocket.NotifyDisconnect.Title="WebSocket client disconnected"
@@ -15,3 +16,6 @@ OBSWebsocket.Server.StartFailed.Message="The WebSockets server failed to start,
OBSWebsocket.ProfileChanged.Started="WebSockets server enabled in this profile. Server started."
OBSWebsocket.ProfileChanged.Stopped="WebSockets server disabled in this profile. Server stopped."
OBSWebsocket.ProfileChanged.Restarted="WebSockets server port changed in this profile. Server restarted."
+OBSWebsocket.InitialPasswordSetup.Title="obs-websocket - Server Password Configuration"
+OBSWebsocket.InitialPasswordSetup.Text="It looks like you are running obs-websocket for the first time. Do you want to configure a password now for the WebSockets server? Setting a password is highly recommended."
+OBSWebsocket.InitialPasswordSetup.DismissedText="You can configure a server password later in the WebSockets Server Settings. (Under the Tools menu of OBS Studio)"
diff --git a/docs/partials/introduction.md b/docs/partials/introduction.md
index 496363e7..c63111bf 100644
--- a/docs/partials/introduction.md
+++ b/docs/partials/introduction.md
@@ -5,6 +5,8 @@ Messages are exchanged between the client and the server as JSON objects.
This protocol is based on the original OBS Remote protocol created by Bill Hamilton, with new commands specific to OBS Studio. As of v5.0.0, backwards compatability with the protocol will not be kept.
# Authentication
+**Starting with obs-websocket 4.9, authentication is enabled by default and users are encouraged to configure a password on first run.**
+
`obs-websocket` uses SHA256 to transmit credentials.
A request for [`GetAuthRequired`](#getauthrequired) returns two elements:
diff --git a/src/Config.cpp b/src/Config.cpp
index 1a17065f..3cb9730e 100644
--- a/src/Config.cpp
+++ b/src/Config.cpp
@@ -18,9 +18,13 @@ with this program. If not, see
#include
+#include
#include
#include
#include
+#include
+#include
+#include
#define SECTION_NAME "WebsocketAPI"
#define PARAM_ENABLE "ServerEnabled"
@@ -32,6 +36,8 @@ with this program. If not, see
#define PARAM_SECRET "AuthSecret"
#define PARAM_SALT "AuthSalt"
+#define GLOBAL_AUTH_SETUP_PROMPTED "AuthSetupPrompted"
+
#include "Utils.h"
#include "WSServer.h"
@@ -45,7 +51,7 @@ Config::Config() :
LockToIPv4(false),
DebugEnabled(false),
AlertsEnabled(true),
- AuthRequired(false),
+ AuthRequired(true),
Secret(""),
Salt(""),
SettingsLoaded(false)
@@ -130,6 +136,70 @@ config_t* Config::GetConfigStore()
return obs_frontend_get_profile_config();
}
+void Config::MigrateFromGlobalSettings()
+{
+ config_t* source = obs_frontend_get_global_config();
+ config_t* destination = obs_frontend_get_profile_config();
+
+ if(config_has_user_value(source, SECTION_NAME, PARAM_ENABLE)) {
+ bool value = config_get_bool(source, SECTION_NAME, PARAM_ENABLE);
+ config_set_bool(destination, SECTION_NAME, PARAM_ENABLE, value);
+
+ config_remove_value(source, SECTION_NAME, PARAM_ENABLE);
+ }
+
+ if(config_has_user_value(source, SECTION_NAME, PARAM_PORT)) {
+ uint64_t value = config_get_uint(source, SECTION_NAME, PARAM_PORT);
+ config_set_uint(destination, SECTION_NAME, PARAM_PORT, value);
+
+ config_remove_value(source, SECTION_NAME, PARAM_PORT);
+ }
+
+ if(config_has_user_value(source, SECTION_NAME, PARAM_LOCKTOIPV4)) {
+ bool value = config_get_bool(source, SECTION_NAME, PARAM_LOCKTOIPV4);
+ config_set_bool(destination, SECTION_NAME, PARAM_LOCKTOIPV4, value);
+
+ config_remove_value(source, SECTION_NAME, PARAM_LOCKTOIPV4);
+ }
+
+ if(config_has_user_value(source, SECTION_NAME, PARAM_DEBUG)) {
+ bool value = config_get_bool(source, SECTION_NAME, PARAM_DEBUG);
+ config_set_bool(destination, SECTION_NAME, PARAM_DEBUG, value);
+
+ config_remove_value(source, SECTION_NAME, PARAM_DEBUG);
+ }
+
+ if(config_has_user_value(source, SECTION_NAME, PARAM_ALERT)) {
+ bool value = config_get_bool(source, SECTION_NAME, PARAM_ALERT);
+ config_set_bool(destination, SECTION_NAME, PARAM_ALERT, value);
+
+ config_remove_value(source, SECTION_NAME, PARAM_ALERT);
+ }
+
+ if(config_has_user_value(source, SECTION_NAME, PARAM_AUTHREQUIRED)) {
+ bool value = config_get_bool(source, SECTION_NAME, PARAM_AUTHREQUIRED);
+ config_set_bool(destination, SECTION_NAME, PARAM_AUTHREQUIRED, value);
+
+ config_remove_value(source, SECTION_NAME, PARAM_AUTHREQUIRED);
+ }
+
+ if(config_has_user_value(source, SECTION_NAME, PARAM_SECRET)) {
+ const char* value = config_get_string(source, SECTION_NAME, PARAM_SECRET);
+ config_set_string(destination, SECTION_NAME, PARAM_SECRET, value);
+
+ config_remove_value(source, SECTION_NAME, PARAM_SECRET);
+ }
+
+ if(config_has_user_value(source, SECTION_NAME, PARAM_SALT)) {
+ const char* value = config_get_string(source, SECTION_NAME, PARAM_SALT);
+ config_set_string(destination, SECTION_NAME, PARAM_SALT, value);
+
+ config_remove_value(source, SECTION_NAME, PARAM_SALT);
+ }
+
+ config_save(destination);
+}
+
QString Config::GenerateSalt()
{
// Generate 32 random chars
@@ -233,68 +303,46 @@ void Config::OnFrontendEvent(enum obs_frontend_event event, void* param)
}
}
}
+ else if (event == OBS_FRONTEND_EVENT_FINISHED_LOADING) {
+ FirstRunPasswordSetup();
+ }
}
-void Config::MigrateFromGlobalSettings()
+void Config::FirstRunPasswordSetup()
{
- config_t* source = obs_frontend_get_global_config();
- config_t* destination = obs_frontend_get_profile_config();
-
- if(config_has_user_value(source, SECTION_NAME, PARAM_ENABLE)) {
- bool value = config_get_bool(source, SECTION_NAME, PARAM_ENABLE);
- config_set_bool(destination, SECTION_NAME, PARAM_ENABLE, value);
-
- config_remove_value(source, SECTION_NAME, PARAM_ENABLE);
+ // check if we already showed the auth setup prompt to the user, independently of the current settings (tied to the current profile)
+ config_t* globalConfig = obs_frontend_get_global_config();
+ bool alreadyPrompted = config_get_bool(globalConfig, SECTION_NAME, GLOBAL_AUTH_SETUP_PROMPTED);
+ if (alreadyPrompted) {
+ return;
}
- if(config_has_user_value(source, SECTION_NAME, PARAM_PORT)) {
- uint64_t value = config_get_uint(source, SECTION_NAME, PARAM_PORT);
- config_set_uint(destination, SECTION_NAME, PARAM_PORT, value);
+ // lift the flag up and save it
+ config_set_bool(globalConfig, SECTION_NAME, GLOBAL_AUTH_SETUP_PROMPTED, true);
+ config_save(globalConfig);
- config_remove_value(source, SECTION_NAME, PARAM_PORT);
+ // check if the password is already set
+ auto config = GetConfig();
+ if (!(config->Secret.isEmpty()) && !(config->Salt.isEmpty())) {
+ return;
}
+
+ obs_frontend_push_ui_translation(obs_module_get_string);
+ QString dialogTitle = QObject::tr("OBSWebsocket.InitialPasswordSetup.Title");
+ QString dialogText = QObject::tr("OBSWebsocket.InitialPasswordSetup.Text");
+ QString dismissedText = QObject::tr("OBSWebsocket.InitialPasswordSetup.DismissedText");
+ obs_frontend_pop_ui_translation();
+
+ auto mainWindow = reinterpret_cast(
+ obs_frontend_get_main_window()
+ );
- if(config_has_user_value(source, SECTION_NAME, PARAM_LOCKTOIPV4)) {
- bool value = config_get_bool(source, SECTION_NAME, PARAM_LOCKTOIPV4);
- config_set_bool(destination, SECTION_NAME, PARAM_LOCKTOIPV4, value);
-
- config_remove_value(source, SECTION_NAME, PARAM_LOCKTOIPV4);
+ QMessageBox::StandardButton response = QMessageBox::question(mainWindow, dialogTitle, dialogText);
+ if (response == QMessageBox::Yes) {
+ ShowPasswordSetting();
}
-
- if(config_has_user_value(source, SECTION_NAME, PARAM_DEBUG)) {
- bool value = config_get_bool(source, SECTION_NAME, PARAM_DEBUG);
- config_set_bool(destination, SECTION_NAME, PARAM_DEBUG, value);
-
- config_remove_value(source, SECTION_NAME, PARAM_DEBUG);
+ else {
+ // tell the user they still can set the password later in our settings dialog
+ QMessageBox::information(mainWindow, dialogTitle, dismissedText);
}
-
- if(config_has_user_value(source, SECTION_NAME, PARAM_ALERT)) {
- bool value = config_get_bool(source, SECTION_NAME, PARAM_ALERT);
- config_set_bool(destination, SECTION_NAME, PARAM_ALERT, value);
-
- config_remove_value(source, SECTION_NAME, PARAM_ALERT);
- }
-
- if(config_has_user_value(source, SECTION_NAME, PARAM_AUTHREQUIRED)) {
- bool value = config_get_bool(source, SECTION_NAME, PARAM_AUTHREQUIRED);
- config_set_bool(destination, SECTION_NAME, PARAM_AUTHREQUIRED, value);
-
- config_remove_value(source, SECTION_NAME, PARAM_AUTHREQUIRED);
- }
-
- if(config_has_user_value(source, SECTION_NAME, PARAM_SECRET)) {
- const char* value = config_get_string(source, SECTION_NAME, PARAM_SECRET);
- config_set_string(destination, SECTION_NAME, PARAM_SECRET, value);
-
- config_remove_value(source, SECTION_NAME, PARAM_SECRET);
- }
-
- if(config_has_user_value(source, SECTION_NAME, PARAM_SALT)) {
- const char* value = config_get_string(source, SECTION_NAME, PARAM_SALT);
- config_set_string(destination, SECTION_NAME, PARAM_SALT, value);
-
- config_remove_value(source, SECTION_NAME, PARAM_SALT);
- }
-
- config_save(destination);
}
diff --git a/src/Config.h b/src/Config.h
index f21e396a..1aa29ebf 100644
--- a/src/Config.h
+++ b/src/Config.h
@@ -55,4 +55,5 @@ class Config {
private:
static void OnFrontendEvent(enum obs_frontend_event event, void* param);
+ static void FirstRunPasswordSetup();
};
diff --git a/src/forms/settings-dialog.cpp b/src/forms/settings-dialog.cpp
index ca686dfe..f852186c 100644
--- a/src/forms/settings-dialog.cpp
+++ b/src/forms/settings-dialog.cpp
@@ -16,12 +16,16 @@ You should have received a copy of the GNU General Public License along
with this program. If not, see
*/
+#include "settings-dialog.h"
+
#include
+#include
+#include
#include "../obs-websocket.h"
#include "../Config.h"
#include "../WSServer.h"
-#include "settings-dialog.h"
+
#define CHANGE_ME "changeme"
@@ -35,9 +39,6 @@ SettingsDialog::SettingsDialog(QWidget* parent) :
this, &SettingsDialog::AuthCheckboxChanged);
connect(ui->buttonBox, &QDialogButtonBox::accepted,
this, &SettingsDialog::FormAccepted);
-
-
- AuthCheckboxChanged();
}
void SettingsDialog::showEvent(QShowEvent* event) {
@@ -50,8 +51,12 @@ void SettingsDialog::showEvent(QShowEvent* event) {
ui->debugEnabled->setChecked(conf->DebugEnabled);
ui->alertsEnabled->setChecked(conf->AlertsEnabled);
+ ui->authRequired->blockSignals(true);
ui->authRequired->setChecked(conf->AuthRequired);
+ ui->authRequired->blockSignals(false);
+
ui->password->setText(CHANGE_ME);
+ ui->password->setEnabled(ui->authRequired->isChecked());
}
void SettingsDialog::ToggleShowHide() {
@@ -61,11 +66,30 @@ void SettingsDialog::ToggleShowHide() {
setVisible(false);
}
+void SettingsDialog::PreparePasswordEntry() {
+ ui->authRequired->blockSignals(true);
+ ui->authRequired->setChecked(true);
+ ui->authRequired->blockSignals(false);
+ ui->password->setEnabled(true);
+ ui->password->setFocus();
+}
+
void SettingsDialog::AuthCheckboxChanged() {
- if (ui->authRequired->isChecked())
+ if (ui->authRequired->isChecked()) {
ui->password->setEnabled(true);
- else
- ui->password->setEnabled(false);
+ }
+ else {
+ obs_frontend_push_ui_translation(obs_module_get_string);
+ QString authDisabledWarning = QObject::tr("OBSWebsocket.Settings.AuthDisabledWarning");
+ obs_frontend_pop_ui_translation();
+
+ QMessageBox::StandardButton response = QMessageBox::question(this, "obs-websocket", authDisabledWarning);
+ if (response == QMessageBox::Yes) {
+ ui->password->setEnabled(false);
+ } else {
+ ui->authRequired->setChecked(true);
+ }
+ }
}
void SettingsDialog::FormAccepted() {
diff --git a/src/forms/settings-dialog.h b/src/forms/settings-dialog.h
index 138ae395..f67f1f98 100644
--- a/src/forms/settings-dialog.h
+++ b/src/forms/settings-dialog.h
@@ -31,6 +31,7 @@ public:
~SettingsDialog();
void showEvent(QShowEvent* event);
void ToggleShowHide();
+ void PreparePasswordEntry();
private Q_SLOTS:
void AuthCheckboxChanged();
diff --git a/src/obs-websocket.cpp b/src/obs-websocket.cpp
index b198ebb7..4fb3e729 100644
--- a/src/obs-websocket.cpp
+++ b/src/obs-websocket.cpp
@@ -47,6 +47,7 @@ OBS_MODULE_USE_DEFAULT_LOCALE("obs-websocket", "en-US")
ConfigPtr _config;
WSServerPtr _server;
WSEventsPtr _eventsSystem;
+SettingsDialog* settingsDialog = nullptr;
bool obs_module_load(void) {
blog(LOG_INFO, "you can haz websockets (version %s)", OBS_WEBSOCKET_VERSION);
@@ -64,14 +65,14 @@ bool obs_module_load(void) {
// UI setup
obs_frontend_push_ui_translation(obs_module_get_string);
QMainWindow* mainWindow = (QMainWindow*)obs_frontend_get_main_window();
- SettingsDialog* settingsDialog = new SettingsDialog(mainWindow);
+ settingsDialog = new SettingsDialog(mainWindow);
obs_frontend_pop_ui_translation();
const char* menuActionText =
obs_module_text("OBSWebsocket.Settings.DialogTitle");
QAction* menuAction =
(QAction*)obs_frontend_add_tools_menu_qaction(menuActionText);
- QObject::connect(menuAction, &QAction::triggered, [settingsDialog] {
+ QObject::connect(menuAction, &QAction::triggered, [] {
// The settings dialog belongs to the main window. Should be ok
// to pass the pointer to this QAction belonging to the main window
settingsDialog->ToggleShowHide();
@@ -115,3 +116,10 @@ WSServerPtr GetServer() {
WSEventsPtr GetEventsSystem() {
return _eventsSystem;
}
+
+void ShowPasswordSetting() {
+ if (settingsDialog) {
+ settingsDialog->PreparePasswordEntry();
+ settingsDialog->setVisible(true);
+ }
+}
diff --git a/src/obs-websocket.h b/src/obs-websocket.h
index 5f118d28..38da66a4 100644
--- a/src/obs-websocket.h
+++ b/src/obs-websocket.h
@@ -55,6 +55,7 @@ typedef std::shared_ptr WSEventsPtr;
ConfigPtr GetConfig();
WSServerPtr GetServer();
WSEventsPtr GetEventsSystem();
+void ShowPasswordSetting();
#define OBS_WEBSOCKET_VERSION "4.8.0"