qBittorrent
WebApplication Class Referencefinal

#include <webapplication.h>

Inheritance diagram for WebApplication:
Collaboration diagram for WebApplication:

Classes

struct  TranslatedFile
 

Public Member Functions

 WebApplication (QObject *parent=nullptr)
 
 ~WebApplication () override
 
Http::Response processRequest (const Http::Request &request, const Http::Environment &env) override
 
QString clientId () const override
 
WebSessionsession () override
 
void sessionStart () override
 
void sessionEnd () override
 
const Http::Requestrequest () const
 
const Http::Environmentenv () const
 
- Public Member Functions inherited from Http::IRequestHandler
virtual ~IRequestHandler ()
 
- Public Member Functions inherited from ISessionManager
virtual ~ISessionManager ()=default
 

Private Member Functions

void doProcessRequest ()
 
void configure ()
 
void registerAPIController (const QString &scope, APIController *controller)
 
void declarePublicAPI (const QString &apiPath)
 
void sendFile (const QString &path)
 
void sendWebUIFile ()
 
void translateDocument (QString &data) const
 
QString generateSid () const
 
void sessionInitialize ()
 
bool isAuthNeeded ()
 
bool isPublicAPI (const QString &scope, const QString &action) const
 
bool isCrossSiteRequest (const Http::Request &request) const
 
bool validateHostHeader (const QStringList &domains) const
 
QHostAddress resolveClientAddress () const
 
- Private Member Functions inherited from Http::ResponseBuilder
void status (uint code=200, const QString &text=QLatin1String("OK"))
 
void setHeader (const Header &header)
 
void print (const QString &text, const QString &type=CONTENT_TYPE_HTML)
 
void print (const QByteArray &data, const QString &type=CONTENT_TYPE_HTML)
 
void clear ()
 
Response response () const
 

Private Attributes

QHash< QString, WebSession * > m_sessions
 
WebSessionm_currentSession = nullptr
 
Http::Request m_request
 
Http::Environment m_env
 
QHash< QString, QString > m_params
 
const QString m_cacheID
 
const QRegularExpression m_apiPathPattern {QLatin1String("^/api/v2/(?<scope>[A-Za-z_][A-Za-z_0-9]*)/(?<action>[A-Za-z_][A-Za-z_0-9]*)$")}
 
QHash< QString, APIController * > m_apiControllers
 
QSet< QString > m_publicAPIs
 
bool m_isAltUIUsed = false
 
QString m_rootFolder
 
QHash< QString, TranslatedFilem_translatedFiles
 
QString m_currentLocale
 
QTranslator m_translator
 
bool m_translationFileLoaded = false
 
bool m_isLocalAuthEnabled
 
bool m_isAuthSubnetWhitelistEnabled
 
QVector< Utils::Net::Subnetm_authSubnetWhitelist
 
int m_sessionTimeout
 
QStringList m_domainList
 
bool m_isCSRFProtectionEnabled
 
bool m_isSecureCookieEnabled
 
bool m_isHostHeaderValidationEnabled
 
bool m_isHttpsEnabled
 
bool m_isReverseProxySupportEnabled
 
QVector< QHostAddress > m_trustedReverseProxyList
 
QHostAddress m_clientAddress
 
QVector< Http::Headerm_prebuiltHeaders
 

Detailed Description

Definition at line 70 of file webapplication.h.

Constructor & Destructor Documentation

◆ WebApplication()

WebApplication::WebApplication ( QObject *  parent = nullptr)
explicit

Definition at line 119 of file webapplication.cpp.

120  : QObject(parent)
121  , m_cacheID {QString::number(Utils::Random::rand(), 36)}
122 {
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));
131 
132  declarePublicAPI(QLatin1String("auth/login"));
133 
134  configure();
136 }
static Preferences * instance()
void changed()
const QString m_cacheID
void declarePublicAPI(const QString &apiPath)
void registerAPIController(const QString &scope, APIController *controller)
uint32_t rand(uint32_t min=0, uint32_t max=std::numeric_limits< uint32_t >::max())
Definition: random.cpp:132

References Preferences::changed(), configure(), declarePublicAPI(), Preferences::instance(), and registerAPIController().

Here is the call graph for this function:

◆ ~WebApplication()

WebApplication::~WebApplication ( )
override

Definition at line 138 of file webapplication.cpp.

139 {
140  // cleanup sessions data
141  qDeleteAll(m_sessions);
142 }
QHash< QString, WebSession * > m_sessions

References m_sessions.

Member Function Documentation

◆ clientId()

QString WebApplication::clientId ( ) const
overridevirtual

Implements ISessionManager.

Definition at line 534 of file webapplication.cpp.

535 {
536  return m_clientAddress.toString();
537 }
QHostAddress m_clientAddress

References m_clientAddress.

◆ configure()

void WebApplication::configure ( )
private

