diff --git a/src/base/http/types.h b/src/base/http/types.h index 4ff26ea7889..a63a601d0f0 100644 --- a/src/base/http/types.h +++ b/src/base/http/types.h @@ -52,6 +52,7 @@ namespace Http inline const char HEADER_REFERRER_POLICY[] = "referrer-policy"; inline const char HEADER_SET_COOKIE[] = "set-cookie"; inline const char HEADER_X_CONTENT_TYPE_OPTIONS[] = "x-content-type-options"; + inline const char HEADER_X_FORWARDED_FOR[] = "x-forwarded-for"; inline const char HEADER_X_FORWARDED_HOST[] = "x-forwarded-host"; inline const char HEADER_X_FRAME_OPTIONS[] = "x-frame-options"; inline const char HEADER_X_XSS_PROTECTION[] = "x-xss-protection"; diff --git a/src/base/preferences.cpp b/src/base/preferences.cpp index 868fd74db69..1af6cd76a18 100644 --- a/src/base/preferences.cpp +++ b/src/base/preferences.cpp @@ -761,6 +761,26 @@ void Preferences::setWebUICustomHTTPHeaders(const QString &headers) setValue("Preferences/WebUI/CustomHTTPHeaders", headers); } +bool Preferences::isWebUIReverseProxySupportEnabled() const +{ + return value("Preferences/WebUI/ReverseProxySupportEnabled", false).toBool(); +} + +void Preferences::setWebUIReverseProxySupportEnabled(const bool enabled) +{ + setValue("Preferences/WebUI/ReverseProxySupportEnabled", enabled); +} + +QString Preferences::getWebUITrustedReverseProxiesList() const +{ + return value("Preferences/WebUI/TrustedReverseProxiesList").toString(); +} + +void Preferences::setWebUITrustedReverseProxiesList(const QString &addr) +{ + setValue("Preferences/WebUI/TrustedReverseProxiesList", addr); +} + bool Preferences::isDynDNSEnabled() const { return value("Preferences/DynDNS/Enabled", false).toBool(); diff --git a/src/base/preferences.h b/src/base/preferences.h index 96313588ccc..f3336184941 100644 --- a/src/base/preferences.h +++ b/src/base/preferences.h @@ -225,6 +225,12 @@ class Preferences : public QObject QString getWebUICustomHTTPHeaders() const; void setWebUICustomHTTPHeaders(const QString &headers); + // Reverse proxy + bool isWebUIReverseProxySupportEnabled() const; + void setWebUIReverseProxySupportEnabled(bool enabled); + QString getWebUITrustedReverseProxiesList() const; + void setWebUITrustedReverseProxiesList(const QString &addr); + // Dynamic DNS bool isDynDNSEnabled() const; void setDynDNSEnabled(bool enabled); diff --git a/src/gui/optionsdialog.cpp b/src/gui/optionsdialog.cpp index 438f647a69a..4ed42877fb1 100644 --- a/src/gui/optionsdialog.cpp +++ b/src/gui/optionsdialog.cpp @@ -515,6 +515,8 @@ OptionsDialog::OptionsDialog(QWidget *parent) 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); + connect(m_ui->groupEnableReverseProxySupport, &QGroupBox::toggled, this, &ThisType::enableApplyButton); + connect(m_ui->textTrustedReverseProxiesList, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); #endif // DISABLE_WEBUI // RSS tab @@ -871,6 +873,9 @@ void OptionsDialog::saveOptions() // Custom HTTP headers pref->setWebUICustomHTTPHeadersEnabled(m_ui->groupWebUIAddCustomHTTPHeaders->isChecked()); pref->setWebUICustomHTTPHeaders(m_ui->textWebUICustomHTTPHeaders->toPlainText()); + // Reverse proxy + pref->setWebUIReverseProxySupportEnabled(m_ui->groupEnableReverseProxySupport->isChecked()); + pref->setWebUITrustedReverseProxiesList(m_ui->textTrustedReverseProxiesList->text()); } // End Web UI // End preferences @@ -1274,6 +1279,9 @@ void OptionsDialog::loadOptions() // Custom HTTP headers m_ui->groupWebUIAddCustomHTTPHeaders->setChecked(pref->isWebUICustomHTTPHeadersEnabled()); m_ui->textWebUICustomHTTPHeaders->setPlainText(pref->getWebUICustomHTTPHeaders()); + // Reverse proxy + m_ui->groupEnableReverseProxySupport->setChecked(pref->isWebUIReverseProxySupportEnabled()); + m_ui->textTrustedReverseProxiesList->setText(pref->getWebUITrustedReverseProxiesList()); // End Web UI preferences } diff --git a/src/gui/optionsdialog.ui b/src/gui/optionsdialog.ui index 8b394e971cf..9d89ee60bd7 100644 --- a/src/gui/optionsdialog.ui +++ b/src/gui/optionsdialog.ui @@ -3287,6 +3287,36 @@ Use ';' to split multiple entries. Can use wildcard '*'. + + + + + Enable reverse proxy support + + + true + + + + + + + + Trusted proxies list: + + + + + + + Specify reverse proxy IPs in order to use forwarded client address (X-Forwarded-For attribute), use ';' to split multiple entries. + + + + + + + diff --git a/src/webui/api/appcontroller.cpp b/src/webui/api/appcontroller.cpp index 6cbb5c2d6b4..0b5e3a3af2b 100644 --- a/src/webui/api/appcontroller.cpp +++ b/src/webui/api/appcontroller.cpp @@ -259,6 +259,9 @@ void AppController::preferencesAction() // Custom HTTP headers data["web_ui_use_custom_http_headers_enabled"] = pref->isWebUICustomHTTPHeadersEnabled(); data["web_ui_custom_http_headers"] = pref->getWebUICustomHTTPHeaders(); + // Reverse proxy + data["web_ui_reverse_proxy_enabled"] = pref->isWebUIReverseProxySupportEnabled(); + data["web_ui_reverse_proxies_list"] = pref->getWebUITrustedReverseProxiesList(); // Update my dynamic domain name data["dyndns_enabled"] = pref->isDynDNSEnabled(); data["dyndns_service"] = pref->getDynDNSService(); @@ -680,6 +683,11 @@ void AppController::setPreferencesAction() pref->setWebUICustomHTTPHeadersEnabled(it.value().toBool()); if (hasKey("web_ui_custom_http_headers")) pref->setWebUICustomHTTPHeaders(it.value().toString()); + // Reverse proxy + if (hasKey("web_ui_reverse_proxy_enabled")) + pref->setWebUIReverseProxySupportEnabled(it.value().toBool()); + if (hasKey("web_ui_reverse_proxies_list")) + pref->setWebUITrustedReverseProxiesList(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 4333cee9e63..d71531ce4da 100644 --- a/src/webui/webapplication.cpp +++ b/src/webui/webapplication.cpp @@ -404,6 +404,24 @@ void WebApplication::configure() m_prebuiltHeaders.push_back({header, value}); } } + + m_isReverseProxySupportEnabled = pref->isWebUIReverseProxySupportEnabled(); + if (m_isReverseProxySupportEnabled) + { + m_trustedReverseProxyList.clear(); + + const QStringList proxyList = pref->getWebUITrustedReverseProxiesList().split(';', Qt::SkipEmptyParts); + + for (const QString &proxy : proxyList) + { + QHostAddress ip; + if (ip.setAddress(proxy)) + m_trustedReverseProxyList.push_back(ip); + } + + if (m_trustedReverseProxyList.isEmpty()) + m_isReverseProxySupportEnabled = false; + } } void WebApplication::registerAPIController(const QString &scope, APIController *controller) @@ -495,6 +513,9 @@ Http::Response WebApplication::processRequest(const Http::Request &request, cons throw UnauthorizedHTTPError(); } + // reverse proxy resolve client address + m_clientAddress = resolveClientAddress(); + sessionInitialize(); doProcessRequest(); } @@ -512,7 +533,7 @@ Http::Response WebApplication::processRequest(const Http::Request &request, cons QString WebApplication::clientId() const { - return env().clientAddress.toString(); + return m_clientAddress.toString(); } void WebApplication::sessionInitialize() @@ -567,9 +588,9 @@ QString WebApplication::generateSid() const bool WebApplication::isAuthNeeded() { - if (!m_isLocalAuthEnabled && Utils::Net::isLoopbackAddress(m_env.clientAddress)) + if (!m_isLocalAuthEnabled && Utils::Net::isLoopbackAddress(m_clientAddress)) return false; - if (m_isAuthSubnetWhitelistEnabled && Utils::Net::isIPInRange(m_env.clientAddress, m_authSubnetWhitelist)) + if (m_isAuthSubnetWhitelistEnabled && Utils::Net::isIPInRange(m_clientAddress, m_authSubnetWhitelist)) return false; return true; } @@ -705,6 +726,40 @@ bool WebApplication::validateHostHeader(const QStringList &domains) const return false; } +QHostAddress WebApplication::resolveClientAddress() const +{ + if (!m_isReverseProxySupportEnabled) + return m_env.clientAddress; + + // Only reverse proxy can overwrite client address + if (!m_trustedReverseProxyList.contains(m_env.clientAddress)) + return m_env.clientAddress; + + const QString forwardedFor = m_request.headers.value(Http::HEADER_X_FORWARDED_FOR); + + if (!forwardedFor.isEmpty()) + { + // client address is the 1st global IP in X-Forwarded-For or, if none available, the 1st IP in the list + const QStringList remoteIpList = forwardedFor.split(',', Qt::SkipEmptyParts); + + if (!remoteIpList.isEmpty()) + { + QHostAddress clientAddress; + + for (const QString &remoteIp : remoteIpList) + { + if (clientAddress.setAddress(remoteIp) && clientAddress.isGlobal()) + return clientAddress; + } + + if (clientAddress.setAddress(remoteIpList[0])) + return clientAddress; + } + } + + return m_env.clientAddress; +} + // WebSession WebSession::WebSession(const QString &sid) diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index 0f60acd10d1..6f1a8ad82c4 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -114,6 +114,8 @@ class WebApplication final bool isCrossSiteRequest(const Http::Request &request) const; bool validateHostHeader(const QStringList &domains) const; + QHostAddress resolveClientAddress() const; + // Persistent data QHash m_sessions; @@ -154,5 +156,10 @@ class WebApplication final bool m_isHostHeaderValidationEnabled; bool m_isHttpsEnabled; + // Reverse proxy + bool m_isReverseProxySupportEnabled; + QVector m_trustedReverseProxyList; + QHostAddress m_clientAddress; + QVector m_prebuiltHeaders; }; diff --git a/src/webui/www/private/views/preferences.html b/src/webui/www/private/views/preferences.html index fd6f56aef2c..319cd08a670 100644 --- a/src/webui/www/private/views/preferences.html +++ b/src/webui/www/private/views/preferences.html @@ -838,6 +838,18 @@ + + + + + QBT_TR(Enable reverse proxy support)QBT_TR[CONTEXT=OptionsDialog] + + + + QBT_TR(Trusted proxies list:)QBT_TR[CONTEXT=OptionsDialog] + + + @@ -1279,6 +1291,7 @@ updateAlternativeWebUISettings: updateAlternativeWebUISettings, updateHostHeaderValidationSettings: updateHostHeaderValidationSettings, updateWebUICustomHTTPHeadersSettings: updateWebUICustomHTTPHeadersSettings, + updateWebUIReverseProxySettings: updateWebUIReverseProxySettings, updateDynDnsSettings: updateDynDnsSettings, registerDynDns: registerDynDns, applyPreferences: applyPreferences @@ -1511,6 +1524,11 @@ $('webUICustomHTTPHeadersTextarea').setProperty('disabled', !isEnabled); }; + const updateWebUIReverseProxySettings = function() { + const isEnabled = $('webUIReverseProxySupportCheckbox').getProperty('checked'); + $('webUIReverseProxiesListTextarea').setProperty('disabled', !isEnabled); + }; + const updateDynDnsSettings = function() { const isDynDnsEnabled = $('use_dyndns_checkbox').getProperty('checked'); $('dyndns_select').setProperty('disabled', !isDynDnsEnabled); @@ -1886,6 +1904,11 @@ $('webUICustomHTTPHeadersTextarea').setProperty('value', pref.web_ui_custom_http_headers); updateWebUICustomHTTPHeadersSettings(); + // Reverse Proxy + $('webUIReverseProxySupportCheckbox').setProperty('checked', pref.web_ui_reverse_proxy_support_enabled); + $('webUIReverseProxiesListTextarea').setProperty('value', pref.web_ui_trusted_reverse_proxies_list); + updateWebUIReverseProxySettings(); + // Update my dynamic domain name $('use_dyndns_checkbox').setProperty('checked', pref.dyndns_enabled); $('dyndns_select').setProperty('value', pref.dyndns_service); @@ -2277,6 +2300,10 @@ settings.set('web_ui_use_custom_http_headers_enabled', $('webUIUseCustomHTTPHeadersCheckbox').getProperty('checked')); settings.set('web_ui_custom_http_headers', $('webUICustomHTTPHeadersTextarea').getProperty('value')); + // Reverse Proxy + settings.set('web_ui_reverse_proxy_support_enabled', $('webUIReverseProxySupportCheckbox').getProperty('checked')); + settings.set('web_ui_trusted_reverse_proxies_list', $('webUIReverseProxiesListTextarea').getProperty('value')); + // Update my dynamic domain name settings.set('dyndns_enabled', $('use_dyndns_checkbox').getProperty('checked')); settings.set('dyndns_service', $('dyndns_select').getProperty('value'));