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"
37 #include <QJsonDocument>
38 #include <QMimeDatabase>
40 #include <QNetworkCookie>
41 #include <QRegularExpression>
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")};
75 QStringMap
parseCookie(const QStringView cookieStr
)
77 // [rfc6265] 4.2.1. Syntax
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();
88 const QString value
= Utils::String::unquote(cookie
.mid(idx
+ 1).trimmed()).toString();
89 ret
.insert(name
, value
);
94 QUrl
urlFromHostHeader(const QString
&hostHeader
)
96 if (!hostHeader
.contains(QLatin1String("://")))
97 return {QLatin1String("http://") + 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
)
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"));
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();
152 if (request().path
.startsWith(PATH_PREFIX_ICONS
))
154 const QString imageFilename
{request().path
.mid(PATH_PREFIX_ICONS
.size())};
155 sendFile(QLatin1String(":/icons/") + imageFilename
);
162 (request().path
!= QLatin1String("/")
164 : QLatin1String("/index.html"))
170 + (session() ? PRIVATE_FOLDER
: PUBLIC_FOLDER
)
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
);
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
);
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());
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_]*)\\]");
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);
221 const QString loadedText
= m_translationFileLoaded
222 ? m_translator
.translate(context
.toUtf8().constData(), sourceText
.toUtf8().constData())
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('\'', "'");
230 translation
.replace('\"', """);
232 data
.replace(i
, regexMatch
.capturedLength(), translation
);
233 i
+= translation
.length();
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
255 const Http::Environment
&WebApplication::env() const
260 void WebApplication::doProcessRequest()
262 const QRegularExpressionMatch match
= m_apiPathPattern
.match(request().path
);
263 if (!match
.hasMatch())
269 const QString action
= match
.captured(QLatin1String("action"));
270 const QString scope
= match
.captured(QLatin1String("scope"));
272 APIController
*controller
= m_apiControllers
.value(scope
);
274 throw NotFoundHTTPError();
276 if (!session() && !isPublicAPI(scope
, action
))
277 throw ForbiddenHTTPError();
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
);
291 case QMetaType::QString
:
293 print(result
.toString(), Http::CONTENT_TYPE_TXT
);
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());
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();
331 LogMsg(tr("Using built-in Web UI."));
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.")
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")});
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
=
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(':');
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
);
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;
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
)});
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()};
470 const QMimeType mimeType
{QMimeDatabase().mimeTypeForFileNameAndData(path
, data
)};
471 const bool isTranslatable
{mimeType
.inherits(QLatin1String("text/plain"))};
473 // Translate the file
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;
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());
501 m_params
= m_request
.posts
;
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();
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
);
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;
560 m_currentSession
->updateTimestamp();
565 qDebug() << Q_FUNC_INFO
<< "session does not exist!";
569 if (!m_currentSession
&& !isAuthNeeded())
573 QString
WebApplication::generateSid() const
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
));
589 bool WebApplication::isAuthNeeded()
591 if (!m_isLocalAuthEnabled
&& Utils::Net::isLoopbackAddress(m_clientAddress
))
593 if (m_isAuthSubnetWhitelistEnabled
&& Utils::Net::isIPInRange(m_clientAddress
, m_authSubnetWhitelist
))
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
))
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
669 // sent with CORS requests, as well as with POST requests
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'")
675 .arg(m_env
.clientAddress
.toString(), originValue
, targetOrigin
)
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'")
685 .arg(m_env
.clientAddress
.toString(), refererValue
, targetOrigin
)
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
])
709 // try matching host header with local address
710 const bool sameAddr
= m_env
.localAddress
.isEqual(QHostAddress(requestHost
));
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
))
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
])
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
;
765 WebSession::WebSession(const QString
&sid
)
771 QString
WebSession::id() const
776 bool WebSession::hasExpired(const qint64 seconds
) const
780 return m_timer
.hasExpired(seconds
* 1000);
783 void WebSession::updateTimestamp()
788 QVariant
WebSession::getData(const QString
&id
) const
790 return m_data
.value(id
);
793 void WebSession::setData(const QString
&id
, const QVariant
&data
)