Definition at line 318 of file webapplication.cpp.

319 {
320  const auto *pref = Preferences::instance();
321 
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))
326  {
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));
334  }
335 
336  const QString newLocale = pref->getLocale();
337  if (m_currentLocale != newLocale)
338  {
339  m_currentLocale = newLocale;
340  m_translatedFiles.clear();
341 
342  m_translationFileLoaded = m_translator.load(m_rootFolder + QLatin1String("/translations/webui_") + newLocale);
344  {
345  LogMsg(tr("Web UI translation for selected locale (%1) has been successfully loaded.")
346  .arg(newLocale));
347  }
348  else
349  {
350  LogMsg(tr("Couldn't load Web UI translation for selected locale (%1).").arg(newLocale), Log::WARNING);
351  }
352  }
353 
354  m_isLocalAuthEnabled = pref->isWebUiLocalAuthEnabled();
355  m_isAuthSubnetWhitelistEnabled = pref->isWebUiAuthSubnetWhitelistEnabled();
356  m_authSubnetWhitelist = pref->getWebUiAuthSubnetWhitelist();
357  m_sessionTimeout = pref->getWebUISessionTimeout();
358 
359  m_domainList = pref->getServerDomains().split(';', Qt::SkipEmptyParts);
360  std::for_each(m_domainList.begin(), m_domainList.end(), [](QString &entry) { entry = entry.trimmed(); });
361 
362  m_isCSRFProtectionEnabled = pref->isWebUiCSRFProtectionEnabled();
363  m_isSecureCookieEnabled = pref->isWebUiSecureCookieEnabled();
364  m_isHostHeaderValidationEnabled = pref->isWebUIHostHeaderValidationEnabled();
365  m_isHttpsEnabled = pref->isWebUiHttpsEnabled();
366 
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")});
370 
371  if (!m_isAltUIUsed)
372  m_prebuiltHeaders.push_back({QLatin1String(Http::HEADER_REFERRER_POLICY), QLatin1String("same-origin")});
373 
374  const bool isClickjackingProtectionEnabled = pref->isWebUiClickjackingProtectionEnabled();
375  if (isClickjackingProtectionEnabled)
376  m_prebuiltHeaders.push_back({QLatin1String(Http::HEADER_X_FRAME_OPTIONS), QLatin1String("SAMEORIGIN")});
377 
378  const QString contentSecurityPolicy =
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});
386 
387  if (pref->isWebUICustomHTTPHeadersEnabled())
388  {
389  const QString customHeaders = pref->getWebUICustomHTTPHeaders();
390  const QList<QStringView> customHeaderLines = QStringView(customHeaders).trimmed().split(u'\n', Qt::SkipEmptyParts);
391 
392  for (const QStringView line : customHeaderLines)
393  {
394  const int idx = line.indexOf(':');
395  if (idx < 0)
396  {
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;
400  }
401 
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});
405  }
406  }
407 
408  m_isReverseProxySupportEnabled = pref->isWebUIReverseProxySupportEnabled();
410  {
412 
413  const QStringList proxyList = pref->getWebUITrustedReverseProxiesList().split(';', Qt::SkipEmptyParts);
414 
415  for (const QString &proxy : proxyList)
416  {
417  QHostAddress ip;
418  if (ip.setAddress(proxy))
419  m_trustedReverseProxyList.push_back(ip);
420  }
421 
422  if (m_trustedReverseProxyList.isEmpty())
424  }
425 }
bool m_isAuthSubnetWhitelistEnabled
bool m_isReverseProxySupportEnabled
QVector< Http::Header > m_prebuiltHeaders
QStringList m_domainList
bool m_translationFileLoaded
bool m_isHostHeaderValidationEnabled
QVector< Utils::Net::Subnet > m_authSubnetWhitelist
QString m_currentLocale
bool m_isCSRFProtectionEnabled
QHash< QString, TranslatedFile > m_translatedFiles
QVector< QHostAddress > m_trustedReverseProxyList
bool m_isSecureCookieEnabled
QTranslator m_translator
QString m_rootFolder
void LogMsg(const QString &message, const Log::MsgType &type)
Definition: logger.cpp:125
const char HEADER_X_FRAME_OPTIONS[]
Definition: types.h:57
const char HEADER_REFERRER_POLICY[]
Definition: types.h:52
const char HEADER_X_XSS_PROTECTION[]
Definition: types.h:58
const char HEADER_X_CONTENT_TYPE_OPTIONS[]
Definition: types.h:54
const char HEADER_CONTENT_SECURITY_POLICY[]
Definition: types.h:46
@ WARNING
Definition: logger.h:47
QString expandPathAbs(const QString &path)
Definition: fs.cpp:309
T value(const QString &key, const T &defaultValue={})
Definition: preferences.cpp:64
const QString WWW_FOLDER

