Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WebUI reverse proxy source IP resolution #15047

Merged
merged 12 commits into from
Jun 23, 2021
1 change: 1 addition & 0 deletions src/base/http/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Chocobo1 marked this conversation as resolved.
Show resolved Hide resolved
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";
Expand Down
20 changes: 20 additions & 0 deletions src/base/preferences.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
6 changes: 6 additions & 0 deletions src/base/preferences.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions src/gui/optionsdialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
30 changes: 30 additions & 0 deletions src/gui/optionsdialog.ui
Original file line number Diff line number Diff line change
Expand Up @@ -3287,6 +3287,36 @@ Use ';' to split multiple entries. Can use wildcard '*'.</string>
</item>
</layout>
</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>
<widget class="QGroupBox" name="checkDynDNS">
Expand Down
8 changes: 8 additions & 0 deletions src/webui/api/appcontroller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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());
Expand Down
69 changes: 62 additions & 7 deletions src/webui/webapplication.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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();
}
Expand All @@ -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()
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -651,7 +672,7 @@ bool WebApplication::isCrossSiteRequest(const Http::Request &request) const
const bool isInvalid = !isSameOrigin(urlFromHostHeader(targetOrigin), originValue);
if (isInvalid)
LogMsg(tr("WebUI: Origin header & Target origin mismatch! Source IP: '%1'. Origin header: '%2'. Target origin: '%3'")
.arg(m_env.clientAddress.toString(), originValue, targetOrigin)
.arg(m_clientAddress.toString(), originValue, targetOrigin)
Chocobo1 marked this conversation as resolved.
Show resolved Hide resolved
, Log::WARNING);
return isInvalid;
}
Expand All @@ -661,7 +682,7 @@ bool WebApplication::isCrossSiteRequest(const Http::Request &request) const
const bool isInvalid = !isSameOrigin(urlFromHostHeader(targetOrigin), refererValue);
if (isInvalid)
LogMsg(tr("WebUI: Referer header & Target origin mismatch! Source IP: '%1'. Referer header: '%2'. Target origin: '%3'")
.arg(m_env.clientAddress.toString(), refererValue, targetOrigin)
.arg(m_clientAddress.toString(), refererValue, targetOrigin)
, Log::WARNING);
return isInvalid;
}
Expand All @@ -679,7 +700,7 @@ bool WebApplication::validateHostHeader(const QStringList &domains) const
if ((requestPort != -1) && (m_env.localPort != requestPort))
{
LogMsg(tr("WebUI: Invalid Host header, port mismatch. Request source IP: '%1'. Server port: '%2'. Received Host header: '%3'")
.arg(m_env.clientAddress.toString()).arg(m_env.localPort)
.arg(m_clientAddress.toString()).arg(m_env.localPort)
.arg(m_request.headers[Http::HEADER_HOST])
, Log::WARNING);
return false;
Expand All @@ -700,11 +721,45 @@ bool WebApplication::validateHostHeader(const QStringList &domains) const
}

LogMsg(tr("WebUI: Invalid Host header. Request source IP: '%1'. Received Host header: '%2'")
.arg(m_env.clientAddress.toString(), m_request.headers[Http::HEADER_HOST])
.arg(m_clientAddress.toString(), m_request.headers[Http::HEADER_HOST])
, Log::WARNING);
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)
Expand Down
7 changes: 7 additions & 0 deletions src/webui/webapplication.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<QString, WebSession *> m_sessions;

Expand Down Expand Up @@ -154,5 +156,10 @@ class WebApplication final
bool m_isHostHeaderValidationEnabled;
bool m_isHttpsEnabled;

// Reverse proxy
bool m_isReverseProxySupportEnabled;
QVector<QHostAddress> m_trustedReverseProxyList;
QHostAddress m_clientAddress;

QVector<Http::Header> m_prebuiltHeaders;
};
27 changes: 27 additions & 0 deletions src/webui/www/private/views/preferences.html
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,18 @@
</legend>
<textarea id="webUICustomHTTPHeadersTextarea" placeholder="QBT_TR(Header: value pairs, one per line)QBT_TR[CONTEXT=OptionsDialog]" style="width: 90%;"></textarea>
</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 class="settings">
Expand Down Expand Up @@ -1279,6 +1291,7 @@
updateAlternativeWebUISettings: updateAlternativeWebUISettings,
updateHostHeaderValidationSettings: updateHostHeaderValidationSettings,
updateWebUICustomHTTPHeadersSettings: updateWebUICustomHTTPHeadersSettings,
updateWebUIReverseProxySettings: updateWebUIReverseProxySettings,
updateDynDnsSettings: updateDynDnsSettings,
registerDynDns: registerDynDns,
applyPreferences: applyPreferences
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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'));
Expand Down