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(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 for (int ii = 0; ii < m_directories.size(); ++ii) {
308 const QString &dir = m_directories.at(ii).first;
309 const Mode mode = m_directories.at(ii).second;
310
311 QString dirFile = dir + QLatin1Char('/') + fileName;
312
313 if (!QFile::exists(dirFile)) {
314 const QHash<QString, QString>::const_iterator it = m_aliases.constFind(fileName);
315 if (it != m_aliases.constEnd())
316 dirFile = dir + QLatin1Char('/') + it.value();
317 }
318
319 QFile file(dirFile);
320 if (file.open(QIODevice::ReadOnly)) {
321
322 if (mode == Disconnect)
323 return true;
324
325 QByteArray data = file.readAll();
326 if (m_contentSubstitutedFileNames.contains(QLatin1Char('/') + fileName))
327 data.replace(QByteArrayLiteral("{{ServerBaseUrl}}"), baseUrl().toString().toUtf8());
328
329 QByteArray response
330 = "HTTP/1.0 200 OK\r\nContent-type: text/html; charset=UTF-8\r\nContent-length: ";
331 response += QByteArray::number(data.size());
332 response += "\r\n\r\n";
333 response += data;
334
335 if (mode == Delay) {
336 m_toSend.append(std::make_pair(socket, response));
337 QTimer::singleShot(500, this, &TestHTTPServer::sendOne);
338 return false;
339 }
340
341 if (response.length() <= m_chunkSize) {
342 socket->write(response);
343 return true;
344 }
345
346 socket->write(response.left(m_chunkSize));
347 for (qsizetype offset = m_chunkSize, end = response.length(); offset < end;
348 offset += m_chunkSize) {
349 m_toSend.append(std::make_pair(socket, response.mid(offset, m_chunkSize)));
350 }
351
352 QTimer::singleShot(1, this, &TestHTTPServer::sendChunk);
353 return false;
354 }
355 }
356
357 socket->write("HTTP/1.0 404 Not found\r\nContent-type: text/html; charset=UTF-8\r\n\r\n");
358
359 return true;
360}
361
362void TestHTTPServer::sendDelayedItem()
363{
364 sendOne();
365}
366
367void TestHTTPServer::sendOne()
368{
369 if (!m_toSend.isEmpty()) {
370 m_toSend.first().first->write(m_toSend.first().second);
371 m_toSend.first().first->close();
372 m_toSend.removeFirst();
373 }
374}
375
376void TestHTTPServer::sendChunk()
377{
378 const auto chunk = m_toSend.takeFirst();
379 chunk.first->write(chunk.second);
380 if (m_toSend.isEmpty())
381 chunk.first->close();
382 else
383 QTimer::singleShot(1, this, &TestHTTPServer::sendChunk);
384}
385
386void TestHTTPServer::serveGET(QTcpSocket *socket, const QByteArray &data)
387{
388 const QHash<QTcpSocket *, QByteArray>::iterator it = m_dataCache.find(socket);
389 if (it == m_dataCache.end())
390 return;
391
392 QByteArray &total = it.value();
393 total.append(data);
394
395 if (total.contains("\n\r\n")) {
396 bool close = true;
397 if (total.startsWith("GET /")) {
398 const int space = total.indexOf(' ', 4);
399 if (space != -1)
400 close = reply(socket, total.mid(5, space - 5));
401 }
402 m_dataCache.erase(it);
403 if (close)
404 socket->disconnectFromHost();
405 }
406}
407
408ThreadedTestHTTPServer::ThreadedTestHTTPServer(const QString &dir, TestHTTPServer::Mode mode) :
409 m_port(0)
410{
411 m_dirs[dir] = mode;
412 start();
413}
414
415ThreadedTestHTTPServer::ThreadedTestHTTPServer(const QHash<QString, TestHTTPServer::Mode> &dirs) :
416 m_dirs(dirs), m_port(0)
417{
418 start();
419}
420
422{
423 quit();
424 wait();
425}
426
428{
429 return localHostUrl(m_port);
430}
431
432QUrl ThreadedTestHTTPServer::url(const QString &documentPath) const
433{
434 return baseUrl().resolved(documentPath);
435}
436
437QString ThreadedTestHTTPServer::urlString(const QString &documentPath) const
438{
439 return url(documentPath).toString();
440}
441
443{
444 TestHTTPServer server;
445 {
446 QMutexLocker locker(&m_mutex);
447 QVERIFY2(server.listen(), qPrintable(server.errorString()));
448 m_port = server.port();
449 for (QHash<QString, TestHTTPServer::Mode>::ConstIterator i = m_dirs.constBegin();
450 i != m_dirs.constEnd(); ++i) {
451 server.serveDirectory(i.key(), i.value());
452 }
453 m_condition.wakeAll();
454 }
455 exec();
456}
457
458void ThreadedTestHTTPServer::start()
459{
460 QMutexLocker locker(&m_mutex);
461 QThread::start();
462 m_condition.wait(&m_mutex);
463}
464
465QT_END_NAMESPACE
466
467#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)