References Utils::Fs::expandPathAbs(), Http::HEADER_CONTENT_SECURITY_POLICY, Http::HEADER_REFERRER_POLICY, Http::HEADER_X_CONTENT_TYPE_OPTIONS, Http::HEADER_X_FRAME_OPTIONS, Http::HEADER_X_XSS_PROTECTION, Preferences::instance(), LogMsg(), m_authSubnetWhitelist, m_currentLocale, m_domainList, m_isAltUIUsed, m_isAuthSubnetWhitelistEnabled, m_isCSRFProtectionEnabled, m_isHostHeaderValidationEnabled, m_isHttpsEnabled, m_isLocalAuthEnabled, m_isSecureCookieEnabled, m_prebuiltHeaders, m_rootFolder, m_sessionTimeout, m_translatedFiles, m_translationFileLoaded, m_translator, Log::WARNING, and WWW_FOLDER.

Referenced by WebApplication().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ declarePublicAPI()

void WebApplication::declarePublicAPI ( const QString &  apiPath)
private

Definition at line 435 of file webapplication.cpp.

436 {
437  m_publicAPIs << apiPath;
438 }
QSet< QString > m_publicAPIs

References m_publicAPIs.

Referenced by WebApplication().

Here is the caller graph for this function:

◆ doProcessRequest()

void WebApplication::doProcessRequest ( )
private

Definition at line 260 of file webapplication.cpp.

261 {
262  const QRegularExpressionMatch match = m_apiPathPattern.match(request().path);
263  if (!match.hasMatch())
264  {
265  sendWebUIFile();
266  return;
267  }
268 
269  const QString action = match.captured(QLatin1String("action"));
270  const QString scope = match.captured(QLatin1String("scope"));
271 
272  APIController *controller = m_apiControllers.value(scope);
273  if (!controller)
274  throw NotFoundHTTPError();
275 
276  if (!session() && !isPublicAPI(scope, action))
277  throw ForbiddenHTTPError();
278 
279  DataMap data;
280  for (const Http::UploadedFile &torrent : request().files)
281  data[torrent.filename] = torrent.data;
282 
283  try
284  {
285  const QVariant result = controller->run(action, m_params, data);
286  switch (result.userType())
287  {
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;
295  }
296  }
297  catch (const APIError &error)
298  {
299  // re-throw as HTTPError
300  switch (error.type())
301  {
303  throw ForbiddenHTTPError(error.message());
305  throw UnsupportedMediaTypeHTTPError(error.message());
307  throw BadRequestHTTPError(error.message());
309  throw ConflictHTTPError(error.message());
311  throw NotFoundHTTPError(error.message());
312  default:
313  Q_ASSERT(false);
314  }
315  }
316 }
QHash< QString, QByteArray > DataMap
Definition: apicontroller.h:39
QVariant run(const QString &action, const StringMap &params, const DataMap &data={})
APIErrorType type() const
Definition: apierror.cpp:37
QString message() const noexcept
Definition: exceptions.cpp:36
void print(const QString &text, const QString &type=CONTENT_TYPE_HTML)
WebSession * session() override
const Http::Request & request() const
QHash< QString, APIController * > m_apiControllers
bool isPublicAPI(const QString &scope, const QString &action) const
QHash< QString, QString > m_params
const QRegularExpression m_apiPathPattern
flag icons free of to any person obtaining a copy of this software and associated documentation files(the "Software")
const char CONTENT_TYPE_JSON[]
Definition: types.h:68
const char CONTENT_TYPE_TXT[]
Definition: types.h:66
action
Definition: tstool.py:143

References AccessDenied, tstool::action, BadData, BadParams, Conflict, Http::CONTENT_TYPE_JSON, Http::CONTENT_TYPE_TXT, files(), isPublicAPI(), m_apiControllers, m_apiPathPattern, m_params, Exception::message(), NotFound, Http::ResponseBuilder::print(), request(), APIController::run(), sendWebUIFile(), session(), and APIError::type().

Referenced by processRequest().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ env()

const Http::Environment & WebApplication::env ( ) const

Definition at line 255 of file webapplication.cpp.

256 {
257  return m_env;
258 }
Http::Environment m_env

References m_env.

Referenced by processRequest().

Here is the caller graph for this function:

◆ generateSid()

QString WebApplication::generateSid ( ) const
private

Definition at line 573 of file webapplication.cpp.

574 {
575  QString sid;
576 
577  do
578  {
579  const quint32 tmp[] =
582  sid = QByteArray::fromRawData(reinterpret_cast<const char *>(tmp), sizeof(tmp)).toBase64();
583  }
584  while (m_sessions.contains(sid));
585 
586  return sid;
587 }

References m_sessions, and Utils::Random::rand().

Referenced by sessionStart().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ isAuthNeeded()

bool WebApplication::isAuthNeeded ( )
private

Definition at line 589 of file webapplication.cpp.

