| 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 | |
| 40 | QT_USE_NAMESPACE |
| 41 | |
| 42 | class tst_QHsts : public QObject |
| 43 | { |
| 44 | Q_OBJECT |
| 45 | private Q_SLOTS: |
| 46 | void testSingleKnownHost_data(); |
| 47 | void testSingleKnownHost(); |
| 48 | void testMultilpeKnownHosts(); |
| 49 | void testPolicyExpiration(); |
| 50 | void testSTSHeaderParser(); |
| 51 | void testStore(); |
| 52 | }; |
| 53 | |
| 54 | void 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 | |
| 103 | void 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 | |
| 116 | void 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 | |
| 175 | void 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 | |
| 214 | void tst_QHsts::() |
| 215 | { |
| 216 | QHstsHeaderParser parser; |
| 217 | using = QPair<QByteArray, QByteArray>; |
| 218 | using = 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 | |
| 319 | const QLatin1String storeDir("." ); |
| 320 | |
| 321 | struct 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 | |
| 331 | void 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 | |
| 388 | QTEST_MAIN(tst_QHsts) |
| 389 | |
| 390 | #include "tst_qhsts.moc" |
| 391 | |