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

source code of qtdeclarative/tests/auto/shared/testhttpserver.cpp