590 {
592  return false;
594  return false;
595  return true;
596 }
bool isLoopbackAddress(const QHostAddress &addr)
Definition: net.cpp:64
bool isIPInRange(const QHostAddress &addr, const QVector< Subnet > &subnets)
Definition: net.cpp:71

References Utils::Net::isIPInRange(), Utils::Net::isLoopbackAddress(), m_authSubnetWhitelist, m_clientAddress, m_isAuthSubnetWhitelistEnabled, and m_isLocalAuthEnabled.

Referenced by sessionInitialize().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ isCrossSiteRequest()

bool WebApplication::isCrossSiteRequest ( const Http::Request request) const
private

Definition at line 646 of file webapplication.cpp.

647 {
648  // https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Verifying_Same_Origin_with_Standard_Headers
649 
650  const auto isSameOrigin = [](const QUrl &left, const QUrl &right) -> bool
651  {
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()));
656  };
657 
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);
661 
662  if (originValue.isEmpty() && refererValue.isEmpty())
663  {
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;
667  }
668 
669  // sent with CORS requests, as well as with POST requests
670  if (!originValue.isEmpty())
671  {
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;
678  }
679 
680  if (!refererValue.isEmpty())
681  {
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;
688  }
689 
690  return true;
691 }
const char HEADER_ORIGIN[]
Definition: types.h:50
const char HEADER_REFERER[]
Definition: types.h:51
const char HEADER_X_FORWARDED_HOST[]
Definition: types.h:56
const char HEADER_HOST[]
Definition: types.h:49
QUrl urlFromHostHeader(const QString &hostHeader)
QHostAddress clientAddress
Definition: types.h:82
HeaderMap headers
Definition: types.h:106

References Http::Environment::clientAddress, Http::HEADER_HOST, Http::HEADER_ORIGIN, Http::HEADER_REFERER, Http::HEADER_X_FORWARDED_HOST, Http::Request::headers, LogMsg(), m_env, request(), anonymous_namespace{webapplication.cpp}::urlFromHostHeader(), and Log::WARNING.

Referenced by processRequest().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ isPublicAPI()

bool WebApplication::isPublicAPI ( const QString &  scope,
const QString &  action 
) const
private

Definition at line 598 of file webapplication.cpp.

599 {
600  return m_publicAPIs.contains(QString::fromLatin1("%1/%2").arg(scope, action));
601 }

References tstool::action, and m_publicAPIs.

Referenced by doProcessRequest().

Here is the caller graph for this function:

◆ processRequest()

Http::Response WebApplication::processRequest ( const Http::Request request,
const Http::Environment env 
)
overridevirtual

Implements Http::IRequestHandler.

Definition at line 487 of file webapplication.cpp.

488 {
489  m_currentSession = nullptr;
490  m_request = request;
491  m_env = env;
492  m_params.clear();
493 
495  {
496  for (auto iter = m_request.query.cbegin(); iter != m_request.query.cend(); ++iter)
497  m_params[iter.key()] = QString::fromUtf8(iter.value());
498  }
499  else
500  {
502  }
503 
504  // clear response
505  clear();
506 
507  try
508  {
509  // block suspicious requests
512  {
513  throw UnauthorizedHTTPError();
514  }
515 
516  // reverse proxy resolve client address
518 
521  }
522  catch (const HTTPError &error)
523  {
524  status(error.statusCode(), error.statusText());
525  print((!error.message().isEmpty() ? error.message() : error.statusText()), Http::CONTENT_TYPE_TXT);
526  }
527 
528  for (const Http::Header &prebuiltHeader : asConst(m_prebuiltHeaders))
529  setHeader(prebuiltHeader);
530 
531  return response();
532 }
QString statusText() const
Definition: httperror.cpp:43
int statusCode() const
Definition: httperror.cpp:38
Response response() const
void setHeader(const Header &header)
void status(uint code=200, const QString &text=QLatin1String("OK"))
const Http::Environment & env() const
bool isCrossSiteRequest(const Http::Request &request) const
WebSession * m_currentSession
bool validateHostHeader(const QStringList &domains) const
QHostAddress resolveClientAddress() const
Http::Request m_request
constexpr std::add_const_t< T > & asConst(T &t) noexcept
Definition: global.h:42
const char METHOD_GET[]
Definition: types.h:38
QHash< QString, QString > posts
Definition: types.h:108
QHash< QString, QByteArray > query
Definition: types.h:107
QString method
Definition: types.h:104

References asConst(), Http::ResponseBuilder::clear(), Http::CONTENT_TYPE_TXT, doProcessRequest(), env(), isCrossSiteRequest(), m_clientAddress, m_currentSession, m_domainList, m_env, m_isCSRFProtectionEnabled, m_isHostHeaderValidationEnabled, m_params, m_prebuiltHeaders, m_request, Exception::message(), Http::Request::method, Http::METHOD_GET, Http::Request::posts, Http::ResponseBuilder::print(), Http::Request::query, request(), resolveClientAddress(), Http::ResponseBuilder::response(), sessionInitialize(), Http::ResponseBuilder::setHeader(), Http::ResponseBuilder::status(), HTTPError::statusCode(), HTTPError::statusText(), and validateHostHeader().

