Skip to content

Commit

Permalink
WebUI: Add Base Path Configuration
Browse files Browse the repository at this point in the history
Adds a base path configuration option to the WebUI.
WebApplication injects base path into content paths on initial
configuration. Requires a restart to update.
Internally, the path is stripped from the request to maintain the
existing api/file structure.
Requests without the path trigger a 303 redirect response.
Closes qbittorrent#5693
  • Loading branch information
kharenis committed Apr 14, 2021
1 parent bbeca25 commit 5c60447
Show file tree
Hide file tree
Showing 53 changed files with 307 additions and 240 deletions.
Binary file added src/.vs/slnx.sqlite
Binary file not shown.
4 changes: 4 additions & 0 deletions src/base/http/httperror.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,7 @@ InternalServerErrorHTTPError::InternalServerErrorHTTPError(const QString &messag
: HTTPError(500, QLatin1String("Internal Server Error"), message)
{
}
SeeOtherHTTPError::SeeOtherHTTPError(const QString &message)
: HTTPError(303, QLatin1String("See Other"), message)
{
}
6 changes: 6 additions & 0 deletions src/base/http/httperror.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,9 @@ class InternalServerErrorHTTPError : public HTTPError
public:
explicit InternalServerErrorHTTPError(const QString &message = {});
};

class SeeOtherHTTPError : public HTTPError
{
public:
explicit SeeOtherHTTPError(const QString &message = {});
};
1 change: 1 addition & 0 deletions src/base/http/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ namespace Http
const char HEADER_X_FORWARDED_HOST[] = "x-forwarded-host";
const char HEADER_X_FRAME_OPTIONS[] = "x-frame-options";
const char HEADER_X_XSS_PROTECTION[] = "x-xss-protection";
const char HEADER_LOCATION[] = "location";

const char HEADER_REQUEST_METHOD_GET[] = "GET";
const char HEADER_REQUEST_METHOD_HEAD[] = "HEAD";
Expand Down
2 changes: 1 addition & 1 deletion src/base/preferences.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -777,7 +777,7 @@ QString Preferences::getWebUIBasePath() const
return value("Preferences/WebUI/BasePath").toString();
}

void Preferences::setWebUIBasePath(const Qstring &path)
void Preferences::setWebUIBasePath(const QString &path)
{
setValue("Preferences/WebUI/BasePath", path);
}
Expand Down
3 changes: 3 additions & 0 deletions src/gui/optionsdialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ OptionsDialog::OptionsDialog(QWidget *parent)
connect(m_ui->textWebUIHttpsKey, &FileSystemPathLineEdit::selectedPathChanged, this, [this](const QString &s) { webUIHttpsKeyChanged(s, ShowError::Show); });
connect(m_ui->textWebUiUsername, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
connect(m_ui->textWebUiPassword, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
connect(m_ui->textWebUIBasePath, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
connect(m_ui->checkBypassLocalAuth, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkBypassAuthSubnetWhitelist, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkBypassAuthSubnetWhitelist, &QAbstractButton::toggled, m_ui->IPSubnetWhitelistButton, &QPushButton::setEnabled);
Expand Down Expand Up @@ -848,6 +849,7 @@ void OptionsDialog::saveOptions()
pref->setWebUIMaxAuthFailCount(m_ui->spinBanCounter->value());
pref->setWebUIBanDuration(std::chrono::seconds {m_ui->spinBanDuration->value()});
pref->setWebUISessionTimeout(m_ui->spinSessionTimeout->value());
pref->setWebUIBasePath(m_ui->textWebUIBasePath->text());
// Authentication
pref->setWebUiUsername(webUiUsername());
if (!webUiPassword().isEmpty())
Expand Down Expand Up @@ -1255,6 +1257,7 @@ void OptionsDialog::loadOptions()
m_ui->spinBanCounter->setValue(pref->getWebUIMaxAuthFailCount());
m_ui->spinBanDuration->setValue(pref->getWebUIBanDuration().count());
m_ui->spinSessionTimeout->setValue(pref->getWebUISessionTimeout());
m_ui->textWebUIBasePath->setText(pref->getWebUIBasePath());

// Security
m_ui->checkClickjacking->setChecked(pref->isWebUiClickjackingProtectionEnabled());
Expand Down
16 changes: 16 additions & 0 deletions src/gui/optionsdialog.ui
Original file line number Diff line number Diff line change
Expand Up @@ -2945,6 +2945,22 @@ Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="lblWebUiBasePath">
<property name="text">
<string>Base Path:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="textWebUIBasePath">
<property name="toolTip">
<string>
URL path root for the WebUI. Requires a WebUI restart to take effect.
</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkWebUIUPnP">
<property name="text">
Expand Down
3 changes: 3 additions & 0 deletions src/webui/api/appcontroller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ void AppController::preferencesAction()
data["use_https"] = pref->isWebUiHttpsEnabled();
data["web_ui_https_cert_path"] = pref->getWebUIHttpsCertificatePath();
data["web_ui_https_key_path"] = pref->getWebUIHttpsKeyPath();
data["web_ui_base_path"] = pref->getWebUIBasePath();
// Authentication
data["web_ui_username"] = pref->getWebUiUsername();
data["bypass_local_auth"] = !pref->isWebUiLocalAuthEnabled();
Expand Down Expand Up @@ -640,6 +641,8 @@ void AppController::setPreferencesAction()
pref->setWebUIBanDuration(std::chrono::seconds {it.value().toInt()});
if (hasKey("web_ui_session_timeout"))
pref->setWebUISessionTimeout(it.value().toInt());
if (hasKey("web_ui_base_path"))
pref->setWebUIBasePath(it.value().toString());
// Use alternative Web UI
if (hasKey("alternative_webui_enabled"))
pref->setAltWebUiEnabled(it.value().toBool());
Expand Down
34 changes: 28 additions & 6 deletions src/webui/webapplication.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ WebApplication::WebApplication(QObject *parent)

declarePublicAPI(QLatin1String("auth/login"));

fireOnceConfigure();
configure();
connect(Preferences::instance(), &Preferences::changed, this, &WebApplication::configure);
}
Expand Down Expand Up @@ -238,6 +239,7 @@ void WebApplication::translateDocument(QString &data) const

data.replace(QLatin1String("${LANG}"), m_currentLocale.left(2));
data.replace(QLatin1String("${CACHEID}"), m_cacheID);
data.replace(QLatin1String("${BASEPATH}"), m_basePath);
}
}

