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 bool prefixFound = false;
253 for (const QByteArray &prefix : m_waitData.headerPrefixes) {
254 if (line.startsWith(prefix)) {
255 prefixFound = true;
256 break;
257 }
258 }
259 for (QByteArrayView ignore : ignoredHeaders) {
260 if (line.startsWith(ignore)) {
261 prefixFound = true;
262 break;
263 }
264 }
265
266 if (!prefixFound && !m_waitData.headerExactMatches.contains(line)) {
267 qWarning() << "TestHTTPServer: Unexpected header:" << line
268 << "\nExpected exact headers: " << m_waitData.headerExactMatches
269 << "\nExpected header prefixes: " << m_waitData.headerPrefixes;
270 m_state = Failed;
271 socket->disconnectFromHost();
272 return;
273 }
274 }
275 }
276 } else {
277 m_data += socket->readAll();
278 }
279
280 if (!m_data.isEmpty() || m_waitData.body.isEmpty()) {
281 if (m_waitData.body != m_data) {
282 qWarning() << "TestHTTPServer: Unexpected data" << m_data << "\nExpected: " << m_waitData.body;
283 m_state = Failed;
284 } else {
285 socket->write(m_replyData);
286 }
287 socket->disconnectFromHost();
288 }
289}
290
291bool TestHTTPServer::reply(QTcpSocket *socket, const QByteArray &fileNameIn)
292{
293 const QString fileName = QLatin1String(fileNameIn);
294 if (m_redirects.contains(fileName)) {
295 const QByteArray response
296 = "HTTP/1.1 302 Found\r\nContent-length: 0\r\nContent-type: text/html; charset=UTF-8\r\nLocation: "
297 + m_redirects.value(fileName).toUtf8() + "\r\n\r\n";
298 socket->write(response);
299 return true;
300 }
301
302 for (int ii = 0; ii < m_directories.size(); ++ii) {
303 const QString &dir = m_directories.at(ii).first;
304 const Mode mode = m_directories.at(ii).second;
305
306 QString dirFile = dir + QLatin1Char('/') + fileName;
307
308 if (!QFile::exists(dirFile)) {
309 const QHash<QString, QString>::const_iterator it = m_aliases.constFind(fileName);
310 if (it != m_aliases.constEnd())
311 dirFile = dir + QLatin1Char('/') + it.value();
312 }
313
314 QFile file(dirFile);
315 if (file.open(QIODevice::ReadOnly)) {
316
317 if (mode == Disconnect)
318 return true;
319
320 QByteArray data = file.readAll();
321 if (m_contentSubstitutedFileNames.contains(QLatin1Char('/') + fileName))
322 data.replace(QByteArrayLiteral("{{ServerBaseUrl}}"), baseUrl().toString().toUtf8());
323
324 QByteArray response
325 = "HTTP/1.0 200 OK\r\nContent-type: text/html; charset=UTF-8\r\nContent-length: ";
326 response += QByteArray::number(data.size());
327 response += "\r\n\r\n";
328 response += data;
329
330 if (mode == Delay) {
331 m_toSend.append(std::make_pair(socket, response));
332 QTimer::singleShot(500, this, &TestHTTPServer::sendOne);
333 return false;
334 }
335
336 if (response.length() <= m_chunkSize) {
337 socket->write(response);
338 return true;
339 }
340
341 socket->write(response.left(m_chunkSize));
342 for (qsizetype offset = m_chunkSize, end = response.length(); offset < end;
343 offset += m_chunkSize) {
344 m_toSend.append(std::make_pair(socket, response.mid(offset, m_chunkSize)));
345 }
346
347 QTimer::singleShot(1, this, &TestHTTPServer::sendChunk);
348 return false;
349 }
350 }
351
352 socket->write("HTTP/1.0 404 Not found\r\nContent-type: text/html; charset=UTF-8\r\n\r\n");
353
354 return true;
355}
356
357void TestHTTPServer::sendDelayedItem()
358{
359 sendOne();
360}
361
362void TestHTTPServer::sendOne()
363{
364 if (!m_toSend.isEmpty()) {
365 m_toSend.first().first->write(m_toSend.first().second);
366 m_toSend.first().first->close();
367 m_toSend.removeFirst();
368 }
369}
370
371void TestHTTPServer::sendChunk()
372{
373 const auto chunk = m_toSend.takeFirst();
374 chunk.first->write(chunk.second);
375 if (m_toSend.isEmpty())
376 chunk.first->close();
377 else
378 QTimer::singleShot(1, this, &TestHTTPServer::sendChunk);
379}
380
381void TestHTTPServer::serveGET(QTcpSocket *socket, const QByteArray &data)
382{
383 const QHash<QTcpSocket *, QByteArray>::iterator it = m_dataCache.find(socket);
384 if (it == m_dataCache.end())
385 return;
386
387 QByteArray &total = it.value();
388 total.append(data);
389
390 if (total.contains("\n\r\n")) {
391 bool close = true;
392 if (total.startsWith("GET /")) {
393 const int space = total.indexOf(' ', 4);
394 if (space != -1)
395 close = reply(socket, total.mid(5, space - 5));
396 }
397 m_dataCache.erase(it);
398 if (close)
399 socket->disconnectFromHost();
400 }
401}
402
403ThreadedTestHTTPServer::ThreadedTestHTTPServer(const QString &dir, TestHTTPServer::Mode mode) :
404 m_port(0)
405{
406 m_dirs[dir] = mode;
407 start();
408}
409
410ThreadedTestHTTPServer::ThreadedTestHTTPServer(const QHash<QString, TestHTTPServer::Mode> &dirs) :
411 m_dirs(dirs), m_port(0)
412{
413 start();
414}
415
417{
418 quit();
419 wait();
420}
421
423{
424 return localHostUrl(m_port);
425}
426
427QUrl ThreadedTestHTTPServer::url(const QString &documentPath) const
428{
429 return baseUrl().resolved(documentPath);
430}
431
432QString ThreadedTestHTTPServer::urlString(const QString &documentPath) const
433{
434 return url(documentPath).toString();
435}
436
438{
439 TestHTTPServer server;
440 {
441 QMutexLocker locker(&m_mutex);
442 QVERIFY2(server.listen(), qPrintable(server.errorString()));
443 m_port = server.port();
444 for (QHash<QString, TestHTTPServer::Mode>::ConstIterator i = m_dirs.constBegin();
445 i != m_dirs.constEnd(); ++i) {
446 server.serveDirectory(i.key(), i.value());
447 }
448 m_condition.wakeAll();
449 }
450 exec();
451}
452
453void ThreadedTestHTTPServer::start()
454{
455 QMutexLocker locker(&m_mutex);
456 QThread::start();
457 m_condition.wait(&m_mutex);
458}
459
460QT_END_NAMESPACE
461
462#include "moc_testhttpserver_p.cpp"
QString urlString(const QString &documentPath) const
ThreadedTestHTTPServer(const QHash< QString, TestHTTPServer::Mode > &dirs)
QUrl url(const QString &documentPath) const
static QList< QByteArrayView > ignoredHeaders
static QUrl localHostUrl(quint16 port)