Here is the call graph for this function:

◆ registerAPIController()

void WebApplication::registerAPIController ( const QString &  scope,
APIController controller 
)
private

Definition at line 427 of file webapplication.cpp.

428 {
429  Q_ASSERT(controller);
430  Q_ASSERT(!m_apiControllers.value(scope));
431 
432  m_apiControllers[scope] = controller;
433 }

References m_apiControllers.

Referenced by WebApplication().

Here is the caller graph for this function:

◆ request()

const Http::Request & WebApplication::request ( ) const

Definition at line 250 of file webapplication.cpp.

251 {
252  return m_request;
253 }

References m_request.

Referenced by doProcessRequest(), isCrossSiteRequest(), processRequest(), and sendWebUIFile().

Here is the caller graph for this function:

◆ resolveClientAddress()

QHostAddress WebApplication::resolveClientAddress ( ) const
private

Definition at line 729 of file webapplication.cpp.

730 {
732  return m_env.clientAddress;
733 
734  // Only reverse proxy can overwrite client address
736  return m_env.clientAddress;
737 
738  const QString forwardedFor = m_request.headers.value(Http::HEADER_X_FORWARDED_FOR);
739 
740  if (!forwardedFor.isEmpty())
741  {
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);
744 
745  if (!remoteIpList.isEmpty())
746  {
747  QHostAddress clientAddress;
748 
749  for (const QString &remoteIp : remoteIpList)
750  {
751  if (clientAddress.setAddress(remoteIp) && clientAddress.isGlobal())
752  return clientAddress;
753  }
754 
755  if (clientAddress.setAddress(remoteIpList[0]))
756  return clientAddress;
757  }
758  }
759 
760  return m_env.clientAddress;
761 }
const char HEADER_X_FORWARDED_FOR[]
Definition: types.h:55

References Http::Environment::clientAddress, Http::HEADER_X_FORWARDED_FOR, Http::Request::headers, m_env, m_isReverseProxySupportEnabled, m_request, and m_trustedReverseProxyList.

Referenced by processRequest().

Here is the caller graph for this function:

◆ sendFile()

void WebApplication::sendFile ( const QString &  path)
private

Definition at line 440 of file webapplication.cpp.

