1/****************************************************************************
2**
3** Copyright (C) 2017 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the Qt WebGL module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL$
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 or (at your option) any later version
20** approved by the KDE Free Qt Foundation. The licenses are as published by
21** the Free Software Foundation and appearing in the file LICENSE.GPL3
22** included in the packaging of this file. Please review the following
23** information to ensure the GNU General Public License requirements will
24** be met: https://www.gnu.org/licenses/gpl-3.0.html.
25**
26** $QT_END_LICENSE$
27**
28****************************************************************************/
29
30#include "qwebglhttpserver.h"
31
32#include "qwebglintegration.h"
33#include "qwebglwebsocketserver.h"
34
35#include <QtCore/qbuffer.h>
36#include <QtCore/qbytearray.h>
37#include <QtCore/qfile.h>
38#include <QtCore/qpointer.h>
39#include <QtCore/qtimer.h>
40#include <QtCore/qurlquery.h>
41#include <QtGui/qclipboard.h>
42#include <QtGui/qicon.h>
43#include <QtGui/qguiapplication.h>
44#include <QtNetwork/qnetworkinterface.h>
45#include <QtNetwork/qtcpserver.h>
46#include <QtNetwork/qtcpsocket.h>
47
48#include <cctype>
49#include <cstdlib>
50
51QT_BEGIN_NAMESPACE
52
53static Q_LOGGING_CATEGORY(lc, "qt.qpa.webgl.httpserver")
54
55struct HttpRequest {
56 quint16 port = 0;
57
58 bool readMethod(QTcpSocket *socket);
59 bool readUrl(QTcpSocket *socket);
60 bool readStatus(QTcpSocket *socket);
61 bool readHeader(QTcpSocket *socket);
62
63 enum class State {
64 ReadingMethod,
65 ReadingUrl,
66 ReadingStatus,
67 ReadingHeader,
68 ReadingBody,
69 AllDone
70 } state = State::ReadingMethod;
71 QByteArray fragment;
72
73 enum class Method {
74 Unknown,
75 Head,
76 Get,
77 Put,
78 Post,
79 Delete,
80 } method = Method::Unknown;
81 quint32 byteSize = 0;
82 QUrl url;
83 QPair<quint8, quint8> version;
84 QMap<QByteArray, QByteArray> headers;
85};
86
87class QWebGLHttpServerPrivate
88{
89public:
90 QMap<QTcpSocket *, HttpRequest> clients;
91 QMap<QString, QPointer<QIODevice>> customRequestDevices;
92 QTcpServer server;
93 QPointer<QWebGLWebSocketServer> webSocketServer;
94};
95
96QWebGLHttpServer::QWebGLHttpServer(QWebGLWebSocketServer *webSocketServer, QObject *parent) :
97 QObject(parent),
98 d_ptr(new QWebGLHttpServerPrivate)
99{
100 Q_D(QWebGLHttpServer);
101 d->webSocketServer = webSocketServer;
102
103 connect(sender: &d->server, signal: &QTcpServer::newConnection, receiver: this, slot: &QWebGLHttpServer::clientConnected);
104}
105
106QWebGLHttpServer::~QWebGLHttpServer()
107{}
108
109bool QWebGLHttpServer::listen(const QHostAddress &address, quint16 port)
110{
111 Q_D(QWebGLHttpServer);
112 const auto ok = d->server.listen(address, port);
113 qCDebug(lc, "Listening in port %d", port);
114 return ok;
115}
116
117bool QWebGLHttpServer::isListening() const
118{
119 Q_D(const QWebGLHttpServer);
120 return d->server.isListening();
121}
122
123quint16 QWebGLHttpServer::serverPort() const
124{
125 Q_D(const QWebGLHttpServer);
126 return d->server.serverPort();
127}
128
129QIODevice *QWebGLHttpServer::customRequestDevice(const QString &name)
130{
131 Q_D(const QWebGLHttpServer);
132 return d->customRequestDevices.value(akey: name, adefaultValue: nullptr).data();
133}
134
135void QWebGLHttpServer::setCustomRequestDevice(const QString &name, QIODevice *device)
136{
137 Q_D(QWebGLHttpServer);
138 if (d->customRequestDevices.value(akey: name))
139 d->customRequestDevices[name]->deleteLater();
140 d->customRequestDevices.insert(akey: name, avalue: device);
141}
142
143QString QWebGLHttpServer::errorString() const
144{
145 Q_D(const QWebGLHttpServer);
146 return d->server.errorString();
147}
148
149void QWebGLHttpServer::clientConnected()
150{
151 Q_D(QWebGLHttpServer);
152 auto socket = d->server.nextPendingConnection();
153 connect(sender: socket, signal: &QTcpSocket::disconnected, receiver: this, slot: &QWebGLHttpServer::clientDisconnected);
154 connect(sender: socket, signal: &QTcpSocket::readyRead, receiver: this, slot: &QWebGLHttpServer::readData);
155}
156
157void QWebGLHttpServer::clientDisconnected()
158{
159 Q_D(QWebGLHttpServer);
160 auto socket = qobject_cast<QTcpSocket *>(object: sender());
161 Q_ASSERT(socket);
162 d->clients.remove(akey: socket);
163 socket->deleteLater();
164}
165
166void QWebGLHttpServer::readData()
167{
168 Q_D(QWebGLHttpServer);
169 auto socket = qobject_cast<QTcpSocket *>(object: sender());
170 if (!d->clients.contains(akey: socket))
171 d->clients[socket].port = d->server.serverPort();
172
173 auto request = &d->clients[socket];
174 bool error = false;
175
176 request->byteSize += socket->bytesAvailable();
177 if (Q_UNLIKELY(request->byteSize > 2048)) {
178 socket->write(QByteArrayLiteral("HTTP 413 – Request entity too large\r\n"));
179 socket->disconnectFromHost();
180 d->clients.remove(akey: socket);
181 return;
182 }
183
184 if (Q_LIKELY(request->state == HttpRequest::State::ReadingMethod))
185 if (Q_UNLIKELY(error = !request->readMethod(socket)))
186 qCWarning(lc, "QWebGLHttpServer::readData: Invalid Method");
187
188 if (Q_LIKELY(!error && request->state == HttpRequest::State::ReadingUrl))
189 if (Q_UNLIKELY(error = !request->readUrl(socket)))
190 qCWarning(lc, "QWebGLHttpServer::readData: Invalid URL");
191
192 if (Q_LIKELY(!error && request->state == HttpRequest::State::ReadingStatus))
193 if (Q_UNLIKELY(error = !request->readStatus(socket)))
194 qCWarning(lc, "QWebGLHttpServer::readData: Invalid Status");
195
196 if (Q_LIKELY(!error && request->state == HttpRequest::State::ReadingHeader))
197 if (Q_UNLIKELY(error = !request->readHeader(socket)))
198 qCWarning(lc, "QWebGLHttpServer::readData: Invalid Header");
199
200 if (error) {
201 socket->disconnectFromHost();
202 d->clients.remove(akey: socket);
203 } else if (!request->url.isEmpty()) {
204 Q_ASSERT(request->state != HttpRequest::State::ReadingUrl);
205 answerClient(socket, urls: request->url);
206 d->clients.remove(akey: socket);
207 }
208}
209
210void QWebGLHttpServer::answerClient(QTcpSocket *socket, const QUrl &url)
211{
212 Q_D(QWebGLHttpServer);
213 bool disconnect = true;
214 const auto path = url.path();
215
216 qCDebug(lc, "%s requested: %s",
217 qPrintable(socket->localAddress().toString()), qPrintable(path));
218
219 QByteArray answer = QByteArrayLiteral("HTTP/1.1 404 Not Found\r\n"
220 "Content-Type: text/html\r\n"
221 "Content-Length: 136\r\n\r\n"
222 "<html>"
223 "<head><title>404 Not Found</title></head>"
224 "<body bgcolor=\"white\">"
225 "<center><h1>404 Not Found</h1></center>"
226 "</body>"
227 "</html>");
228 const auto addData = [&answer](const QByteArray &contentType, const QByteArray &data)
229 {
230 answer = QByteArrayLiteral("HTTP/1.0 200 OK \r\n");
231 QByteArray ret;
232 const auto dataSize = QString::number(data.size()).toUtf8();
233 answer += QByteArrayLiteral("Content-Type: ") + contentType + QByteArrayLiteral("\r\n") +
234 QByteArrayLiteral("Content-Length: ") + dataSize + QByteArrayLiteral("\r\n\r\n") +
235 data;
236 };
237
238 if (path == QLatin1String("/")) {
239 QFile file(QStringLiteral(":/webgl/index.html"));
240 Q_ASSERT(file.exists());
241 file.open(flags: QIODevice::ReadOnly | QIODevice::Text);
242 Q_ASSERT(file.isOpen());
243 auto data = file.readAll();
244 addData(QByteArrayLiteral("text/html; charset=\"utf-8\""), data);
245 } else if (path == QStringLiteral("/clipboard")) {
246#ifndef QT_NO_CLIPBOARD
247 auto data = qGuiApp->clipboard()->text().toUtf8();
248 addData(QByteArrayLiteral("text/html; charset=\"utf-8\""), data);
249#else
250 qCWarning(lc, "Qt was built without clipboard support");
251#endif
252 } else if (path == QStringLiteral("/webqt.js")) {
253 QFile file(QStringLiteral(":/webgl/webqt.jsx"));
254 Q_ASSERT(file.exists());
255 file.open(flags: QIODevice::ReadOnly | QIODevice::Text);
256 Q_ASSERT(file.isOpen());
257 const auto host = url.host().toUtf8();
258 const auto port = QString::number(d->webSocketServer->port()).toUtf8();
259 QByteArray data = "var host = \"" + host + "\";\r\nvar port = " + port + ";\r\n";
260 data += file.readAll();
261 addData(QByteArrayLiteral("application/javascript"), data);
262 } else if (path == QStringLiteral("/favicon.ico")) {
263 QFile file(QStringLiteral(":/webgl/favicon.ico"));
264 Q_ASSERT(file.exists());
265 file.open(flags: QIODevice::ReadOnly);
266 Q_ASSERT(file.isOpen());
267 auto data = file.readAll();
268 addData(QByteArrayLiteral("image/x-icon"), data);
269 } else if (path == QStringLiteral("/favicon.png")) {
270 QBuffer buffer;
271 qGuiApp->windowIcon().pixmap(w: 16, h: 16).save(device: &buffer, format: "png");
272 addData(QByteArrayLiteral("image/x-icon"), buffer.data());
273 } else if (auto device = d->customRequestDevices.value(akey: path)) {
274 answer = QByteArrayLiteral("HTTP/1.0 200 OK \r\n"
275 "Content-Type: text/plain; charset=\"utf-8\"\r\n"
276 "Connection: Keep.Alive\r\n\r\n") +
277 device->readAll();
278 auto timer = new QTimer(device);
279 timer->setSingleShot(false);
280 connect(sender: timer, signal: &QTimer::timeout, slot: [device, socket]()
281 {
282 if (device->bytesAvailable())
283 socket->write(data: device->readAll());
284 });
285 timer->start(msec: 1000);
286 disconnect = false;
287 }
288 socket->write(data: answer);
289 if (disconnect)
290 socket->disconnectFromHost();
291}
292
293bool HttpRequest::readMethod(QTcpSocket *socket)
294{
295 bool finished = false;
296 while (socket->bytesAvailable() && !finished) {
297 const auto c = socket->read(maxlen: 1).at(i: 0);
298 if (std::isupper(c) && fragment.size() < 6)
299 fragment += c;
300 else
301 finished = true;
302 }
303 if (finished) {
304 if (fragment == "HEAD")
305 method = Method::Head;
306 else if (fragment == "GET")
307 method = Method::Get;
308 else if (fragment == "PUT")
309 method = Method::Put;
310 else if (fragment == "POST")
311 method = Method::Post;
312 else if (fragment == "DELETE")
313 method = Method::Delete;
314 else
315 qCWarning(lc, "QWebGLHttpServer::HttpRequest::readMethod: Invalid operation %s",
316 fragment.data());
317
318 state = State::ReadingUrl;
319 fragment.clear();
320
321 return method != Method::Unknown;
322 }
323 return true;
324}
325
326bool HttpRequest::readUrl(QTcpSocket *socket)
327{
328 bool finished = false;
329 while (socket->bytesAvailable() && !finished) {
330 char c;
331 if (!socket->getChar(c: &c))
332 return false;
333 if (std::isspace(c))
334 finished = true;
335 else
336 fragment += c;
337 }
338 if (finished) {
339 if (!fragment.startsWith(c: "/")) {
340 qCWarning(lc, "QWebGLHttpServer::HttpRequest::readUrl: Invalid URL path %s",
341 fragment.constData());
342 return false;
343 }
344 url.setUrl(QStringLiteral("http://localhost:") + QString::number(port) +
345 QString::fromUtf8(str: fragment));
346 state = State::ReadingStatus;
347 if (!url.isValid()) {
348 qCWarning(lc, "QWebGLHttpServer::HttpRequest::readUrl: Invalid URL %s",
349 fragment.constData());
350 return false;
351 }
352 fragment.clear();
353 return true;
354 }
355 return true;
356}
357
358bool HttpRequest::readStatus(QTcpSocket *socket)
359{
360 bool finished = false;
361 while (socket->bytesAvailable() && !finished) {
362 fragment += socket->read(maxlen: 1);
363 if (fragment.endsWith(c: "\r\n")) {
364 finished = true;
365 fragment.chop(n: 2);
366 }
367 }
368 if (finished) {
369 if (!std::isdigit(fragment.at(i: fragment.size() - 3)) ||
370 !std::isdigit(fragment.at(i: fragment.size() - 1))) {
371 qCWarning(lc, "QWebGLHttpServer::HttpRequest::::readStatus: Invalid version");
372 return false;
373 }
374 version = qMakePair(x: fragment.at(i: fragment.size() - 3) - '0',
375 y: fragment.at(i: fragment.size() - 1) - '0');
376 state = State::ReadingHeader;
377 fragment.clear();
378 }
379 return true;
380}
381
382bool HttpRequest::readHeader(QTcpSocket *socket)
383{
384 while (socket->bytesAvailable()) {
385 fragment += socket->read(maxlen: 1);
386 if (fragment.endsWith(c: "\r\n")) {
387 if (fragment == "\r\n") {
388 state = State::ReadingBody;
389 fragment.clear();
390 return true;
391 } else {
392 fragment.chop(n: 2);
393 const int index = fragment.indexOf(c: ':');
394 if (index == -1)
395 return false;
396
397 const QByteArray key = fragment.mid(index: 0, len: index).trimmed();
398 const QByteArray value = fragment.mid(index: index + 1).trimmed();
399 headers.insert(akey: key, avalue: value);
400 if (QStringLiteral("host").compare(s: key, cs: Qt::CaseInsensitive) == 0) {
401 auto parts = value.split(sep: ':');
402 if (parts.size() == 1) {
403 url.setHost(host: parts.first());
404 url.setPort(80);
405 } else {
406 url.setHost(host: parts.first());
407 url.setPort(std::strtoul(nptr: parts.at(i: 1).constData(), endptr: nullptr, base: 10));
408 }
409 }
410 fragment.clear();
411 }
412 }
413 }
414 return false;
415}
416
417QT_END_NAMESPACE
418

source code of qtwebglplugin/src/plugins/platforms/webgl/qwebglhttpserver.cpp