Browse Source

Add WebUI reverse proxy source IP resolution (#15047)

Co-authored-by: qix67
Co-authored-by: HiFiPhile <admin@hifiphile.com>
adaptive-webui-19844
HiFiPhile 3 years ago committed by GitHub
parent
commit
f5315d9ba7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      src/base/http/types.h
  2. 20
      src/base/preferences.cpp
  3. 6
      src/base/preferences.h
  4. 8
      src/gui/optionsdialog.cpp
  5. 30
      src/gui/optionsdialog.ui
  6. 8
      src/webui/api/appcontroller.cpp
  7. 61
      src/webui/webapplication.cpp
  8. 7
      src/webui/webapplication.h
  9. 27
      src/webui/www/private/views/preferences.html

1
src/base/http/types.h

@ -52,6 +52,7 @@ namespace Http
inline const char HEADER_REFERRER_POLICY[] = "referrer-policy"; inline const char HEADER_REFERRER_POLICY[] = "referrer-policy";
inline const char HEADER_SET_COOKIE[] = "set-cookie"; 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_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_FORWARDED_HOST[] = "x-forwarded-host";
inline const char HEADER_X_FRAME_OPTIONS[] = "x-frame-options"; inline const char HEADER_X_FRAME_OPTIONS[] = "x-frame-options";
inline const char HEADER_X_XSS_PROTECTION[] = "x-xss-protection"; inline const char HEADER_X_XSS_PROTECTION[] = "x-xss-protection";

20
src/base/preferences.cpp

@ -761,6 +761,26 @@ void Preferences::setWebUICustomHTTPHeaders(const QString &headers)
setValue("Preferences/WebUI/CustomHTTPHeaders", 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 bool Preferences::isDynDNSEnabled() const
{ {
return value("Preferences/DynDNS/Enabled", false).toBool(); return value("Preferences/DynDNS/Enabled", false).toBool();

6
src/base/preferences.h

@ -225,6 +225,12 @@ public:
QString getWebUICustomHTTPHeaders() const; QString getWebUICustomHTTPHeaders() const;
void setWebUICustomHTTPHeaders(const QString &headers); void setWebUICustomHTTPHeaders(const QString &headers);
// Reverse proxy
bool isWebUIReverseProxySupportEnabled() const;
void setWebUIReverseProxySupportEnabled(bool enabled);
QString getWebUITrustedReverseProxiesList() const;
void setWebUITrustedReverseProxiesList(const QString &addr);
// Dynamic DNS // Dynamic DNS
bool isDynDNSEnabled() const; bool isDynDNSEnabled() const;
void setDynDNSEnabled(bool enabled); void setDynDNSEnabled(bool enabled);

8
src/gui/optionsdialog.cpp

@ -515,6 +515,8 @@ OptionsDialog::OptionsDialog(QWidget *parent)
connect(m_ui->textWebUIRootFolder, &FileSystemPathLineEdit::selectedPathChanged, this, &ThisType::enableApplyButton); connect(m_ui->textWebUIRootFolder, &FileSystemPathLineEdit::selectedPathChanged, this, &ThisType::enableApplyButton);
connect(m_ui->groupWebUIAddCustomHTTPHeaders, &QGroupBox::toggled, this, &ThisType::enableApplyButton); connect(m_ui->groupWebUIAddCustomHTTPHeaders, &QGroupBox::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->textWebUICustomHTTPHeaders, &QPlainTextEdit::textChanged, this, &OptionsDialog::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 #endif // DISABLE_WEBUI
// RSS tab // RSS tab
@ -871,6 +873,9 @@ void OptionsDialog::saveOptions()
// Custom HTTP headers // Custom HTTP headers
pref->setWebUICustomHTTPHeadersEnabled(m_ui->groupWebUIAddCustomHTTPHeaders->isChecked()); pref->setWebUICustomHTTPHeadersEnabled(m_ui->groupWebUIAddCustomHTTPHeaders->isChecked());
pref->setWebUICustomHTTPHeaders(m_ui->textWebUICustomHTTPHeaders->toPlainText()); pref->setWebUICustomHTTPHeaders(m_ui->textWebUICustomHTTPHeaders->toPlainText());
// Reverse proxy
pref->setWebUIReverseProxySupportEnabled(m_ui->groupEnableReverseProxySupport->isChecked());
pref->setWebUITrustedReverseProxiesList(m_ui->textTrustedReverseProxiesList->text());
} }
// End Web UI // End Web UI
// End preferences // End preferences
@ -1274,6 +1279,9 @@ void OptionsDialog::loadOptions()
// Custom HTTP headers // Custom HTTP headers
m_ui->groupWebUIAddCustomHTTPHeaders->setChecked(pref->isWebUICustomHTTPHeadersEnabled()); m_ui->groupWebUIAddCustomHTTPHeaders->setChecked(pref->isWebUICustomHTTPHeadersEnabled());
m_ui->textWebUICustomHTTPHeaders->setPlainText(pref->getWebUICustomHTTPHeaders()); m_ui->textWebUICustomHTTPHeaders->setPlainText(pref->getWebUICustomHTTPHeaders());
// Reverse proxy
m_ui->groupEnableReverseProxySupport->setChecked(pref->isWebUIReverseProxySupportEnabled());
m_ui->textTrustedReverseProxiesList->setText(pref->getWebUITrustedReverseProxiesList());
// End Web UI preferences // End Web UI preferences
} }

30
src/gui/optionsdialog.ui

@ -3287,6 +3287,36 @@ Use ';' to split multiple entries. Can use wildcard '*'.</string>
</item> </item>
</layout> </layout>
</widget> </widget>
</item>
<item>
<widget class="QGroupBox" name="groupEnableReverseProxySupport">
<property name="title">
<string>Enable reverse proxy support</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_331">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_131">
<item>
<widget class="QLabel" name="lblReverseProxiesList">
<property name="text">
<string>Trusted proxies list:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="textTrustedReverseProxiesList">
<property name="toolTip">
<string>Specify reverse proxy IPs in order to use forwarded client address (X-Forwarded-For attribute), use ';' to split multiple entries.</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item> </item>
<item> <item>
<widget class="QGroupBox" name="checkDynDNS"> <widget class="QGroupBox" name="checkDynDNS">

8
src/webui/api/appcontroller.cpp

@ -259,6 +259,9 @@ void AppController::preferencesAction()
// Custom HTTP headers // Custom HTTP headers
data["web_ui_use_custom_http_headers_enabled"] = pref->isWebUICustomHTTPHeadersEnabled(); data["web_ui_use_custom_http_headers_enabled"] = pref->isWebUICustomHTTPHeadersEnabled();
data["web_ui_custom_http_headers"] = pref->getWebUICustomHTTPHeaders(); 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 // Update my dynamic domain name
data["dyndns_enabled"] = pref->isDynDNSEnabled(); data["dyndns_enabled"] = pref->isDynDNSEnabled();
data["dyndns_service"] = pref->getDynDNSService(); data["dyndns_service"] = pref->getDynDNSService();
@ -680,6 +683,11 @@ void AppController::setPreferencesAction()
pref->setWebUICustomHTTPHeadersEnabled(it.value().toBool()); pref->setWebUICustomHTTPHeadersEnabled(it.value().toBool());
if (hasKey("web_ui_custom_http_headers")) if (hasKey("web_ui_custom_http_headers"))
pref->setWebUICustomHTTPHeaders(it.value().toString()); 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 // Update my dynamic domain name
if (hasKey("dyndns_enabled")) if (hasKey("dyndns_enabled"))
pref->setDynDNSEnabled(it.value().toBool()); pref->setDynDNSEnabled(it.value().toBool());

61
src/webui/webapplication.cpp

@ -404,6 +404,24 @@ void WebApplication::configure()
m_prebuiltHeaders.push_back({header, value}); 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) void WebApplication::registerAPIController(const QString &scope, APIController *controller)
@ -495,6 +513,9 @@ Http::Response WebApplication::processRequest(const Http::Request &request, cons
throw UnauthorizedHTTPError(); throw UnauthorizedHTTPError();
} }
// reverse proxy resolve client address
m_clientAddress = resolveClientAddress();
sessionInitialize(); sessionInitialize();
doProcessRequest(); doProcessRequest();
} }
@ -512,7 +533,7 @@ Http::Response WebApplication::processRequest(const Http::Request &request, cons
QString WebApplication::clientId() const QString WebApplication::clientId() const
{ {
return env().clientAddress.toString(); return m_clientAddress.toString();
} }
void WebApplication::sessionInitialize() void WebApplication::sessionInitialize()
@ -567,9 +588,9 @@ QString WebApplication::generateSid() const
bool WebApplication::isAuthNeeded() bool WebApplication::isAuthNeeded()
{ {
if (!m_isLocalAuthEnabled && Utils::Net::isLoopbackAddress(m_env.clientAddress)) if (!m_isLocalAuthEnabled && Utils::Net::isLoopbackAddress(m_clientAddress))
return false; 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 false;
return true; return true;
} }
@ -705,6 +726,40 @@ bool WebApplication::validateHostHeader(const QStringList &domains) const
return false; 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::WebSession(const QString &sid) WebSession::WebSession(const QString &sid)

7
src/webui/webapplication.h

@ -114,6 +114,8 @@ private:
bool isCrossSiteRequest(const Http::Request &request) const; bool isCrossSiteRequest(const Http::Request &request) const;
bool validateHostHeader(const QStringList &domains) const; bool validateHostHeader(const QStringList &domains) const;
QHostAddress resolveClientAddress() const;
// Persistent data // Persistent data
QHash<QString, WebSession *> m_sessions; QHash<QString, WebSession *> m_sessions;
@ -154,5 +156,10 @@ private:
bool m_isHostHeaderValidationEnabled; bool m_isHostHeaderValidationEnabled;
bool m_isHttpsEnabled; bool m_isHttpsEnabled;
// Reverse proxy
bool m_isReverseProxySupportEnabled;
QVector<QHostAddress> m_trustedReverseProxyList;
QHostAddress m_clientAddress;
QVector<Http::Header> m_prebuiltHeaders; QVector<Http::Header> m_prebuiltHeaders;
}; };

