Add HTTP strict tranport security support to QNAM
HTTP Strict Transport Security (HSTS) is a web security policy that allows a web server to declare that user agents should only interact with it using secure HTTPS connections. HSTS is described by RFC6797. This patch introduces a new API in Network Access Manager to enable this policy or disable it (default - STS is disabled). We also implement QHstsCache which caches known HTTS hosts, does host name lookup and domain name matching; QHstsHeaderParser to parse HSTS headers with HSTS policies. A new autotest added to test the caching, host name matching and headers parsing. [ChangeLog][QtNetwork] Added HTTP Strict Transport Security to QNAM Task-number: QTPM-238 Change-Id: Iabb5920344bf204a0d3036284f0d60675c29315c Reviewed-by: Timur Pocheptsov <timur.pocheptsov@qt.io>
This commit is contained in:
parent
da0241a2e7
commit
83f4f9b401
@ -39,7 +39,8 @@ HEADERS += \
|
||||
access/qhttpmultipart.h \
|
||||
access/qhttpmultipart_p.h \
|
||||
access/qnetworkfile_p.h \
|
||||
access/qhttp2protocolhandler_p.h
|
||||
access/qhttp2protocolhandler_p.h \
|
||||
access/qhsts_p.h
|
||||
|
||||
SOURCES += \
|
||||
access/qftp.cpp \
|
||||
@ -72,7 +73,8 @@ SOURCES += \
|
||||
access/qhttpthreaddelegate.cpp \
|
||||
access/qhttpmultipart.cpp \
|
||||
access/qnetworkfile.cpp \
|
||||
access/qhttp2protocolhandler.cpp
|
||||
access/qhttp2protocolhandler.cpp \
|
||||
access/qhsts.cpp
|
||||
|
||||
mac: LIBS_PRIVATE += -framework Security
|
||||
|
||||
|
522
src/network/access/qhsts.cpp
Normal file
522
src/network/access/qhsts.cpp
Normal file
@ -0,0 +1,522 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2017 The Qt Company Ltd.
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of the QtNetwork module of the Qt Toolkit.
|
||||
**
|
||||
** $QT_BEGIN_LICENSE:LGPL$
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** GNU Lesser General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU Lesser
|
||||
** General Public License version 3 as published by the Free Software
|
||||
** Foundation and appearing in the file LICENSE.LGPL3 included in the
|
||||
** packaging of this file. Please review the following information to
|
||||
** ensure the GNU Lesser General Public License version 3 requirements
|
||||
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
|
||||
**
|
||||
** GNU General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU
|
||||
** General Public License version 2.0 or (at your option) the GNU General
|
||||
** Public license version 3 or any later version approved by the KDE Free
|
||||
** Qt Foundation. The licenses are as published by the Free Software
|
||||
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
|
||||
** included in the packaging of this file. Please review the following
|
||||
** information to ensure the GNU General Public License requirements will
|
||||
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
|
||||
** https://www.gnu.org/licenses/gpl-3.0.html.
|
||||
**
|
||||
** $QT_END_LICENSE$
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#include "qhsts_p.h"
|
||||
|
||||
#include "QtCore/qstringlist.h"
|
||||
|
||||
#include "QtCore/private/qipaddress_p.h"
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
static bool expired_policy(const QDateTime &expires)
|
||||
{
|
||||
return !expires.isValid() || expires <= QDateTime::currentDateTimeUtc();
|
||||
}
|
||||
|
||||
static bool has_valid_domain_name(const QUrl &url)
|
||||
{
|
||||
if (!url.isValid())
|
||||
return false;
|
||||
|
||||
const QString host(url.host());
|
||||
if (!host.size())
|
||||
return false;
|
||||
|
||||
// RFC6797 8.1.1
|
||||
// If the substring matching the host production from the Request-URI
|
||||
// (of the message to which the host responded) syntactically matches
|
||||
//the IP-literal or IPv4address productions from Section 3.2.2 of
|
||||
//[RFC3986], then the UA MUST NOT note this host as a Known HSTS Host.
|
||||
using namespace QIPAddressUtils;
|
||||
|
||||
IPv4Address ipv4Addr = {};
|
||||
if (parseIp4(ipv4Addr, host.constBegin(), host.constEnd()))
|
||||
return false;
|
||||
|
||||
IPv6Address ipv6Addr = {};
|
||||
// Unlike parseIp4, parseIp6 returns nullptr if it managed to parse IPv6
|
||||
// address successfully.
|
||||
if (!parseIp6(ipv6Addr, host.constBegin(), host.constEnd()))
|
||||
return false;
|
||||
|
||||
// TODO: for now we do not test IPvFuture address, it must be addressed
|
||||
// by introducing parseIpFuture (actually, there is an implementation
|
||||
// in QUrl that can be adopted/modified/moved to QIPAddressUtils).
|
||||
return true;
|
||||
}
|
||||
|
||||
QHstsCache::QHstsCache()
|
||||
{
|
||||
// Top-level domain without any label.
|
||||
children.push_back(Domain());
|
||||
}
|
||||
|
||||
void QHstsCache::updateFromHeaders(const QList<QPair<QByteArray, QByteArray>> &headers,
|
||||
const QUrl &url)
|
||||
{
|
||||
if (!has_valid_domain_name(url))
|
||||
return;
|
||||
|
||||
QHstsHeaderParser parser;
|
||||
if (parser.parse(headers))
|
||||
updateKnownHost(url, parser.expirationDate(), parser.includeSubDomains());
|
||||
}
|
||||
|
||||
void QHstsCache::updateKnownHost(const QUrl &originalUrl, const QDateTime &expires,
|
||||
bool includeSubDomains)
|
||||
{
|
||||
if (!has_valid_domain_name(originalUrl))
|
||||
return;
|
||||
|
||||
// HSTS is a per-host policy, regardless of protocol, port or any of the other
|
||||
// details in an URL; so we only want the host part. We still package this as
|
||||
// a QUrl since this handles IDNA 2003 (RFC3490) for us, as required by
|
||||
// HSTS (RFC6797, section 10).
|
||||
QUrl url;
|
||||
url.setHost(originalUrl.host());
|
||||
|
||||
// 1. Update our hosts:
|
||||
QStringList labels(url.host().split(QLatin1Char('.')));
|
||||
std::reverse(labels.begin(), labels.end());
|
||||
|
||||
size_type domainIndex = 0;
|
||||
for (int i = 0, e = labels.size(); i < e; ++i) {
|
||||
Q_ASSERT(domainIndex < children.size());
|
||||
auto &subDomains = children[domainIndex].labels;
|
||||
const auto &label = labels[i];
|
||||
auto pos = std::lower_bound(subDomains.begin(), subDomains.end(), label);
|
||||
if (pos == subDomains.end() || pos->label != label) {
|
||||
// A new, previously unknown host.
|
||||
if (expired_policy(expires)) {
|
||||
// Nothing to do at all - we did not know this host previously,
|
||||
// we do not have to - since its policy expired.
|
||||
return;
|
||||
}
|
||||
|
||||
pos = subDomains.insert(pos, label);
|
||||
domainIndex = children.size();
|
||||
pos->domainIndex = domainIndex;
|
||||
children.resize(children.size() + (e - i));
|
||||
|
||||
for (int j = i + 1; j < e; ++j) {
|
||||
auto &newDomain = children[domainIndex];
|
||||
newDomain.labels.push_back(labels[j]);
|
||||
newDomain.labels.back().domainIndex = ++domainIndex;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
domainIndex = pos->domainIndex;
|
||||
}
|
||||
|
||||
Q_ASSERT(domainIndex > 0 && domainIndex < children.size());
|
||||
children[domainIndex].setHostPolicy(expires, includeSubDomains);
|
||||
}
|
||||
|
||||
bool QHstsCache::isKnownHost(const QUrl &originalUrl) const
|
||||
{
|
||||
if (!has_valid_domain_name(originalUrl))
|
||||
return false;
|
||||
|
||||
QUrl url;
|
||||
url.setHost(originalUrl.host());
|
||||
|
||||
QStringList labels(url.host().split(QLatin1Char('.')));
|
||||
std::reverse(labels.begin(), labels.end());
|
||||
|
||||
Q_ASSERT(children.size());
|
||||
size_type domainIndex = 0;
|
||||
for (int i = 0, e = labels.size(); i < e; ++i) {
|
||||
Q_ASSERT(domainIndex < children.size());
|
||||
const auto &subDomains = children[domainIndex].labels;
|
||||
auto pos = std::lower_bound(subDomains.begin(), subDomains.end(), labels[i]);
|
||||
if (pos == subDomains.end() || pos->label != labels[i])
|
||||
return false;
|
||||
|
||||
Q_ASSERT(pos->domainIndex < children.size());
|
||||
domainIndex = pos->domainIndex;
|
||||
auto &domain = children[domainIndex];
|
||||
if (domain.validateHostPolicy() && (i + 1 == e || domain.includeSubDomains)) {
|
||||
/*
|
||||
RFC6797, 8.2. Known HSTS Host Domain Name Matching
|
||||
|
||||
* Superdomain Match
|
||||
If a label-for-label match between an entire Known HSTS Host's
|
||||
domain name and a right-hand portion of the given domain name
|
||||
is found, then this Known HSTS Host's domain name is a
|
||||
superdomain match for the given domain name. There could be
|
||||
multiple superdomain matches for a given domain name.
|
||||
* Congruent Match
|
||||
If a label-for-label match between a Known HSTS Host's domain
|
||||
name and the given domain name is found -- i.e., there are no
|
||||
further labels to compare -- then the given domain name
|
||||
congruently matches this Known HSTS Host.
|
||||
*/
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void QHstsCache::clear()
|
||||
{
|
||||
children.resize(1);
|
||||
children[0].labels.clear();
|
||||
// Top-level is never known:
|
||||
Q_ASSERT(!children[0].isKnownHost);
|
||||
}
|
||||
|
||||
// The parser is quite simple: 'nextToken' knowns exactly what kind of tokens
|
||||
// are valid and it will return false if something else was found; then
|
||||
// we immediately stop parsing. 'parseDirective' knows how these tokens can
|
||||
// be combined into a valid directive and if some weird combination of
|
||||
// valid tokens is found - we immediately stop.
|
||||
// And finally we call parseDirective again and again until some error found or
|
||||
// we have no more bytes in the header.
|
||||
|
||||
// The following isXXX functions are based on RFC2616, 2.2 Basic Rules.
|
||||
|
||||
static bool isCHAR(int c)
|
||||
{
|
||||
// CHAR = <any US-ASCII character (octets 0 - 127)>
|
||||
return c >= 0 && c <= 127;
|
||||
}
|
||||
|
||||
static bool isCTL(int c)
|
||||
{
|
||||
// CTL = <any US-ASCII control character
|
||||
// (octets 0 - 31) and DEL (127)>
|
||||
return (c >= 0 && c <= 31) || c == 127;
|
||||
}
|
||||
|
||||
|
||||
static bool isLWS(int c)
|
||||
{
|
||||
// LWS = [CRLF] 1*( SP | HT )
|
||||
//
|
||||
// CRLF = CR LF
|
||||
// CR = <US-ASCII CR, carriage return (13)>
|
||||
// LF = <US-ASCII LF, linefeed (10)>
|
||||
// SP = <US-ASCII SP, space (32)>
|
||||
// HT = <US-ASCII HT, horizontal-tab (9)>
|
||||
//
|
||||
// CRLF is handled by the time we parse a header (they were replaced with
|
||||
// spaces). We only have to deal with remaining SP|HT
|
||||
return c == ' ' || c == '\t';
|
||||
}
|
||||
|
||||
static bool isTEXT(char c)
|
||||
{
|
||||
// TEXT = <any OCTET except CTLs,
|
||||
// but including LWS>
|
||||
return !isCTL(c) || isLWS(c);
|
||||
}
|
||||
|
||||
static bool isSeparator(char c)
|
||||
{
|
||||
// separators = "(" | ")" | "<" | ">" | "@"
|
||||
// | "," | ";" | ":" | "\" | <">
|
||||
// | "/" | "[" | "]" | "?" | "="
|
||||
// | "{" | "}" | SP | HT
|
||||
static const char separators[] = "()<>@,;:\\\"/[]?={}";
|
||||
static const char *end = separators + sizeof separators - 1;
|
||||
return isLWS(c) || std::find(separators, end, c) != end;
|
||||
}
|
||||
|
||||
static QByteArray unescapeMaxAge(const QByteArray &value)
|
||||
{
|
||||
if (value.size() < 2 || value[0] != '"')
|
||||
return value;
|
||||
|
||||
Q_ASSERT(value[value.size() - 1] == '"');
|
||||
return value.mid(1, value.size() - 2);
|
||||
}
|
||||
|
||||
static bool isTOKEN(char c)
|
||||
{
|
||||
// token = 1*<any CHAR except CTLs or separators>
|
||||
return isCHAR(c) && !isCTL(c) && !isSeparator(c);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
RFC6797, 6.1 Strict-Transport-Security HTTP Response Header Field.
|
||||
Syntax:
|
||||
|
||||
Strict-Tranposrt-Security = "Strict-Transport-Security" ":"
|
||||
[ directive ] *( ";" [ directive ] )
|
||||
|
||||
directive = directive-name [ "=" directive-value ]
|
||||
directive-name = token
|
||||
directive-value = token | quoted-string
|
||||
|
||||
RFC 2616, 2.2 Basic Rules.
|
||||
|
||||
token = 1*<any CHAR except CTLs or separators>
|
||||
quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
|
||||
|
||||
|
||||
qdtext = <any TEXT except <">>
|
||||
quoted-pair = "\" CHAR
|
||||
|
||||
*/
|
||||
|
||||
bool QHstsHeaderParser::parse(const QList<QPair<QByteArray, QByteArray>> &headers)
|
||||
{
|
||||
for (const auto &h : headers) {
|
||||
// We use '==' since header name was already 'trimmed' for us:
|
||||
if (h.first == "Strict-Transport-Security") {
|
||||
header = h.second;
|
||||
// RFC6797, 8.1:
|
||||
//
|
||||
// The UA MUST ignore any STS header fields not conforming to the
|
||||
// grammar specified in Section 6.1 ("Strict-Transport-Security HTTP
|
||||
// Response Header Field").
|
||||
//
|
||||
// If a UA receives more than one STS header field in an HTTP
|
||||
// response message over secure transport, then the UA MUST process
|
||||
// only the first such header field.
|
||||
//
|
||||
// We read this as: ignore all invalid headers and take the first valid:
|
||||
if (parseSTSHeader() && maxAgeFound) {
|
||||
expiry = QDateTime::currentDateTimeUtc().addSecs(maxAge);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In case it was set by a syntactically correct header (but without
|
||||
// REQUIRED max-age directive):
|
||||
subDomainsFound = false;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool QHstsHeaderParser::parseSTSHeader()
|
||||
{
|
||||
expiry = QDateTime();
|
||||
maxAgeFound = false;
|
||||
subDomainsFound = false;
|
||||
maxAge = 0;
|
||||
tokenPos = 0;
|
||||
token.clear();
|
||||
|
||||
while (tokenPos < header.size()) {
|
||||
if (!parseDirective())
|
||||
return false;
|
||||
|
||||
if (token.size() && token != ";") {
|
||||
// After a directive we can only have a ";" or no more tokens.
|
||||
// Invalid syntax.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QHstsHeaderParser::parseDirective()
|
||||
{
|
||||
// RFC 6797, 6.1:
|
||||
//
|
||||
// directive = directive-name [ "=" directive-value ]
|
||||
// directive-name = token
|
||||
// directive-value = token | quoted-string
|
||||
|
||||
|
||||
// RFC 2616, 2.2:
|
||||
//
|
||||
// token = 1*<any CHAR except CTLs or separators>
|
||||
|
||||
if (!nextToken())
|
||||
return false;
|
||||
|
||||
if (!token.size()) // No more data, but no error.
|
||||
return true;
|
||||
|
||||
if (token == ";") // That's a weird grammar, but that's what it is.
|
||||
return true;
|
||||
|
||||
if (!isTOKEN(token[0])) // Not a valid directive-name.
|
||||
return false;
|
||||
|
||||
const QByteArray directiveName = token;
|
||||
// 2. Try to read "=" or ";".
|
||||
if (!nextToken())
|
||||
return false;
|
||||
|
||||
QByteArray directiveValue;
|
||||
if (token == ";") // No directive-value
|
||||
return processDirective(directiveName, directiveValue);
|
||||
|
||||
if (token == "=") {
|
||||
// We expect a directive-value now:
|
||||
if (!nextToken() || !token.size())
|
||||
return false;
|
||||
directiveValue = token;
|
||||
} else if (token.size()) {
|
||||
// Invalid syntax:
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!processDirective(directiveName, directiveValue))
|
||||
return false;
|
||||
|
||||
// Read either ";", or 'end of header', or some invalid token.
|
||||
return nextToken();
|
||||
}
|
||||
|
||||
bool QHstsHeaderParser::processDirective(const QByteArray &name, const QByteArray &value)
|
||||
{
|
||||
Q_ASSERT(name.size());
|
||||
// RFC6797 6.1/3 Directive names are case-insensitive
|
||||
const auto lcName = name.toLower();
|
||||
if (lcName == "max-age") {
|
||||
// RFC 6797, 6.1.1
|
||||
// The syntax of the max-age directive's REQUIRED value (after
|
||||
// quoted-string unescaping, if necessary) is defined as:
|
||||
//
|
||||
// max-age-value = delta-seconds
|
||||
if (maxAgeFound) {
|
||||
// RFC 6797, 6.1/2:
|
||||
// All directives MUST appear only once in an STS header field.
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray unescapedValue = unescapeMaxAge(value);
|
||||
if (!unescapedValue.size())
|
||||
return false;
|
||||
|
||||
bool ok = false;
|
||||
const qint64 age = unescapedValue.toLongLong(&ok);
|
||||
if (!ok || age < 0)
|
||||
return false;
|
||||
|
||||
maxAge = age;
|
||||
maxAgeFound = true;
|
||||
} else if (lcName == "includesubdomains") {
|
||||
// RFC 6797, 6.1.2. The includeSubDomains Directive.
|
||||
// The OPTIONAL "includeSubDomains" directive is a valueless directive.
|
||||
|
||||
if (subDomainsFound) {
|
||||
// RFC 6797, 6.1/2:
|
||||
// All directives MUST appear only once in an STS header field.
|
||||
return false;
|
||||
}
|
||||
|
||||
subDomainsFound = true;
|
||||
} // else we do nothing, skip unknown directives (RFC 6797, 6.1/5)
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QHstsHeaderParser::nextToken()
|
||||
{
|
||||
// Returns true if we found a valid token or we have no more data (token is
|
||||
// empty then).
|
||||
|
||||
token.clear();
|
||||
|
||||
// Fortunately enough, by this point qhttpnetworkreply already got rid of
|
||||
// [CRLF] parts, but we can have 1*(SP|HT) yet.
|
||||
while (tokenPos < header.size() && isLWS(header[tokenPos]))
|
||||
++tokenPos;
|
||||
|
||||
if (tokenPos == header.size())
|
||||
return true;
|
||||
|
||||
const char ch = header[tokenPos];
|
||||
if (ch == ';' || ch == '=') {
|
||||
token.append(ch);
|
||||
++tokenPos;
|
||||
return true;
|
||||
}
|
||||
|
||||
// RFC 2616, 2.2.
|
||||
//
|
||||
// quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
|
||||
// qdtext = <any TEXT except <">>
|
||||
if (ch == '"') {
|
||||
int last = tokenPos + 1;
|
||||
while (last < header.size()) {
|
||||
if (header[last] == '"') {
|
||||
// The end of a quoted-string.
|
||||
break;
|
||||
} else if (header[last] == '\\') {
|
||||
// quoted-pair = "\" CHAR
|
||||
if (last + 1 < header.size() && isCHAR(header[last + 1]))
|
||||
last += 2;
|
||||
else
|
||||
return false;
|
||||
} else {
|
||||
if (!isTEXT(header[last]))
|
||||
return false;
|
||||
++last;
|
||||
}
|
||||
}
|
||||
|
||||
if (last >= header.size()) // no closing '"':
|
||||
return false;
|
||||
|
||||
token = header.mid(tokenPos, last - tokenPos + 1);
|
||||
tokenPos = last + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
// RFC 2616, 2.2:
|
||||
//
|
||||
// token = 1*<any CHAR except CTLs or separators>
|
||||
if (!isTOKEN(ch))
|
||||
return false;
|
||||
|
||||
int last = tokenPos + 1;
|
||||
while (last < header.size() && isTOKEN(header[last]))
|
||||
++last;
|
||||
|
||||
token = header.mid(tokenPos, last - tokenPos);
|
||||
tokenPos = last;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QT_END_NAMESPACE
|
169
src/network/access/qhsts_p.h
Normal file
169
src/network/access/qhsts_p.h
Normal file
@ -0,0 +1,169 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2017 The Qt Company Ltd.
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of the QtNetwork module of the Qt Toolkit.
|
||||
**
|
||||
** $QT_BEGIN_LICENSE:LGPL$
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** GNU Lesser General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU Lesser
|
||||
** General Public License version 3 as published by the Free Software
|
||||
** Foundation and appearing in the file LICENSE.LGPL3 included in the
|
||||
** packaging of this file. Please review the following information to
|
||||
** ensure the GNU Lesser General Public License version 3 requirements
|
||||
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
|
||||
**
|
||||
** GNU General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU
|
||||
** General Public License version 2.0 or (at your option) the GNU General
|
||||
** Public license version 3 or any later version approved by the KDE Free
|
||||
** Qt Foundation. The licenses are as published by the Free Software
|
||||
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
|
||||
** included in the packaging of this file. Please review the following
|
||||
** information to ensure the GNU General Public License requirements will
|
||||
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
|
||||
** https://www.gnu.org/licenses/gpl-3.0.html.
|
||||
**
|
||||
** $QT_END_LICENSE$
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#ifndef QHSTS_P_H
|
||||
#define QHSTS_P_H
|
||||
|
||||
//
|
||||
// W A R N I N G
|
||||
// -------------
|
||||
//
|
||||
// This file is not part of the Qt API. It exists for the convenience
|
||||
// of the Network Access API. This header file may change from
|
||||
// version to version without notice, or even be removed.
|
||||
//
|
||||
// We mean it.
|
||||
//
|
||||
|
||||
#include <QtCore/qbytearray.h>
|
||||
#include <QtCore/qdatetime.h>
|
||||
#include <QtCore/qstring.h>
|
||||
#include <QtCore/qglobal.h>
|
||||
#include <QtCore/qvector.h>
|
||||
#include <QtCore/qlist.h>
|
||||
#include <QtCore/qpair.h>
|
||||
#include <QtCore/qurl.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
class Q_AUTOTEST_EXPORT QHstsCache
|
||||
{
|
||||
public:
|
||||
|
||||
QHstsCache();
|
||||
|
||||
void updateFromHeaders(const QList<QPair<QByteArray, QByteArray>> &headers,
|
||||
const QUrl &url);
|
||||
void updateKnownHost(const QUrl &url, const QDateTime &expires,
|
||||
bool includeSubDomains);
|
||||
bool isKnownHost(const QUrl &url) const;
|
||||
|
||||
void clear();
|
||||
|
||||
private:
|
||||
|
||||
using size_type = std::vector<int>::size_type;
|
||||
|
||||
struct DomainLabel
|
||||
{
|
||||
DomainLabel() = default;
|
||||
DomainLabel(const QString &name) : label(name) { }
|
||||
|
||||
bool operator < (const DomainLabel &rhs) const
|
||||
{ return label < rhs.label; }
|
||||
|
||||
QString label;
|
||||
size_type domainIndex;
|
||||
};
|
||||
|
||||
struct Domain
|
||||
{
|
||||
void setHostPolicy(const QDateTime &expiration, bool subs)
|
||||
{
|
||||
expirationTime = expiration;
|
||||
isKnownHost = expirationTime.isValid()
|
||||
&& expirationTime > QDateTime::currentDateTimeUtc();
|
||||
includeSubDomains = subs;
|
||||
}
|
||||
|
||||
bool validateHostPolicy()
|
||||
{
|
||||
if (!isKnownHost)
|
||||
return false;
|
||||
|
||||
if (expirationTime > QDateTime::currentDateTimeUtc())
|
||||
return true;
|
||||
|
||||
isKnownHost = false;
|
||||
includeSubDomains = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isKnownHost = false;
|
||||
bool includeSubDomains = false;
|
||||
QDateTime expirationTime;
|
||||
std::vector<DomainLabel> labels;
|
||||
};
|
||||
|
||||
/*
|
||||
Each Domain represents a DNS name or prefix thereof; each entry in its
|
||||
std::vector<DomainLabel> labels pairs the next fragment of a DNS name
|
||||
with the index into 'children' at which to find another Domain object.
|
||||
The root Domain, children[0], has top-level-domain DomainLabel entries,
|
||||
such as "com", "org" and "net"; the entry in 'children' at the index it
|
||||
pairs with "com" is the Domain entry for .com; if that has "example" in
|
||||
its labels, it'll be paired with the index of the entry in 'children'
|
||||
that represents example.com; from which, in turn, we can find the
|
||||
Domain object for www.example.com, and so on.
|
||||
*/
|
||||
mutable std::vector<Domain> children;
|
||||
};
|
||||
|
||||
class Q_AUTOTEST_EXPORT QHstsHeaderParser
|
||||
{
|
||||
public:
|
||||
|
||||
bool parse(const QList<QPair<QByteArray, QByteArray>> &headers);
|
||||
|
||||
QDateTime expirationDate() const { return expiry; }
|
||||
bool includeSubDomains() const { return subDomainsFound; }
|
||||
|
||||
private:
|
||||
|
||||
bool parseSTSHeader();
|
||||
bool parseDirective();
|
||||
bool processDirective(const QByteArray &name, const QByteArray &value);
|
||||
bool nextToken();
|
||||
|
||||
QByteArray header;
|
||||
QByteArray token;
|
||||
|
||||
QDateTime expiry;
|
||||
int tokenPos = 0;
|
||||
bool maxAgeFound = false;
|
||||
qint64 maxAge = 0;
|
||||
bool subDomainsFound = false;
|
||||
};
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#endif
|
@ -692,6 +692,55 @@ void QNetworkAccessManager::setCookieJar(QNetworkCookieJar *cookieJar)
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
\since 5.9
|
||||
|
||||
Enables HTTP Strict Transport Security (HSTS, RFC6797). When processing a
|
||||
request, QNetworkAccessManager automatically replaces "http" scheme with
|
||||
"https" and uses a secure transport if a host is a known HSTS host.
|
||||
Port 80 if it's set explicitly is replaced by port 443.
|
||||
|
||||
When HSTS is enabled, for each HTTP response containing HSTS header and
|
||||
received over a secure transport, QNetworkAccessManager will update its HSTS
|
||||
cache, either remembering a host with a valid policy or removing a host with
|
||||
expired/disabled HSTS policy.
|
||||
|
||||
\sa disableStrictTransportSecurity(), strictTransportSecurityEnabled()
|
||||
*/
|
||||
void QNetworkAccessManager::enableStrictTransportSecurity()
|
||||
{
|
||||
Q_D(QNetworkAccessManager);
|
||||
d->stsEnabled = true;
|
||||
}
|
||||
|
||||
/*!
|
||||
\since 5.9
|
||||
|
||||
Disables HTTP Strict Transport Security (HSTS). HSTS headers in responses would
|
||||
be ignored, no scheme/port mapping is done.
|
||||
|
||||
\sa enableStrictTransportSecurity()
|
||||
*/
|
||||
void QNetworkAccessManager::disableStrictTransportSecurity()
|
||||
{
|
||||
Q_D(QNetworkAccessManager);
|
||||
d->stsEnabled = false;
|
||||
}
|
||||
|
||||
/*!
|
||||
\since 5.9
|
||||
|
||||
Returns true if HTTP Strict Transport Security (HSTS) was enabled. By default
|
||||
HSTS is disabled.
|
||||
|
||||
\sa enableStrictTransportSecurity
|
||||
*/
|
||||
bool QNetworkAccessManager::strictTransportSecurityEnabled() const
|
||||
{
|
||||
Q_D(const QNetworkAccessManager);
|
||||
return d->stsEnabled;
|
||||
}
|
||||
|
||||
/*!
|
||||
Posts a request to obtain the network headers for \a request
|
||||
and returns a new QNetworkReply object which will contain such headers.
|
||||
@ -1299,6 +1348,24 @@ QNetworkReply *QNetworkAccessManager::createRequest(QNetworkAccessManager::Opera
|
||||
|| scheme == QLatin1String("https") || scheme == QLatin1String("preconnect-https")
|
||||
#endif
|
||||
) {
|
||||
#ifndef QT_NO_SSL
|
||||
if (strictTransportSecurityEnabled() && d->stsCache.isKnownHost(request.url())) {
|
||||
QUrl stsUrl(request.url());
|
||||
// RFC6797, 8.3:
|
||||
// The UA MUST replace the URI scheme with "https" [RFC2818],
|
||||
// and if the URI contains an explicit port component of "80",
|
||||
// then the UA MUST convert the port component to be "443", or
|
||||
// if the URI contains an explicit port component that is not
|
||||
// equal to "80", the port component value MUST be preserved;
|
||||
// otherwise,
|
||||
// if the URI does not contain an explicit port component, the UA
|
||||
// MUST NOT add one.
|
||||
if (stsUrl.port() == 80)
|
||||
stsUrl.setPort(443);
|
||||
stsUrl.setScheme(QLatin1String("https"));
|
||||
request.setUrl(stsUrl);
|
||||
}
|
||||
#endif
|
||||
QNetworkReplyHttpImpl *reply = new QNetworkReplyHttpImpl(this, request, op, outgoingData);
|
||||
#ifndef QT_NO_BEARERMANAGEMENT
|
||||
connect(this, SIGNAL(networkSessionConnected()),
|
||||
|
@ -120,6 +120,10 @@ public:
|
||||
QNetworkCookieJar *cookieJar() const;
|
||||
void setCookieJar(QNetworkCookieJar *cookieJar);
|
||||
|
||||
void enableStrictTransportSecurity();
|
||||
void disableStrictTransportSecurity();
|
||||
bool strictTransportSecurityEnabled() const;
|
||||
|
||||
QNetworkReply *head(const QNetworkRequest &request);
|
||||
QNetworkReply *get(const QNetworkRequest &request);
|
||||
QNetworkReply *post(const QNetworkRequest &request, QIODevice *data);
|
||||
|
@ -56,6 +56,7 @@
|
||||
#include "qnetworkaccesscache_p.h"
|
||||
#include "qnetworkaccessbackend_p.h"
|
||||
#include "qnetworkrequest.h"
|
||||
#include "qhsts_p.h"
|
||||
#include "private/qobject_p.h"
|
||||
#include "QtNetwork/qnetworkproxy.h"
|
||||
#include "QtNetwork/qnetworksession.h"
|
||||
@ -205,8 +206,13 @@ public:
|
||||
QNetworkAccessCache objectCache;
|
||||
static inline QNetworkAccessCache *getObjectCache(QNetworkAccessBackend *backend)
|
||||
{ return &backend->manager->objectCache; }
|
||||
|
||||
Q_AUTOTEST_EXPORT static void clearAuthenticationCache(QNetworkAccessManager *manager);
|
||||
Q_AUTOTEST_EXPORT static void clearConnectionCache(QNetworkAccessManager *manager);
|
||||
|
||||
QHstsCache stsCache;
|
||||
bool stsEnabled = false;
|
||||
|
||||
#ifndef QT_NO_BEARERMANAGEMENT
|
||||
Q_AUTOTEST_EXPORT static const QWeakPointer<const QNetworkSession> getNetworkSession(const QNetworkAccessManager *manager);
|
||||
#endif
|
||||
|
@ -732,7 +732,11 @@ void QNetworkReply::setSslConfiguration(const QSslConfiguration &config)
|
||||
You can clear the list of errors you want to ignore by calling this
|
||||
function with an empty list.
|
||||
|
||||
\sa sslConfiguration(), sslErrors(), QSslSocket::ignoreSslErrors()
|
||||
\note If HTTP Strict Transport Security is enabled for QNetworkAccessManager,
|
||||
this function has no effect.
|
||||
|
||||
\sa sslConfiguration(), sslErrors(), QSslSocket::ignoreSslErrors(),
|
||||
QNetworkAccessManager::enableStrictTransportSecurity()
|
||||
*/
|
||||
void QNetworkReply::ignoreSslErrors(const QList<QSslError> &errors)
|
||||
{
|
||||
@ -799,6 +803,9 @@ void QNetworkReply::ignoreSslErrorsImplementation(const QList<QSslError> &)
|
||||
sslErrors() signal, which indicates which errors were
|
||||
found.
|
||||
|
||||
\note If HTTP Strict Transport Security is enabled for QNetworkAccessManager,
|
||||
this function has no effect.
|
||||
|
||||
\sa sslConfiguration(), sslErrors(), QSslSocket::ignoreSslErrors()
|
||||
*/
|
||||
void QNetworkReply::ignoreSslErrors()
|
||||
|
@ -52,6 +52,7 @@
|
||||
#include "QtCore/qelapsedtimer.h"
|
||||
#include "QtNetwork/qsslconfiguration.h"
|
||||
#include "qhttpthreaddelegate_p.h"
|
||||
#include "qhsts_p.h"
|
||||
#include "qthread.h"
|
||||
#include "QtCore/qcoreapplication.h"
|
||||
|
||||
@ -384,6 +385,12 @@ void QNetworkReplyHttpImpl::ignoreSslErrors()
|
||||
{
|
||||
Q_D(QNetworkReplyHttpImpl);
|
||||
|
||||
if (d->managerPrivate && d->managerPrivate->stsEnabled
|
||||
&& d->managerPrivate->stsCache.isKnownHost(url())) {
|
||||
// We cannot ignore any Security Transport-related errors for this host.
|
||||
return;
|
||||
}
|
||||
|
||||
d->pendingIgnoreAllSslErrors = true;
|
||||
}
|
||||
|
||||
@ -391,6 +398,12 @@ void QNetworkReplyHttpImpl::ignoreSslErrorsImplementation(const QList<QSslError>
|
||||
{
|
||||
Q_D(QNetworkReplyHttpImpl);
|
||||
|
||||
if (d->managerPrivate && d->managerPrivate->stsEnabled
|
||||
&& d->managerPrivate->stsCache.isKnownHost(url())) {
|
||||
// We cannot ignore any Security Transport-related errors for this host.
|
||||
return;
|
||||
}
|
||||
|
||||
// the pending list is set if QNetworkReply::ignoreSslErrors(const QList<QSslError> &errors)
|
||||
// is called before QNetworkAccessManager::get() (or post(), etc.)
|
||||
d->pendingIgnoreSslErrorsList = errors;
|
||||
@ -1179,6 +1192,15 @@ void QNetworkReplyHttpImplPrivate::replyDownloadMetaData(const QList<QPair<QByte
|
||||
statusCode = sc;
|
||||
reasonPhrase = rp;
|
||||
|
||||
#ifndef QT_NO_SSL
|
||||
// We parse this header only if we're using secure transport:
|
||||
//
|
||||
// RFC6797, 8.1
|
||||
// If an HTTP response is received over insecure transport, the UA MUST
|
||||
// ignore any present STS header field(s).
|
||||
if (url.scheme() == QLatin1String("https") && managerPrivate->stsEnabled)
|
||||
managerPrivate->stsCache.updateFromHeaders(hm, url);
|
||||
#endif
|
||||
// Download buffer
|
||||
if (!db.isNull()) {
|
||||
downloadBufferPointer = db;
|
||||
|
@ -13,11 +13,13 @@ SUBDIRS=\
|
||||
qhttpnetworkreply \
|
||||
qabstractnetworkcache \
|
||||
hpack \
|
||||
http2
|
||||
http2 \
|
||||
hsts
|
||||
|
||||
!qtConfig(private_tests): SUBDIRS -= \
|
||||
qhttpnetworkconnection \
|
||||
qhttpnetworkreply \
|
||||
qftp \
|
||||
hpack \
|
||||
http2
|
||||
http2 \
|
||||
hsts
|
||||
|
6
tests/auto/network/access/hsts/hsts.pro
Normal file
6
tests/auto/network/access/hsts/hsts.pro
Normal file
@ -0,0 +1,6 @@
|
||||
QT += core core-private network network-private testlib
|
||||
CONFIG += testcase parallel_test c++11
|
||||
TEMPLATE = app
|
||||
TARGET = tst_qhsts
|
||||
|
||||
SOURCES += tst_qhsts.cpp
|
318
tests/auto/network/access/hsts/tst_qhsts.cpp
Normal file
318
tests/auto/network/access/hsts/tst_qhsts.cpp
Normal file
@ -0,0 +1,318 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2016 The Qt Company Ltd.
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of the test suite of the Qt Toolkit.
|
||||
**
|
||||
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** GNU General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU
|
||||
** General Public License version 3 as published by the Free Software
|
||||
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
||||
** included in the packaging of this file. Please review the following
|
||||
** information to ensure the GNU General Public License requirements will
|
||||
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
||||
**
|
||||
** $QT_END_LICENSE$
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#include <QtTest/QtTest>
|
||||
|
||||
#include <QtCore/qdatetime.h>
|
||||
#include <QtCore/qvector.h>
|
||||
#include <QtCore/qpair.h>
|
||||
#include <QtCore/qurl.h>
|
||||
|
||||
#include <QtNetwork/private/qhsts_p.h>
|
||||
|
||||
QT_USE_NAMESPACE
|
||||
|
||||
class tst_QHsts : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
private Q_SLOTS:
|
||||
void testSingleKnownHost_data();
|
||||
void testSingleKnownHost();
|
||||
void testMultilpeKnownHosts();
|
||||
void testPolicyExpiration();
|
||||
void testSTSHeaderParser();
|
||||
};
|
||||
|
||||
void tst_QHsts::testSingleKnownHost_data()
|
||||
{
|
||||
QTest::addColumn<QUrl>("knownHost");
|
||||
QTest::addColumn<QDateTime>("policyExpires");
|
||||
QTest::addColumn<bool>("includeSubDomains");
|
||||
QTest::addColumn<QUrl>("hostToTest");
|
||||
QTest::addColumn<bool>("isKnown");
|
||||
|
||||
const QDateTime currentUTC = QDateTime::currentDateTimeUtc();
|
||||
const QUrl knownHost(QLatin1String("http://example.com"));
|
||||
const QUrl validSubdomain(QLatin1String("https://sub.example.com/ohoho"));
|
||||
const QUrl unknownDomain(QLatin1String("http://example.org"));
|
||||
const QUrl subSubdomain(QLatin1String("https://level3.level2.example.com"));
|
||||
|
||||
const QDateTime validDate(currentUTC.addSecs(1000));
|
||||
QTest::newRow("same-known") << knownHost << validDate << false << knownHost << true;
|
||||
QTest::newRow("subexcluded") << knownHost << validDate << false << validSubdomain << false;
|
||||
QTest::newRow("subincluded") << knownHost << validDate << true << validSubdomain << true;
|
||||
QTest::newRow("unknown-subexcluded") << knownHost << validDate << false << unknownDomain << false;
|
||||
QTest::newRow("unknown-subincluded") << knownHost << validDate << true << unknownDomain << false;
|
||||
QTest::newRow("sub-subdomain-subincluded") << knownHost << validDate << true << subSubdomain << true;
|
||||
QTest::newRow("sub-subdomain-subexcluded") << knownHost << validDate << false << subSubdomain << false;
|
||||
|
||||
const QDateTime invalidDate;
|
||||
QTest::newRow("invalid-time") << knownHost << invalidDate << false << knownHost << false;
|
||||
QTest::newRow("invalid-time-subexcluded") << knownHost << invalidDate << false
|
||||
<< validSubdomain << false;
|
||||
QTest::newRow("invalid-time-subincluded") << knownHost << invalidDate << true
|
||||
<< validSubdomain << false;
|
||||
|
||||
const QDateTime expiredDate(currentUTC.addSecs(-1000));
|
||||
QTest::newRow("expired-time") << knownHost << expiredDate << false << knownHost << false;
|
||||
QTest::newRow("expired-time-subexcluded") << knownHost << expiredDate << false
|
||||
<< validSubdomain << false;
|
||||
QTest::newRow("expired-time-subincluded") << knownHost << expiredDate << true
|
||||
<< validSubdomain << false;
|
||||
const QUrl ipAsHost(QLatin1String("http://127.0.0.1"));
|
||||
QTest::newRow("ip-address-in-hostname") << ipAsHost << validDate << false
|
||||
<< ipAsHost << false;
|
||||
|
||||
const QUrl anyIPv4AsHost(QLatin1String("http://0.0.0.0"));
|
||||
QTest::newRow("anyip4-address-in-hostname") << anyIPv4AsHost << validDate
|
||||
<< false << anyIPv4AsHost << false;
|
||||
const QUrl anyIPv6AsHost(QLatin1String("http://[::]"));
|
||||
QTest::newRow("anyip6-address-in-hostname") << anyIPv6AsHost << validDate
|
||||
<< false << anyIPv6AsHost << false;
|
||||
|
||||
}
|
||||
|
||||
void tst_QHsts::testSingleKnownHost()
|
||||
{
|
||||
QFETCH(const QUrl, knownHost);
|
||||
QFETCH(const QDateTime, policyExpires);
|
||||
QFETCH(const bool, includeSubDomains);
|
||||
QFETCH(const QUrl, hostToTest);
|
||||
QFETCH(const bool, isKnown);
|
||||
|
||||
QHstsCache cache;
|
||||
cache.updateKnownHost(knownHost, policyExpires, includeSubDomains);
|
||||
QCOMPARE(cache.isKnownHost(hostToTest), isKnown);
|
||||
}
|
||||
|
||||
void tst_QHsts::testMultilpeKnownHosts()
|
||||
{
|
||||
const QDateTime currentUTC = QDateTime::currentDateTimeUtc();
|
||||
const QDateTime validDate(currentUTC.addSecs(10000));
|
||||
const QDateTime expiredDate(currentUTC.addSecs(-10000));
|
||||
const QUrl exampleCom(QLatin1String("https://example.com"));
|
||||
const QUrl subExampleCom(QLatin1String("https://sub.example.com"));
|
||||
|
||||
QHstsCache cache;
|
||||
// example.com is HSTS and includes subdomains:
|
||||
cache.updateKnownHost(exampleCom, validDate, true);
|
||||
QVERIFY(cache.isKnownHost(exampleCom));
|
||||
QVERIFY(cache.isKnownHost(subExampleCom));
|
||||
// example.com can set its policy not to include subdomains:
|
||||
cache.updateKnownHost(exampleCom, validDate, false);
|
||||
QVERIFY(!cache.isKnownHost(subExampleCom));
|
||||
// but sub.example.com can set its own policy:
|
||||
cache.updateKnownHost(subExampleCom, validDate, false);
|
||||
QVERIFY(cache.isKnownHost(subExampleCom));
|
||||
// let's say example.com's policy has expired:
|
||||
cache.updateKnownHost(exampleCom, expiredDate, false);
|
||||
QVERIFY(!cache.isKnownHost(exampleCom));
|
||||
// it should not affect sub.example.com's policy:
|
||||
QVERIFY(cache.isKnownHost(subExampleCom));
|
||||
|
||||
// clear cache and invalidate all policies:
|
||||
cache.clear();
|
||||
QVERIFY(!cache.isKnownHost(exampleCom));
|
||||
QVERIFY(!cache.isKnownHost(subExampleCom));
|
||||
|
||||
// siblings:
|
||||
const QUrl anotherSub(QLatin1String("https://sub2.example.com"));
|
||||
cache.updateKnownHost(subExampleCom, validDate, true);
|
||||
cache.updateKnownHost(anotherSub, validDate, true);
|
||||
QVERIFY(cache.isKnownHost(subExampleCom));
|
||||
QVERIFY(cache.isKnownHost(anotherSub));
|
||||
// they cannot set superdomain's policy:
|
||||
QVERIFY(!cache.isKnownHost(exampleCom));
|
||||
// a sibling cannot set another sibling's policy:
|
||||
cache.updateKnownHost(anotherSub, expiredDate, false);
|
||||
QVERIFY(cache.isKnownHost(subExampleCom));
|
||||
QVERIFY(!cache.isKnownHost(anotherSub));
|
||||
QVERIFY(!cache.isKnownHost(exampleCom));
|
||||
// let's make example.com known again:
|
||||
cache.updateKnownHost(exampleCom, validDate, true);
|
||||
// a subdomain cannot affect its superdomain's policy:
|
||||
cache.updateKnownHost(subExampleCom, expiredDate, true);
|
||||
QVERIFY(cache.isKnownHost(exampleCom));
|
||||
// and this superdomain includes subdomains in its HSTS policy:
|
||||
QVERIFY(cache.isKnownHost(subExampleCom));
|
||||
QVERIFY(cache.isKnownHost(anotherSub));
|
||||
|
||||
// a subdomain (with its subdomains) cannot affect its superdomain's policy:
|
||||
cache.updateKnownHost(exampleCom, expiredDate, true);
|
||||
cache.updateKnownHost(subExampleCom, validDate, true);
|
||||
QVERIFY(cache.isKnownHost(subExampleCom));
|
||||
QVERIFY(!cache.isKnownHost(exampleCom));
|
||||
}
|
||||
|
||||
void tst_QHsts::testPolicyExpiration()
|
||||
{
|
||||
QDateTime currentUTC = QDateTime::currentDateTimeUtc();
|
||||
const QUrl exampleCom(QLatin1String("http://example.com"));
|
||||
const QUrl subdomain(QLatin1String("http://subdomain.example.com"));
|
||||
const qint64 lifeTimeMS = 50;
|
||||
|
||||
QHstsCache cache;
|
||||
// start with 'includeSubDomains' and 5 s. lifetime:
|
||||
cache.updateKnownHost(exampleCom, currentUTC.addMSecs(lifeTimeMS), true);
|
||||
QVERIFY(cache.isKnownHost(exampleCom));
|
||||
QVERIFY(cache.isKnownHost(subdomain));
|
||||
// wait for approx. a half of lifetime:
|
||||
QTest::qWait(lifeTimeMS / 2);
|
||||
|
||||
if (QDateTime::currentDateTimeUtc() < currentUTC.addMSecs(lifeTimeMS)) {
|
||||
// Should still be valid:
|
||||
QVERIFY(cache.isKnownHost(exampleCom));
|
||||
QVERIFY(cache.isKnownHost(subdomain));
|
||||
}
|
||||
|
||||
QTest::qWait(lifeTimeMS);
|
||||
// expired:
|
||||
QVERIFY(!cache.isKnownHost(exampleCom));
|
||||
QVERIFY(!cache.isKnownHost(subdomain));
|
||||
|
||||
// now check that superdomain's policy expires, but not subdomain's policy:
|
||||
currentUTC = QDateTime::currentDateTimeUtc();
|
||||
cache.updateKnownHost(exampleCom, currentUTC.addMSecs(lifeTimeMS / 5), true);
|
||||
cache.updateKnownHost(subdomain, currentUTC.addMSecs(lifeTimeMS), true);
|
||||
QVERIFY(cache.isKnownHost(exampleCom));
|
||||
QVERIFY(cache.isKnownHost(subdomain));
|
||||
QTest::qWait(lifeTimeMS / 2);
|
||||
if (QDateTime::currentDateTimeUtc() < currentUTC.addMSecs(lifeTimeMS)) {
|
||||
QVERIFY(!cache.isKnownHost(exampleCom));
|
||||
QVERIFY(cache.isKnownHost(subdomain));
|
||||
}
|
||||
}
|
||||
|
||||
void tst_QHsts::testSTSHeaderParser()
|
||||
{
|
||||
QHstsHeaderParser parser;
|
||||
using Header = QPair<QByteArray, QByteArray>;
|
||||
using Headers = QList<Header>;
|
||||
|
||||
QVERIFY(!parser.includeSubDomains());
|
||||
QVERIFY(!parser.expirationDate().isValid());
|
||||
Headers list;
|
||||
QVERIFY(!parser.parse(list));
|
||||
QVERIFY(!parser.includeSubDomains());
|
||||
QVERIFY(!parser.expirationDate().isValid());
|
||||
|
||||
list << Header("Strict-Transport-security", "200");
|
||||
QVERIFY(!parser.parse(list));
|
||||
QVERIFY(!parser.includeSubDomains());
|
||||
QVERIFY(!parser.expirationDate().isValid());
|
||||
|
||||
// This header is missing REQUIRED max-age directive, so we'll ignore it:
|
||||
list << Header("Strict-Transport-Security", "includeSubDomains");
|
||||
QVERIFY(!parser.parse(list));
|
||||
QVERIFY(!parser.includeSubDomains());
|
||||
QVERIFY(!parser.expirationDate().isValid());
|
||||
|
||||
list.pop_back();
|
||||
list << Header("Strict-Transport-Security", "includeSubDomains;max-age=1000");
|
||||
QVERIFY(parser.parse(list));
|
||||
QVERIFY(parser.expirationDate() > QDateTime::currentDateTimeUtc());
|
||||
QVERIFY(parser.includeSubDomains());
|
||||
|
||||
list.pop_back();
|
||||
// Invalid (includeSubDomains twice):
|
||||
list << Header("Strict-Transport-Security", "max-age = 1000 ; includeSubDomains;includeSubDomains");
|
||||
QVERIFY(!parser.parse(list));
|
||||
QVERIFY(!parser.includeSubDomains());
|
||||
QVERIFY(!parser.expirationDate().isValid());
|
||||
|
||||
list.pop_back();
|
||||
// Invalid (weird number of seconds):
|
||||
list << Header("Strict-Transport-Security", "max-age=-1000 ; includeSubDomains");
|
||||
QVERIFY(!parser.parse(list));
|
||||
QVERIFY(!parser.includeSubDomains());
|
||||
QVERIFY(!parser.expirationDate().isValid());
|
||||
|
||||
list.pop_back();
|
||||
// Note, directives are case-insensitive + we should ignore unknown directive.
|
||||
list << Header("Strict-Transport-Security", ";max-age=1000 ;includesubdomains;;"
|
||||
"nowsomeunknownheader=\"somevaluewithescapes\\;\"");
|
||||
QVERIFY(parser.parse(list));
|
||||
QVERIFY(parser.includeSubDomains());
|
||||
QVERIFY(parser.expirationDate().isValid());
|
||||
|
||||
list.pop_back();
|
||||
// Check that we know how to unescape max-age:
|
||||
list << Header("Strict-Transport-Security", "max-age=\"1000\"");
|
||||
QVERIFY(parser.parse(list));
|
||||
QVERIFY(!parser.includeSubDomains());
|
||||
QVERIFY(parser.expirationDate().isValid());
|
||||
|
||||
list.pop_back();
|
||||
// The only STS header, with invalid syntax though, to be ignored:
|
||||
list << Header("Strict-Transport-Security", "max-age; max-age=15768000");
|
||||
QVERIFY(!parser.parse(list));
|
||||
QVERIFY(!parser.includeSubDomains());
|
||||
QVERIFY(!parser.expirationDate().isValid());
|
||||
|
||||
// Now we check that our parse chosses the first valid STS header and ignores
|
||||
// others:
|
||||
list.clear();
|
||||
list << Header("Strict-Transport-Security", "includeSubdomains; max-age=\"hehehe\";");
|
||||
list << Header("Strict-Transport-Security", "max-age=10101");
|
||||
QVERIFY(parser.parse(list));
|
||||
QVERIFY(!parser.includeSubDomains());
|
||||
QVERIFY(parser.expirationDate().isValid());
|
||||
|
||||
|
||||
list.clear();
|
||||
list << Header("Strict-Transport-Security", "max-age=0");
|
||||
QVERIFY(parser.parse(list));
|
||||
QVERIFY(!parser.includeSubDomains());
|
||||
QVERIFY(parser.expirationDate() <= QDateTime::currentDateTimeUtc());
|
||||
|
||||
// Parsing is case-insensitive:
|
||||
list.pop_back();
|
||||
list << Header("Strict-Transport-Security", "Max-aGE=1000; InclUdesUbdomains");
|
||||
QVERIFY(parser.parse(list));
|
||||
QVERIFY(parser.includeSubDomains());
|
||||
QVERIFY(parser.expirationDate().isValid());
|
||||
|
||||
// Grammar of STS header is quite permissive, let's check we can parse
|
||||
// some weird but valid header:
|
||||
list.pop_back();
|
||||
list << Header("Strict-Transport-Security", ";;; max-age = 17; ; ; ; ;;; ;;"
|
||||
";;; ; includeSubdomains ;;thisIsUnknownDirective;;;;");
|
||||
QVERIFY(parser.parse(list));
|
||||
QVERIFY(parser.includeSubDomains());
|
||||
QVERIFY(parser.expirationDate().isValid());
|
||||
|
||||
list.pop_back();
|
||||
list << Header("Strict-Transport-Security", "max-age=1000; includeSubDomains bogon");
|
||||
QVERIFY(!parser.parse(list));
|
||||
QVERIFY(!parser.includeSubDomains());
|
||||
QVERIFY(!parser.expirationDate().isValid());
|
||||
}
|
||||
|
||||
QTEST_MAIN(tst_QHsts)
|
||||
|
||||
#include "tst_qhsts.moc"
|
Loading…
Reference in New Issue
Block a user