1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "testhttpserver_p.h"
5#include <QTcpSocket>
6#include <QDebug>
7#include <QFile>
8#include <QTimer>
9#include <QTest>
10#include <QQmlFile>
11
12QT_BEGIN_NAMESPACE
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
58static QList<QByteArrayView> ignoredHeaders = {
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(sender: &m_server, signal: &QTcpServer::newConnection, context: this, slot: &TestHTTPServer::newConnection);
76}
77
78bool TestHTTPServer::listen()
79{
80 return m_server.listen(address: QHostAddress::LocalHost, port: 0);
81}
82
83QUrl TestHTTPServer::baseUrl() const
84{
85 return localHostUrl(port: 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(relative: 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(t: qMakePair(value1: dir, value2&: 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(key: filename, value: alias);
121}
122
123void TestHTTPServer::addRedirect(const QString &filename, const QString &redirectName)
124{
125 m_redirects.insert(key: filename, value: redirectName);
126}
127
128void TestHTTPServer::registerFileNameForContentSubstitution(const QString &fileName)
129{
130 m_contentSubstitutedFileNames.insert(value: 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(flags: QIODevice::ReadOnly))
140 return false;
141
142 QFile replyFile(QQmlFile::urlToLocalFileOrQrc(reply));
143 if (!replyFile.open(flags: QIODevice::ReadOnly))
144 return false;
145
146 m_bodyData = QByteArray();
147 if (body.isValid()) {
148 QFile bodyFile(QQmlFile::urlToLocalFileOrQrc(body));
149 if (!bodyFile.open(flags: 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(before: '\r', after: "");
161 if (headers_done) {
162 m_waitData.body.append(a: line);
163 } else if (line.at(i: 0) == '\n') {
164 headers_done = true;
165 } else if (line.endsWith(bv: "{{Ignore}}\n")) {
166 m_waitData.headerPrefixes.append(t: line.left(n: line.size() - strlen(s: "{{Ignore}}\n")));
167 } else {
168 line.replace(before: "{{ServerHostUrl}}", after: serverHostUrl);
169 m_waitData.headerExactMatches.append(t: line);
170 }
171 }
172
173 m_replyData = replyFile.readAll();
174
175 if (!m_replyData.endsWith(c: '\n'))
176 m_replyData.append(c: '\n');
177 m_replyData.append(s: "Content-length: ");
178 m_replyData.append(a: QByteArray::number(m_bodyData.size()));
179 m_replyData.append(s: "\n\n");
180
181 for (int ii = 0; ii < m_replyData.size(); ++ii) {
182 if (m_replyData.at(i: ii) == '\n' && (!ii || m_replyData.at(i: ii - 1) != '\r')) {
183 m_replyData.insert(i: ii, c: '\r');
184 ++ii;
185 }
186 }
187 m_replyData.append(a: 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(key: socket, value: QByteArray());
205
206 QObject::connect(sender: socket, signal: &QAbstractSocket::disconnected, context: this, slot: &TestHTTPServer::disconnected);
207 QObject::connect(sender: socket, signal: &QIODevice::readyRead, context: this, slot: &TestHTTPServer::readyRead);
208}
209
210void TestHTTPServer::disconnected()
211{
212 QTcpSocket *socket = qobject_cast<QTcpSocket *>(object: sender());
213 if (!socket)
214 return;
215
216 m_dataCache.remove(key: socket);
217 for (int ii = 0; ii < m_toSend.size(); ++ii) {
218 if (m_toSend.at(i: ii).first == socket) {
219 m_toSend.removeAt(i: ii);
220 --ii;
221 }
222 }
223 socket->disconnect();
224 socket->deleteLater();
225}
226
227void TestHTTPServer::readyRead()
228{
229 QTcpSocket *socket = qobject_cast<QTcpSocket *>(object: 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(before: '\r', after: "");
247 if (line.at(i: 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(bv: prefix)) {
255 prefixFound = true;
256 break;
257 }
258 }
259 for (QByteArrayView ignore : ignoredHeaders) {
260 if (line.startsWith(bv: ignore)) {
261 prefixFound = true;
262 break;
263 }
264 }
265
266 if (!prefixFound && !m_waitData.headerExactMatches.contains(t: 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(data: 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(key: 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(key: fileName).toUtf8() + "\r\n\r\n";
298 socket->write(data: response);
299 return true;
300 }
301
302 for (int ii = 0; ii < m_directories.size(); ++ii) {
303 const QString &dir = m_directories.at(i: ii).first;
304 const Mode mode = m_directories.at(i: ii).second;
305
306 QString dirFile = dir + QLatin1Char('/') + fileName;
307
308 if (!QFile::exists(fileName: dirFile)) {
309 const QHash<QString, QString>::const_iterator it = m_aliases.constFind(key: fileName);
310 if (it != m_aliases.constEnd())
311 dirFile = dir + QLatin1Char('/') + it.value();
312 }
313
314 QFile file(dirFile);
315 if (file.open(flags: QIODevice::ReadOnly)) {
316
317 if (mode == Disconnect)
318 return true;
319
320 QByteArray data = file.readAll();
321 if (m_contentSubstitutedFileNames.contains(value: QLatin1Char('/') + fileName))
322 data.replace(QByteArrayLiteral("{{ServerBaseUrl}}"), after: 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(t: qMakePair(value1&: socket, value2&: response));
332 QTimer::singleShot(interval: 500, receiver: this, slot: &TestHTTPServer::sendOne);
333 return false;
334 } else {
335 socket->write(data: response);
336 return true;
337 }
338 }
339 }
340
341 socket->write(data: "HTTP/1.0 404 Not found\r\nContent-type: text/html; charset=UTF-8\r\n\r\n");
342
343 return true;
344}
345
346void TestHTTPServer::sendDelayedItem()
347{
348 sendOne();
349}
350
351void TestHTTPServer::sendOne()
352{
353 if (!m_toSend.isEmpty()) {
354 m_toSend.first().first->write(data: m_toSend.first().second);
355 m_toSend.first().first->close();
356 m_toSend.removeFirst();
357 }
358}
359
360void TestHTTPServer::serveGET(QTcpSocket *socket, const QByteArray &data)
361{
362 const QHash<QTcpSocket *, QByteArray>::iterator it = m_dataCache.find(key: socket);
363 if (it == m_dataCache.end())
364 return;
365
366 QByteArray &total = it.value();
367 total.append(a: data);
368
369 if (total.contains(bv: "\n\r\n")) {
370 bool close = true;
371 if (total.startsWith(bv: "GET /")) {
372 const int space = total.indexOf(ch: ' ', from: 4);
373 if (space != -1)
374 close = reply(socket, fileNameIn: total.mid(index: 5, len: space - 5));
375 }
376 m_dataCache.erase(it);
377 if (close)
378 socket->disconnectFromHost();
379 }
380}
381
382ThreadedTestHTTPServer::ThreadedTestHTTPServer(const QString &dir, TestHTTPServer::Mode mode) :
383 m_port(0)
384{
385 m_dirs[dir] = mode;
386 start();
387}
388
389ThreadedTestHTTPServer::ThreadedTestHTTPServer(const QHash<QString, TestHTTPServer::Mode> &dirs) :
390 m_dirs(dirs), m_port(0)
391{
392 start();
393}
394
395ThreadedTestHTTPServer::~ThreadedTestHTTPServer()
396{
397 quit();
398 wait();
399}
400
401QUrl ThreadedTestHTTPServer::baseUrl() const
402{
403 return localHostUrl(port: m_port);
404}
405
406QUrl ThreadedTestHTTPServer::url(const QString &documentPath) const
407{
408 return baseUrl().resolved(relative: documentPath);
409}
410
411QString ThreadedTestHTTPServer::urlString(const QString &documentPath) const
412{
413 return url(documentPath).toString();
414}
415
416void ThreadedTestHTTPServer::run()
417{
418 TestHTTPServer server;
419 {
420 QMutexLocker locker(&m_mutex);
421 QVERIFY2(server.listen(), qPrintable(server.errorString()));
422 m_port = server.port();
423 for (QHash<QString, TestHTTPServer::Mode>::ConstIterator i = m_dirs.constBegin();
424 i != m_dirs.constEnd(); ++i) {
425 server.serveDirectory(dir: i.key(), mode: i.value());
426 }
427 m_condition.wakeAll();
428 }
429 exec();
430}
431
432void ThreadedTestHTTPServer::start()
433{
434 QMutexLocker locker(&m_mutex);
435 QThread::start();
436 m_condition.wait(lockedMutex: &m_mutex);
437}
438
439QT_END_NAMESPACE
440
441#include "moc_testhttpserver_p.cpp"
442

Provided by KDAB

Privacy Policy
Start learning QML with our Intro Training
Find out more

source code of qtdeclarative/src/quicktestutils/qml/testhttpserver.cpp