Sync translations from Transifex and run lupdate
[qBittorrent.git] / src / webui / webapplication.cpp
blobf471da42eb491f15c25ffe3023ca68aa3257a58e
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2014 Vladimir Golovnev <glassez@yandex.ru>
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2
8 * of the License, or (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * In addition, as a special exception, the copyright holders give permission to
20 * link this program with the OpenSSL project's "OpenSSL" library (or with
21 * modified versions of it that use the same license as the "OpenSSL" library),
22 * and distribute the linked executables. You must obey the GNU General Public
23 * License in all respects for all of the code used other than "OpenSSL". If you
24 * modify file(s), you may extend this exception to your version of the file(s),
25 * but you are not obligated to do so. If you do not wish to do so, delete this
26 * exception statement from your version.
29 #include "webapplication.h"
31 #include <algorithm>
33 #include <QDateTime>
34 #include <QDebug>
35 #include <QFile>
36 #include <QFileInfo>
37 #include <QJsonDocument>
38 #include <QMimeDatabase>
39 #include <QMimeType>
40 #include <QNetworkCookie>
41 #include <QRegularExpression>
42 #include <QUrl>
44 #include "base/algorithm.h"
45 #include "base/global.h"
46 #include "base/http/httperror.h"
47 #include "base/logger.h"
48 #include "base/preferences.h"
49 #include "base/types.h"
50 #include "base/utils/bytearray.h"
51 #include "base/utils/fs.h"
52 #include "base/utils/misc.h"
53 #include "base/utils/random.h"
54 #include "base/utils/string.h"
55 #include "api/apierror.h"
56 #include "api/appcontroller.h"
57 #include "api/authcontroller.h"
58 #include "api/logcontroller.h"
59 #include "api/rsscontroller.h"
60 #include "api/searchcontroller.h"
61 #include "api/synccontroller.h"
62 #include "api/torrentscontroller.h"
63 #include "api/transfercontroller.h"
65 const int MAX_ALLOWED_FILESIZE = 10 * 1024 * 1024;
66 const char C_SID[] = "SID"; // name of session id cookie
68 const QString PATH_PREFIX_ICONS {QStringLiteral("/icons/")};
69 const QString WWW_FOLDER {QStringLiteral(":/www")};
70 const QString PUBLIC_FOLDER {QStringLiteral("/public")};
71 const QString PRIVATE_FOLDER {QStringLiteral("/private")};
73 namespace
75 QStringMap parseCookie(const QStringView cookieStr)
77 // [rfc6265] 4.2.1. Syntax
78 QStringMap ret;
79 const QList<QStringView> cookies = cookieStr.split(u';', Qt::SkipEmptyParts);
81 for (const auto &cookie : cookies)
83 const int idx = cookie.indexOf('=');
84 if (idx < 0)
85 continue;
87 const QString name = cookie.left(idx).trimmed().toString();
88 const QString value = Utils::String::unquote(cookie.mid(idx + 1).trimmed()).toString();
89 ret.insert(name, value);
91 return ret;
94 QUrl urlFromHostHeader(const QString &hostHeader)
96 if (!hostHeader.contains(QLatin1String("://")))
97 return {QLatin1String("http://") + hostHeader};
98 return hostHeader;
101 QString getCachingInterval(QString contentType)
103 contentType = contentType.toLower();
105 if (contentType.startsWith(QLatin1String("image/")))
106 return QLatin1String("private, max-age=604800"); // 1 week
108 if ((contentType == Http::CONTENT_TYPE_CSS)
109 || (contentType == Http::CONTENT_TYPE_JS))
111 // short interval in case of program update
112 return QLatin1String("private, max-age=43200"); // 12 hrs
115 return QLatin1String("no-store");
119 WebApplication::WebApplication(QObject *parent)
120 : QObject(parent)
121 , m_cacheID {QString::number(Utils::Random::rand(), 36)}
123 registerAPIController(QLatin1String("app"), new AppController(this, this));
124 registerAPIController(QLatin1String("auth"), new AuthController(this, this));
125 registerAPIController(QLatin1String("log"), new LogController(this, this));
126 registerAPIController(QLatin1String("rss"), new RSSController(this, this));
127 registerAPIController(QLatin1String("search"), new SearchController(this, this));
128 registerAPIController(QLatin1String("sync"), new SyncController(this, this));
129 registerAPIController(QLatin1String("torrents"), new TorrentsController(this, this));
130 registerAPIController(QLatin1String("transfer"), new TransferController(this, this));
132 declarePublicAPI(QLatin1String("auth/login"));
134 configure();
135 connect(Preferences::instance(), &Preferences::changed, this, &WebApplication::configure);
138 WebApplication::~WebApplication()
140 // cleanup sessions data
141 qDeleteAll(m_sessions);
144 void WebApplication::sendWebUIFile()
146 const QStringList pathItems {request().path.split('/', Qt::SkipEmptyParts)};
147 if (pathItems.contains(".") || pathItems.contains(".."))
148 throw InternalServerErrorHTTPError();
150 if (!m_isAltUIUsed)
152 if (request().path.startsWith(PATH_PREFIX_ICONS))
154 const QString imageFilename {request().path.mid(PATH_PREFIX_ICONS.size())};
155 sendFile(QLatin1String(":/icons/") + imageFilename);
156 return;
160 const QString path
162 (request().path != QLatin1String("/")
163 ? request().path
164 : QLatin1String("/index.html"))
167 QString localPath
169 m_rootFolder
170 + (session() ? PRIVATE_FOLDER : PUBLIC_FOLDER)
171 + path
174 QFileInfo fileInfo {localPath};
176 if (!fileInfo.exists() && session())
178 // try to send public file if there is no private one
179 localPath = m_rootFolder + PUBLIC_FOLDER + path;
180 fileInfo.setFile(localPath);
183 if (m_isAltUIUsed)
185 #ifdef Q_OS_UNIX
186 if (!Utils::Fs::isRegularFile(localPath))
188 status(500, "Internal Server Error");
189 print(tr("Unacceptable file type, only regular file is allowed."), Http::CONTENT_TYPE_TXT);
190 return;
192 #endif
194 while (fileInfo.filePath() != m_rootFolder)
196 if (fileInfo.isSymLink())
197 throw InternalServerErrorHTTPError(tr("Symlinks inside alternative UI folder are forbidden."));
199 fileInfo.setFile(fileInfo.path());
203 sendFile(localPath);
206 void WebApplication::translateDocument(QString &data) const
208 const QRegularExpression regex("QBT_TR\\((([^\\)]|\\)(?!QBT_TR))+)\\)QBT_TR\\[CONTEXT=([a-zA-Z_][a-zA-Z0-9_]*)\\]");
210 int i = 0;
211 bool found = true;
212 while (i < data.size() && found)
214 QRegularExpressionMatch regexMatch;
215 i = data.indexOf(regex, i, &regexMatch);
216 if (i >= 0)
218 const QString sourceText = regexMatch.captured(1);
219 const QString context = regexMatch.captured(3);
221 const QString loadedText = m_translationFileLoaded
222 ? m_translator.translate(context.toUtf8().constData(), sourceText.toUtf8().constData())
223 : QString();
224 // `loadedText` is empty when translation is not provided
225 // it should fallback to `sourceText`
226 QString translation = loadedText.isEmpty() ? sourceText : loadedText;
228 // Use HTML code for quotes to prevent issues with JS
229 translation.replace('\'', "&#39;");
230 translation.replace('\"', "&#34;");
232 data.replace(i, regexMatch.capturedLength(), translation);
233 i += translation.length();
235 else
237 found = false; // no more translatable strings
240 data.replace(QLatin1String("${LANG}"), m_currentLocale.left(2));
241 data.replace(QLatin1String("${CACHEID}"), m_cacheID);
245 WebSession *WebApplication::session()
247 return m_currentSession;
250 const Http::Request &WebApplication::request() const
252 return m_request;
255 const Http::Environment &WebApplication::env() const
257 return m_env;
260 void WebApplication::doProcessRequest()
262 const QRegularExpressionMatch match = m_apiPathPattern.match(request().path);
263 if (!match.hasMatch())
265 sendWebUIFile();
266 return;
269 const QString action = match.captured(QLatin1String("action"));
270 const QString scope = match.captured(QLatin1String("scope"));
272 APIController *controller = m_apiControllers.value(scope);
273 if (!controller)
274 throw NotFoundHTTPError();
276 if (!session() && !isPublicAPI(scope, action))
277 throw ForbiddenHTTPError();
279 DataMap data;
280 for (const Http::UploadedFile &torrent : request().files)
281 data[torrent.filename] = torrent.data;
285 const QVariant result = controller->run(action, m_params, data);
286 switch (result.userType())
288 case QMetaType::QJsonDocument:
289 print(result.toJsonDocument().toJson(QJsonDocument::Compact), Http::CONTENT_TYPE_JSON);
290 break;
291 case QMetaType::QString:
292 default:
293 print(result.toString(), Http::CONTENT_TYPE_TXT);
294 break;
297 catch (const APIError &error)
299 // re-throw as HTTPError
300 switch (error.type())
302 case APIErrorType::AccessDenied:
303 throw ForbiddenHTTPError(error.message());
304 case APIErrorType::BadData:
305 throw UnsupportedMediaTypeHTTPError(error.message());
306 case APIErrorType::BadParams:
307 throw BadRequestHTTPError(error.message());
308 case APIErrorType::Conflict:
309 throw ConflictHTTPError(error.message());
310 case APIErrorType::NotFound:
311 throw NotFoundHTTPError(error.message());
312 default:
313 Q_ASSERT(false);
318 void WebApplication::configure()
320 const auto *pref = Preferences::instance();
322 const bool isAltUIUsed = pref->isAltWebUiEnabled();
323 const QString rootFolder = Utils::Fs::expandPathAbs(
324 !isAltUIUsed ? WWW_FOLDER : pref->getWebUiRootFolder());
325 if ((isAltUIUsed != m_isAltUIUsed) || (rootFolder != m_rootFolder))
327 m_isAltUIUsed = isAltUIUsed;
328 m_rootFolder = rootFolder;
329 m_translatedFiles.clear();
330 if (!m_isAltUIUsed)
331 LogMsg(tr("Using built-in Web UI."));
332 else
333 LogMsg(tr("Using custom Web UI. Location: \"%1\".").arg(m_rootFolder));
336 const QString newLocale = pref->getLocale();
337 if (m_currentLocale != newLocale)
339 m_currentLocale = newLocale;
340 m_translatedFiles.clear();
342 m_translationFileLoaded = m_translator.load(m_rootFolder + QLatin1String("/translations/webui_") + newLocale);
343 if (m_translationFileLoaded)
345 LogMsg(tr("Web UI translation for selected locale (%1) has been successfully loaded.")
346 .arg(newLocale));
348 else
350 LogMsg(tr("Couldn't load Web UI translation for selected locale (%1).").arg(newLocale), Log::WARNING);
354 m_isLocalAuthEnabled = pref->isWebUiLocalAuthEnabled();
355 m_isAuthSubnetWhitelistEnabled = pref->isWebUiAuthSubnetWhitelistEnabled();
356 m_authSubnetWhitelist = pref->getWebUiAuthSubnetWhitelist();
357 m_sessionTimeout = pref->getWebUISessionTimeout();
359 m_domainList = pref->getServerDomains().split(';', Qt::SkipEmptyParts);
360 std::for_each(m_domainList.begin(), m_domainList.end(), [](QString &entry) { entry = entry.trimmed(); });
362 m_isCSRFProtectionEnabled = pref->isWebUiCSRFProtectionEnabled();
363 m_isSecureCookieEnabled = pref->isWebUiSecureCookieEnabled();
364 m_isHostHeaderValidationEnabled = pref->isWebUIHostHeaderValidationEnabled();
365 m_isHttpsEnabled = pref->isWebUiHttpsEnabled();
367 m_prebuiltHeaders.clear();
368 m_prebuiltHeaders.push_back({QLatin1String(Http::HEADER_X_XSS_PROTECTION), QLatin1String("1; mode=block")});
369 m_prebuiltHeaders.push_back({QLatin1String(Http::HEADER_X_CONTENT_TYPE_OPTIONS), QLatin1String("nosniff")});
371 if (!m_isAltUIUsed)
372 m_prebuiltHeaders.push_back({QLatin1String(Http::HEADER_REFERRER_POLICY), QLatin1String("same-origin")});
374 const bool isClickjackingProtectionEnabled = pref->isWebUiClickjackingProtectionEnabled();
375 if (isClickjackingProtectionEnabled)
376 m_prebuiltHeaders.push_back({QLatin1String(Http::HEADER_X_FRAME_OPTIONS), QLatin1String("SAMEORIGIN")});
378 const QString contentSecurityPolicy =
379 (m_isAltUIUsed
380 ? QLatin1String("")
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())
385 m_prebuiltHeaders.push_back({QLatin1String(Http::HEADER_CONTENT_SECURITY_POLICY), contentSecurityPolicy});
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(':');
395 if (idx < 0)
397 // require separator `:` to be present even if `value` field can be empty
398 LogMsg(tr("Missing ':' separator in WebUI custom HTTP header: \"%1\"").arg(line.toString()), Log::WARNING);
399 continue;
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)
417 QHostAddress ip;
418 if (ip.setAddress(proxy))
419 m_trustedReverseProxyList.push_back(ip);
422 if (m_trustedReverseProxyList.isEmpty())
423 m_isReverseProxySupportEnabled = false;
427 void WebApplication::registerAPIController(const QString &scope, APIController *controller)
429 Q_ASSERT(controller);
430 Q_ASSERT(!m_apiControllers.value(scope));
432 m_apiControllers[scope] = controller;
435 void WebApplication::declarePublicAPI(const QString &apiPath)
437 m_publicAPIs << apiPath;
440 void WebApplication::sendFile(const QString &path)
442 const QDateTime lastModified {QFileInfo(path).lastModified()};
444 // find translated file in cache
445 const auto it = m_translatedFiles.constFind(path);
446 if ((it != m_translatedFiles.constEnd()) && (lastModified <= it->lastModified))
448 print(it->data, it->mimeType);
449 setHeader({Http::HEADER_CACHE_CONTROL, getCachingInterval(it->mimeType)});
450 return;
453 QFile file {path};
454 if (!file.open(QIODevice::ReadOnly))
456 qDebug("File %s was not found!", qUtf8Printable(path));
457 throw NotFoundHTTPError();
460 if (file.size() > MAX_ALLOWED_FILESIZE)
462 qWarning("%s: exceeded the maximum allowed file size!", qUtf8Printable(path));
463 throw InternalServerErrorHTTPError(tr("Exceeded the maximum allowed file size (%1)!")
464 .arg(Utils::Misc::friendlyUnit(MAX_ALLOWED_FILESIZE)));
467 QByteArray data {file.readAll()};
468 file.close();
470 const QMimeType mimeType {QMimeDatabase().mimeTypeForFileNameAndData(path, data)};
471 const bool isTranslatable {mimeType.inherits(QLatin1String("text/plain"))};
473 // Translate the file
474 if (isTranslatable)
476 QString dataStr {data};
477 translateDocument(dataStr);
478 data = dataStr.toUtf8();
480 m_translatedFiles[path] = {data, mimeType.name(), lastModified}; // caching translated file
483 print(data, mimeType.name());
484 setHeader({Http::HEADER_CACHE_CONTROL, getCachingInterval(mimeType.name())});
487 Http::Response WebApplication::processRequest(const Http::Request &request, const Http::Environment &env)
489 m_currentSession = nullptr;
490 m_request = request;
491 m_env = env;
492 m_params.clear();
494 if (m_request.method == Http::METHOD_GET)
496 for (auto iter = m_request.query.cbegin(); iter != m_request.query.cend(); ++iter)
497 m_params[iter.key()] = QString::fromUtf8(iter.value());
499 else
501 m_params = m_request.posts;
504 // clear response
505 clear();
509 // block suspicious requests
510 if ((m_isCSRFProtectionEnabled && isCrossSiteRequest(m_request))
511 || (m_isHostHeaderValidationEnabled && !validateHostHeader(m_domainList)))
513 throw UnauthorizedHTTPError();
516 // reverse proxy resolve client address
517 m_clientAddress = resolveClientAddress();
519 sessionInitialize();
520 doProcessRequest();
522 catch (const HTTPError &error)
524 status(error.statusCode(), error.statusText());
525 print((!error.message().isEmpty() ? error.message() : error.statusText()), Http::CONTENT_TYPE_TXT);
528 for (const Http::Header &prebuiltHeader : asConst(m_prebuiltHeaders))
529 setHeader(prebuiltHeader);
531 return response();
534 QString WebApplication::clientId() const
536 return m_clientAddress.toString();
539 void WebApplication::sessionInitialize()
541 Q_ASSERT(!m_currentSession);
543 const QString sessionId {parseCookie(m_request.headers.value(QLatin1String("cookie"))).value(C_SID)};
545 // TODO: Additional session check
547 if (!sessionId.isEmpty())
549 m_currentSession = m_sessions.value(sessionId);
550 if (m_currentSession)
552 if (m_currentSession->hasExpired(m_sessionTimeout))
554 // session is outdated - removing it
555 delete m_sessions.take(sessionId);
556 m_currentSession = nullptr;
558 else
560 m_currentSession->updateTimestamp();
563 else
565 qDebug() << Q_FUNC_INFO << "session does not exist!";
569 if (!m_currentSession && !isAuthNeeded())
570 sessionStart();
573 QString WebApplication::generateSid() const
575 QString sid;
579 const quint32 tmp[] =
580 {Utils::Random::rand(), Utils::Random::rand(), Utils::Random::rand()
581 , Utils::Random::rand(), Utils::Random::rand(), Utils::Random::rand()};
582 sid = QByteArray::fromRawData(reinterpret_cast<const char *>(tmp), sizeof(tmp)).toBase64();
584 while (m_sessions.contains(sid));
586 return sid;
589 bool WebApplication::isAuthNeeded()
591 if (!m_isLocalAuthEnabled && Utils::Net::isLoopbackAddress(m_clientAddress))
592 return false;
593 if (m_isAuthSubnetWhitelistEnabled && Utils::Net::isIPInRange(m_clientAddress, m_authSubnetWhitelist))
594 return false;
595 return true;
598 bool WebApplication::isPublicAPI(const QString &scope, const QString &action) const
600 return m_publicAPIs.contains(QString::fromLatin1("%1/%2").arg(scope, action));
603 void WebApplication::sessionStart()
605 Q_ASSERT(!m_currentSession);
607 // remove outdated sessions
608 Algorithm::removeIf(m_sessions, [this](const QString &, const WebSession *session)
610 if (session->hasExpired(m_sessionTimeout))
612 delete session;
613 return true;
616 return false;
619 m_currentSession = new WebSession(generateSid());
620 m_sessions[m_currentSession->id()] = m_currentSession;
622 QNetworkCookie cookie(C_SID, m_currentSession->id().toUtf8());
623 cookie.setHttpOnly(true);
624 cookie.setSecure(m_isSecureCookieEnabled && m_isHttpsEnabled);
625 cookie.setPath(QLatin1String("/"));
626 QByteArray cookieRawForm = cookie.toRawForm();
627 if (m_isCSRFProtectionEnabled)
628 cookieRawForm.append("; SameSite=Strict");
629 setHeader({Http::HEADER_SET_COOKIE, cookieRawForm});
632 void WebApplication::sessionEnd()
634 Q_ASSERT(m_currentSession);
636 QNetworkCookie cookie(C_SID);
637 cookie.setPath(QLatin1String("/"));
638 cookie.setExpirationDate(QDateTime::currentDateTime().addDays(-1));
640 delete m_sessions.take(m_currentSession->id());
641 m_currentSession = nullptr;
643 setHeader({Http::HEADER_SET_COOKIE, cookie.toRawForm()});
646 bool WebApplication::isCrossSiteRequest(const Http::Request &request) const
648 // https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Verifying_Same_Origin_with_Standard_Headers
650 const auto isSameOrigin = [](const QUrl &left, const QUrl &right) -> bool
652 // [rfc6454] 5. Comparing Origins
653 return ((left.port() == right.port())
654 // && (left.scheme() == right.scheme()) // not present in this context
655 && (left.host() == right.host()));
658 const QString targetOrigin = request.headers.value(Http::HEADER_X_FORWARDED_HOST, request.headers.value(Http::HEADER_HOST));
659 const QString originValue = request.headers.value(Http::HEADER_ORIGIN);
660 const QString refererValue = request.headers.value(Http::HEADER_REFERER);
662 if (originValue.isEmpty() && refererValue.isEmpty())
664 // owasp.org recommends to block this request, but doing so will inevitably lead Web API users to spoof headers
665 // so lets be permissive here
666 return false;
669 // sent with CORS requests, as well as with POST requests
670 if (!originValue.isEmpty())
672 const bool isInvalid = !isSameOrigin(urlFromHostHeader(targetOrigin), originValue);
673 if (isInvalid)
674 LogMsg(tr("WebUI: Origin header & Target origin mismatch! Source IP: '%1'. Origin header: '%2'. Target origin: '%3'")
675 .arg(m_env.clientAddress.toString(), originValue, targetOrigin)
676 , Log::WARNING);
677 return isInvalid;
680 if (!refererValue.isEmpty())
682 const bool isInvalid = !isSameOrigin(urlFromHostHeader(targetOrigin), refererValue);
683 if (isInvalid)
684 LogMsg(tr("WebUI: Referer header & Target origin mismatch! Source IP: '%1'. Referer header: '%2'. Target origin: '%3'")
685 .arg(m_env.clientAddress.toString(), refererValue, targetOrigin)
686 , Log::WARNING);
687 return isInvalid;
690 return true;
693 bool WebApplication::validateHostHeader(const QStringList &domains) const
695 const QUrl hostHeader = urlFromHostHeader(m_request.headers[Http::HEADER_HOST]);
696 const QString requestHost = hostHeader.host();
698 // (if present) try matching host header's port with local port
699 const int requestPort = hostHeader.port();
700 if ((requestPort != -1) && (m_env.localPort != requestPort))
702 LogMsg(tr("WebUI: Invalid Host header, port mismatch. Request source IP: '%1'. Server port: '%2'. Received Host header: '%3'")
703 .arg(m_env.clientAddress.toString()).arg(m_env.localPort)
704 .arg(m_request.headers[Http::HEADER_HOST])
705 , Log::WARNING);
706 return false;
709 // try matching host header with local address
710 const bool sameAddr = m_env.localAddress.isEqual(QHostAddress(requestHost));
712 if (sameAddr)
713 return true;
715 // try matching host header with domain list
716 for (const auto &domain : domains)
718 const QRegularExpression domainRegex {Utils::String::wildcardToRegexPattern(domain), QRegularExpression::CaseInsensitiveOption};
719 if (requestHost.contains(domainRegex))
720 return true;
723 LogMsg(tr("WebUI: Invalid Host header. Request source IP: '%1'. Received Host header: '%2'")
724 .arg(m_env.clientAddress.toString(), m_request.headers[Http::HEADER_HOST])
725 , Log::WARNING);
726 return false;
729 QHostAddress WebApplication::resolveClientAddress() const
731 if (!m_isReverseProxySupportEnabled)
732 return m_env.clientAddress;
734 // Only reverse proxy can overwrite client address
735 if (!m_trustedReverseProxyList.contains(m_env.clientAddress))
736 return m_env.clientAddress;
738 const QString forwardedFor = m_request.headers.value(Http::HEADER_X_FORWARDED_FOR);
740 if (!forwardedFor.isEmpty())
742 // client address is the 1st global IP in X-Forwarded-For or, if none available, the 1st IP in the list
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;
760 return m_env.clientAddress;
763 // WebSession
765 WebSession::WebSession(const QString &sid)
766 : m_sid {sid}
768 updateTimestamp();
771 QString WebSession::id() const
773 return m_sid;
776 bool WebSession::hasExpired(const qint64 seconds) const
778 if (seconds <= 0)
779 return false;
780 return m_timer.hasExpired(seconds * 1000);
783 void WebSession::updateTimestamp()
785 m_timer.start();
788 QVariant WebSession::getData(const QString &id) const
790 return m_data.value(id);
793 void WebSession::setData(const QString &id, const QVariant &data)
795 m_data[id] = data;