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 "http2srv.h" |
32 | |
33 | #include <QtNetwork/private/http2protocol_p.h> |
34 | #include <QtNetwork/qnetworkaccessmanager.h> |
35 | #include <QtNetwork/qhttp2configuration.h> |
36 | #include <QtNetwork/qnetworkrequest.h> |
37 | #include <QtNetwork/qnetworkreply.h> |
38 | |
39 | #include <QtCore/qglobal.h> |
40 | #include <QtCore/qobject.h> |
41 | #include <QtCore/qthread.h> |
42 | #include <QtCore/qurl.h> |
43 | |
44 | #ifndef QT_NO_SSL |
45 | #ifndef QT_NO_OPENSSL |
46 | #include <QtNetwork/private/qsslsocket_openssl_symbols_p.h> |
47 | #endif // NO_OPENSSL |
48 | #endif // NO_SSL |
49 | |
50 | #include <cstdlib> |
51 | #include <memory> |
52 | #include <string> |
53 | |
54 | #include "emulationdetector.h" |
55 | |
56 | #if (!defined(QT_NO_OPENSSL) && OPENSSL_VERSION_NUMBER >= 0x10002000L && !defined(OPENSSL_NO_TLSEXT)) \ |
57 | || QT_CONFIG(schannel) |
58 | // HTTP/2 over TLS requires ALPN/NPN to negotiate the protocol version. |
59 | const bool clearTextHTTP2 = false; |
60 | #else |
61 | // No ALPN/NPN support to negotiate HTTP/2, we'll use cleartext 'h2c' with |
62 | // a protocol upgrade procedure. |
63 | const bool clearTextHTTP2 = true; |
64 | #endif |
65 | |
66 | Q_DECLARE_METATYPE(H2Type) |
67 | Q_DECLARE_METATYPE(QNetworkRequest::Attribute) |
68 | |
69 | QT_BEGIN_NAMESPACE |
70 | |
71 | QHttp2Configuration qt_defaultH2Configuration() |
72 | { |
73 | QHttp2Configuration config; |
74 | config.setStreamReceiveWindowSize(Http2::qtDefaultStreamReceiveWindowSize); |
75 | config.setSessionReceiveWindowSize(Http2::maxSessionReceiveWindowSize); |
76 | config.setServerPushEnabled(false); |
77 | return config; |
78 | } |
79 | |
80 | RawSettings qt_H2ConfigurationToSettings(const QHttp2Configuration &config = qt_defaultH2Configuration()) |
81 | { |
82 | RawSettings settings; |
83 | settings[Http2::Settings::ENABLE_PUSH_ID] = config.serverPushEnabled(); |
84 | settings[Http2::Settings::INITIAL_WINDOW_SIZE_ID] = config.streamReceiveWindowSize(); |
85 | if (config.maxFrameSize() != Http2::minPayloadLimit) |
86 | settings[Http2::Settings::MAX_FRAME_SIZE_ID] = config.maxFrameSize(); |
87 | return settings; |
88 | } |
89 | |
90 | |
91 | class tst_Http2 : public QObject |
92 | { |
93 | Q_OBJECT |
94 | public: |
95 | tst_Http2(); |
96 | ~tst_Http2(); |
97 | public slots: |
98 | void init(); |
99 | private slots: |
100 | // Tests: |
101 | void singleRequest_data(); |
102 | void singleRequest(); |
103 | void multipleRequests(); |
104 | void flowControlClientSide(); |
105 | void flowControlServerSide(); |
106 | void pushPromise(); |
107 | void goaway_data(); |
108 | void goaway(); |
109 | void earlyResponse(); |
110 | void connectToHost_data(); |
111 | void connectToHost(); |
112 | void maxFrameSize(); |
113 | void http2DATAFrames(); |
114 | |
115 | void authenticationRequired_data(); |
116 | void authenticationRequired(); |
117 | |
118 | void redirect_data(); |
119 | void redirect(); |
120 | |
121 | protected slots: |
122 | // Slots to listen to our in-process server: |
123 | void serverStarted(quint16 port); |
124 | void clientPrefaceOK(); |
125 | void clientPrefaceError(); |
126 | void serverSettingsAcked(); |
127 | void invalidFrame(); |
128 | void invalidRequest(quint32 streamID); |
129 | void decompressionFailed(quint32 streamID); |
130 | void receivedRequest(quint32 streamID); |
131 | void receivedData(quint32 streamID); |
132 | void windowUpdated(quint32 streamID); |
133 | void replyFinished(); |
134 | void replyFinishedWithError(); |
135 | |
136 | private: |
137 | void clearHTTP2State(); |
138 | // Run event for 'ms' milliseconds. |
139 | // The default value '5000' is enough for |
140 | // small payload. |
141 | void runEventLoop(int ms = 5000); |
142 | void stopEventLoop(); |
143 | Http2Server *newServer(const RawSettings &serverSettings, H2Type connectionType, |
144 | const RawSettings &clientSettings = qt_H2ConfigurationToSettings()); |
145 | // Send a get or post request, depending on a payload (empty or not). |
146 | void sendRequest(int streamNumber, |
147 | QNetworkRequest::Priority priority = QNetworkRequest::NormalPriority, |
148 | const QByteArray &payload = QByteArray(), |
149 | const QHttp2Configuration &clientConfiguration = qt_defaultH2Configuration()); |
150 | QUrl requestUrl(H2Type connnectionType) const; |
151 | |
152 | quint16 serverPort = 0; |
153 | QThread *workerThread = nullptr; |
154 | std::unique_ptr<QNetworkAccessManager> manager; |
155 | |
156 | QTestEventLoop eventLoop; |
157 | |
158 | int nRequests = 0; |
159 | int nSentRequests = 0; |
160 | |
161 | int windowUpdates = 0; |
162 | bool prefaceOK = false; |
163 | bool serverGotSettingsACK = false; |
164 | bool POSTResponseHEADOnly = true; |
165 | |
166 | static const RawSettings defaultServerSettings; |
167 | }; |
168 | |
169 | #define STOP_ON_FAILURE \ |
170 | if (QTest::currentTestFailed()) \ |
171 | return; |
172 | |
173 | const RawSettings tst_Http2::defaultServerSettings{{Http2::Settings::MAX_CONCURRENT_STREAMS_ID, 100}}; |
174 | |
175 | namespace { |
176 | |
177 | // Our server lives/works on a different thread so we invoke its 'deleteLater' |
178 | // instead of simple 'delete'. |
179 | struct ServerDeleter |
180 | { |
181 | static void cleanup(Http2Server *srv) |
182 | { |
183 | if (srv) { |
184 | srv->stopSendingDATAFrames(); |
185 | QMetaObject::invokeMethod(obj: srv, member: "deleteLater" , type: Qt::QueuedConnection); |
186 | } |
187 | } |
188 | }; |
189 | |
190 | using ServerPtr = QScopedPointer<Http2Server, ServerDeleter>; |
191 | |
192 | H2Type defaultConnectionType() |
193 | { |
194 | return clearTextHTTP2 ? H2Type::h2c : H2Type::h2Alpn; |
195 | } |
196 | |
197 | } // unnamed namespace |
198 | |
199 | tst_Http2::tst_Http2() |
200 | : workerThread(new QThread) |
201 | { |
202 | workerThread->start(); |
203 | } |
204 | |
205 | tst_Http2::~tst_Http2() |
206 | { |
207 | workerThread->quit(); |
208 | workerThread->wait(time: 5000); |
209 | |
210 | if (workerThread->isFinished()) { |
211 | delete workerThread; |
212 | } else { |
213 | connect(sender: workerThread, signal: &QThread::finished, |
214 | receiver: workerThread, slot: &QThread::deleteLater); |
215 | } |
216 | } |
217 | |
218 | void tst_Http2::init() |
219 | { |
220 | manager.reset(p: new QNetworkAccessManager); |
221 | } |
222 | |
223 | void tst_Http2::singleRequest_data() |
224 | { |
225 | QTest::addColumn<QNetworkRequest::Attribute>(name: "h2Attribute" ); |
226 | QTest::addColumn<H2Type>(name: "connectionType" ); |
227 | |
228 | // 'Clear text' that should always work, either via the protocol upgrade |
229 | // or as direct. |
230 | QTest::addRow(format: "h2c-upgrade" ) << QNetworkRequest::Http2AllowedAttribute << H2Type::h2c; |
231 | QTest::addRow(format: "h2c-direct" ) << QNetworkRequest::Http2DirectAttribute << H2Type::h2cDirect; |
232 | |
233 | if (!clearTextHTTP2) { |
234 | // Qt with TLS where TLS-backend supports ALPN. |
235 | QTest::addRow(format: "h2-ALPN" ) << QNetworkRequest::Http2AllowedAttribute << H2Type::h2Alpn; |
236 | } |
237 | |
238 | #if QT_CONFIG(ssl) |
239 | QTest::addRow(format: "h2-direct" ) << QNetworkRequest::Http2DirectAttribute << H2Type::h2Direct; |
240 | #endif |
241 | } |
242 | |
243 | void tst_Http2::singleRequest() |
244 | { |
245 | clearHTTP2State(); |
246 | |
247 | #if QT_CONFIG(securetransport) |
248 | // Normally on macOS we use plain text only for SecureTransport |
249 | // does not support ALPN on the server side. With 'direct encrytped' |
250 | // we have to use TLS sockets (== private key) and thus suppress a |
251 | // keychain UI asking for permission to use a private key. |
252 | // Our CI has this, but somebody testing locally - will have a problem. |
253 | qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN" , QByteArray("1" )); |
254 | auto envRollback = qScopeGuard([](){ |
255 | qunsetenv("QT_SSL_USE_TEMPORARY_KEYCHAIN" ); |
256 | }); |
257 | #endif |
258 | |
259 | serverPort = 0; |
260 | nRequests = 1; |
261 | |
262 | QFETCH(const H2Type, connectionType); |
263 | ServerPtr srv(newServer(serverSettings: defaultServerSettings, connectionType)); |
264 | |
265 | QMetaObject::invokeMethod(obj: srv.data(), member: "startServer" , type: Qt::QueuedConnection); |
266 | runEventLoop(); |
267 | |
268 | QVERIFY(serverPort != 0); |
269 | |
270 | auto url = requestUrl(connnectionType: connectionType); |
271 | url.setPath(path: "/index.html" ); |
272 | |
273 | QNetworkRequest request(url); |
274 | QFETCH(const QNetworkRequest::Attribute, h2Attribute); |
275 | request.setAttribute(code: h2Attribute, value: QVariant(true)); |
276 | |
277 | auto reply = manager->get(request); |
278 | #if QT_CONFIG(ssl) |
279 | QSignalSpy encSpy(reply, &QNetworkReply::encrypted); |
280 | #endif // QT_CONFIG(ssl) |
281 | |
282 | connect(sender: reply, signal: &QNetworkReply::finished, receiver: this, slot: &tst_Http2::replyFinished); |
283 | // Since we're using self-signed certificates, |
284 | // ignore SSL errors: |
285 | reply->ignoreSslErrors(); |
286 | |
287 | runEventLoop(); |
288 | STOP_ON_FAILURE |
289 | |
290 | QVERIFY(nRequests == 0); |
291 | QVERIFY(prefaceOK); |
292 | QVERIFY(serverGotSettingsACK); |
293 | |
294 | QCOMPARE(reply->error(), QNetworkReply::NoError); |
295 | QVERIFY(reply->isFinished()); |
296 | |
297 | #if QT_CONFIG(ssl) |
298 | if (connectionType == H2Type::h2Alpn || connectionType == H2Type::h2Direct) |
299 | QCOMPARE(encSpy.count(), 1); |
300 | #endif // QT_CONFIG(ssl) |
301 | } |
302 | |
303 | void tst_Http2::multipleRequests() |
304 | { |
305 | clearHTTP2State(); |
306 | |
307 | serverPort = 0; |
308 | nRequests = 10; |
309 | |
310 | ServerPtr srv(newServer(serverSettings: defaultServerSettings, connectionType: defaultConnectionType())); |
311 | |
312 | QMetaObject::invokeMethod(obj: srv.data(), member: "startServer" , type: Qt::QueuedConnection); |
313 | |
314 | runEventLoop(); |
315 | QVERIFY(serverPort != 0); |
316 | |
317 | // Just to make the order a bit more interesting |
318 | // we'll index this randomly: |
319 | const QNetworkRequest::Priority priorities[] = { |
320 | QNetworkRequest::HighPriority, |
321 | QNetworkRequest::NormalPriority, |
322 | QNetworkRequest::LowPriority |
323 | }; |
324 | |
325 | for (int i = 0; i < nRequests; ++i) |
326 | sendRequest(streamNumber: i, priority: priorities[QRandomGenerator::global()->bounded(highest: 3)]); |
327 | |
328 | runEventLoop(); |
329 | STOP_ON_FAILURE |
330 | |
331 | QVERIFY(nRequests == 0); |
332 | QVERIFY(prefaceOK); |
333 | QVERIFY(serverGotSettingsACK); |
334 | } |
335 | |
336 | void tst_Http2::flowControlClientSide() |
337 | { |
338 | // Create a server but impose limits: |
339 | // 1. Small client receive windows so server's responses cause client |
340 | // streams to suspend and protocol handler has to send WINDOW_UPDATE |
341 | // frames. |
342 | // 2. Few concurrent streams supported by the server, to test protocol |
343 | // handler in the client can suspend and then resume streams. |
344 | using namespace Http2; |
345 | |
346 | clearHTTP2State(); |
347 | |
348 | serverPort = 0; |
349 | nRequests = 10; |
350 | windowUpdates = 0; |
351 | |
352 | QHttp2Configuration params; |
353 | // A small window size for a session, and even a smaller one per stream - |
354 | // this will result in WINDOW_UPDATE frames both on connection stream and |
355 | // per stream. |
356 | params.setSessionReceiveWindowSize(Http2::defaultSessionWindowSize * 5); |
357 | params.setStreamReceiveWindowSize(Http2::defaultSessionWindowSize); |
358 | |
359 | const RawSettings serverSettings = {{Settings::MAX_CONCURRENT_STREAMS_ID, quint32(3)}}; |
360 | ServerPtr srv(newServer(serverSettings, connectionType: defaultConnectionType(), clientSettings: qt_H2ConfigurationToSettings(config: params))); |
361 | |
362 | const QByteArray respond(int(Http2::defaultSessionWindowSize * 10), 'x'); |
363 | srv->setResponseBody(respond); |
364 | |
365 | QMetaObject::invokeMethod(obj: srv.data(), member: "startServer" , type: Qt::QueuedConnection); |
366 | |
367 | runEventLoop(); |
368 | QVERIFY(serverPort != 0); |
369 | |
370 | for (int i = 0; i < nRequests; ++i) |
371 | sendRequest(streamNumber: i, priority: QNetworkRequest::NormalPriority, payload: {}, clientConfiguration: params); |
372 | |
373 | runEventLoop(ms: 120000); |
374 | STOP_ON_FAILURE |
375 | |
376 | QVERIFY(nRequests == 0); |
377 | QVERIFY(prefaceOK); |
378 | QVERIFY(serverGotSettingsACK); |
379 | QVERIFY(windowUpdates > 0); |
380 | } |
381 | |
382 | void tst_Http2::flowControlServerSide() |
383 | { |
384 | // Quite aggressive test: |
385 | // low MAX_FRAME_SIZE forces a lot of small DATA frames, |
386 | // payload size exceedes stream/session RECV window sizes |
387 | // so that our implementation should deal with WINDOW_UPDATE |
388 | // on a session/stream level correctly + resume/suspend streams |
389 | // to let all replies finish without any error. |
390 | using namespace Http2; |
391 | |
392 | if (EmulationDetector::isRunningArmOnX86()) |
393 | QSKIP("Test is too slow to run on emulator" ); |
394 | |
395 | clearHTTP2State(); |
396 | |
397 | serverPort = 0; |
398 | nRequests = 10; |
399 | |
400 | const RawSettings serverSettings = {{Settings::MAX_CONCURRENT_STREAMS_ID, 7}}; |
401 | |
402 | ServerPtr srv(newServer(serverSettings, connectionType: defaultConnectionType())); |
403 | |
404 | const QByteArray payload(int(Http2::defaultSessionWindowSize * 500), 'x'); |
405 | |
406 | QMetaObject::invokeMethod(obj: srv.data(), member: "startServer" , type: Qt::QueuedConnection); |
407 | |
408 | runEventLoop(); |
409 | QVERIFY(serverPort != 0); |
410 | |
411 | for (int i = 0; i < nRequests; ++i) |
412 | sendRequest(streamNumber: i, priority: QNetworkRequest::NormalPriority, payload); |
413 | |
414 | runEventLoop(ms: 120000); |
415 | STOP_ON_FAILURE |
416 | |
417 | QVERIFY(nRequests == 0); |
418 | QVERIFY(prefaceOK); |
419 | QVERIFY(serverGotSettingsACK); |
420 | } |
421 | |
422 | void tst_Http2::pushPromise() |
423 | { |
424 | // We will first send some request, the server should reply and also emulate |
425 | // PUSH_PROMISE sending us another response as promised. |
426 | using namespace Http2; |
427 | |
428 | clearHTTP2State(); |
429 | |
430 | serverPort = 0; |
431 | nRequests = 1; |
432 | |
433 | QHttp2Configuration params; |
434 | // Defaults are good, except ENABLE_PUSH: |
435 | params.setServerPushEnabled(true); |
436 | |
437 | ServerPtr srv(newServer(serverSettings: defaultServerSettings, connectionType: defaultConnectionType(), clientSettings: qt_H2ConfigurationToSettings(config: params))); |
438 | srv->enablePushPromise(enabled: true, path: QByteArray("/script.js" )); |
439 | |
440 | QMetaObject::invokeMethod(obj: srv.data(), member: "startServer" , type: Qt::QueuedConnection); |
441 | runEventLoop(); |
442 | |
443 | QVERIFY(serverPort != 0); |
444 | |
445 | auto url = requestUrl(connnectionType: defaultConnectionType()); |
446 | url.setPath(path: "/index.html" ); |
447 | |
448 | QNetworkRequest request(url); |
449 | request.setAttribute(code: QNetworkRequest::Http2AllowedAttribute, value: QVariant(true)); |
450 | request.setHttp2Configuration(params); |
451 | |
452 | auto reply = manager->get(request); |
453 | connect(sender: reply, signal: &QNetworkReply::finished, receiver: this, slot: &tst_Http2::replyFinished); |
454 | // Since we're using self-signed certificates, ignore SSL errors: |
455 | reply->ignoreSslErrors(); |
456 | |
457 | runEventLoop(); |
458 | STOP_ON_FAILURE |
459 | |
460 | QVERIFY(nRequests == 0); |
461 | QVERIFY(prefaceOK); |
462 | QVERIFY(serverGotSettingsACK); |
463 | |
464 | QCOMPARE(reply->error(), QNetworkReply::NoError); |
465 | QVERIFY(reply->isFinished()); |
466 | |
467 | // Now, the most interesting part! |
468 | nSentRequests = 0; |
469 | nRequests = 1; |
470 | // Create an additional request (let's say, we parsed reply and realized we |
471 | // need another resource): |
472 | |
473 | url.setPath(path: "/script.js" ); |
474 | QNetworkRequest promisedRequest(url); |
475 | promisedRequest.setAttribute(code: QNetworkRequest::Http2AllowedAttribute, value: QVariant(true)); |
476 | reply = manager->get(request: promisedRequest); |
477 | connect(sender: reply, signal: &QNetworkReply::finished, receiver: this, slot: &tst_Http2::replyFinished); |
478 | reply->ignoreSslErrors(); |
479 | |
480 | runEventLoop(); |
481 | |
482 | // Let's check that NO request was actually made: |
483 | QCOMPARE(nSentRequests, 0); |
484 | // Decreased by replyFinished(): |
485 | QCOMPARE(nRequests, 0); |
486 | QCOMPARE(reply->error(), QNetworkReply::NoError); |
487 | QVERIFY(reply->isFinished()); |
488 | } |
489 | |
490 | void tst_Http2::goaway_data() |
491 | { |
492 | // For now we test only basic things in two very simple scenarios: |
493 | // - server sends GOAWAY immediately or |
494 | // - server waits for some time (enough for ur to init several streams on a |
495 | // client side); then suddenly it replies with GOAWAY, never processing any |
496 | // request. |
497 | if (clearTextHTTP2) |
498 | QSKIP("This test requires TLS with ALPN to work" ); |
499 | |
500 | QTest::addColumn<int>(name: "responseTimeoutMS" ); |
501 | QTest::newRow(dataTag: "ImmediateGOAWAY" ) << 0; |
502 | QTest::newRow(dataTag: "DelayedGOAWAY" ) << 1000; |
503 | } |
504 | |
505 | void tst_Http2::goaway() |
506 | { |
507 | using namespace Http2; |
508 | |
509 | QFETCH(const int, responseTimeoutMS); |
510 | |
511 | clearHTTP2State(); |
512 | |
513 | serverPort = 0; |
514 | nRequests = 3; |
515 | |
516 | ServerPtr srv(newServer(serverSettings: defaultServerSettings, connectionType: defaultConnectionType())); |
517 | srv->emulateGOAWAY(timeout: responseTimeoutMS); |
518 | QMetaObject::invokeMethod(obj: srv.data(), member: "startServer" , type: Qt::QueuedConnection); |
519 | runEventLoop(); |
520 | |
521 | QVERIFY(serverPort != 0); |
522 | |
523 | auto url = requestUrl(connnectionType: defaultConnectionType()); |
524 | // We have to store these replies, so that we can check errors later. |
525 | std::vector<QNetworkReply *> replies(nRequests); |
526 | for (int i = 0; i < nRequests; ++i) { |
527 | url.setPath(path: QString("/%1" ).arg(a: i)); |
528 | QNetworkRequest request(url); |
529 | request.setAttribute(code: QNetworkRequest::Http2AllowedAttribute, value: QVariant(true)); |
530 | replies[i] = manager->get(request); |
531 | QCOMPARE(replies[i]->error(), QNetworkReply::NoError); |
532 | connect(sender: replies[i], signal: &QNetworkReply::errorOccurred, receiver: this, slot: &tst_Http2::replyFinishedWithError); |
533 | // Since we're using self-signed certificates, ignore SSL errors: |
534 | replies[i]->ignoreSslErrors(); |
535 | } |
536 | |
537 | runEventLoop(ms: 5000 + responseTimeoutMS); |
538 | STOP_ON_FAILURE |
539 | |
540 | // No request processed, no 'replyFinished' slot calls: |
541 | QCOMPARE(nRequests, 0); |
542 | // Our server did not bother to send anything except a single GOAWAY frame: |
543 | QVERIFY(!prefaceOK); |
544 | QVERIFY(!serverGotSettingsACK); |
545 | } |
546 | |
547 | void tst_Http2::earlyResponse() |
548 | { |
549 | // In this test we'd like to verify client side can handle HEADERS frame while |
550 | // its stream is in 'open' state. To achieve this, we send a POST request |
551 | // with some payload, so that the client is first sending HEADERS and then |
552 | // DATA frames without END_STREAM flag set yet (thus the stream is in Stream::open |
553 | // state). Upon receiving the client's HEADERS frame our server ('redirector') |
554 | // immediately (without trying to read any DATA frames) responds with status |
555 | // code 308. The client should properly handle this. |
556 | |
557 | clearHTTP2State(); |
558 | |
559 | serverPort = 0; |
560 | nRequests = 1; |
561 | |
562 | ServerPtr targetServer(newServer(serverSettings: defaultServerSettings, connectionType: defaultConnectionType())); |
563 | |
564 | QMetaObject::invokeMethod(obj: targetServer.data(), member: "startServer" , type: Qt::QueuedConnection); |
565 | runEventLoop(); |
566 | |
567 | QVERIFY(serverPort != 0); |
568 | |
569 | const quint16 targetPort = serverPort; |
570 | serverPort = 0; |
571 | |
572 | ServerPtr redirector(newServer(serverSettings: defaultServerSettings, connectionType: defaultConnectionType())); |
573 | redirector->redirectOpenStream(targetPort); |
574 | |
575 | QMetaObject::invokeMethod(obj: redirector.data(), member: "startServer" , type: Qt::QueuedConnection); |
576 | runEventLoop(); |
577 | |
578 | QVERIFY(serverPort); |
579 | sendRequest(streamNumber: 1, priority: QNetworkRequest::NormalPriority, payload: {1000000, Qt::Uninitialized}); |
580 | |
581 | runEventLoop(); |
582 | STOP_ON_FAILURE |
583 | |
584 | QVERIFY(nRequests == 0); |
585 | QVERIFY(prefaceOK); |
586 | QVERIFY(serverGotSettingsACK); |
587 | } |
588 | |
589 | void tst_Http2::connectToHost_data() |
590 | { |
591 | // The attribute to set on a new request: |
592 | QTest::addColumn<QNetworkRequest::Attribute>(name: "requestAttribute" ); |
593 | // The corresponding (to the attribute above) connection type the |
594 | // server will use: |
595 | QTest::addColumn<H2Type>(name: "connectionType" ); |
596 | |
597 | #if QT_CONFIG(ssl) |
598 | QTest::addRow(format: "encrypted-h2-direct" ) << QNetworkRequest::Http2DirectAttribute << H2Type::h2Direct; |
599 | if (!clearTextHTTP2) |
600 | QTest::addRow(format: "encrypted-h2-ALPN" ) << QNetworkRequest::Http2AllowedAttribute << H2Type::h2Alpn; |
601 | #endif // QT_CONFIG(ssl) |
602 | // This works for all configurations, tests 'preconnect-http' scheme: |
603 | // h2 with protocol upgrade is not working for now (the logic is a bit |
604 | // complicated there ...). |
605 | QTest::addRow(format: "h2-direct" ) << QNetworkRequest::Http2DirectAttribute << H2Type::h2cDirect; |
606 | } |
607 | |
608 | void tst_Http2::connectToHost() |
609 | { |
610 | // QNetworkAccessManager::connectToHostEncrypted() and connectToHost() |
611 | // creates a special request with 'preconnect-https' or 'preconnect-http' |
612 | // schemes. At the level of the protocol handler we are supposed to report |
613 | // these requests as finished and wait for the real requests. This test will |
614 | // connect to a server with the first reply 'finished' signal meaning we |
615 | // indeed connected. At this point we check that a client preface was not |
616 | // sent yet, and no response received. Then we send the second (the real) |
617 | // request and do our usual checks. Since our server closes its listening |
618 | // socket on the first incoming connection (would not accept a new one), |
619 | // the successful completion of the second requests also means we were able |
620 | // to find a cached connection and re-use it. |
621 | |
622 | QFETCH(const QNetworkRequest::Attribute, requestAttribute); |
623 | QFETCH(const H2Type, connectionType); |
624 | |
625 | clearHTTP2State(); |
626 | |
627 | serverPort = 0; |
628 | nRequests = 2; |
629 | |
630 | ServerPtr targetServer(newServer(serverSettings: defaultServerSettings, connectionType)); |
631 | |
632 | #if QT_CONFIG(ssl) |
633 | Q_ASSERT(!clearTextHTTP2 || connectionType != H2Type::h2Alpn); |
634 | |
635 | #if QT_CONFIG(securetransport) |
636 | // Normally on macOS we use plain text only for SecureTransport |
637 | // does not support ALPN on the server side. With 'direct encrytped' |
638 | // we have to use TLS sockets (== private key) and thus suppress a |
639 | // keychain UI asking for permission to use a private key. |
640 | // Our CI has this, but somebody testing locally - will have a problem. |
641 | qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN" , QByteArray("1" )); |
642 | auto envRollback = qScopeGuard([](){ |
643 | qunsetenv("QT_SSL_USE_TEMPORARY_KEYCHAIN" ); |
644 | }); |
645 | #endif // QT_CONFIG(securetransport) |
646 | |
647 | #else |
648 | Q_ASSERT(connectionType == H2Type::h2c || connectionType == H2Type::h2cDirect); |
649 | Q_ASSERT(targetServer->isClearText()); |
650 | #endif // QT_CONFIG(ssl) |
651 | |
652 | QMetaObject::invokeMethod(obj: targetServer.data(), member: "startServer" , type: Qt::QueuedConnection); |
653 | runEventLoop(); |
654 | |
655 | QVERIFY(serverPort != 0); |
656 | |
657 | auto url = requestUrl(connnectionType: connectionType); |
658 | url.setPath(path: "/index.html" ); |
659 | |
660 | QNetworkReply *reply = nullptr; |
661 | // Here some mess with how we create this first reply: |
662 | #if QT_CONFIG(ssl) |
663 | if (!targetServer->isClearText()) { |
664 | // Let's emulate what QNetworkAccessManager::connectToHostEncrypted() does. |
665 | // Alas, we cannot use it directly, since it does not return the reply and |
666 | // also does not know the difference between H2 with ALPN or direct. |
667 | auto copyUrl = url; |
668 | copyUrl.setScheme(QLatin1String("preconnect-https" )); |
669 | QNetworkRequest request(copyUrl); |
670 | request.setAttribute(code: requestAttribute, value: true); |
671 | reply = manager->get(request); |
672 | // Since we're using self-signed certificates, ignore SSL errors: |
673 | reply->ignoreSslErrors(); |
674 | } else |
675 | #endif // QT_CONFIG(ssl) |
676 | { |
677 | // Emulating what QNetworkAccessManager::connectToHost() does with |
678 | // additional information that it cannot provide (the attribute). |
679 | auto copyUrl = url; |
680 | copyUrl.setScheme(QLatin1String("preconnect-http" )); |
681 | QNetworkRequest request(copyUrl); |
682 | request.setAttribute(code: requestAttribute, value: true); |
683 | reply = manager->get(request); |
684 | } |
685 | |
686 | connect(sender: reply, signal: &QNetworkReply::finished, slot: [this, reply]() { |
687 | --nRequests; |
688 | eventLoop.exitLoop(); |
689 | QCOMPARE(reply->error(), QNetworkReply::NoError); |
690 | QVERIFY(reply->isFinished()); |
691 | // Nothing received back: |
692 | QVERIFY(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isNull()); |
693 | QCOMPARE(reply->readAll().size(), 0); |
694 | }); |
695 | |
696 | runEventLoop(); |
697 | STOP_ON_FAILURE |
698 | |
699 | QCOMPARE(nRequests, 1); |
700 | |
701 | QNetworkRequest request(url); |
702 | request.setAttribute(code: requestAttribute, value: QVariant(true)); |
703 | reply = manager->get(request); |
704 | connect(sender: reply, signal: &QNetworkReply::finished, receiver: this, slot: &tst_Http2::replyFinished); |
705 | // Note, unlike the first request, when the connection is ecnrytped, we |
706 | // do not ignore TLS errors on this reply - we should re-use existing |
707 | // connection, there TLS errors were already ignored. |
708 | |
709 | runEventLoop(); |
710 | STOP_ON_FAILURE |
711 | |
712 | QVERIFY(nRequests == 0); |
713 | QVERIFY(prefaceOK); |
714 | QVERIFY(serverGotSettingsACK); |
715 | |
716 | QCOMPARE(reply->error(), QNetworkReply::NoError); |
717 | QVERIFY(reply->isFinished()); |
718 | } |
719 | |
720 | void tst_Http2::maxFrameSize() |
721 | { |
722 | #if !QT_CONFIG(ssl) |
723 | QSKIP("TLS support is needed for this test" ); |
724 | #endif // QT_CONFIG(ssl) |
725 | |
726 | // Here we test we send 'MAX_FRAME_SIZE' setting in our |
727 | // 'SETTINGS'. If done properly, our server will not chunk |
728 | // the payload into several DATA frames. |
729 | |
730 | #if QT_CONFIG(securetransport) |
731 | // Normally on macOS we use plain text only for SecureTransport |
732 | // does not support ALPN on the server side. With 'direct encrytped' |
733 | // we have to use TLS sockets (== private key) and thus suppress a |
734 | // keychain UI asking for permission to use a private key. |
735 | // Our CI has this, but somebody testing locally - will have a problem. |
736 | qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN" , QByteArray("1" )); |
737 | auto envRollback = qScopeGuard([](){ |
738 | qunsetenv("QT_SSL_USE_TEMPORARY_KEYCHAIN" ); |
739 | }); |
740 | #endif // QT_CONFIG(securetransport) |
741 | |
742 | auto connectionType = H2Type::h2Alpn; |
743 | auto attribute = QNetworkRequest::Http2AllowedAttribute; |
744 | if (clearTextHTTP2) { |
745 | connectionType = H2Type::h2Direct; |
746 | attribute = QNetworkRequest::Http2DirectAttribute; |
747 | } |
748 | |
749 | auto h2Config = qt_defaultH2Configuration(); |
750 | h2Config.setMaxFrameSize(Http2::minPayloadLimit * 3); |
751 | |
752 | serverPort = 0; |
753 | nRequests = 1; |
754 | |
755 | ServerPtr srv(newServer(serverSettings: defaultServerSettings, connectionType, |
756 | clientSettings: qt_H2ConfigurationToSettings(config: h2Config))); |
757 | srv->setResponseBody(QByteArray(Http2::minPayloadLimit * 2, 'q')); |
758 | QMetaObject::invokeMethod(obj: srv.data(), member: "startServer" , type: Qt::QueuedConnection); |
759 | runEventLoop(); |
760 | QVERIFY(serverPort != 0); |
761 | |
762 | const QSignalSpy frameCounter(srv.data(), &Http2Server::sendingData); |
763 | auto url = requestUrl(connnectionType: connectionType); |
764 | url.setPath(path: QString("/stream1.html" )); |
765 | |
766 | QNetworkRequest request(url); |
767 | request.setAttribute(code: attribute, value: QVariant(true)); |
768 | request.setHeader(header: QNetworkRequest::ContentTypeHeader, value: QVariant("text/plain" )); |
769 | request.setHttp2Configuration(h2Config); |
770 | |
771 | QNetworkReply *reply = manager->get(request); |
772 | reply->ignoreSslErrors(); |
773 | connect(sender: reply, signal: &QNetworkReply::finished, receiver: this, slot: &tst_Http2::replyFinished); |
774 | |
775 | runEventLoop(); |
776 | STOP_ON_FAILURE |
777 | |
778 | // Normally, with a 16kb limit, our server would split such |
779 | // a response into 3 'DATA' frames (16kb + 16kb + 0|END_STREAM). |
780 | QCOMPARE(frameCounter.count(), 1); |
781 | |
782 | QVERIFY(nRequests == 0); |
783 | QVERIFY(prefaceOK); |
784 | QVERIFY(serverGotSettingsACK); |
785 | } |
786 | |
787 | void tst_Http2::http2DATAFrames() |
788 | { |
789 | using namespace Http2; |
790 | |
791 | { |
792 | // 0. DATA frame with payload, no padding. |
793 | |
794 | FrameWriter writer(FrameType::DATA, FrameFlag::EMPTY, 1); |
795 | writer.append(val: uchar(1)); |
796 | writer.append(val: uchar(2)); |
797 | writer.append(val: uchar(3)); |
798 | |
799 | const Frame frame = writer.outboundFrame(); |
800 | const auto &buffer = frame.buffer; |
801 | // Frame's header is 9 bytes + 3 bytes of payload |
802 | // (+ 0 bytes of padding and no padding length): |
803 | QCOMPARE(int(buffer.size()), 12); |
804 | |
805 | QVERIFY(!frame.padding()); |
806 | QCOMPARE(int(frame.payloadSize()), 3); |
807 | QCOMPARE(int(frame.dataSize()), 3); |
808 | QCOMPARE(frame.dataBegin() - buffer.data(), 9); |
809 | QCOMPARE(char(*frame.dataBegin()), uchar(1)); |
810 | } |
811 | |
812 | { |
813 | // 1. DATA with padding. |
814 | |
815 | const int padLength = 10; |
816 | FrameWriter writer(FrameType::DATA, FrameFlag::END_STREAM | FrameFlag::PADDED, 1); |
817 | writer.append(val: uchar(padLength)); // The length of padding is 1 byte long. |
818 | writer.append(val: uchar(1)); |
819 | for (int i = 0; i < padLength; ++i) |
820 | writer.append(val: uchar(0)); |
821 | |
822 | const Frame frame = writer.outboundFrame(); |
823 | const auto &buffer = frame.buffer; |
824 | // Frame's header is 9 bytes + 1 byte for padding length |
825 | // + 1 byte of data + 10 bytes of padding: |
826 | QCOMPARE(int(buffer.size()), 21); |
827 | |
828 | QCOMPARE(frame.padding(), padLength); |
829 | QCOMPARE(int(frame.payloadSize()), 12); // Includes padding, its length + data. |
830 | QCOMPARE(int(frame.dataSize()), 1); |
831 | |
832 | // Skipping 9 bytes long header and padding length: |
833 | QCOMPARE(frame.dataBegin() - buffer.data(), 10); |
834 | |
835 | QCOMPARE(char(frame.dataBegin()[0]), uchar(1)); |
836 | QCOMPARE(char(frame.dataBegin()[1]), uchar(0)); |
837 | |
838 | QVERIFY(frame.flags().testFlag(FrameFlag::END_STREAM)); |
839 | QVERIFY(frame.flags().testFlag(FrameFlag::PADDED)); |
840 | } |
841 | { |
842 | // 2. DATA with PADDED flag, but 0 as padding length. |
843 | |
844 | FrameWriter writer(FrameType::DATA, FrameFlag::END_STREAM | FrameFlag::PADDED, 1); |
845 | |
846 | writer.append(val: uchar(0)); // Number of padding bytes is 1 byte long. |
847 | writer.append(val: uchar(1)); |
848 | |
849 | const Frame frame = writer.outboundFrame(); |
850 | const auto &buffer = frame.buffer; |
851 | |
852 | // Frame's header is 9 bytes + 1 byte for padding length + 1 byte of data |
853 | // + 0 bytes of padding: |
854 | QCOMPARE(int(buffer.size()), 11); |
855 | |
856 | QCOMPARE(frame.padding(), 0); |
857 | QCOMPARE(int(frame.payloadSize()), 2); // Includes padding (0 bytes), its length + data. |
858 | QCOMPARE(int(frame.dataSize()), 1); |
859 | |
860 | // Skipping 9 bytes long header and padding length: |
861 | QCOMPARE(frame.dataBegin() - buffer.data(), 10); |
862 | |
863 | QCOMPARE(char(*frame.dataBegin()), uchar(1)); |
864 | |
865 | QVERIFY(frame.flags().testFlag(FrameFlag::END_STREAM)); |
866 | QVERIFY(frame.flags().testFlag(FrameFlag::PADDED)); |
867 | } |
868 | } |
869 | |
870 | void tst_Http2::authenticationRequired_data() |
871 | { |
872 | QTest::addColumn<bool>(name: "success" ); |
873 | QTest::addColumn<bool>(name: "responseHEADOnly" ); |
874 | |
875 | QTest::addRow(format: "failed-auth" ) << false << true; |
876 | QTest::addRow(format: "successful-auth" ) << true << true; |
877 | // Include a DATA frame in the response from the remote server. An example would be receiving a |
878 | // JSON response on a request along with the 401 error. |
879 | QTest::addRow(format: "failed-auth-with-response" ) << false << false; |
880 | QTest::addRow(format: "successful-auth-with-response" ) << true << false; |
881 | } |
882 | |
883 | void tst_Http2::authenticationRequired() |
884 | { |
885 | clearHTTP2State(); |
886 | serverPort = 0; |
887 | QFETCH(const bool, responseHEADOnly); |
888 | POSTResponseHEADOnly = responseHEADOnly; |
889 | |
890 | QFETCH(const bool, success); |
891 | |
892 | ServerPtr targetServer(newServer(serverSettings: defaultServerSettings, connectionType: defaultConnectionType())); |
893 | targetServer->setResponseBody("Hello" ); |
894 | targetServer->setAuthenticationHeader("Basic realm=\"Shadow\"" ); |
895 | |
896 | QMetaObject::invokeMethod(obj: targetServer.data(), member: "startServer" , type: Qt::QueuedConnection); |
897 | runEventLoop(); |
898 | |
899 | QVERIFY(serverPort != 0); |
900 | |
901 | nRequests = 1; |
902 | |
903 | auto url = requestUrl(connnectionType: defaultConnectionType()); |
904 | url.setPath(path: "/index.html" ); |
905 | QNetworkRequest request(url); |
906 | |
907 | QByteArray expectedBody = "Hello, World!" ; |
908 | request.setHeader(header: QNetworkRequest::ContentTypeHeader, value: "application/x-www-form-urlencoded" ); |
909 | request.setAttribute(code: QNetworkRequest::Http2AllowedAttribute, value: QVariant(true)); |
910 | QScopedPointer<QNetworkReply> reply; |
911 | reply.reset(other: manager->post(request, data: expectedBody)); |
912 | |
913 | bool authenticationRequested = false; |
914 | connect(sender: manager.get(), signal: &QNetworkAccessManager::authenticationRequired, context: reply.get(), |
915 | slot: [&](QNetworkReply *, QAuthenticator *auth) { |
916 | authenticationRequested = true; |
917 | if (success) { |
918 | auth->setUser("admin" ); |
919 | auth->setPassword("admin" ); |
920 | } |
921 | }); |
922 | |
923 | QByteArray receivedBody; |
924 | connect(sender: targetServer.get(), signal: &Http2Server::receivedDATAFrame, context: reply.get(), |
925 | slot: [&receivedBody](quint32 streamID, const QByteArray &body) { |
926 | if (streamID == 3) // The expected body is on the retry, so streamID == 3 |
927 | receivedBody += body; |
928 | }); |
929 | |
930 | if (success) |
931 | connect(sender: reply.get(), signal: &QNetworkReply::finished, receiver: this, slot: &tst_Http2::replyFinished); |
932 | else |
933 | connect(sender: reply.get(), signal: &QNetworkReply::errorOccurred, receiver: this, slot: &tst_Http2::replyFinishedWithError); |
934 | // Since we're using self-signed certificates, |
935 | // ignore SSL errors: |
936 | reply->ignoreSslErrors(); |
937 | |
938 | runEventLoop(); |
939 | STOP_ON_FAILURE |
940 | |
941 | if (!success) |
942 | QCOMPARE(reply->error(), QNetworkReply::AuthenticationRequiredError); |
943 | // else: no error (is checked in tst_Http2::replyFinished) |
944 | |
945 | QVERIFY(authenticationRequested); |
946 | |
947 | const auto isAuthenticated = [](QByteArray bv) { |
948 | return bv == "Basic YWRtaW46YWRtaW4=" ; // admin:admin |
949 | }; |
950 | // Get the "authorization" header out from the server and make sure it's as expected: |
951 | auto = targetServer->requestAuthorizationHeader(); |
952 | QCOMPARE(isAuthenticated(reqAuthHeader), success); |
953 | if (success) |
954 | QCOMPARE(receivedBody, expectedBody); |
955 | // In the `!success` case we need to wait for the server to emit this or it might cause issues |
956 | // in the next test running after this. In the `success` case we anyway expect it to have been |
957 | // received. |
958 | QTRY_VERIFY(serverGotSettingsACK); |
959 | } |
960 | |
961 | void tst_Http2::redirect_data() |
962 | { |
963 | QTest::addColumn<int>(name: "maxRedirects" ); |
964 | QTest::addColumn<int>(name: "redirectCount" ); |
965 | QTest::addColumn<bool>(name: "success" ); |
966 | |
967 | QTest::addRow(format: "1-redirects-none-allowed-failure" ) << 0 << 1 << false; |
968 | QTest::addRow(format: "1-redirects-success" ) << 1 << 1 << true; |
969 | QTest::addRow(format: "2-redirects-1-allowed-failure" ) << 1 << 2 << false; |
970 | } |
971 | |
972 | void tst_Http2::redirect() |
973 | { |
974 | QFETCH(const int, maxRedirects); |
975 | QFETCH(const int, redirectCount); |
976 | QFETCH(const bool, success); |
977 | const QByteArray redirectUrl = "/b.html" ; |
978 | |
979 | clearHTTP2State(); |
980 | serverPort = 0; |
981 | |
982 | ServerPtr targetServer(newServer(serverSettings: defaultServerSettings, connectionType: defaultConnectionType())); |
983 | targetServer->setRedirect(redirectUrl, count: redirectCount); |
984 | |
985 | QMetaObject::invokeMethod(obj: targetServer.data(), member: "startServer" , type: Qt::QueuedConnection); |
986 | runEventLoop(); |
987 | |
988 | QVERIFY(serverPort != 0); |
989 | |
990 | nRequests = 1 + maxRedirects; |
991 | |
992 | auto originalUrl = requestUrl(connnectionType: defaultConnectionType()); |
993 | auto url = originalUrl; |
994 | url.setPath(path: "/index.html" ); |
995 | QNetworkRequest request(url); |
996 | request.setAttribute(code: QNetworkRequest::Http2AllowedAttribute, value: QVariant(true)); |
997 | request.setAttribute(code: QNetworkRequest::RedirectPolicyAttribute, value: QNetworkRequest::NoLessSafeRedirectPolicy); |
998 | request.setMaximumRedirectsAllowed(maxRedirects); |
999 | |
1000 | QScopedPointer<QNetworkReply> reply; |
1001 | reply.reset(other: manager->get(request)); |
1002 | |
1003 | if (success) { |
1004 | connect(sender: reply.get(), signal: &QNetworkReply::finished, receiver: this, slot: &tst_Http2::replyFinished); |
1005 | } else { |
1006 | connect(sender: reply.get(), signal: &QNetworkReply::errorOccurred, receiver: this, |
1007 | slot: &tst_Http2::replyFinishedWithError); |
1008 | } |
1009 | |
1010 | // Since we're using self-signed certificates, |
1011 | // ignore SSL errors: |
1012 | reply->ignoreSslErrors(); |
1013 | |
1014 | runEventLoop(); |
1015 | STOP_ON_FAILURE |
1016 | |
1017 | if (success) { |
1018 | QCOMPARE(reply->error(), QNetworkReply::NoError); |
1019 | QCOMPARE(reply->url().toString(), |
1020 | originalUrl.resolved(QString::fromLatin1(redirectUrl)).toString()); |
1021 | } else if (maxRedirects < redirectCount) { |
1022 | QCOMPARE(reply->error(), QNetworkReply::TooManyRedirectsError); |
1023 | } |
1024 | QTRY_VERIFY(serverGotSettingsACK); |
1025 | } |
1026 | |
1027 | void tst_Http2::serverStarted(quint16 port) |
1028 | { |
1029 | serverPort = port; |
1030 | stopEventLoop(); |
1031 | } |
1032 | |
1033 | void tst_Http2::clearHTTP2State() |
1034 | { |
1035 | windowUpdates = 0; |
1036 | prefaceOK = false; |
1037 | serverGotSettingsACK = false; |
1038 | POSTResponseHEADOnly = true; |
1039 | } |
1040 | |
1041 | void tst_Http2::runEventLoop(int ms) |
1042 | { |
1043 | eventLoop.enterLoopMSecs(ms); |
1044 | } |
1045 | |
1046 | void tst_Http2::stopEventLoop() |
1047 | { |
1048 | eventLoop.exitLoop(); |
1049 | } |
1050 | |
1051 | Http2Server *tst_Http2::newServer(const RawSettings &serverSettings, H2Type connectionType, |
1052 | const RawSettings &clientSettings) |
1053 | { |
1054 | using namespace Http2; |
1055 | auto srv = new Http2Server(connectionType, serverSettings, clientSettings); |
1056 | |
1057 | using Srv = Http2Server; |
1058 | using Cl = tst_Http2; |
1059 | |
1060 | connect(sender: srv, signal: &Srv::serverStarted, receiver: this, slot: &Cl::serverStarted); |
1061 | connect(sender: srv, signal: &Srv::clientPrefaceOK, receiver: this, slot: &Cl::clientPrefaceOK); |
1062 | connect(sender: srv, signal: &Srv::clientPrefaceError, receiver: this, slot: &Cl::clientPrefaceError); |
1063 | connect(sender: srv, signal: &Srv::serverSettingsAcked, receiver: this, slot: &Cl::serverSettingsAcked); |
1064 | connect(sender: srv, signal: &Srv::invalidFrame, receiver: this, slot: &Cl::invalidFrame); |
1065 | connect(sender: srv, signal: &Srv::invalidRequest, receiver: this, slot: &Cl::invalidRequest); |
1066 | connect(sender: srv, signal: &Srv::receivedRequest, receiver: this, slot: &Cl::receivedRequest); |
1067 | connect(sender: srv, signal: &Srv::receivedData, receiver: this, slot: &Cl::receivedData); |
1068 | connect(sender: srv, signal: &Srv::windowUpdate, receiver: this, slot: &Cl::windowUpdated); |
1069 | |
1070 | srv->moveToThread(thread: workerThread); |
1071 | |
1072 | return srv; |
1073 | } |
1074 | |
1075 | void tst_Http2::sendRequest(int streamNumber, |
1076 | QNetworkRequest::Priority priority, |
1077 | const QByteArray &payload, |
1078 | const QHttp2Configuration &h2Config) |
1079 | { |
1080 | auto url = requestUrl(connnectionType: defaultConnectionType()); |
1081 | url.setPath(path: QString("/stream%1.html" ).arg(a: streamNumber)); |
1082 | |
1083 | QNetworkRequest request(url); |
1084 | request.setAttribute(code: QNetworkRequest::Http2AllowedAttribute, value: QVariant(true)); |
1085 | request.setAttribute(code: QNetworkRequest::FollowRedirectsAttribute, value: QVariant(true)); |
1086 | request.setHeader(header: QNetworkRequest::ContentTypeHeader, value: QVariant("text/plain" )); |
1087 | request.setPriority(priority); |
1088 | request.setHttp2Configuration(h2Config); |
1089 | |
1090 | QNetworkReply *reply = nullptr; |
1091 | if (payload.size()) |
1092 | reply = manager->post(request, data: payload); |
1093 | else |
1094 | reply = manager->get(request); |
1095 | |
1096 | reply->ignoreSslErrors(); |
1097 | connect(sender: reply, signal: &QNetworkReply::finished, receiver: this, slot: &tst_Http2::replyFinished); |
1098 | } |
1099 | |
1100 | QUrl tst_Http2::requestUrl(H2Type connectionType) const |
1101 | { |
1102 | #if !QT_CONFIG(ssl) |
1103 | Q_ASSERT(connectionType != H2Type::h2Alpn && connectionType != H2Type::h2Direct); |
1104 | #endif |
1105 | static auto url = QUrl(QLatin1String(clearTextHTTP2 ? "http://127.0.0.1" : "https://127.0.0.1" )); |
1106 | url.setPort(serverPort); |
1107 | // Clear text may mean no-TLS-at-all or crappy-TLS-without-ALPN. |
1108 | switch (connectionType) { |
1109 | case H2Type::h2Alpn: |
1110 | case H2Type::h2Direct: |
1111 | url.setScheme(QStringLiteral("https" )); |
1112 | break; |
1113 | case H2Type::h2c: |
1114 | case H2Type::h2cDirect: |
1115 | url.setScheme(QStringLiteral("http" )); |
1116 | break; |
1117 | } |
1118 | |
1119 | return url; |
1120 | } |
1121 | |
1122 | void tst_Http2::clientPrefaceOK() |
1123 | { |
1124 | prefaceOK = true; |
1125 | } |
1126 | |
1127 | void tst_Http2::clientPrefaceError() |
1128 | { |
1129 | prefaceOK = false; |
1130 | } |
1131 | |
1132 | void tst_Http2::serverSettingsAcked() |
1133 | { |
1134 | serverGotSettingsACK = true; |
1135 | if (!nRequests) |
1136 | stopEventLoop(); |
1137 | } |
1138 | |
1139 | void tst_Http2::invalidFrame() |
1140 | { |
1141 | } |
1142 | |
1143 | void tst_Http2::invalidRequest(quint32 streamID) |
1144 | { |
1145 | Q_UNUSED(streamID) |
1146 | } |
1147 | |
1148 | void tst_Http2::decompressionFailed(quint32 streamID) |
1149 | { |
1150 | Q_UNUSED(streamID) |
1151 | } |
1152 | |
1153 | void tst_Http2::receivedRequest(quint32 streamID) |
1154 | { |
1155 | ++nSentRequests; |
1156 | qDebug() << " server got a request on stream" << streamID; |
1157 | Http2Server *srv = qobject_cast<Http2Server *>(object: sender()); |
1158 | Q_ASSERT(srv); |
1159 | QMetaObject::invokeMethod(obj: srv, member: "sendResponse" , type: Qt::QueuedConnection, |
1160 | Q_ARG(quint32, streamID), |
1161 | Q_ARG(bool, false /*non-empty body*/)); |
1162 | } |
1163 | |
1164 | void tst_Http2::receivedData(quint32 streamID) |
1165 | { |
1166 | qDebug() << " server got a 'POST' request on stream" << streamID; |
1167 | Http2Server *srv = qobject_cast<Http2Server *>(object: sender()); |
1168 | Q_ASSERT(srv); |
1169 | QMetaObject::invokeMethod(obj: srv, member: "sendResponse" , type: Qt::QueuedConnection, |
1170 | Q_ARG(quint32, streamID), |
1171 | Q_ARG(bool, POSTResponseHEADOnly /*true = HEADERS only*/)); |
1172 | } |
1173 | |
1174 | void tst_Http2::windowUpdated(quint32 streamID) |
1175 | { |
1176 | Q_UNUSED(streamID) |
1177 | |
1178 | ++windowUpdates; |
1179 | } |
1180 | |
1181 | void tst_Http2::replyFinished() |
1182 | { |
1183 | QVERIFY(nRequests); |
1184 | |
1185 | if (const auto reply = qobject_cast<QNetworkReply *>(object: sender())) { |
1186 | if (reply->error() != QNetworkReply::NoError) |
1187 | stopEventLoop(); |
1188 | |
1189 | QCOMPARE(reply->error(), QNetworkReply::NoError); |
1190 | |
1191 | const QVariant http2Used(reply->attribute(code: QNetworkRequest::Http2WasUsedAttribute)); |
1192 | if (!http2Used.isValid() || !http2Used.toBool()) |
1193 | stopEventLoop(); |
1194 | |
1195 | QVERIFY(http2Used.isValid()); |
1196 | QVERIFY(http2Used.toBool()); |
1197 | |
1198 | const QVariant spdyUsed(reply->attribute(code: QNetworkRequest::SpdyWasUsedAttribute)); |
1199 | if (!spdyUsed.isValid() || spdyUsed.toBool()) |
1200 | stopEventLoop(); |
1201 | |
1202 | QVERIFY(spdyUsed.isValid()); |
1203 | QVERIFY(!spdyUsed.toBool()); |
1204 | |
1205 | const QVariant code(reply->attribute(code: QNetworkRequest::HttpStatusCodeAttribute)); |
1206 | if (!code.isValid() || !code.canConvert<int>() || code.value<int>() != 200) |
1207 | stopEventLoop(); |
1208 | |
1209 | QVERIFY(code.isValid()); |
1210 | QVERIFY(code.canConvert<int>()); |
1211 | QCOMPARE(code.value<int>(), 200); |
1212 | } |
1213 | |
1214 | --nRequests; |
1215 | if (!nRequests && serverGotSettingsACK) |
1216 | stopEventLoop(); |
1217 | } |
1218 | |
1219 | void tst_Http2::replyFinishedWithError() |
1220 | { |
1221 | QVERIFY(nRequests); |
1222 | |
1223 | if (const auto reply = qobject_cast<QNetworkReply *>(object: sender())) { |
1224 | // For now this is a 'generic' code, it just verifies some error was |
1225 | // reported without testing its type. |
1226 | if (reply->error() == QNetworkReply::NoError) |
1227 | stopEventLoop(); |
1228 | QVERIFY(reply->error() != QNetworkReply::NoError); |
1229 | } |
1230 | |
1231 | --nRequests; |
1232 | if (!nRequests) |
1233 | stopEventLoop(); |
1234 | } |
1235 | |
1236 | QT_END_NAMESPACE |
1237 | |
1238 | QTEST_MAIN(tst_Http2) |
1239 | |
1240 | #include "tst_http2.moc" |
1241 | |