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 | |
42 | Inside the test case, an instance of TestHTTPServer should be created, with the |
43 | appropriate port to listen on. The server will listen on the localhost interface. |
44 | |
45 | Directories to serve can then be added to server, which will be added as "roots". |
46 | Each root can be added as a Normal, Delay or Disconnect root. Requests for files |
47 | within a Normal root are returned immediately. Request for files within a Delay |
48 | root are delayed for 500ms, and then served. Requests for files within a Disconnect |
49 | directory cause the server to disconnect immediately. A request for a file that isn't |
50 | found in any root will return a 404 error. |
51 | |
52 | If you have the following directory structure: |
53 | |
54 | \code |
55 | disconnect/disconnectTest.qml |
56 | files/main.qml |
57 | files/Button.qml |
58 | files/content/WebView.qml |
59 | slowFiles/slowMain.qml |
60 | \endcode |
61 | it can be added like this: |
62 | \code |
63 | TestHTTPServer server; |
64 | QVERIFY2(server.listen(14445), qPrintable(server.errorString())); |
65 | server.serveDirectory("disconnect", TestHTTPServer::Disconnect); |
66 | server.serveDirectory("files"); |
67 | server.serveDirectory("slowFiles", TestHTTPServer::Delay); |
68 | \endcode |
69 | |
70 | The 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 | |
81 | static 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 | |
90 | TestHTTPServer::TestHTTPServer() |
91 | : m_state(AwaitingHeader) |
92 | { |
93 | QObject::connect(sender: &m_server, signal: &QTcpServer::newConnection, receiver: this, slot: &TestHTTPServer::newConnection); |
94 | } |
95 | |
96 | bool TestHTTPServer::listen() |
97 | { |
98 | return m_server.listen(address: QHostAddress::LocalHost, port: 0); |
99 | } |
100 | |
101 | QUrl TestHTTPServer::baseUrl() const |
102 | { |
103 | return localHostUrl(port: m_server.serverPort()); |
104 | } |
105 | |
106 | quint16 TestHTTPServer::port() const |
107 | { |
108 | return m_server.serverPort(); |
109 | } |
110 | |
111 | QUrl TestHTTPServer::url(const QString &documentPath) const |
112 | { |
113 | return baseUrl().resolved(relative: documentPath); |
114 | } |
115 | |
116 | QString TestHTTPServer::urlString(const QString &documentPath) const |
117 | { |
118 | return url(documentPath).toString(); |
119 | } |
120 | |
121 | QString TestHTTPServer::errorString() const |
122 | { |
123 | return m_server.errorString(); |
124 | } |
125 | |
126 | bool 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 | */ |
136 | void TestHTTPServer::addAlias(const QString &filename, const QString &alias) |
137 | { |
138 | m_aliases.insert(akey: filename, avalue: alias); |
139 | } |
140 | |
141 | void TestHTTPServer::addRedirect(const QString &filename, const QString &redirectName) |
142 | { |
143 | m_redirects.insert(akey: filename, avalue: redirectName); |
144 | } |
145 | |
146 | void TestHTTPServer::registerFileNameForContentSubstitution(const QString &fileName) |
147 | { |
148 | m_contentSubstitutedFileNames.insert(value: fileName); |
149 | } |
150 | |
151 | bool 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 = 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 | |
214 | bool TestHTTPServer::hasFailed() const |
215 | { |
216 | return m_state == Failed; |
217 | } |
218 | |
219 | void 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 | |
232 | void 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 | |
249 | void 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 | |
298 | bool 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 | |
353 | void TestHTTPServer::sendDelayedItem() |
354 | { |
355 | sendOne(); |
356 | } |
357 | |
358 | void 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 | |
367 | void 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 | |
389 | ThreadedTestHTTPServer::ThreadedTestHTTPServer(const QString &dir, TestHTTPServer::Mode mode) : |
390 | m_port(0) |
391 | { |
392 | m_dirs[dir] = mode; |
393 | start(); |
394 | } |
395 | |
396 | ThreadedTestHTTPServer::ThreadedTestHTTPServer(const QHash<QString, TestHTTPServer::Mode> &dirs) : |
397 | m_dirs(dirs), m_port(0) |
398 | { |
399 | start(); |
400 | } |
401 | |
402 | ThreadedTestHTTPServer::~ThreadedTestHTTPServer() |
403 | { |
404 | quit(); |
405 | wait(); |
406 | } |
407 | |
408 | QUrl ThreadedTestHTTPServer::baseUrl() const |
409 | { |
410 | return localHostUrl(port: m_port); |
411 | } |
412 | |
413 | QUrl ThreadedTestHTTPServer::url(const QString &documentPath) const |
414 | { |
415 | return baseUrl().resolved(relative: documentPath); |
416 | } |
417 | |
418 | QString ThreadedTestHTTPServer::urlString(const QString &documentPath) const |
419 | { |
420 | return url(documentPath).toString(); |
421 | } |
422 | |
423 | void 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 | |
439 | void ThreadedTestHTTPServer::start() |
440 | { |
441 | QMutexLocker locker(&m_mutex); |
442 | QThread::start(); |
443 | m_condition.wait(lockedMutex: &m_mutex); |
444 | } |
445 | |