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