441 {
442  const QDateTime lastModified {QFileInfo(path).lastModified()};
443 
444  // find translated file in cache
445  const auto it = m_translatedFiles.constFind(path);
446  if ((it != m_translatedFiles.constEnd()) && (lastModified <= it->lastModified))
447  {
448  print(it->data, it->mimeType);
450  return;
451  }
452 
453  QFile file {path};
454  if (!file.open(QIODevice::ReadOnly))
455  {
456  qDebug("File %s was not found!", qUtf8Printable(path));
457  throw NotFoundHTTPError();
458  }
459 
460  if (file.size() > MAX_ALLOWED_FILESIZE)
461  {
462  qWarning("%s: exceeded the maximum allowed file size!", qUtf8Printable(path));
463  throw InternalServerErrorHTTPError(tr("Exceeded the maximum allowed file size (%1)!")
465  }
466 
467  QByteArray data {file.readAll()};
468  file.close();
469 
470  const QMimeType mimeType {QMimeDatabase().mimeTypeForFileNameAndData(path, data)};
471  const bool isTranslatable {mimeType.inherits(QLatin1String("text/plain"))};
472 
473  // Translate the file
474  if (isTranslatable)
475  {
476  QString dataStr {data};
477  translateDocument(dataStr);
478  data = dataStr.toUtf8();
479 
480  m_translatedFiles[path] = {data, mimeType.name(), lastModified}; // caching translated file
481  }
482 
483  print(data, mimeType.name());
485 }
void translateDocument(QString &data) const
const char HEADER_CACHE_CONTROL[]
Definition: types.h:41
QString friendlyUnit(qint64 bytes, bool isSpeed=false)
Definition: misc.cpp:261
QString getCachingInterval(QString contentType)
file(GLOB QBT_TS_FILES "${qBittorrent_SOURCE_DIR}/src/lang/*.ts") set_source_files_properties($
Definition: CMakeLists.txt:5
const int MAX_ALLOWED_FILESIZE

References file(), Utils::Misc::friendlyUnit(), anonymous_namespace{webapplication.cpp}::getCachingInterval(), Http::HEADER_CACHE_CONTROL, m_translatedFiles, MAX_ALLOWED_FILESIZE, Http::ResponseBuilder::print(), Http::ResponseBuilder::setHeader(), and translateDocument().

Referenced by sendWebUIFile().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ sendWebUIFile()

void WebApplication::sendWebUIFile ( )
private

Definition at line 144 of file webapplication.cpp.

145 {
146  const QStringList pathItems {request().path.split('/', Qt::SkipEmptyParts)};
147  if (pathItems.contains(".") || pathItems.contains(".."))
149 
150  if (!m_isAltUIUsed)
151  {
152  if (request().path.startsWith(PATH_PREFIX_ICONS))
153  {
154  const QString imageFilename {request().path.mid(PATH_PREFIX_ICONS.size())};
155  sendFile(QLatin1String(":/icons/") + imageFilename);
156  return;
157  }
158  }
159 
160  const QString path
161  {
162  (request().path != QLatin1String("/")
163  ? request().path
164  : QLatin1String("/index.html"))
165  };
166 
167  QString localPath
168  {
171  + path
172  };
173 
174  QFileInfo fileInfo {localPath};
175 
176  if (!fileInfo.exists() && session())
177  {
178  // try to send public file if there is no private one
179  localPath = m_rootFolder + PUBLIC_FOLDER + path;
180  fileInfo.setFile(localPath);
181  }
182 
183  if (m_isAltUIUsed)
184  {
185 #ifdef Q_OS_UNIX
186  if (!Utils::Fs::isRegularFile(localPath))
187  {
188  status(500, "Internal Server Error");
189  print(tr("Unacceptable file type, only regular file is allowed."), Http::CONTENT_TYPE_TXT);
190  return;
191  }
192 #endif
193 
194  while (fileInfo.filePath() != m_rootFolder)
195  {
196  if (fileInfo.isSymLink())
197  throw InternalServerErrorHTTPError(tr("Symlinks inside alternative UI folder are forbidden."));
198 
199  fileInfo.setFile(fileInfo.path());
200  }
201  }
202 
203  sendFile(localPath);
204 }
void sendFile(const QString &path)
bool isRegularFile(const QString &path)
Definition: fs.cpp:321
QString path
Definition: types.h:105
const QString PATH_PREFIX_ICONS
const QString PRIVATE_FOLDER
const QString PUBLIC_FOLDER

References Http::CONTENT_TYPE_TXT, Utils::Fs::isRegularFile(), m_isAltUIUsed, m_rootFolder, Http::Request::path, PATH_PREFIX_ICONS, Http::ResponseBuilder::print(), PRIVATE_FOLDER, PUBLIC_FOLDER, request(), sendFile(), session(), and Http::ResponseBuilder::status().

Referenced by doProcessRequest().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ session()

WebSession * WebApplication::session ( )
overridevirtual

Implements ISessionManager.

Definition at line 245 of file webapplication.cpp.

246 {
247  return m_currentSession;
248 }

References m_currentSession.

Referenced by doProcessRequest(), sendWebUIFile(), and sessionStart().

Here is the caller graph for this function:

◆ sessionEnd()

void WebApplication::sessionEnd ( )
overridevirtual

Implements ISessionManager.

Definition at line 632 of file webapplication.cpp.

633 {
634  Q_ASSERT(m_currentSession);
635 
636  QNetworkCookie cookie(C_SID);
637  cookie.setPath(QLatin1String("/"));
638  cookie.setExpirationDate(QDateTime::currentDateTime().addDays(-1));
639 
640  delete m_sessions.take(m_currentSession->id());
641  m_currentSession = nullptr;
642 
643  setHeader({Http::HEADER_SET_COOKIE, cookie.toRawForm()});
644 }
QString id() const override
const char HEADER_SET_COOKIE[]
Definition: types.h:53
const char C_SID[]

References C_SID, Http::HEADER_SET_COOKIE, WebSession::id(), m_currentSession, m_sessions, and Http::ResponseBuilder::setHeader().

Here is the call graph for this function:

◆ sessionInitialize()

void WebApplication::sessionInitialize ( )
private

Definition at line 539 of file webapplication.cpp.

540 {
541  Q_ASSERT(!m_currentSession);
542 
543  const QString sessionId {parseCookie(m_request.headers.value(QLatin1String("cookie"))).value(C_SID)};
544 
545  // TODO: Additional session check
546 
547  if (!sessionId.isEmpty())
548  {
549  m_currentSession = m_sessions.value(sessionId);
550  if (m_currentSession)
551  {
553  {
554  // session is outdated - removing it
555  delete m_sessions.take(sessionId);
556  m_currentSession = nullptr;
557  }
558  else
559  {
561  }
562  }
563  else
564  {
565  qDebug() << Q_FUNC_INFO << "session does not exist!";
566  }
567  }
568 
569  if (!m_currentSession && !isAuthNeeded())
570  sessionStart();
571 }
void sessionStart() override
bool hasExpired(qint64 seconds) const
void updateTimestamp()
QStringMap parseCookie(const QStringView cookieStr)

References C_SID, WebSession::hasExpired(), Http::Request::headers, isAuthNeeded(), m_currentSession, m_request, m_sessions, m_sessionTimeout, anonymous_namespace{webapplication.cpp}::parseCookie(), sessionStart(), and WebSession::updateTimestamp().

Referenced by processRequest().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ sessionStart()

void WebApplication::sessionStart ( )
overridevirtual

Implements ISessionManager.

Definition at line 603 of file webapplication.cpp.

604 {
605  Q_ASSERT(!m_currentSession);
606 
607  // remove outdated sessions
608  Algorithm::removeIf(m_sessions, [this](const QString &, const WebSession *session)
609  {
611  {
612  delete session;
613  return true;
614  }
615 
616  return false;
617  });
618 
621 
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();
628  cookieRawForm.append("; SameSite=Strict");
629  setHeader({Http::HEADER_SET_COOKIE, cookieRawForm});
630 }
QString generateSid() const
void removeIf(T &dict, BinaryPredicate &&p)
Definition: algorithm.h:50

References C_SID, generateSid(), WebSession::hasExpired(), Http::HEADER_SET_COOKIE, WebSession::id(), m_currentSession, m_isCSRFProtectionEnabled, m_isHttpsEnabled, m_isSecureCookieEnabled, m_sessions, m_sessionTimeout, Algorithm::removeIf(), session(), and Http::ResponseBuilder::setHeader().

Referenced by sessionInitialize().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ translateDocument()

void WebApplication::translateDocument ( QString &  data) const
private

Definition at line 206 of file webapplication.cpp.

207 {
208  const QRegularExpression regex("QBT_TR\\((([^\\)]|\\)(?!QBT_TR))+)\\)QBT_TR\\[CONTEXT=([a-zA-Z_][a-zA-Z0-9_]*)\\]");
209 
210  int i = 0;
211  bool found = true;
212  while (i < data.size() && found)
213  {
214  QRegularExpressionMatch regexMatch;
215  i = data.indexOf(regex, i, &regexMatch);
216  if (i >= 0)
217  {
218  const QString sourceText = regexMatch.captured(1);
219  const QString context = regexMatch.captured(3);
220 
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;
227 
228  // Use HTML code for quotes to prevent issues with JS
229  translation.replace('\'', "&#39;");
230  translation.replace('\"', "&#34;");
231 
232  data.replace(i, regexMatch.capturedLength(), translation);
233  i += translation.length();
234  }
235  else
236  {
237  found = false; // no more translatable strings
238  }
239 
240  data.replace(QLatin1String("${LANG}"), m_currentLocale.left(2));
241  data.replace(QLatin1String("${CACHEID}"), m_cacheID);
242  }
243 }

References m_cacheID, m_currentLocale, m_translationFileLoaded, and m_translator.

Referenced by sendFile().

Here is the caller graph for this function:

◆ validateHostHeader()

bool WebApplication::validateHostHeader ( const QStringList &  domains) const
private

Definition at line 693 of file webapplication.cpp.

694 {
695  const QUrl hostHeader = urlFromHostHeader(m_request.headers[Http::HEADER_HOST]);
696  const QString requestHost = hostHeader.host();
697 
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))
701  {
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)
705  , Log::WARNING);
706  return false;
707  }
708 
709  // try matching host header with local address
710  const bool sameAddr = m_env.localAddress.isEqual(QHostAddress(requestHost));
711 
712  if (sameAddr)
713  return true;
714 
715  // try matching host header with domain list
716  for (const auto &domain : domains)
717  {
718  const QRegularExpression domainRegex {Utils::String::wildcardToRegexPattern(domain), QRegularExpression::CaseInsensitiveOption};
719  if (requestHost.contains(domainRegex))
720  return true;
721  }
722 
723  LogMsg(tr("WebUI: Invalid Host header. Request source IP: '%1'. Received Host header: '%2'")
725  , Log::WARNING);
726  return false;
727 }
QString wildcardToRegexPattern(const QString &pattern)
Definition: string.cpp:57
QHostAddress localAddress
Definition: types.h:79
quint16 localPort
Definition: types.h:80