Expand All @@ -256,6 +258,24 @@ const Http::Environment &WebApplication::env() const
return m_env;
}

void WebApplication::doProcessPath()
{
if(!m_basePath.isEmpty())
{
if(m_request.path.indexOf(m_basePath) == 0)
{
m_request.path.remove(0, m_basePath.length());
}
else
{
//Set location header for redirect
setHeader({Http::HEADER_LOCATION, m_basePath + m_request.path});

throw SeeOtherHTTPError(m_basePath + m_request.path);
}
}
}

void WebApplication::doProcessRequest()
{
const QRegularExpressionMatch match = m_apiPathPattern.match(request().path);
Expand Down Expand Up @@ -314,6 +334,13 @@ void WebApplication::doProcessRequest()
}
}

//For configurations that require a restart to take effect
void WebApplication::fireOnceConfigure()
{
const auto *pref = Preferences::instance();
m_basePath = pref->getWebUIBasePath();
}

void WebApplication::configure()
{
const auto *pref = Preferences::instance();
Expand Down Expand Up @@ -362,7 +389,6 @@ void WebApplication::configure()
m_isSecureCookieEnabled = pref->isWebUiSecureCookieEnabled();
m_isHostHeaderValidationEnabled = pref->isWebUIHostHeaderValidationEnabled();
m_isHttpsEnabled = pref->isWebUiHttpsEnabled();
m_basePath = pref->getWebUIBasePath();

m_prebuiltHeaders.clear();
m_prebuiltHeaders.push_back({QLatin1String(Http::HEADER_X_XSS_PROTECTION), QLatin1String("1; mode=block")});
Expand Down Expand Up @@ -496,11 +522,7 @@ Http::Response WebApplication::processRequest(const Http::Request &request, cons
}

sessionInitialize();

//Replace base path prefix with a '/'
if(!m_basePath.isEmpty() && m_request.path.indexOf(m_basePath == 0))
m_request.path.replace(0, m_basePath.length, QChar('/'));

doProcessPath();
doProcessRequest();
}
catch (const HTTPError &error)
Expand Down
2 changes: 2 additions & 0 deletions src/webui/webapplication.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ class WebApplication final

private:
void doProcessRequest();
void doProcessPath();
void configure();
void fireOnceConfigure();

