1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the test suite of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 as published by the Free Software
20** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21** included in the packaging of this file. Please review the following
22** information to ensure the GNU General Public License requirements will
23** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24**
25** $QT_END_LICENSE$
26**
27****************************************************************************/
28
29#include <QtTest/QtTest>
30
31#include <QtCore/qdatetime.h>
32#include <QtCore/qvector.h>
33#include <QtCore/qpair.h>
34#include <QtCore/qurl.h>
35#include <QtCore/qdir.h>
36
37#include <QtNetwork/private/qhstsstore_p.h>
38#include <QtNetwork/private/qhsts_p.h>
39
40QT_USE_NAMESPACE
41
42class tst_QHsts : public QObject
43{
44 Q_OBJECT
45private Q_SLOTS:
46 void testSingleKnownHost_data();
47 void testSingleKnownHost();
48 void testMultilpeKnownHosts();
49 void testPolicyExpiration();
50 void testSTSHeaderParser();
51 void testStore();
52};
53
54void tst_QHsts::testSingleKnownHost_data()
55{
56 QTest::addColumn<QUrl>(name: "knownHost");
57 QTest::addColumn<QDateTime>(name: "policyExpires");
58 QTest::addColumn<bool>(name: "includeSubDomains");
59 QTest::addColumn<QUrl>(name: "hostToTest");
60 QTest::addColumn<bool>(name: "isKnown");
61
62 const QDateTime currentUTC = QDateTime::currentDateTimeUtc();
63 const QUrl knownHost(QLatin1String("http://example.com"));
64 const QUrl validSubdomain(QLatin1String("https://sub.example.com/ohoho"));
65 const QUrl unknownDomain(QLatin1String("http://example.org"));
66 const QUrl subSubdomain(QLatin1String("https://level3.level2.example.com"));
67
68 const QDateTime validDate(currentUTC.addSecs(secs: 1000));
69 QTest::newRow(dataTag: "same-known") << knownHost << validDate << false << knownHost << true;
70 QTest::newRow(dataTag: "subexcluded") << knownHost << validDate << false << validSubdomain << false;
71 QTest::newRow(dataTag: "subincluded") << knownHost << validDate << true << validSubdomain << true;
72 QTest::newRow(dataTag: "unknown-subexcluded") << knownHost << validDate << false << unknownDomain << false;
73 QTest::newRow(dataTag: "unknown-subincluded") << knownHost << validDate << true << unknownDomain << false;
74 QTest::newRow(dataTag: "sub-subdomain-subincluded") << knownHost << validDate << true << subSubdomain << true;
75 QTest::newRow(dataTag: "sub-subdomain-subexcluded") << knownHost << validDate << false << subSubdomain << false;
76
77 const QDateTime invalidDate;
78 QTest::newRow(dataTag: "invalid-time") << knownHost << invalidDate << false << knownHost << false;
79 QTest::newRow(dataTag: "invalid-time-subexcluded") << knownHost << invalidDate << false
80 << validSubdomain << false;
81 QTest::newRow(dataTag: "invalid-time-subincluded") << knownHost << invalidDate << true
82 << validSubdomain << false;
83
84 const QDateTime expiredDate(currentUTC.addSecs(secs: -1000));
85 QTest::newRow(dataTag: "expired-time") << knownHost << expiredDate << false << knownHost << false;
86 QTest::newRow(dataTag: "expired-time-subexcluded") << knownHost << expiredDate << false
87 << validSubdomain << false;
88 QTest::newRow(dataTag: "expired-time-subincluded") << knownHost << expiredDate << true
89 << validSubdomain << false;
90 const QUrl ipAsHost(QLatin1String("http://127.0.0.1"));
91 QTest::newRow(dataTag: "ip-address-in-hostname") << ipAsHost << validDate << false
92 << ipAsHost << false;
93
94 const QUrl anyIPv4AsHost(QLatin1String("http://0.0.0.0"));
95 QTest::newRow(dataTag: "anyip4-address-in-hostname") << anyIPv4AsHost << validDate
96 << false << anyIPv4AsHost << false;
97 const QUrl anyIPv6AsHost(QLatin1String("http://[::]"));
98 QTest::newRow(dataTag: "anyip6-address-in-hostname") << anyIPv6AsHost << validDate
99 << false << anyIPv6AsHost << false;
100
101}
102
103void tst_QHsts::testSingleKnownHost()
104{
105 QFETCH(const QUrl, knownHost);
106 QFETCH(const QDateTime, policyExpires);
107 QFETCH(const bool, includeSubDomains);
108 QFETCH(const QUrl, hostToTest);
109 QFETCH(const bool, isKnown);
110
111 QHstsCache cache;
112 cache.updateKnownHost(url: knownHost, expires: policyExpires, includeSubDomains);
113 QCOMPARE(cache.isKnownHost(hostToTest), isKnown);
114}
115
116void tst_QHsts::testMultilpeKnownHosts()
117{
118 const QDateTime currentUTC = QDateTime::currentDateTimeUtc();
119 const QDateTime validDate(currentUTC.addSecs(secs: 10000));
120 const QDateTime expiredDate(currentUTC.addSecs(secs: -10000));
121 const QUrl exampleCom(QLatin1String("https://example.com"));
122 const QUrl subExampleCom(QLatin1String("https://sub.example.com"));
123
124 QHstsCache cache;
125 // example.com is HSTS and includes subdomains:
126 cache.updateKnownHost(url: exampleCom, expires: validDate, includeSubDomains: true);
127 QVERIFY(cache.isKnownHost(exampleCom));
128 QVERIFY(cache.isKnownHost(subExampleCom));
129 // example.com can set its policy not to include subdomains:
130 cache.updateKnownHost(url: exampleCom, expires: validDate, includeSubDomains: false);
131 QVERIFY(!cache.isKnownHost(subExampleCom));
132 // but sub.example.com can set its own policy:
133 cache.updateKnownHost(url: subExampleCom, expires: validDate, includeSubDomains: false);
134 QVERIFY(cache.isKnownHost(subExampleCom));
135 // let's say example.com's policy has expired:
136 cache.updateKnownHost(url: exampleCom, expires: expiredDate, includeSubDomains: false);
137 QVERIFY(!cache.isKnownHost(exampleCom));
138 // it should not affect sub.example.com's policy:
139 QVERIFY(cache.isKnownHost(subExampleCom));
140
141 // clear cache and invalidate all policies:
142 cache.clear();
143 QVERIFY(!cache.isKnownHost(exampleCom));
144 QVERIFY(!cache.isKnownHost(subExampleCom));
145
146 // siblings:
147 const QUrl anotherSub(QLatin1String("https://sub2.example.com"));
148 cache.updateKnownHost(url: subExampleCom, expires: validDate, includeSubDomains: true);
149 cache.updateKnownHost(url: anotherSub, expires: validDate, includeSubDomains: true);
150 QVERIFY(cache.isKnownHost(subExampleCom));
151 QVERIFY(cache.isKnownHost(anotherSub));
152 // they cannot set superdomain's policy:
153 QVERIFY(!cache.isKnownHost(exampleCom));
154 // a sibling cannot set another sibling's policy:
155 cache.updateKnownHost(url: anotherSub, expires: expiredDate, includeSubDomains: false);
156 QVERIFY(cache.isKnownHost(subExampleCom));
157 QVERIFY(!cache.isKnownHost(anotherSub));
158 QVERIFY(!cache.isKnownHost(exampleCom));
159 // let's make example.com known again:
160 cache.updateKnownHost(url: exampleCom, expires: validDate, includeSubDomains: true);
161 // a subdomain cannot affect its superdomain's policy:
162 cache.updateKnownHost(url: subExampleCom, expires: expiredDate, includeSubDomains: true);
163 QVERIFY(cache.isKnownHost(exampleCom));
164 // and this superdomain includes subdomains in its HSTS policy:
165 QVERIFY(cache.isKnownHost(subExampleCom));
166 QVERIFY(cache.isKnownHost(anotherSub));
167
168 // a subdomain (with its subdomains) cannot affect its superdomain's policy:
169 cache.updateKnownHost(url: exampleCom, expires: expiredDate, includeSubDomains: true);
170 cache.updateKnownHost(url: subExampleCom, expires: validDate, includeSubDomains: true);
171 QVERIFY(cache.isKnownHost(subExampleCom));
172 QVERIFY(!cache.isKnownHost(exampleCom));
173}
174
175void tst_QHsts::testPolicyExpiration()
176{
177 QDateTime currentUTC = QDateTime::currentDateTimeUtc();
178 const QUrl exampleCom(QLatin1String("http://example.com"));
179 const QUrl subdomain(QLatin1String("http://subdomain.example.com"));
180 const qint64 lifeTimeMS = 50;
181
182 QHstsCache cache;
183 // start with 'includeSubDomains' and 5 s. lifetime:
184 cache.updateKnownHost(url: exampleCom, expires: currentUTC.addMSecs(msecs: lifeTimeMS), includeSubDomains: true);
185 QVERIFY(cache.isKnownHost(exampleCom));
186 QVERIFY(cache.isKnownHost(subdomain));
187 // wait for approx. a half of lifetime:
188 QTest::qWait(ms: lifeTimeMS / 2);
189
190 if (QDateTime::currentDateTimeUtc() < currentUTC.addMSecs(msecs: lifeTimeMS)) {
191 // Should still be valid:
192 QVERIFY(cache.isKnownHost(exampleCom));
193 QVERIFY(cache.isKnownHost(subdomain));
194 }
195
196 QTest::qWait(ms: lifeTimeMS);
197 // expired:
198 QVERIFY(!cache.isKnownHost(exampleCom));
199 QVERIFY(!cache.isKnownHost(subdomain));
200
201 // now check that superdomain's policy expires, but not subdomain's policy:
202 currentUTC = QDateTime::currentDateTimeUtc();
203 cache.updateKnownHost(url: exampleCom, expires: currentUTC.addMSecs(msecs: lifeTimeMS / 5), includeSubDomains: true);
204 cache.updateKnownHost(url: subdomain, expires: currentUTC.addMSecs(msecs: lifeTimeMS), includeSubDomains: true);
205 QVERIFY(cache.isKnownHost(exampleCom));
206 QVERIFY(cache.isKnownHost(subdomain));
207 QTest::qWait(ms: lifeTimeMS / 2);
208 if (QDateTime::currentDateTimeUtc() < currentUTC.addMSecs(msecs: lifeTimeMS)) {
209 QVERIFY(!cache.isKnownHost(exampleCom));
210 QVERIFY(cache.isKnownHost(subdomain));
211 }
212}
213
214void tst_QHsts::testSTSHeaderParser()
215{
216 QHstsHeaderParser parser;
217 using Header = QPair<QByteArray, QByteArray>;
218 using Headers = QList<Header>;
219
220 QVERIFY(!parser.includeSubDomains());
221 QVERIFY(!parser.expirationDate().isValid());
222 Headers list;
223 QVERIFY(!parser.parse(list));
224 QVERIFY(!parser.includeSubDomains());
225 QVERIFY(!parser.expirationDate().isValid());
226
227 list << Header("Strict-Transport-security", "200");
228 QVERIFY(!parser.parse(list));
229 QVERIFY(!parser.includeSubDomains());
230 QVERIFY(!parser.expirationDate().isValid());
231
232 // This header is missing REQUIRED max-age directive, so we'll ignore it:
233 list << Header("Strict-Transport-Security", "includeSubDomains");
234 QVERIFY(!parser.parse(list));
235 QVERIFY(!parser.includeSubDomains());
236 QVERIFY(!parser.expirationDate().isValid());
237
238 list.pop_back();
239 list << Header("Strict-Transport-Security", "includeSubDomains;max-age=1000");
240 QVERIFY(parser.parse(list));
241 QVERIFY(parser.expirationDate() > QDateTime::currentDateTimeUtc());
242 QVERIFY(parser.includeSubDomains());
243
244 list.pop_back();
245 // Invalid (includeSubDomains twice):
246 list << Header("Strict-Transport-Security", "max-age = 1000 ; includeSubDomains;includeSubDomains");
247 QVERIFY(!parser.parse(list));
248 QVERIFY(!parser.includeSubDomains());
249 QVERIFY(!parser.expirationDate().isValid());
250
251 list.pop_back();
252 // Invalid (weird number of seconds):
253 list << Header("Strict-Transport-Security", "max-age=-1000 ; includeSubDomains");
254 QVERIFY(!parser.parse(list));
255 QVERIFY(!parser.includeSubDomains());
256 QVERIFY(!parser.expirationDate().isValid());
257
258 list.pop_back();
259 // Note, directives are case-insensitive + we should ignore unknown directive.
260 list << Header("Strict-Transport-Security", ";max-age=1000 ;includesubdomains;;"
261 "nowsomeunknownheader=\"somevaluewithescapes\\;\"");
262 QVERIFY(parser.parse(list));
263 QVERIFY(parser.includeSubDomains());
264 QVERIFY(parser.expirationDate().isValid());
265
266 list.pop_back();
267 // Check that we know how to unescape max-age:
268 list << Header("Strict-Transport-Security", "max-age=\"1000\"");
269 QVERIFY(parser.parse(list));
270 QVERIFY(!parser.includeSubDomains());
271 QVERIFY(parser.expirationDate().isValid());
272
273 list.pop_back();
274 // The only STS header, with invalid syntax though, to be ignored:
275 list << Header("Strict-Transport-Security", "max-age; max-age=15768000");
276 QVERIFY(!parser.parse(list));
277 QVERIFY(!parser.includeSubDomains());
278 QVERIFY(!parser.expirationDate().isValid());
279
280 // Now we check that our parse chosses the first valid STS header and ignores
281 // others:
282 list.clear();
283 list << Header("Strict-Transport-Security", "includeSubdomains; max-age=\"hehehe\";");
284 list << Header("Strict-Transport-Security", "max-age=10101");
285 QVERIFY(parser.parse(list));
286 QVERIFY(!parser.includeSubDomains());
287 QVERIFY(parser.expirationDate().isValid());
288
289
290 list.clear();
291 list << Header("Strict-Transport-Security", "max-age=0");
292 QVERIFY(parser.parse(list));
293 QVERIFY(!parser.includeSubDomains());
294 QVERIFY(parser.expirationDate() <= QDateTime::currentDateTimeUtc());
295
296 // Parsing is case-insensitive:
297 list.pop_back();
298 list << Header("Strict-Transport-Security", "Max-aGE=1000; InclUdesUbdomains");
299 QVERIFY(parser.parse(list));
300 QVERIFY(parser.includeSubDomains());
301 QVERIFY(parser.expirationDate().isValid());
302
303 // Grammar of STS header is quite permissive, let's check we can parse
304 // some weird but valid header:
305 list.pop_back();
306 list << Header("Strict-Transport-Security", ";;; max-age = 17; ; ; ; ;;; ;;"
307 ";;; ; includeSubdomains ;;thisIsUnknownDirective;;;;");
308 QVERIFY(parser.parse(list));
309 QVERIFY(parser.includeSubDomains());
310 QVERIFY(parser.expirationDate().isValid());
311
312 list.pop_back();
313 list << Header("Strict-Transport-Security", "max-age=1000; includeSubDomains bogon");
314 QVERIFY(!parser.parse(list));
315 QVERIFY(!parser.includeSubDomains());
316 QVERIFY(!parser.expirationDate().isValid());
317}
318
319const QLatin1String storeDir(".");
320
321struct TestStoreDeleter
322{
323 ~TestStoreDeleter()
324 {
325 QDir cwd;
326 if (!cwd.remove(fileName: QHstsStore::absoluteFilePath(dirName: storeDir)))
327 qWarning() << "tst_QHsts::testStore: failed to remove the hsts store file";
328 }
329};
330
331void tst_QHsts::testStore()
332{
333 // Delete the store's file after we finish the test.
334 TestStoreDeleter cleaner;
335
336 const QUrl exampleCom(QStringLiteral("http://example.com"));
337 const QUrl subDomain(QStringLiteral("http://subdomain.example.com"));
338 const QDateTime validDate(QDateTime::currentDateTimeUtc().addDays(days: 1));
339
340 {
341 // We start from an empty cache and empty store:
342 QHstsCache cache;
343 QHstsStore store(storeDir);
344 cache.setStore(&store);
345 QVERIFY(!cache.isKnownHost(exampleCom));
346 QVERIFY(!cache.isKnownHost(subDomain));
347 // (1) This will also store the policy:
348 cache.updateKnownHost(url: exampleCom, expires: validDate, includeSubDomains: true);
349 QVERIFY(cache.isKnownHost(exampleCom));
350 QVERIFY(cache.isKnownHost(subDomain));
351 }
352 {
353 // Test the policy stored at (1):
354 QHstsCache cache;
355 QHstsStore store(storeDir);
356 cache.setStore(&store);
357 QVERIFY(cache.isKnownHost(exampleCom));
358 QVERIFY(cache.isKnownHost(subDomain));
359 // (2) Remove subdomains:
360 cache.updateKnownHost(url: exampleCom, expires: validDate, includeSubDomains: false);
361 QVERIFY(!cache.isKnownHost(subDomain));
362 }
363 {
364 // Test the previous update (2):
365 QHstsCache cache;
366 QHstsStore store(storeDir);
367 cache.setStore(&store);
368 QVERIFY(cache.isKnownHost(exampleCom));
369 QVERIFY(!cache.isKnownHost(subDomain));
370 }
371 {
372 QHstsCache cache;
373 cache.updateKnownHost(url: subDomain, expires: validDate, includeSubDomains: false);
374 QVERIFY(cache.isKnownHost(subDomain));
375 QHstsStore store(storeDir);
376 // (3) This should store policy from cache, over old policy from store:
377 cache.setStore(&store);
378 }
379 {
380 // Test that (3) was stored:
381 QHstsCache cache;
382 QHstsStore store(storeDir);
383 cache.setStore(&store);
384 QVERIFY(cache.isKnownHost(subDomain));
385 }
386}
387
388QTEST_MAIN(tst_QHsts)
389
390#include "tst_qhsts.moc"
391

source code of qtbase/tests/auto/network/access/hsts/tst_qhsts.cpp