References Http::Environment::clientAddress, Http::HEADER_HOST, Http::Request::headers, Http::Environment::localAddress, Http::Environment::localPort, LogMsg(), m_env, m_request, anonymous_namespace{webapplication.cpp}::urlFromHostHeader(), Log::WARNING, and Utils::String::wildcardToRegexPattern().

Referenced by processRequest().

Here is the call graph for this function:
Here is the caller graph for this function:

Member Data Documentation

◆ m_apiControllers

QHash<QString, APIController *> WebApplication::m_apiControllers
private

Definition at line 131 of file webapplication.h.

Referenced by doProcessRequest(), and registerAPIController().

◆ m_apiPathPattern

const QRegularExpression WebApplication::m_apiPathPattern {QLatin1String("^/api/v2/(?<scope>[A-Za-z_][A-Za-z_0-9]*)/(?<action>[A-Za-z_][A-Za-z_0-9]*)$")}
private

Definition at line 129 of file webapplication.h.

Referenced by doProcessRequest().

◆ m_authSubnetWhitelist

QVector<Utils::Net::Subnet> WebApplication::m_authSubnetWhitelist
private

Definition at line 149 of file webapplication.h.

Referenced by configure(), and isAuthNeeded().

◆ m_cacheID

const QString WebApplication::m_cacheID
private