27
src/webui/www/private/views/preferences.html

@ -838,6 +838,18 @@
</legend> </legend>
<textarea id="webUICustomHTTPHeadersTextarea" placeholder="QBT_TR(Header: value pairs, one per line)QBT_TR[CONTEXT=OptionsDialog]" style="width: 90%;"></textarea> <textarea id="webUICustomHTTPHeadersTextarea" placeholder="QBT_TR(Header: value pairs, one per line)QBT_TR[CONTEXT=OptionsDialog]" style="width: 90%;"></textarea>
</fieldset> </fieldset>
<fieldset class="settings">
<legend>
<input type="checkbox" id="webUIReverseProxySupportCheckbox" onclick="qBittorrent.Preferences.updateWebUIReverseProxySettings();" />
<label for="webUIReverseProxySupportCheckbox">QBT_TR(Enable reverse proxy support)QBT_TR[CONTEXT=OptionsDialog]</label>
</legend>
<div class="formRow">
<input type="text" id="webUIReverseProxiesListTextarea" />
<label for="webUIReverseProxiesListTextarea" class="leftLabelLarge">QBT_TR(Trusted proxies list:)QBT_TR[CONTEXT=OptionsDialog]</label>
</div>
</fieldset>
</fieldset> </fieldset>
<fieldset class="settings"> <fieldset class="settings">
@ -1279,6 +1291,7 @@
updateAlternativeWebUISettings: updateAlternativeWebUISettings, updateAlternativeWebUISettings: updateAlternativeWebUISettings,
updateHostHeaderValidationSettings: updateHostHeaderValidationSettings, updateHostHeaderValidationSettings: updateHostHeaderValidationSettings,
updateWebUICustomHTTPHeadersSettings: updateWebUICustomHTTPHeadersSettings, updateWebUICustomHTTPHeadersSettings: updateWebUICustomHTTPHeadersSettings,
updateWebUIReverseProxySettings: updateWebUIReverseProxySettings,
updateDynDnsSettings: updateDynDnsSettings, updateDynDnsSettings: updateDynDnsSettings,
registerDynDns: registerDynDns, registerDynDns: registerDynDns,
applyPreferences: applyPreferences applyPreferences: applyPreferences
@ -1511,6 +1524,11 @@
$('webUICustomHTTPHeadersTextarea').setProperty('disabled', !isEnabled); $('webUICustomHTTPHeadersTextarea').setProperty('disabled', !isEnabled);
}; };
const updateWebUIReverseProxySettings = function() {
const isEnabled = $('webUIReverseProxySupportCheckbox').getProperty('checked');
$('webUIReverseProxiesListTextarea').setProperty('disabled', !isEnabled);
};
const updateDynDnsSettings = function() { const updateDynDnsSettings = function() {
const isDynDnsEnabled = $('use_dyndns_checkbox').getProperty('checked'); const isDynDnsEnabled = $('use_dyndns_checkbox').getProperty('checked');
$('dyndns_select').setProperty('disabled', !isDynDnsEnabled); $('dyndns_select').setProperty('disabled', !isDynDnsEnabled);
@ -1886,6 +1904,11 @@
$('webUICustomHTTPHeadersTextarea').setProperty('value', pref.web_ui_custom_http_headers); $('webUICustomHTTPHeadersTextarea').setProperty('value', pref.web_ui_custom_http_headers);
updateWebUICustomHTTPHeadersSettings(); 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 // Update my dynamic domain name
$('use_dyndns_checkbox').setProperty('checked', pref.dyndns_enabled); $('use_dyndns_checkbox').setProperty('checked', pref.dyndns_enabled);
$('dyndns_select').setProperty('value', pref.dyndns_service); $('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_use_custom_http_headers_enabled', $('webUIUseCustomHTTPHeadersCheckbox').getProperty('checked'));
settings.set('web_ui_custom_http_headers', $('webUICustomHTTPHeadersTextarea').getProperty('value')); 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 // Update my dynamic domain name
settings.set('dyndns_enabled', $('use_dyndns_checkbox').getProperty('checked')); settings.set('dyndns_enabled', $('use_dyndns_checkbox').getProperty('checked'));
settings.set('dyndns_service', $('dyndns_select').getProperty('value')); settings.set('dyndns_service', $('dyndns_select').getProperty('value'));

Loading…
Cancel
Save