37 #include <QJsonDocument>
38 #include <QMimeDatabase>
40 #include <QNetworkCookie>
41 #include <QRegularExpression>
79 const QList<QStringView> cookies = cookieStr.split(u
';', Qt::SkipEmptyParts);
81 for (
const auto &cookie : cookies)
83 const int idx = cookie.indexOf(
'=');
87 const QString name = cookie.left(idx).trimmed().toString();
89 ret.insert(name,
value);
96 if (!hostHeader.contains(QLatin1String(
"://")))
97 return {QLatin1String(
"http://") + hostHeader};
103 contentType = contentType.toLower();
105 if (contentType.startsWith(QLatin1String(
"image/")))
106 return QLatin1String(
"private, max-age=604800");
112 return QLatin1String(
"private, max-age=43200");
115 return QLatin1String(
"no-store");
121 , m_cacheID {QString::number(
Utils::Random::
rand(), 36)}
146 const QStringList pathItems {
request().
path.split(
'/', Qt::SkipEmptyParts)};
147 if (pathItems.contains(
".") || pathItems.contains(
".."))
155 sendFile(QLatin1String(
":/icons/") + imageFilename);
164 : QLatin1String(
"/index.html"))
174 QFileInfo fileInfo {localPath};
176 if (!fileInfo.exists() &&
session())
180 fileInfo.setFile(localPath);
188 status(500,
"Internal Server Error");
196 if (fileInfo.isSymLink())
199 fileInfo.setFile(fileInfo.path());
208 const QRegularExpression regex(
"QBT_TR\\((([^\\)]|\\)(?!QBT_TR))+)\\)QBT_TR\\[CONTEXT=([a-zA-Z_][a-zA-Z0-9_]*)\\]");
212 while (i < data.size() && found)
214 QRegularExpressionMatch regexMatch;
215 i = data.indexOf(regex, i, ®exMatch);
218 const QString sourceText = regexMatch.captured(1);
219 const QString context = regexMatch.captured(3);
222 ?
m_translator.translate(context.toUtf8().constData(), sourceText.toUtf8().constData())
226 QString translation = loadedText.isEmpty() ? sourceText : loadedText;
229 translation.replace(
'\'',
"'");
230 translation.replace(
'\"',
""");
232 data.replace(i, regexMatch.capturedLength(), translation);
233 i += translation.length();
241 data.replace(QLatin1String(
"${CACHEID}"),
m_cacheID);
263 if (!match.hasMatch())
269 const QString
action = match.captured(QLatin1String(
"action"));
270 const QString scope = match.captured(QLatin1String(
"scope"));
281 data[torrent.filename] = torrent.data;
286 switch (result.userType())
288 case QMetaType::QJsonDocument:
291 case QMetaType::QString:
300 switch (error.
type())
322 const bool isAltUIUsed = pref->isAltWebUiEnabled();
324 !isAltUIUsed ?
WWW_FOLDER : pref->getWebUiRootFolder());
331 LogMsg(tr(
"Using built-in Web UI."));
336 const QString newLocale = pref->getLocale();
345 LogMsg(tr(
"Web UI translation for selected locale (%1) has been successfully loaded.")
350 LogMsg(tr(
"Couldn't load Web UI translation for selected locale (%1).").arg(newLocale),
Log::WARNING);
359 m_domainList = pref->getServerDomains().split(
';', Qt::SkipEmptyParts);
374 const bool isClickjackingProtectionEnabled = pref->isWebUiClickjackingProtectionEnabled();
375 if (isClickjackingProtectionEnabled)
378 const QString contentSecurityPolicy =
381 : QLatin1String(
"default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; script-src 'self' 'unsafe-inline'; object-src 'none'; form-action 'self';"))
382 + (isClickjackingProtectionEnabled ? QLatin1String(
" frame-ancestors 'self';") : QLatin1String(
""))
383 + (
m_isHttpsEnabled ? QLatin1String(
" upgrade-insecure-requests;") : QLatin1String(
""));
384 if (!contentSecurityPolicy.isEmpty())
387 if (pref->isWebUICustomHTTPHeadersEnabled())
389 const QString customHeaders = pref->getWebUICustomHTTPHeaders();
390 const QList<QStringView> customHeaderLines = QStringView(customHeaders).trimmed().split(u
'\n', Qt::SkipEmptyParts);
392 for (
const QStringView line : customHeaderLines)
394 const int idx = line.indexOf(
':');
398 LogMsg(tr(
"Missing ':' separator in WebUI custom HTTP header: \"%1\"").arg(line.toString()),
Log::WARNING);
402 const QString header = line.left(idx).trimmed().toString();
403 const QString
value = line.mid(idx + 1).trimmed().toString();
404 m_prebuiltHeaders.push_back({header,
value});
408 m_isReverseProxySupportEnabled = pref->isWebUIReverseProxySupportEnabled();
409 if (m_isReverseProxySupportEnabled)
411 m_trustedReverseProxyList.clear();
413 const QStringList proxyList = pref->getWebUITrustedReverseProxiesList().split(
';', Qt::SkipEmptyParts);
415 for (
const QString &proxy : proxyList)
418 if (ip.setAddress(proxy))
419 m_trustedReverseProxyList.push_back(ip);
422 if (m_trustedReverseProxyList.isEmpty())
423 m_isReverseProxySupportEnabled =
false;
429 Q_ASSERT(controller);
442 const QDateTime lastModified {QFileInfo(path).lastModified()};
448 print(it->data, it->mimeType);
454 if (!
file.open(QIODevice::ReadOnly))
456 qDebug(
"File %s was not found!", qUtf8Printable(path));
462 qWarning(
"%s: exceeded the maximum allowed file size!", qUtf8Printable(path));
467 QByteArray data {
file.readAll()};
470 const QMimeType mimeType {QMimeDatabase().mimeTypeForFileNameAndData(path, data)};
471 const bool isTranslatable {mimeType.inherits(QLatin1String(
"text/plain"))};
476 QString dataStr {data};
478 data = dataStr.toUtf8();
483 print(data, mimeType.name());
497 m_params[iter.key()] = QString::fromUtf8(iter.value());
547 if (!sessionId.isEmpty())
565 qDebug() << Q_FUNC_INFO <<
"session does not exist!";
579 const quint32 tmp[] =
582 sid = QByteArray::fromRawData(
reinterpret_cast<const char *
>(tmp),
sizeof(tmp)).toBase64();
623 cookie.setHttpOnly(
true);
625 cookie.setPath(QLatin1String(
"/"));
626 QByteArray cookieRawForm = cookie.toRawForm();
628 cookieRawForm.append(
"; SameSite=Strict");
636 QNetworkCookie cookie(
C_SID);
637 cookie.setPath(QLatin1String(
"/"));
638 cookie.setExpirationDate(QDateTime::currentDateTime().addDays(-1));
650 const auto isSameOrigin = [](
const QUrl &left,
const QUrl &right) ->
bool
653 return ((left.port() == right.port())
655 && (left.host() == right.host()));
662 if (originValue.isEmpty() && refererValue.isEmpty())
670 if (!originValue.isEmpty())
672 const bool isInvalid = !isSameOrigin(
urlFromHostHeader(targetOrigin), originValue);
674 LogMsg(tr(
"WebUI: Origin header & Target origin mismatch! Source IP: '%1'. Origin header: '%2'. Target origin: '%3'")
680 if (!refererValue.isEmpty())
682 const bool isInvalid = !isSameOrigin(
urlFromHostHeader(targetOrigin), refererValue);
684 LogMsg(tr(
"WebUI: Referer header & Target origin mismatch! Source IP: '%1'. Referer header: '%2'. Target origin: '%3'")
696 const QString requestHost = hostHeader.host();
699 const int requestPort = hostHeader.port();
702 LogMsg(tr(
"WebUI: Invalid Host header, port mismatch. Request source IP: '%1'. Server port: '%2'. Received Host header: '%3'")
716 for (
const auto &domain : domains)
719 if (requestHost.contains(domainRegex))
723 LogMsg(tr(
"WebUI: Invalid Host header. Request source IP: '%1'. Received Host header: '%2'")
740 if (!forwardedFor.isEmpty())
743 const QStringList remoteIpList = forwardedFor.split(
',', Qt::SkipEmptyParts);
745 if (!remoteIpList.isEmpty())
747 QHostAddress clientAddress;
749 for (
const QString &remoteIp : remoteIpList)
751 if (clientAddress.setAddress(remoteIp) && clientAddress.isGlobal())
752 return clientAddress;
755 if (clientAddress.setAddress(remoteIpList[0]))
756 return clientAddress;
780 return m_timer.hasExpired(seconds * 1000);
QHash< QString, QByteArray > DataMap
QVariant run(const QString &action, const StringMap ¶ms, const DataMap &data={})
APIErrorType type() const
QString message() const noexcept
QString statusText() const
Response response() const
void setHeader(const Header &header)
void print(const QString &text, const QString &type=CONTENT_TYPE_HTML)
void status(uint code=200, const QString &text=QLatin1String("OK"))
static Preferences * instance()
bool m_isAuthSubnetWhitelistEnabled
WebSession * session() override
bool m_isReverseProxySupportEnabled
QString generateSid() const
void sendFile(const QString &path)
QHostAddress m_clientAddress
const Http::Request & request() const
const Http::Environment & env() const
QHash< QString, WebSession * > m_sessions
QVector< Http::Header > m_prebuiltHeaders
bool m_isLocalAuthEnabled
QHash< QString, APIController * > m_apiControllers
bool isPublicAPI(const QString &scope, const QString &action) const
bool m_translationFileLoaded
bool isCrossSiteRequest(const Http::Request &request) const
WebSession * m_currentSession
bool validateHostHeader(const QStringList &domains) const
bool m_isHostHeaderValidationEnabled
QVector< Utils::Net::Subnet > m_authSubnetWhitelist
void sessionStart() override
void declarePublicAPI(const QString &apiPath)
bool m_isCSRFProtectionEnabled
QHash< QString, TranslatedFile > m_translatedFiles
QHash< QString, QString > m_params
~WebApplication() override
QVector< QHostAddress > m_trustedReverseProxyList
void registerAPIController(const QString &scope, APIController *controller)
const QRegularExpression m_apiPathPattern
WebApplication(QObject *parent=nullptr)
void translateDocument(QString &data) const
QHostAddress resolveClientAddress() const
bool m_isSecureCookieEnabled
Http::Response processRequest(const Http::Request &request, const Http::Environment &env) override
QString clientId() const override
void sessionEnd() override
QSet< QString > m_publicAPIs
void setData(const QString &id, const QVariant &data) override
QString id() const override
WebSession(const QString &sid)
bool hasExpired(qint64 seconds) const
QVariant getData(const QString &id) const override
constexpr std::add_const_t< T > & asConst(T &t) noexcept
flag icons free of to any person obtaining a copy of this software and associated documentation files(the "Software")
void LogMsg(const QString &message, const Log::MsgType &type)
void removeIf(T &dict, BinaryPredicate &&p)
const char HEADER_X_FRAME_OPTIONS[]
const char HEADER_REFERRER_POLICY[]
const char HEADER_ORIGIN[]
const char CONTENT_TYPE_JS[]
const char HEADER_X_XSS_PROTECTION[]
const char CONTENT_TYPE_JSON[]
const char HEADER_X_CONTENT_TYPE_OPTIONS[]
const char HEADER_X_FORWARDED_FOR[]
const char HEADER_CONTENT_SECURITY_POLICY[]
const char CONTENT_TYPE_CSS[]
const char HEADER_REFERER[]
const char HEADER_X_FORWARDED_HOST[]
const char HEADER_CACHE_CONTROL[]
const char HEADER_SET_COOKIE[]
const char CONTENT_TYPE_TXT[]
bool isRegularFile(const QString &path)
QString expandPathAbs(const QString &path)
QString friendlyUnit(qint64 bytes, bool isSpeed=false)
bool isLoopbackAddress(const QHostAddress &addr)
bool isIPInRange(const QHostAddress &addr, const QVector< Subnet > &subnets)
uint32_t rand(uint32_t min=0, uint32_t max=std::numeric_limits< uint32_t >::max())
T unquote(const T &str, const QString "es=QChar('"'))
QString wildcardToRegexPattern(const QString &pattern)
T value(const QString &key, const T &defaultValue={})
QString getCachingInterval(QString contentType)
QUrl urlFromHostHeader(const QString &hostHeader)
QStringMap parseCookie(const QStringView cookieStr)
file(GLOB QBT_TS_FILES "${qBittorrent_SOURCE_DIR}/src/lang/*.ts") set_source_files_properties($
QHostAddress localAddress
QHostAddress clientAddress
QHash< QString, QString > posts
QHash< QString, QByteArray > query
QMap< QString, QString > QStringMap
const QString PATH_PREFIX_ICONS
const QString PRIVATE_FOLDER
const int MAX_ALLOWED_FILESIZE
const QString PUBLIC_FOLDER