Definition at line 127 of file webapplication.h.

Referenced by translateDocument().

◆ m_clientAddress

QHostAddress WebApplication::m_clientAddress
private

Definition at line 162 of file webapplication.h.

Referenced by clientId(), isAuthNeeded(), and processRequest().

◆ m_currentLocale

QString WebApplication::m_currentLocale
private

Definition at line 143 of file webapplication.h.

Referenced by configure(), and translateDocument().

◆ m_currentSession

WebSession* WebApplication::m_currentSession = nullptr
private

◆ m_domainList

QStringList WebApplication::m_domainList
private

Definition at line 153 of file webapplication.h.

Referenced by configure(), and processRequest().

◆ m_env

Http::Environment WebApplication::m_env
private

◆ m_isAltUIUsed

bool WebApplication::m_isAltUIUsed = false
private

Definition at line 133 of file webapplication.h.

Referenced by configure(), and sendWebUIFile().

◆ m_isAuthSubnetWhitelistEnabled

bool WebApplication::m_isAuthSubnetWhitelistEnabled
private

Definition at line 148 of file webapplication.h.

Referenced by configure(), and isAuthNeeded().

◆ m_isCSRFProtectionEnabled

bool WebApplication::m_isCSRFProtectionEnabled
private

Definition at line 154 of file webapplication.h.

Referenced by configure(), processRequest(), and sessionStart().

◆ m_isHostHeaderValidationEnabled

bool WebApplication::m_isHostHeaderValidationEnabled
private

Definition at line 156 of file webapplication.h.

Referenced by configure(), and processRequest().

◆ m_isHttpsEnabled

bool WebApplication::m_isHttpsEnabled
private

Definition at line 157 of file webapplication.h.

Referenced by configure(), and sessionStart().

◆ m_isLocalAuthEnabled

bool WebApplication::m_isLocalAuthEnabled
private

Definition at line 147 of file webapplication.h.

Referenced by configure(), and isAuthNeeded().

◆ m_isReverseProxySupportEnabled

bool WebApplication::m_isReverseProxySupportEnabled
private

Definition at line 160 of file webapplication.h.

Referenced by resolveClientAddress().

◆ m_isSecureCookieEnabled

bool WebApplication::m_isSecureCookieEnabled
private

Definition at line 155 of file webapplication.h.

Referenced by configure(), and sessionStart().

◆ m_params

QHash<QString, QString> WebApplication::m_params
private

Definition at line 126 of file webapplication.h.

Referenced by doProcessRequest(), and processRequest().

◆ m_prebuiltHeaders

QVector<Http::Header> WebApplication::m_prebuiltHeaders
private

Definition at line 164 of file webapplication.h.

Referenced by configure(), and processRequest().

◆ m_publicAPIs

QSet<QString> WebApplication::m_publicAPIs
private

Definition at line 132 of file webapplication.h.

Referenced by declarePublicAPI(), and isPublicAPI().

◆ m_request

Http::Request WebApplication::m_request
private

◆ m_rootFolder

QString WebApplication::m_rootFolder
private

Definition at line 134 of file webapplication.h.

Referenced by configure(), and sendWebUIFile().

◆ m_sessions

QHash<QString, WebSession *> WebApplication::m_sessions
private

◆ m_sessionTimeout

int WebApplication::m_sessionTimeout
private

Definition at line 150 of file webapplication.h.

Referenced by configure(), sessionInitialize(), and sessionStart().

◆ m_translatedFiles

QHash<QString, TranslatedFile> WebApplication::m_translatedFiles
private

Definition at line 142 of file webapplication.h.

Referenced by configure(), and sendFile().

◆ m_translationFileLoaded

bool WebApplication::m_translationFileLoaded = false
private

Definition at line 145 of file webapplication.h.

Referenced by configure(), and translateDocument().

◆ m_translator

QTranslator WebApplication::m_translator
private

Definition at line 144 of file webapplication.h.

Referenced by configure(), and translateDocument().

◆ m_trustedReverseProxyList

QVector<QHostAddress> WebApplication::m_trustedReverseProxyList
private

Definition at line 161 of file webapplication.h.

Referenced by resolveClientAddress().


The documentation for this class was generated from the following files: