Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
testhttpserver.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
5#include <QTcpSocket>
6#include <QDebug>
7#include <QFile>
8#include <QTimer>
9#include <QTest>
10#include <QQmlFile>
11
13
14/*!
15\internal
16\class TestHTTPServer
17\brief provides a very, very basic HTTP server for testing.
18
19Inside the test case, an instance of TestHTTPServer should be created, with the
20appropriate port to listen on. The server will listen on the localhost interface.
21
22Directories to serve can then be added to server, which will be added as "roots".
23Each root can be added as a Normal, Delay or Disconnect root. Requests for files
24within a Normal root are returned immediately. Request for files within a Delay
25root are delayed for 500ms, and then served. Requests for files within a Disconnect
26directory cause the server to disconnect immediately. A request for a file that isn't
27found in any root will return a 404 error.
28
29If you have the following directory structure:
30
31\code
32disconnect/disconnectTest.qml
33files/main.qml
34files/Button.qml
35files/content/WebView.qml
36slowFiles/slowMain.qml
37\endcode
38it can be added like this:
39\code
40TestHTTPServer server;
41QVERIFY2(server.listen(14445), qPrintable(server.errorString()));
42server.serveDirectory("disconnect", TestHTTPServer::Disconnect);
43server.serveDirectory("files");
44server.serveDirectory("slowFiles", TestHTTPServer::Delay);
45\endcode
46
47The following request urls will then result in the appropriate action:
48\table
49\header \li URL \li Action
50\row \li http://localhost:14445/disconnectTest.qml \li Disconnection
51\row \li http://localhost:14445/main.qml \li main.qml returned immediately
52\row \li http://localhost:14445/Button.qml \li Button.qml returned immediately
53\row \li http://localhost:14445/content/WebView.qml \li content/WebView.qml returned immediately
54\row \li http://localhost:14445/slowMain.qml \li slowMain.qml returned after 500ms
55\endtable
56*/
57
59 "HTTP2-Settings", // We ignore this
60 "Upgrade", // We ignore this as well
61};
62
63static QUrl localHostUrl(quint16 port)
64{
65 QUrl url;
66 url.setScheme(QStringLiteral("http"));
67 url.setHost(QStringLiteral("127.0.0.1"));
68 url.setPort(port);
69 return url;
70}
71
72TestHTTPServer::TestHTTPServer()
73 : m_state(AwaitingHeader)
74{
75 QObject::connect(&m_server, &QTcpServer::newConnection, this, &TestHTTPServer::newConnection);
76}
77
78bool TestHTTPServer::listen()
79{
80 return m_server.listen(QHostAddress::LocalHost, 0);
81}
82
83QUrl TestHTTPServer::baseUrl() const
84{
85 return localHostUrl(m_server.serverPort());
86}
87
88quint16 TestHTTPServer::port() const
89{
90 return m_server.serverPort();
91}
92
93QUrl TestHTTPServer::url(const QString &documentPath) const
94{
95 return baseUrl().resolved(QUrl{documentPath});
96}
97
98QString TestHTTPServer::urlString(const QString &documentPath) const
99{
100 return url(documentPath).toString();
101}
102
103QString TestHTTPServer::errorString() const
104{
105 return m_server.errorString();
106}
107
108bool TestHTTPServer::serveDirectory(const QString &dir, Mode mode)
109{
110 m_directories.append(std::make_pair(dir, mode));
111 return true;
112}
113
114/*
115 Add an alias, so that if filename is requested and does not exist,
116 alias may be returned.
117*/
118void TestHTTPServer::addAlias(const QString &filename, const QString &alias)
119{
120 m_aliases.insert(filename, alias);
121}
122
123void TestHTTPServer::addRedirect(const QString &filename, const QString &redirectName)
124{
125 m_redirects.insert(filename, redirectName);
126}
127
128void TestHTTPServer::registerFileNameForContentSubstitution(const QString &fileName)
129{
130 m_contentSubstitutedFileNames.insert(fileName);
131}
132
133bool TestHTTPServer::wait(const QUrl &expect, const QUrl &reply, const QUrl &body)
134{
135 m_state = AwaitingHeader;
136 m_data.clear();
137
138 QFile expectFile(QQmlFile::urlToLocalFileOrQrc(expect));
139 if (!expectFile.open(QIODevice::ReadOnly))
140 return false;
141
142 QFile replyFile(QQmlFile::urlToLocalFileOrQrc(reply));
143 if (!replyFile.open(QIODevice::ReadOnly))
144 return false;
145
146 m_bodyData = QByteArray();
147 if (body.isValid()) {
148 QFile bodyFile(QQmlFile::urlToLocalFileOrQrc(body));
149 if (!bodyFile.open(QIODevice::ReadOnly))
150 return false;
151 m_bodyData = bodyFile.readAll();
152 }
153
154 const QByteArray serverHostUrl
155 = QByteArrayLiteral("127.0.0.1:")+ QByteArray::number(m_server.serverPort());
156
157 QByteArray line;
158 bool headers_done = false;
159 while (!(line = expectFile.readLine()).isEmpty()) {
160 line.replace('\r', "");
161 if (headers_done) {
162 m_waitData.body.append(line);
163 } else if (line.at(0) == '\n') {
164 headers_done = true;
165 } else if (line.endsWith("{{Ignore}}\n")) {
166 m_waitData.headerPrefixes.append(line.left(line.size() - strlen("{{Ignore}}\n")));
167 } else {
168 line.replace("{{ServerHostUrl}}", serverHostUrl);
169 m_waitData.headerExactMatches.append(line);
170 }
171 }
172
173 m_replyData = replyFile.readAll();
174
175 if (!m_replyData.endsWith('\n'))
176 m_replyData.append('\n');
177 m_replyData.append("Content-length: ");
178 m_replyData.append(QByteArray::number(m_bodyData.size()));
179 m_replyData.append("\n\n");
180
181 for (int ii = 0; ii < m_replyData.size(); ++ii) {
182 if (m_replyData.at(ii) == '\n' && (!ii || m_replyData.at(ii - 1) != '\r')) {
183 m_replyData.insert(ii, '\r');
184 ++ii;
185 }
186 }
187 m_replyData.append(m_bodyData);
188
189 return true;
190}
191
192bool TestHTTPServer::hasFailed() const
193{
194 return m_state == Failed;
195}
196
197void TestHTTPServer::newConnection()
198{
199 QTcpSocket *socket = m_server.nextPendingConnection();
200 if (!socket)
201 return;
202
203 if (!m_directories.isEmpty())
204 m_dataCache.insert(socket, QByteArray());
205
206 QObject::connect(socket, &QAbstractSocket::disconnected, this, &TestHTTPServer::disconnected);
207 QObject::connect(socket, &QIODevice::readyRead, this, &TestHTTPServer::readyRead);
208}
209
210void TestHTTPServer::disconnected()
211{
212 QTcpSocket *socket = qobject_cast<QTcpSocket *>(sender());
213 if (!socket)
214 return;
215
216 m_dataCache.remove(socket);
217 for (int ii = 0; ii < m_toSend.size(); ++ii) {
218 if (m_toSend.at(ii).first == socket) {
219 m_toSend.removeAt(ii);
220 --ii;
221 }
222 }
223 socket->disconnect();
224 socket->deleteLater();
225}
226
227void TestHTTPServer::readyRead()
228{
229 QTcpSocket *socket = qobject_cast<QTcpSocket *>(sender());
230 if (!socket || socket->state() == QTcpSocket::ClosingState)
231 return;
232
233 if (!m_directories.isEmpty()) {
234 serveGET(socket, socket->readAll());
235 return;
236 }
237
238 if (m_state == Failed || (m_waitData.body.isEmpty() && m_waitData.headerExactMatches.size() == 0)) {
239 qWarning() << "TestHTTPServer: Unexpected data" << socket->readAll();
240 return;
241 }
242
243 if (m_state == AwaitingHeader) {
244 QByteArray line;
245 while (!(line = socket->readLine()).isEmpty()) {
246 line.replace('\r', "");
247 if (line.at(0) == '\n') {
248 m_state = AwaitingData;
249 m_data += socket->readAll();
250 break;
251 } else {
252 qsizetype prefixIndex = line.indexOf(':');
253 if (prefixIndex > 0) {
254 const QByteArray prefix = line.left(prefixIndex);
255 line = std::move(line).replace(prefix, prefix.toLower());
256 }
257 bool prefixFound = false;
258 for (const QByteArray &prefix : m_waitData.headerPrefixes) {
259 if (line.startsWith(prefix)) {
260 prefixFound = true;
261 break;
262 }
263 }
264 for (QByteArrayView ignore : ignoredHeaders) {
265 if (line.startsWith(ignore)) {
266 prefixFound = true;
267 break;
268 }
269 }
270
271 if (!prefixFound && !m_waitData.headerExactMatches.contains(line)) {
272 qWarning() << "TestHTTPServer: Unexpected header:" << line
273 << "\nExpected exact headers: " << m_waitData.headerExactMatches
274 << "\nExpected header prefixes: " << m_waitData.headerPrefixes;
275 m_state = Failed;
276 socket->disconnectFromHost();
277 return;
278 }
279 }
280 }
281 } else {
282 m_data += socket->readAll();
283 }
284
285 if (!m_data.isEmpty() || m_waitData.body.isEmpty()) {
286 if (m_waitData.body != m_data) {
287 qWarning() << "TestHTTPServer: Unexpected data" << m_data << "\nExpected: " << m_waitData.body;
288 m_state = Failed;
289 } else {
290 socket->write(m_replyData);
291 }
292 socket->disconnectFromHost();
293 }
294}
295
296bool TestHTTPServer::reply(QTcpSocket *socket, const QByteArray &fileNameIn)
297{
298 const QString fileName = QLatin1String(fileNameIn);
299 if (m_redirects.contains(fileName)) {
300 const QByteArray response
301 = "HTTP/1.1 302 Found\r\nContent-length: 0\r\nContent-type: text/html; charset=UTF-8\r\nLocation: "
302 + m_redirects.value(fileName).toUtf8() + "\r\n\r\n";
303 socket->write(response);
304 return true;
305 }
306
307 if (!fileName.contains(QLatin1StringView(".."))) {
308 for (int ii = 0; ii < m_directories.size(); ++ii) {
309 const QString &dir = m_directories.at(ii).first;
310 const Mode mode = m_directories.at(ii).second;
311
312 QString dirFile = dir + QLatin1Char('/') + fileName;
313
314 if (!QFile::exists(dirFile)) {
315 const QHash<QString, QString>::const_iterator it = m_aliases.constFind(fileName);
316 if (it != m_aliases.constEnd())
317 dirFile = dir + QLatin1Char('/') + it.value();
318 }
319
320 QFile file(dirFile);
321 if (file.open(QIODevice::ReadOnly)) {
322
323 if (mode == Disconnect)
324 return true;
325
326 QByteArray data = file.readAll();
327 if (m_contentSubstitutedFileNames.contains(QLatin1Char('/') + fileName))
328 data.replace(QByteArrayLiteral("{{ServerBaseUrl}}"), baseUrl().toString().toUtf8());
329
330 QByteArray response
331 = "HTTP/1.0 200 OK\r\nContent-type: text/html; charset=UTF-8\r\nContent-length: ";
332 response += QByteArray::number(data.size());
333 response += "\r\n\r\n";
334 response += data;
335
336 if (mode == Delay) {
337 m_toSend.append(std::make_pair(socket, response));
338 QTimer::singleShot(500, this, &TestHTTPServer::sendOne);
339 return false;
340 }
341
342 if (response.length() <= m_chunkSize) {
343 socket->write(response);
344 return true;
345 }
346
347 socket->write(response.left(m_chunkSize));
348 for (qsizetype offset = m_chunkSize, end = response.length(); offset < end;
349 offset += m_chunkSize) {
350 m_toSend.append(std::make_pair(socket, response.mid(offset, m_chunkSize)));
351 }
352
353 QTimer::singleShot(1, this, &TestHTTPServer::sendChunk);
354 return false;
355 }
356 }
357 }
358 socket->write("HTTP/1.0 404 Not found\r\nContent-type: text/html; charset=UTF-8\r\n\r\n");
359
360 return true;
361}
362
363void TestHTTPServer::sendDelayedItem()
364{
365 sendOne();
366}
367
368void TestHTTPServer::sendOne()
369{
370 if (!m_toSend.isEmpty()) {
371 m_toSend.first().first->write(m_toSend.first().second);
372 m_toSend.first().first->close();
373 m_toSend.removeFirst();
374 }
375}
376
377void TestHTTPServer::sendChunk()
378{
379 const auto chunk = m_toSend.takeFirst();
380 chunk.first->write(chunk.second);
381 if (m_toSend.isEmpty())
382 chunk.first->close();
383 else
384 QTimer::singleShot(1, this, &TestHTTPServer::sendChunk);
385}
386
387void TestHTTPServer::serveGET(QTcpSocket *socket, const QByteArray &data)
388{
389 const QHash<QTcpSocket *, QByteArray>::iterator it = m_dataCache.find(socket);
390 if (it == m_dataCache.end())
391 return;
392
393 QByteArray &total = it.value();
394 total.append(data);
395
396 if (total.contains("\n\r\n")) {
397 bool close = true;
398 if (total.startsWith("GET /")) {
399 const int space = total.indexOf(' ', 4);
400 if (space != -1)
401 close = reply(socket, total.mid(5, space - 5));
402 }
403 m_dataCache.erase(it);
404 if (close)
405 socket->disconnectFromHost();
406 }
407}
408
409ThreadedTestHTTPServer::ThreadedTestHTTPServer(const QString &dir, TestHTTPServer::Mode mode) :
410 m_port(0)
411{
412 m_dirs[dir] = mode;
413 start();
414}
415
416ThreadedTestHTTPServer::ThreadedTestHTTPServer(const QHash<QString, TestHTTPServer::Mode> &dirs) :
417 m_dirs(dirs), m_port(0)
418{
419 start();
420}
421
423{
424 quit();
425 wait();
426}
427
429{
430 return localHostUrl(m_port);
431}
432
433QUrl ThreadedTestHTTPServer::url(const QString &documentPath) const
434{
435 return baseUrl().resolved(QUrl{documentPath});
436}
437
438QString ThreadedTestHTTPServer::urlString(const QString &documentPath) const
439{
440 return url(documentPath).toString();
441}
442
444{
445 TestHTTPServer server;
446 {
447 QMutexLocker locker(&m_mutex);
448 QVERIFY2(server.listen(), qPrintable(server.errorString()));
449 m_port = server.port();
450 for (QHash<QString, TestHTTPServer::Mode>::ConstIterator i = m_dirs.constBegin();
451 i != m_dirs.constEnd(); ++i) {
452 server.serveDirectory(i.key(), i.value());
453 }
454 m_condition.wakeAll();
455 }
456 exec();
457}
458
459void ThreadedTestHTTPServer::start()
460{
461 QMutexLocker locker(&m_mutex);
462 QThread::start();
463 m_condition.wait(&m_mutex);
464}
465
466QT_END_NAMESPACE
467
468#include "moc_testhttpserver_p.cpp"
QString urlString(const QString &documentPath) const
ThreadedTestHTTPServer(const QHash< QString, TestHTTPServer::Mode > &dirs)
QUrl url(const QString &documentPath) const
Combined button and popup list for selecting options.
static QList< QByteArrayView > ignoredHeaders
static QUrl localHostUrl(quint16 port)