diff --git a/src/base/preferences.cpp b/src/base/preferences.cpp index 80660f17d..54c2a7988 100644 --- a/src/base/preferences.cpp +++ b/src/base/preferences.cpp @@ -743,6 +743,26 @@ void Preferences::setWebUiRootFolder(const QString &path) setValue("Preferences/WebUI/RootFolder", path); } +bool Preferences::isWebUICustomHTTPHeadersEnabled() const +{ + return value("Preferences/WebUI/CustomHTTPHeadersEnabled", false).toBool(); +} + +void Preferences::setWebUICustomHTTPHeadersEnabled(const bool enabled) +{ + setValue("Preferences/WebUI/CustomHTTPHeadersEnabled", enabled); +} + +QString Preferences::getWebUICustomHTTPHeaders() const +{ + return value("Preferences/WebUI/CustomHTTPHeaders").toString(); +} + +void Preferences::setWebUICustomHTTPHeaders(const QString &headers) +{ + setValue("Preferences/WebUI/CustomHTTPHeaders", headers); +} + bool Preferences::isDynDNSEnabled() const { return value("Preferences/DynDNS/Enabled", false).toBool(); diff --git a/src/base/preferences.h b/src/base/preferences.h index aba678a3d..f0744f931 100644 --- a/src/base/preferences.h +++ b/src/base/preferences.h @@ -223,6 +223,12 @@ public: QString getWebUiRootFolder() const; void setWebUiRootFolder(const QString &path); + // WebUI custom HTTP headers + bool isWebUICustomHTTPHeadersEnabled() const; + void setWebUICustomHTTPHeadersEnabled(bool enabled); + QString getWebUICustomHTTPHeaders() const; + void setWebUICustomHTTPHeaders(const QString &headers); + // Dynamic DNS bool isDynDNSEnabled() const; void setDynDNSEnabled(bool enabled); diff --git a/src/gui/optionsdialog.cpp b/src/gui/optionsdialog.cpp index aa4f43703..2e757eadb 100644 --- a/src/gui/optionsdialog.cpp +++ b/src/gui/optionsdialog.cpp @@ -503,6 +503,8 @@ OptionsDialog::OptionsDialog(QWidget *parent) connect(m_ui->DNSPasswordTxt, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); connect(m_ui->groupAltWebUI, &QGroupBox::toggled, this, &ThisType::enableApplyButton); connect(m_ui->textWebUIRootFolder, &FileSystemPathLineEdit::selectedPathChanged, this, &ThisType::enableApplyButton); + connect(m_ui->groupWebUIAddCustomHTTPHeaders, &QGroupBox::toggled, this, &ThisType::enableApplyButton); + connect(m_ui->textWebUICustomHTTPHeaders, &QPlainTextEdit::textChanged, this, &OptionsDialog::enableApplyButton); #endif // DISABLE_WEBUI // RSS tab @@ -862,6 +864,9 @@ void OptionsDialog::saveOptions() // Alternative UI pref->setAltWebUiEnabled(m_ui->groupAltWebUI->isChecked()); pref->setWebUiRootFolder(m_ui->textWebUIRootFolder->selectedPath()); + // Custom HTTP headers + pref->setWebUICustomHTTPHeadersEnabled(m_ui->groupWebUIAddCustomHTTPHeaders->isChecked()); + pref->setWebUICustomHTTPHeaders(m_ui->textWebUICustomHTTPHeaders->toPlainText()); } // End Web UI // End preferences @@ -1242,6 +1247,9 @@ void OptionsDialog::loadOptions() m_ui->groupAltWebUI->setChecked(pref->isAltWebUiEnabled()); m_ui->textWebUIRootFolder->setSelectedPath(pref->getWebUiRootFolder()); + // Custom HTTP headers + m_ui->groupWebUIAddCustomHTTPHeaders->setChecked(pref->isWebUICustomHTTPHeadersEnabled()); + m_ui->textWebUICustomHTTPHeaders->setPlainText(pref->getWebUICustomHTTPHeaders()); // End Web UI preferences } diff --git a/src/gui/optionsdialog.ui b/src/gui/optionsdialog.ui index 98ece945e..2e7e91873 100644 --- a/src/gui/optionsdialog.ui +++ b/src/gui/optionsdialog.ui @@ -3220,6 +3220,28 @@ Use ';' to split multiple entries. Can use wildcard '*'. + + + + Add custom HTTP headers + + + true + + + + + + QPlainTextEdit::NoWrap + + + Header: value pairs, one per line + + + + + + diff --git a/src/webui/api/appcontroller.cpp b/src/webui/api/appcontroller.cpp index c7136efea..4dd335cd9 100644 --- a/src/webui/api/appcontroller.cpp +++ b/src/webui/api/appcontroller.cpp @@ -243,6 +243,9 @@ void AppController::preferencesAction() data["web_ui_csrf_protection_enabled"] = pref->isWebUiCSRFProtectionEnabled(); data["web_ui_secure_cookie_enabled"] = pref->isWebUiSecureCookieEnabled(); data["web_ui_host_header_validation_enabled"] = pref->isWebUIHostHeaderValidationEnabled(); + // Custom HTTP headers + data["web_ui_use_custom_http_headers_enabled"] = pref->isWebUICustomHTTPHeadersEnabled(); + data["web_ui_custom_http_headers"] = pref->getWebUICustomHTTPHeaders(); // Update my dynamic domain name data["dyndns_enabled"] = pref->isDynDNSEnabled(); data["dyndns_service"] = pref->getDynDNSService(); @@ -623,6 +626,11 @@ void AppController::setPreferencesAction() pref->setWebUiSecureCookieEnabled(it.value().toBool()); if (hasKey("web_ui_host_header_validation_enabled")) pref->setWebUIHostHeaderValidationEnabled(it.value().toBool()); + // Custom HTTP headers + if (hasKey("web_ui_use_custom_http_headers_enabled")) + pref->setWebUICustomHTTPHeadersEnabled(it.value().toBool()); + if (hasKey("web_ui_custom_http_headers")) + pref->setWebUICustomHTTPHeaders(it.value().toString()); // Update my dynamic domain name if (hasKey("dyndns_enabled")) pref->setDynDNSEnabled(it.value().toBool()); diff --git a/src/webui/webapplication.cpp b/src/webui/webapplication.cpp index 41a3b4e59..8635eb61c 100644 --- a/src/webui/webapplication.cpp +++ b/src/webui/webapplication.cpp @@ -347,6 +347,27 @@ void WebApplication::configure() : QLatin1String("default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; script-src 'self' 'unsafe-inline'; object-src 'none'; form-action 'self';")) + (m_isClickjackingProtectionEnabled ? QLatin1String(" frame-ancestors 'self';") : QLatin1String("")) + (m_isHttpsEnabled ? QLatin1String(" upgrade-insecure-requests;") : QLatin1String("")); + + m_useCustomHTTPHeaders = pref->isWebUICustomHTTPHeadersEnabled(); + m_customHTTPHeaders.clear(); + if (m_useCustomHTTPHeaders) { + const QString customHeaders = pref->getWebUICustomHTTPHeaders().trimmed(); + const QVector customHeaderLines = customHeaders.splitRef('\n', QString::SkipEmptyParts); + m_customHTTPHeaders.reserve(customHeaderLines.size()); + + for (const QStringRef &line : customHeaderLines) { + const int idx = line.indexOf(':'); + if (idx < 0) { + // require separator `:` to be present even if `value` field can be empty + LogMsg(tr("Missing ':' separator in WebUI custom HTTP header: \"%1\"").arg(line.toString()), Log::WARNING); + continue; + } + + const QString header = line.left(idx).trimmed().toString(); + const QString value = line.mid(idx + 1).trimmed().toString(); + m_customHTTPHeaders.push_back({header, value}); + } + } } void WebApplication::registerAPIController(const QString &scope, APIController *controller) @@ -451,6 +472,11 @@ Http::Response WebApplication::processRequest(const Http::Request &request, cons if (!m_contentSecurityPolicy.isEmpty()) header(QLatin1String(Http::HEADER_CONTENT_SECURITY_POLICY), m_contentSecurityPolicy); + if (m_useCustomHTTPHeaders) { + for (const CustomHTTPHeader &i : asConst(m_customHTTPHeaders)) + header(i.name, i.value); + } + return response(); } diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index fb70e11a7..db150a409 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -157,4 +157,13 @@ private: bool m_isHostHeaderValidationEnabled; bool m_isHttpsEnabled; QString m_contentSecurityPolicy; + + // Custom HTTP headers + struct CustomHTTPHeader + { + QString name; + QString value; + }; + bool m_useCustomHTTPHeaders; + QVector m_customHTTPHeaders; }; diff --git a/src/webui/www/private/views/preferences.html b/src/webui/www/private/views/preferences.html index a24b37a91..687321130 100644 --- a/src/webui/www/private/views/preferences.html +++ b/src/webui/www/private/views/preferences.html @@ -787,6 +787,14 @@ + +
+ + + + + +
@@ -1139,6 +1147,7 @@ updateBypasssAuthSettings: updateBypasssAuthSettings, updateAlternativeWebUISettings: updateAlternativeWebUISettings, updateHostHeaderValidationSettings: updateHostHeaderValidationSettings, + updateWebUICustomHTTPHeadersSettings: updateWebUICustomHTTPHeadersSettings, updateDynDnsSettings: updateDynDnsSettings, registerDynDns: registerDynDns, applyPreferences: applyPreferences @@ -1381,6 +1390,11 @@ $('webui_domain_textarea').setProperty('disabled', !isHostHeaderValidationEnabled); }; + const updateWebUICustomHTTPHeadersSettings = function() { + const isEnabled = $('webUIUseCustomHTTPHeadersCheckbox').getProperty('checked'); + $('webUICustomHTTPHeadersTextarea').setProperty('disabled', !isEnabled); + }; + const updateDynDnsSettings = function() { const isDynDnsEnabled = $('use_dyndns_checkbox').getProperty('checked'); $('dyndns_select').setProperty('disabled', !isDynDnsEnabled); @@ -1737,6 +1751,11 @@ $('host_header_validation_checkbox').setProperty('checked', pref.web_ui_host_header_validation_enabled); updateHostHeaderValidationSettings(); + // Custom HTTP headers + $('webUIUseCustomHTTPHeadersCheckbox').setProperty('checked', pref.web_ui_use_custom_http_headers_enabled); + $('webUICustomHTTPHeadersTextarea').setProperty('value', pref.web_ui_custom_http_headers); + updateWebUICustomHTTPHeadersSettings(); + // Update my dynamic domain name $('use_dyndns_checkbox').setProperty('checked', pref.dyndns_enabled); $('dyndns_select').setProperty('value', pref.dyndns_service); @@ -2100,11 +2119,16 @@ settings.set('alternative_webui_enabled', alternative_webui_enabled); settings.set('alternative_webui_path', webui_files_location_textarea); + // Security settings.set('web_ui_clickjacking_protection_enabled', $('clickjacking_protection_checkbox').getProperty('checked')); settings.set('web_ui_csrf_protection_enabled', $('csrf_protection_checkbox').getProperty('checked')); settings.set('web_ui_secure_cookie_enabled', $('secureCookieCheckbox').getProperty('checked')); settings.set('web_ui_host_header_validation_enabled', $('host_header_validation_checkbox').getProperty('checked')); + // Custom HTTP headers + settings.set('web_ui_use_custom_http_headers_enabled', $('webUIUseCustomHTTPHeadersCheckbox').getProperty('checked')); + settings.set('web_ui_custom_http_headers', $('webUICustomHTTPHeadersTextarea').getProperty('value')); + // Update my dynamic domain name settings.set('dyndns_enabled', $('use_dyndns_checkbox').getProperty('checked')); settings.set('dyndns_service', $('dyndns_select').getProperty('value'));