void registerAPIController(const QString &scope, APIController *controller);
void declarePublicAPI(const QString &apiPath);
Expand Down
2 changes: 1 addition & 1 deletion src/webui/www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "git",
"url": "https://github.com/qbittorrent/qBittorrent.git"
},
"scripts": {
"${BASEPATH}/scripts": {
"format": "js-beautify private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js",
"lint": "eslint private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js"
},
Expand Down
10 changes: 5 additions & 5 deletions src/webui/www/private/addpeers.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="${LANG}">

<head>
<meta charset="UTF-8" />
<title>QBT_TR(Add Peers)QBT_TR[CONTEXT=PeersAdditionDialog]</title>
<link rel="stylesheet" href="css/style.css?v=${CACHEID}" type="text/css" />
<script src="scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="scripts/lib/mootools-1.2-more.js"></script>
<link rel="stylesheet" href="${BASEPATH}/css/style.css?v=${CACHEID}" type="text/css" />
<script src="${BASEPATH}/scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="${BASEPATH}/scripts/lib/mootools-1.2-more.js"></script>
<script>
'use strict';

Expand Down Expand Up @@ -39,7 +39,7 @@
return

new Request({
url: 'api/v2/torrents/addPeers',
url: '${BASEPATH}/api/v2/torrents/addPeers',
method: 'post',
data: {
hashes: hash,
Expand Down
8 changes: 4 additions & 4 deletions src/webui/www/private/addtrackers.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
<head>
<meta charset="UTF-8" />
<title>QBT_TR(Trackers addition dialog)QBT_TR[CONTEXT=TrackersAdditionDialog]</title>
<link rel="stylesheet" href="css/style.css?v=${CACHEID}" type="text/css" />
<script src="scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="scripts/lib/mootools-1.2-more.js"></script>
<link rel="stylesheet" href="${BASEPATH}/css/style.css?v=${CACHEID}" type="text/css" />
<script src="${BASEPATH}/scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="${BASEPATH}/scripts/lib/mootools-1.2-more.js"></script>
<script>
'use strict';

Expand All @@ -30,7 +30,7 @@
new Event(e).stop();
const hash = new URI().getData('hash');
new Request({
url: 'api/v2/torrents/addTrackers',
url: '${BASEPATH}/api/v2/torrents/addTrackers',
method: 'post',
data: {
hash: hash,
Expand Down
8 changes: 4 additions & 4 deletions src/webui/www/private/confirmdeletion.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
<head>
<meta charset="UTF-8" />
<title>QBT_TR(Deletion confirmation - qBittorrent)QBT_TR[CONTEXT=confirmDeletionDlg]</title>
<link rel="stylesheet" href="css/style.css?v=${CACHEID}" type="text/css" />
<script src="scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="scripts/lib/mootools-1.2-more.js"></script>
<link rel="stylesheet" href="${BASEPATH}/css/style.css?v=${CACHEID}" type="text/css" />
<script src="${BASEPATH}/scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="${BASEPATH}/scripts/lib/mootools-1.2-more.js"></script>
<script>
'use strict';

Expand All @@ -23,7 +23,7 @@
$('confirmBtn').addEvent('click', function(e) {
parent.torrentsTable.deselectAll();
new Event(e).stop();
const cmd = 'api/v2/torrents/delete';
const cmd = '${BASEPATH}/api/v2/torrents/delete';
const deleteFiles = $('deleteFromDiskCB').get('checked');
new Request({
url: cmd,
Expand Down
10 changes: 5 additions & 5 deletions src/webui/www/private/confirmfeeddeletion.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="${LANG}">

<head>
<meta charset="UTF-8" />
<title>QBT_TR(Deletion confirmation)QBT_TR[CONTEXT=RSSWidget]</title>
<link rel="stylesheet" href="css/style.css?v=${CACHEID}" type="text/css" />
<script src="scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="scripts/lib/mootools-1.2-more.js"></script>
<link rel="stylesheet" href="${BASEPATH}/css/style.css?v=${CACHEID}" type="text/css" />
<script src="${BASEPATH}/scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="${BASEPATH}/scripts/lib/mootools-1.2-more.js"></script>
<script>
'use strict';

Expand All @@ -22,7 +22,7 @@
let completionCount = 0;
paths.forEach((path) => {
new Request({
url: 'api/v2/rss/removeItem',
url: '${BASEPATH}/api/v2/rss/removeItem',
noCache: true,
method: 'post',
data: {
Expand Down
6 changes: 3 additions & 3 deletions src/webui/www/private/confirmruleclear.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
<head>
<meta charset="UTF-8" />
<title>QBT_TR(Clear downloaded episodes)QBT_TR[CONTEXT=AutomatedRssDownloader]</title>
<link rel="stylesheet" href="css/style.css?v=${CACHEID}" type="text/css" />
<script src="scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="scripts/lib/mootools-1.2-more.js"></script>
<link rel="stylesheet" href="${BASEPATH}/css/style.css?v=${CACHEID}" type="text/css" />
<script src="${BASEPATH}/scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="${BASEPATH}/scripts/lib/mootools-1.2-more.js"></script>
<script>
'use strict';

Expand Down
8 changes: 4 additions & 4 deletions src/webui/www/private/confirmruledeletion.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
<head>
<meta charset="UTF-8" />
<title>QBT_TR(Rule deletion confirmation)QBT_TR[CONTEXT=AutomatedRssDownloader]</title>
<link rel="stylesheet" href="css/style.css?v=${CACHEID}" type="text/css" />
<script src="scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="scripts/lib/mootools-1.2-more.js"></script>
<link rel="stylesheet" href="${BASEPATH}/css/style.css?v=${CACHEID}" type="text/css" />
<script src="${BASEPATH}/scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="${BASEPATH}/scripts/lib/mootools-1.2-more.js"></script>
<script>
'use strict';

Expand All @@ -23,7 +23,7 @@
let completionCount = 0;
rules.forEach((rule) => {
new Request({
url: 'api/v2/rss/removeRule',
url: '${BASEPATH}/api/v2/rss/removeRule',
noCache: true,
method: 'post',
data: {
Expand Down
14 changes: 7 additions & 7 deletions src/webui/www/private/download.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@
<head>
<meta charset="UTF-8" />
<title>QBT_TR(Add Torrent Links)QBT_TR[CONTEXT=downloadFromURL]</title>
<link rel="stylesheet" href="css/style.css?v=${CACHEID}" type="text/css" />
<link rel="stylesheet" href="css/Window.css?v=${CACHEID}" type="text/css" />
<script src="scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="scripts/lib/mootools-1.2-more.js"></script>
<script src="scripts/download.js?v=${CACHEID}"></script>
<script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script>
<link rel="stylesheet" href="${BASEPATH}/css/style.css?v=${CACHEID}" type="text/css" />
<link rel="stylesheet" href="${BASEPATH}/css/Window.css?v=${CACHEID}" type="text/css" />
<script src="${BASEPATH}/scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="${BASEPATH}/scripts/lib/mootools-1.2-more.js"></script>
<script src="${BASEPATH}/scripts/download.js?v=${CACHEID}"></script>
<script src="${BASEPATH}/scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script>
</head>

<body>
<iframe id="download_frame" name="download_frame" class="invisible" src="about:blank"></iframe>
<form action="api/v2/torrents/add" enctype="multipart/form-data" method="post" id="downloadForm" style="text-align: center;" target="download_frame" autocorrect="off" autocapitalize="none">
<form action="${BASEPATH}/api/v2/torrents/add" enctype="multipart/form-data" method="post" id="downloadForm" style="text-align: center;" target="download_frame" autocorrect="off" autocapitalize="none">
<div style="text-align: center;">
<br />
<h2 class="vcenter">QBT_TR(Download Torrents from their URLs or Magnet links)QBT_TR[CONTEXT=HttpServer]</h2>
Expand Down
16 changes: 8 additions & 8 deletions src/webui/www/private/downloadlimit.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="${LANG}">

<head>
<meta charset="UTF-8" />
<title>QBT_TR(Torrent Download Speed Limiting)QBT_TR[CONTEXT=TransferListWidget]</title>
<link rel="stylesheet" href="css/style.css?v=${CACHEID}" type="text/css" />
<script src="scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="scripts/lib/mootools-1.2-more.js"></script>
<script src="scripts/lib/mocha-0.9.6-yc.js"></script>
<script src="scripts/speedslider.js?v=${CACHEID}"></script>
<link rel="stylesheet" href="${BASEPATH}/css/style.css?v=${CACHEID}" type="text/css" />
<script src="${BASEPATH}/scripts/lib/mootools-1.2-core-yc.js"></script>
<script src="${BASEPATH}/scripts/lib/mootools-1.2-more.js"></script>
<script src="${BASEPATH}/scripts/lib/mocha-0.9.6-yc.js"></script>
<script src="${BASEPATH}/scripts/speedslider.js?v=${CACHEID}"></script>
</head>

<body>
Expand All @@ -29,7 +29,7 @@
const limit = $("dllimitUpdatevalue").value.toInt() * 1024;
if (hashes[0] == "global") {
new Request({
url: 'api/v2/transfer/setDownloadLimit',
url: '${BASEPATH}/api/v2/transfer/setDownloadLimit',
method: 'post',
data: {
'limit': limit
Expand All @@ -42,7 +42,7 @@
}
else {
new Request({
url: 'api/v2/torrents/setDownloadLimit',
url: '${BASEPATH}/api/v2/torrents/setDownloadLimit',
method: 'post',
data: {
'hashes': hashes.join('|'),
Expand Down
Loading

0 comments on commit 5c60447

Please sign in to comment.