@dusted/anqst 1.7.2 → 1.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,965 @@
1
+ #include "AngularHttpBaseServer.h"
2
+
3
+ #include <QDir>
4
+ #include <QFile>
5
+ #include <QFileInfo>
6
+ #include <QCryptographicHash>
7
+ #include <QEventLoop>
8
+ #include <QJsonDocument>
9
+ #include <QJsonObject>
10
+ #include <QNetworkAccessManager>
11
+ #include <QNetworkReply>
12
+ #include <QNetworkRequest>
13
+ #include <QTcpServer>
14
+ #include <QTcpSocket>
15
+ #include <QTimer>
16
+ #include <QUrl>
17
+
18
+ namespace ANQST_WEBBASE_NAMESPACE {
19
+
20
+ namespace {
21
+ QByteArray statusReason(int statusCode) {
22
+ switch (statusCode) {
23
+ case 101:
24
+ return "Switching Protocols";
25
+ case 200:
26
+ return "OK";
27
+ case 301:
28
+ return "Moved Permanently";
29
+ case 302:
30
+ return "Found";
31
+ case 304:
32
+ return "Not Modified";
33
+ case 400:
34
+ return "Bad Request";
35
+ case 401:
36
+ return "Unauthorized";
37
+ case 403:
38
+ return "Forbidden";
39
+ case 405:
40
+ return "Method Not Allowed";
41
+ case 404:
42
+ return "Not Found";
43
+ case 500:
44
+ return "Internal Server Error";
45
+ case 502:
46
+ return "Bad Gateway";
47
+ case 504:
48
+ return "Gateway Timeout";
49
+ default:
50
+ return "Internal Server Error";
51
+ }
52
+ }
53
+
54
+ QString guessContentType(const QString& path) {
55
+ if (path.endsWith(".html")) return QStringLiteral("text/html; charset=utf-8");
56
+ if (path.endsWith(".js")) return QStringLiteral("application/javascript; charset=utf-8");
57
+ if (path.endsWith(".css")) return QStringLiteral("text/css; charset=utf-8");
58
+ if (path.endsWith(".json")) return QStringLiteral("application/json; charset=utf-8");
59
+ if (path.endsWith(".svg")) return QStringLiteral("image/svg+xml");
60
+ if (path.endsWith(".png")) return QStringLiteral("image/png");
61
+ if (path.endsWith(".jpg") || path.endsWith(".jpeg")) return QStringLiteral("image/jpeg");
62
+ return QStringLiteral("application/octet-stream");
63
+ }
64
+
65
+ QVariantMap parseJsonPayload(const QString& message) {
66
+ const QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8());
67
+ if (!doc.isObject()) {
68
+ return QVariantMap();
69
+ }
70
+ return doc.object().toVariantMap();
71
+ }
72
+ } // namespace
73
+
74
+ AngularHttpBaseServer::AngularHttpBaseServer(QObject* parent)
75
+ : QObject(parent)
76
+ , m_httpServer(new QTcpServer(this))
77
+ , m_wsServer(new QTcpServer(this))
78
+ , m_client(nullptr)
79
+ , m_wsHandshakeComplete(false)
80
+ , m_facade(nullptr)
81
+ , m_contentRootMode(ContentRootMode::Unset)
82
+ , m_bridgeObjectName(QString())
83
+ , m_httpPort(0)
84
+ , m_wsPort(0)
85
+ , m_serveMode(ServeMode::LocalContent) {
86
+ connect(m_httpServer, &QTcpServer::newConnection, this, &AngularHttpBaseServer::handleHttpNewConnection);
87
+ connect(m_wsServer, &QTcpServer::newConnection, this, &AngularHttpBaseServer::handleWebSocketConnected);
88
+ }
89
+
90
+ AngularHttpBaseServer::~AngularHttpBaseServer() {
91
+ stop();
92
+ }
93
+
94
+ void AngularHttpBaseServer::setBridgeObjectName(const QString& name) {
95
+ m_bridgeObjectName = name;
96
+ }
97
+
98
+ void AngularHttpBaseServer::setFacade(AnQstHostBridgeFacade* facade) {
99
+ if (m_facade == facade) {
100
+ return;
101
+ }
102
+ if (m_facade != nullptr) {
103
+ disconnect(m_facade, nullptr, this, nullptr);
104
+ }
105
+ m_facade = facade;
106
+ if (m_facade == nullptr) {
107
+ return;
108
+ }
109
+ connect(m_facade, &AnQstHostBridgeFacade::bridgeOutputUpdated, this, [this](const QString& service, const QString& member, const QVariant& value) {
110
+ sendJsonToClient({
111
+ {QStringLiteral("type"), QStringLiteral("outputUpdated")},
112
+ {QStringLiteral("service"), service},
113
+ {QStringLiteral("member"), member},
114
+ {QStringLiteral("value"), value},
115
+ });
116
+ });
117
+ connect(m_facade, &AnQstHostBridgeFacade::bridgeSlotInvocationRequested, this, [this](const QString& requestId, const QString& service, const QString& member, const QVariantList& args) {
118
+ sendJsonToClient({
119
+ {QStringLiteral("type"), QStringLiteral("slotInvocationRequested")},
120
+ {QStringLiteral("requestId"), requestId},
121
+ {QStringLiteral("service"), service},
122
+ {QStringLiteral("member"), member},
123
+ {QStringLiteral("args"), QVariant::fromValue(args)},
124
+ });
125
+ });
126
+ connect(m_facade, &AnQstHostBridgeFacade::bridgeHostError, this, [this](const QVariantMap& payload) {
127
+ sendJsonToClient({
128
+ {QStringLiteral("type"), QStringLiteral("hostError")},
129
+ {QStringLiteral("payload"), payload},
130
+ });
131
+ });
132
+ connect(m_facade, &AnQstHostBridgeFacade::bridgeDropReceived, this, [this](const QString& service, const QString& member, const QVariant& payload, double x, double y) {
133
+ sendJsonToClient({
134
+ {QStringLiteral("type"), QStringLiteral("dropReceived")},
135
+ {QStringLiteral("service"), service},
136
+ {QStringLiteral("member"), member},
137
+ {QStringLiteral("payload"), payload},
138
+ {QStringLiteral("x"), x},
139
+ {QStringLiteral("y"), y},
140
+ });
141
+ });
142
+ connect(m_facade, &AnQstHostBridgeFacade::bridgeHoverUpdated, this, [this](const QString& service, const QString& member, const QVariant& payload, double x, double y) {
143
+ sendJsonToClient({
144
+ {QStringLiteral("type"), QStringLiteral("hoverUpdated")},
145
+ {QStringLiteral("service"), service},
146
+ {QStringLiteral("member"), member},
147
+ {QStringLiteral("payload"), payload},
148
+ {QStringLiteral("x"), x},
149
+ {QStringLiteral("y"), y},
150
+ });
151
+ });
152
+ connect(m_facade, &AnQstHostBridgeFacade::bridgeHoverLeft, this, [this](const QString& service, const QString& member) {
153
+ sendJsonToClient({
154
+ {QStringLiteral("type"), QStringLiteral("hoverLeft")},
155
+ {QStringLiteral("service"), service},
156
+ {QStringLiteral("member"), member},
157
+ });
158
+ });
159
+ }
160
+
161
+ bool AngularHttpBaseServer::configureContent(ContentRootMode mode, const QString& contentRoot, const QString& entryPoint) {
162
+ if (mode == ContentRootMode::Unset || contentRoot.trimmed().isEmpty() || entryPoint.trimmed().isEmpty()) {
163
+ emitServerError(QStringLiteral("DEV_SERVER_CONFIG_INVALID"), QStringLiteral("Invalid development mode content configuration."));
164
+ return false;
165
+ }
166
+ m_contentRootMode = mode;
167
+ m_contentRoot = contentRoot;
168
+ m_entryPoint = entryPoint;
169
+ m_serveMode = ServeMode::LocalContent;
170
+ m_proxyBaseUrl = QUrl();
171
+ return true;
172
+ }
173
+
174
+ bool AngularHttpBaseServer::configureProxyTarget(const QUrl& targetBaseUrl, const QString& entryPoint) {
175
+ if (!targetBaseUrl.isValid() || targetBaseUrl.scheme().isEmpty() || targetBaseUrl.host().isEmpty()) {
176
+ emitServerError(QStringLiteral("DEV_SERVER_PROXY_TARGET_INVALID"), QStringLiteral("Invalid proxy target URL."), {
177
+ {QStringLiteral("targetUrl"), targetBaseUrl.toString()},
178
+ });
179
+ return false;
180
+ }
181
+ const QString scheme = targetBaseUrl.scheme().toLower();
182
+ if (scheme != QStringLiteral("http")) {
183
+ emitServerError(QStringLiteral("DEV_SERVER_PROXY_TARGET_SCHEME_UNSUPPORTED"), QStringLiteral("Proxy target URL must use http."), {
184
+ {QStringLiteral("targetUrl"), targetBaseUrl.toString()},
185
+ {QStringLiteral("scheme"), scheme},
186
+ });
187
+ return false;
188
+ }
189
+ m_serveMode = ServeMode::ProxyTarget;
190
+ m_proxyBaseUrl = targetBaseUrl;
191
+ m_entryPoint = entryPoint.trimmed().isEmpty() ? QStringLiteral("index.html") : entryPoint.trimmed();
192
+ m_contentRootMode = ContentRootMode::Unset;
193
+ m_contentRoot.clear();
194
+ return true;
195
+ }
196
+
197
+ bool AngularHttpBaseServer::start(bool allowLan, quint16 startPort) {
198
+ if (m_serveMode == ServeMode::LocalContent && m_contentRootMode == ContentRootMode::Unset) {
199
+ emitServerError(QStringLiteral("DEV_SERVER_CONFIG_MISSING"), QStringLiteral("Content root must be configured before starting development server."));
200
+ return false;
201
+ }
202
+ if (m_serveMode == ServeMode::ProxyTarget && !m_proxyBaseUrl.isValid()) {
203
+ emitServerError(QStringLiteral("DEV_SERVER_PROXY_TARGET_MISSING"), QStringLiteral("Proxy target must be configured before starting development server."));
204
+ return false;
205
+ }
206
+ m_bindAddress = allowLan ? QHostAddress::Any : QHostAddress::LocalHost;
207
+ if (!startHttp(m_bindAddress, startPort)) {
208
+ return false;
209
+ }
210
+ if (!startWebSocket(m_bindAddress)) {
211
+ stopHttp();
212
+ return false;
213
+ }
214
+ return true;
215
+ }
216
+
217
+ void AngularHttpBaseServer::stop() {
218
+ const auto peers = m_proxyPeers.keys();
219
+ for (QTcpSocket* socket : peers) {
220
+ closeProxyPeer(socket);
221
+ }
222
+ detachCurrentClient();
223
+ stopWebSocket();
224
+ stopHttp();
225
+ }
226
+
227
+ void AngularHttpBaseServer::notifyWidgetReattached() {
228
+ sendJsonToClient({
229
+ {QStringLiteral("type"), QStringLiteral("widgetReattached")},
230
+ {QStringLiteral("message"), QStringLiteral("Widget Reattached")},
231
+ });
232
+ }
233
+
234
+ bool AngularHttpBaseServer::isRunning() const {
235
+ return m_httpServer->isListening() && m_wsServer->isListening();
236
+ }
237
+
238
+ QString AngularHttpBaseServer::url() const {
239
+ if (!m_httpServer->isListening()) {
240
+ return QString();
241
+ }
242
+ const QString host = m_bindAddress == QHostAddress::Any ? QStringLiteral("0.0.0.0") : QStringLiteral("localhost");
243
+ return QStringLiteral("http://%1:%2").arg(host).arg(m_httpPort);
244
+ }
245
+
246
+ quint16 AngularHttpBaseServer::httpPort() const {
247
+ return m_httpPort;
248
+ }
249
+
250
+ quint16 AngularHttpBaseServer::wsPort() const {
251
+ return m_wsPort;
252
+ }
253
+
254
+ QString AngularHttpBaseServer::websocketUrl() const {
255
+ if (!m_wsServer->isListening()) {
256
+ return QString();
257
+ }
258
+ const QString host = m_bindAddress == QHostAddress::Any ? QStringLiteral("0.0.0.0") : QStringLiteral("localhost");
259
+ return QStringLiteral("ws://%1:%2/anqst-bridge").arg(host).arg(m_wsPort);
260
+ }
261
+
262
+ void AngularHttpBaseServer::emitServerError(const QString& code, const QString& message, const QVariantMap& context) {
263
+ QVariantMap payload;
264
+ payload.insert(QStringLiteral("code"), code);
265
+ payload.insert(QStringLiteral("message"), message);
266
+ payload.insert(QStringLiteral("context"), context);
267
+ emit serverError(payload);
268
+ }
269
+
270
+ bool AngularHttpBaseServer::startHttp(const QHostAddress& bindAddress, quint16 startPort) {
271
+ for (quint16 port = startPort; port < static_cast<quint16>(startPort + 200); ++port) {
272
+ if (m_httpServer->listen(bindAddress, port)) {
273
+ m_httpPort = port;
274
+ return true;
275
+ }
276
+ }
277
+ emitServerError(QStringLiteral("DEV_SERVER_HTTP_BIND_FAILED"), QStringLiteral("Failed to bind HTTP development server."), {
278
+ {QStringLiteral("startPort"), startPort},
279
+ });
280
+ return false;
281
+ }
282
+
283
+ bool AngularHttpBaseServer::startWebSocket(const QHostAddress& bindAddress) {
284
+ quint16 candidate = static_cast<quint16>(m_httpPort + 1);
285
+ for (quint16 attempts = 0; attempts < 200; ++attempts, ++candidate) {
286
+ if (m_wsServer->listen(bindAddress, candidate)) {
287
+ m_wsPort = candidate;
288
+ return true;
289
+ }
290
+ }
291
+ emitServerError(QStringLiteral("DEV_SERVER_WS_BIND_FAILED"), QStringLiteral("Failed to bind WebSocket development bridge."), {
292
+ {QStringLiteral("httpPort"), m_httpPort},
293
+ });
294
+ return false;
295
+ }
296
+
297
+ void AngularHttpBaseServer::stopHttp() {
298
+ if (m_httpServer->isListening()) {
299
+ m_httpServer->close();
300
+ }
301
+ m_httpPort = 0;
302
+ }
303
+
304
+ void AngularHttpBaseServer::stopWebSocket() {
305
+ if (m_wsServer->isListening()) {
306
+ m_wsServer->close();
307
+ }
308
+ m_wsPort = 0;
309
+ }
310
+
311
+ void AngularHttpBaseServer::handleHttpNewConnection() {
312
+ while (m_httpServer->hasPendingConnections()) {
313
+ QTcpSocket* socket = m_httpServer->nextPendingConnection();
314
+ handleHttpClient(socket);
315
+ }
316
+ }
317
+
318
+ void AngularHttpBaseServer::handleHttpClient(QTcpSocket* socket) {
319
+ connect(socket, &QTcpSocket::readyRead, this, [this, socket]() {
320
+ const QByteArray raw = socket->readAll();
321
+ const QList<QByteArray> lines = raw.split('\n');
322
+ if (lines.isEmpty()) {
323
+ socket->disconnectFromHost();
324
+ return;
325
+ }
326
+
327
+ const QList<QByteArray> requestLine = lines.first().trimmed().split(' ');
328
+ if (requestLine.size() < 2) {
329
+ socket->disconnectFromHost();
330
+ return;
331
+ }
332
+
333
+ const QString method = QString::fromUtf8(requestLine.at(0));
334
+ const QString target = QString::fromUtf8(requestLine.at(1));
335
+ if (m_serveMode == ServeMode::ProxyTarget) {
336
+ if (target == QStringLiteral("/anqst-dev-config.json")) {
337
+ QString contentType;
338
+ int statusCode = 200;
339
+ const QByteArray body = readHttpProxy(method, target, QByteArray(), &contentType, &statusCode);
340
+ QByteArray response;
341
+ response += "HTTP/1.1 " + QByteArray::number(statusCode) + " " + statusReason(statusCode) + "\r\n";
342
+ response += "Content-Type: " + contentType.toUtf8() + "\r\n";
343
+ response += "Content-Length: " + QByteArray::number(body.size()) + "\r\n";
344
+ response += "Cache-Control: no-cache\r\n";
345
+ response += "Connection: close\r\n";
346
+ response += "\r\n";
347
+ if (method != QStringLiteral("HEAD")) {
348
+ response += body;
349
+ }
350
+ socket->write(response);
351
+ socket->disconnectFromHost();
352
+ return;
353
+ }
354
+
355
+ disconnect(socket, nullptr, this, nullptr);
356
+ if (isWebSocketUpgradeRequest(lines)) {
357
+ handleProxyWebSocketUpgrade(socket, raw, target);
358
+ } else {
359
+ handleProxyHttpRequest(socket, raw, target);
360
+ }
361
+ return;
362
+ }
363
+
364
+ if (method != QStringLiteral("GET") && method != QStringLiteral("HEAD")) {
365
+ socket->write("HTTP/1.1 405 Method Not Allowed\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
366
+ socket->disconnectFromHost();
367
+ return;
368
+ }
369
+
370
+ QString contentType;
371
+ int statusCode = 200;
372
+ const QByteArray body = readHttpAsset(target, &contentType, &statusCode);
373
+
374
+ QByteArray response;
375
+ response += "HTTP/1.1 " + QByteArray::number(statusCode) + " " + statusReason(statusCode) + "\r\n";
376
+ response += "Content-Type: " + contentType.toUtf8() + "\r\n";
377
+ response += "Content-Length: " + QByteArray::number(body.size()) + "\r\n";
378
+ response += "Cache-Control: no-cache\r\n";
379
+ response += "Connection: close\r\n";
380
+ response += "\r\n";
381
+ if (method != QStringLiteral("HEAD")) {
382
+ response += body;
383
+ }
384
+ socket->write(response);
385
+ socket->disconnectFromHost();
386
+ });
387
+ }
388
+
389
+ void AngularHttpBaseServer::handleProxyHttpRequest(QTcpSocket* clientSocket, const QByteArray& rawRequest, const QString& requestTarget) {
390
+ if (!m_proxyBaseUrl.isValid()) {
391
+ clientSocket->write("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
392
+ clientSocket->disconnectFromHost();
393
+ return;
394
+ }
395
+ if (m_proxyBaseUrl.scheme().toLower() == QStringLiteral("https")) {
396
+ clientSocket->write("HTTP/1.1 501 Not Implemented\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
397
+ clientSocket->disconnectFromHost();
398
+ return;
399
+ }
400
+
401
+ QString target = requestTarget;
402
+ if (target.trimmed().isEmpty() || target == QStringLiteral("/")) {
403
+ target = QStringLiteral("/") + m_entryPoint;
404
+ }
405
+ const QUrl upstreamUrl = m_proxyBaseUrl.resolved(QUrl(target));
406
+ const QString host = upstreamUrl.host();
407
+ const quint16 port = upstreamUrl.port(80);
408
+ if (host.isEmpty()) {
409
+ clientSocket->write("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
410
+ clientSocket->disconnectFromHost();
411
+ return;
412
+ }
413
+
414
+ QTcpSocket* upstreamSocket = new QTcpSocket(this);
415
+ m_proxyPeers.insert(clientSocket, upstreamSocket);
416
+ m_proxyPeers.insert(upstreamSocket, clientSocket);
417
+
418
+ connect(clientSocket, &QTcpSocket::readyRead, this, [this, clientSocket]() {
419
+ QTcpSocket* peer = m_proxyPeers.value(clientSocket, nullptr);
420
+ if (peer != nullptr) {
421
+ peer->write(clientSocket->readAll());
422
+ }
423
+ });
424
+ connect(upstreamSocket, &QTcpSocket::readyRead, this, [this, upstreamSocket]() {
425
+ QTcpSocket* peer = m_proxyPeers.value(upstreamSocket, nullptr);
426
+ if (peer != nullptr) {
427
+ peer->write(upstreamSocket->readAll());
428
+ }
429
+ });
430
+ connect(clientSocket, &QTcpSocket::disconnected, this, [this, clientSocket]() { closeProxyPeer(clientSocket); });
431
+ connect(upstreamSocket, &QTcpSocket::disconnected, this, [this, upstreamSocket]() { closeProxyPeer(upstreamSocket); });
432
+ connect(upstreamSocket, &QTcpSocket::connected, this, [this, upstreamSocket, rawRequest, upstreamUrl]() {
433
+ const int headerEnd = rawRequest.indexOf("\r\n\r\n");
434
+ const QByteArray header = headerEnd >= 0 ? rawRequest.left(headerEnd + 4) : rawRequest;
435
+ const QByteArray body = headerEnd >= 0 ? rawRequest.mid(headerEnd + 4) : QByteArray();
436
+ const QList<QByteArray> lines = header.split('\n');
437
+ if (lines.isEmpty()) {
438
+ return;
439
+ }
440
+
441
+ const QList<QByteArray> requestLine = lines.first().trimmed().split(' ');
442
+ if (requestLine.size() < 3) {
443
+ return;
444
+ }
445
+
446
+ QByteArray rewritten;
447
+ rewritten += requestLine.at(0) + " " + upstreamUrl.path(QUrl::FullyEncoded).toUtf8();
448
+ if (!upstreamUrl.query().isEmpty()) {
449
+ rewritten += "?" + upstreamUrl.query(QUrl::FullyEncoded).toUtf8();
450
+ }
451
+ rewritten += " " + requestLine.at(2) + "\r\n";
452
+
453
+ for (int i = 1; i < lines.size(); ++i) {
454
+ const QByteArray line = lines.at(i).trimmed();
455
+ if (line.isEmpty()) {
456
+ break;
457
+ }
458
+ const QByteArray lower = line.toLower();
459
+ if (lower.startsWith("host:") || lower.startsWith("proxy-connection:")) {
460
+ continue;
461
+ }
462
+ rewritten += line + "\r\n";
463
+ }
464
+
465
+ QByteArray hostHeader = "Host: " + upstreamUrl.host().toUtf8();
466
+ const int upstreamPort = upstreamUrl.port(80);
467
+ if (upstreamPort != 80) {
468
+ hostHeader += ":" + QByteArray::number(upstreamPort);
469
+ }
470
+ rewritten += hostHeader + "\r\n";
471
+ rewritten += "\r\n";
472
+ rewritten += body;
473
+ upstreamSocket->write(rewritten);
474
+ });
475
+ connect(upstreamSocket, &QTcpSocket::errorOccurred, this, [clientSocket](QAbstractSocket::SocketError) {
476
+ if (clientSocket != nullptr && clientSocket->isOpen()) {
477
+ clientSocket->write("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
478
+ clientSocket->disconnectFromHost();
479
+ }
480
+ });
481
+
482
+ upstreamSocket->connectToHost(host, port);
483
+ }
484
+
485
+ bool AngularHttpBaseServer::isWebSocketUpgradeRequest(const QList<QByteArray>& lines) const {
486
+ bool hasUpgradeHeader = false;
487
+ bool hasConnectionUpgrade = false;
488
+ for (const QByteArray& rawLine : lines) {
489
+ const QByteArray line = rawLine.trimmed().toLower();
490
+ if (line.startsWith("upgrade:") && line.contains("websocket")) {
491
+ hasUpgradeHeader = true;
492
+ }
493
+ if (line.startsWith("connection:") && line.contains("upgrade")) {
494
+ hasConnectionUpgrade = true;
495
+ }
496
+ }
497
+ return hasUpgradeHeader && hasConnectionUpgrade;
498
+ }
499
+
500
+ void AngularHttpBaseServer::handleProxyWebSocketUpgrade(QTcpSocket* clientSocket, const QByteArray& rawRequest, const QString& requestTarget) {
501
+ if (!m_proxyBaseUrl.isValid()) {
502
+ clientSocket->write("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
503
+ clientSocket->disconnectFromHost();
504
+ return;
505
+ }
506
+
507
+ const QString upstreamScheme = m_proxyBaseUrl.scheme().toLower() == QStringLiteral("https")
508
+ ? QStringLiteral("wss")
509
+ : QStringLiteral("ws");
510
+ if (upstreamScheme == QStringLiteral("wss")) {
511
+ clientSocket->write("HTTP/1.1 501 Not Implemented\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
512
+ clientSocket->disconnectFromHost();
513
+ return;
514
+ }
515
+
516
+ QString target = requestTarget;
517
+ if (target.trimmed().isEmpty() || target == QStringLiteral("/")) {
518
+ target = QStringLiteral("/") + m_entryPoint;
519
+ }
520
+ const QUrl upstreamUrl = m_proxyBaseUrl.resolved(QUrl(target));
521
+ const QString host = upstreamUrl.host();
522
+ const quint16 port = upstreamUrl.port(80);
523
+ if (host.isEmpty()) {
524
+ clientSocket->write("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
525
+ clientSocket->disconnectFromHost();
526
+ return;
527
+ }
528
+
529
+ QTcpSocket* upstreamSocket = new QTcpSocket(this);
530
+ m_proxyPeers.insert(clientSocket, upstreamSocket);
531
+ m_proxyPeers.insert(upstreamSocket, clientSocket);
532
+
533
+ connect(clientSocket, &QTcpSocket::readyRead, this, [this, clientSocket]() {
534
+ QTcpSocket* peer = m_proxyPeers.value(clientSocket, nullptr);
535
+ if (peer != nullptr) {
536
+ peer->write(clientSocket->readAll());
537
+ }
538
+ });
539
+ connect(upstreamSocket, &QTcpSocket::readyRead, this, [this, upstreamSocket]() {
540
+ QTcpSocket* peer = m_proxyPeers.value(upstreamSocket, nullptr);
541
+ if (peer != nullptr) {
542
+ peer->write(upstreamSocket->readAll());
543
+ }
544
+ });
545
+ connect(clientSocket, &QTcpSocket::disconnected, this, [this, clientSocket]() { closeProxyPeer(clientSocket); });
546
+ connect(upstreamSocket, &QTcpSocket::disconnected, this, [this, upstreamSocket]() { closeProxyPeer(upstreamSocket); });
547
+ connect(upstreamSocket, &QTcpSocket::connected, this, [this, upstreamSocket, rawRequest, upstreamUrl]() {
548
+ const QList<QByteArray> lines = rawRequest.split('\n');
549
+ QByteArray rewritten;
550
+ rewritten += "GET " + upstreamUrl.path(QUrl::FullyEncoded).toUtf8();
551
+ if (!upstreamUrl.query().isEmpty()) {
552
+ rewritten += "?" + upstreamUrl.query(QUrl::FullyEncoded).toUtf8();
553
+ }
554
+ rewritten += " HTTP/1.1\r\n";
555
+ for (int i = 1; i < lines.size(); ++i) {
556
+ const QByteArray line = lines.at(i).trimmed();
557
+ if (line.isEmpty()) {
558
+ break;
559
+ }
560
+ const QByteArray lower = line.toLower();
561
+ if (lower.startsWith("host:") || lower.startsWith("connection:") || lower.startsWith("upgrade:")) {
562
+ continue;
563
+ }
564
+ rewritten += line + "\r\n";
565
+ }
566
+ QByteArray hostHeader = "Host: " + upstreamUrl.host().toUtf8();
567
+ const int upstreamPort = upstreamUrl.port(80);
568
+ if (upstreamPort != 80) {
569
+ hostHeader += ":" + QByteArray::number(upstreamPort);
570
+ }
571
+ rewritten += hostHeader + "\r\n";
572
+ rewritten += "Connection: Upgrade\r\n";
573
+ rewritten += "Upgrade: websocket\r\n";
574
+ rewritten += "\r\n";
575
+ upstreamSocket->write(rewritten);
576
+ });
577
+ connect(upstreamSocket, &QTcpSocket::errorOccurred, this, [clientSocket](QAbstractSocket::SocketError) {
578
+ if (clientSocket != nullptr && clientSocket->isOpen()) {
579
+ clientSocket->write("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
580
+ clientSocket->disconnectFromHost();
581
+ }
582
+ });
583
+
584
+ upstreamSocket->connectToHost(host, port);
585
+ }
586
+
587
+ void AngularHttpBaseServer::closeProxyPeer(QTcpSocket* socket) {
588
+ if (socket == nullptr) {
589
+ return;
590
+ }
591
+ QTcpSocket* peer = m_proxyPeers.take(socket);
592
+ if (peer != nullptr) {
593
+ m_proxyPeers.remove(peer);
594
+ disconnect(peer, nullptr, this, nullptr);
595
+ if (peer->isOpen()) {
596
+ peer->close();
597
+ }
598
+ peer->deleteLater();
599
+ }
600
+ disconnect(socket, nullptr, this, nullptr);
601
+ if (socket->isOpen()) {
602
+ socket->close();
603
+ }
604
+ socket->deleteLater();
605
+ }
606
+
607
+ QByteArray AngularHttpBaseServer::readHttpAsset(const QString& requestPath, QString* contentType, int* statusCode) const {
608
+ if (requestPath == QStringLiteral("/anqst-dev-config.json")) {
609
+ QVariantMap config;
610
+ config.insert(QStringLiteral("wsUrl"), websocketUrl());
611
+ config.insert(QStringLiteral("bridgeObject"), m_bridgeObjectName);
612
+ const QJsonDocument doc = QJsonDocument::fromVariant(config);
613
+ *contentType = QStringLiteral("application/json; charset=utf-8");
614
+ *statusCode = 200;
615
+ return doc.toJson(QJsonDocument::Compact);
616
+ }
617
+
618
+ const QString filePath = resolveFilePath(requestPath);
619
+ if (filePath.isEmpty()) {
620
+ *statusCode = 404;
621
+ *contentType = QStringLiteral("text/plain; charset=utf-8");
622
+ return QByteArray("Not Found");
623
+ }
624
+
625
+ QFile file(filePath);
626
+ if (!file.open(QIODevice::ReadOnly)) {
627
+ *statusCode = 404;
628
+ *contentType = QStringLiteral("text/plain; charset=utf-8");
629
+ return QByteArray("Not Found");
630
+ }
631
+
632
+ *statusCode = 200;
633
+ *contentType = guessContentType(filePath);
634
+ return file.readAll();
635
+ }
636
+
637
+ QByteArray AngularHttpBaseServer::readHttpProxy(
638
+ const QString& method,
639
+ const QString& requestPath,
640
+ const QByteArray& body,
641
+ QString* contentType,
642
+ int* statusCode) const {
643
+ Q_UNUSED(body);
644
+ if (requestPath == QStringLiteral("/anqst-dev-config.json")) {
645
+ QVariantMap config;
646
+ config.insert(QStringLiteral("wsUrl"), websocketUrl());
647
+ config.insert(QStringLiteral("bridgeObject"), m_bridgeObjectName);
648
+ const QJsonDocument doc = QJsonDocument::fromVariant(config);
649
+ *contentType = QStringLiteral("application/json; charset=utf-8");
650
+ *statusCode = 200;
651
+ return doc.toJson(QJsonDocument::Compact);
652
+ }
653
+
654
+ QString targetPath = requestPath;
655
+ if (targetPath.isEmpty() || targetPath == QStringLiteral("/")) {
656
+ targetPath = QStringLiteral("/") + m_entryPoint;
657
+ }
658
+ const QUrl upstreamUrl = m_proxyBaseUrl.resolved(QUrl(targetPath));
659
+ if (!upstreamUrl.isValid()) {
660
+ *statusCode = 502;
661
+ *contentType = QStringLiteral("text/plain; charset=utf-8");
662
+ return QByteArray("Bad Gateway");
663
+ }
664
+
665
+ QNetworkRequest request(upstreamUrl);
666
+ request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
667
+
668
+ QNetworkAccessManager manager;
669
+ QNetworkReply* reply = nullptr;
670
+ if (method == QStringLiteral("HEAD")) {
671
+ reply = manager.head(request);
672
+ } else {
673
+ reply = manager.get(request);
674
+ }
675
+
676
+ QEventLoop loop;
677
+ QTimer timeout;
678
+ bool timedOut = false;
679
+ timeout.setSingleShot(true);
680
+ connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
681
+ connect(&timeout, &QTimer::timeout, &loop, [&]() {
682
+ timedOut = true;
683
+ reply->abort();
684
+ loop.quit();
685
+ });
686
+ timeout.start(10000);
687
+ loop.exec();
688
+ timeout.stop();
689
+
690
+ if (timedOut) {
691
+ reply->deleteLater();
692
+ *statusCode = 504;
693
+ *contentType = QStringLiteral("text/plain; charset=utf-8");
694
+ return QByteArray("Gateway Timeout");
695
+ }
696
+
697
+ const int responseStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
698
+ const QVariant contentTypeHeader = reply->header(QNetworkRequest::ContentTypeHeader);
699
+ const QByteArray payload = reply->readAll();
700
+ const QNetworkReply::NetworkError networkError = reply->error();
701
+ reply->deleteLater();
702
+
703
+ if (networkError != QNetworkReply::NoError && responseStatus == 0) {
704
+ *statusCode = 502;
705
+ *contentType = QStringLiteral("text/plain; charset=utf-8");
706
+ return QByteArray("Bad Gateway");
707
+ }
708
+
709
+ *statusCode = responseStatus > 0 ? responseStatus : 200;
710
+ *contentType = contentTypeHeader.toString().isEmpty()
711
+ ? QStringLiteral("application/octet-stream")
712
+ : contentTypeHeader.toString();
713
+ return payload;
714
+ }
715
+
716
+ QString AngularHttpBaseServer::resolveFilePath(const QString& requestPath) const {
717
+ const QString pathOnly = QUrl(requestPath).path();
718
+ QString requested = pathOnly;
719
+ if (requested.isEmpty() || requested == QStringLiteral("/")) {
720
+ requested = QStringLiteral("/") + m_entryPoint;
721
+ }
722
+
723
+ if (m_contentRootMode == ContentRootMode::Filesystem) {
724
+ QDir rootDir(m_contentRoot);
725
+ const QString rel = requested.startsWith('/') ? requested.mid(1) : requested;
726
+ const QString resolved = QDir::cleanPath(rootDir.absoluteFilePath(rel));
727
+ if (!resolved.startsWith(rootDir.absolutePath())) {
728
+ return QString();
729
+ }
730
+ return resolved;
731
+ }
732
+
733
+ if (m_contentRootMode == ContentRootMode::Qrc) {
734
+ QString qrcRoot = m_contentRoot;
735
+ if (qrcRoot.startsWith(QStringLiteral("qrc:/"))) {
736
+ qrcRoot = QStringLiteral(":") + qrcRoot.mid(QStringLiteral("qrc:").size());
737
+ } else if (!qrcRoot.startsWith(':')) {
738
+ qrcRoot.prepend(':');
739
+ }
740
+ if (qrcRoot.endsWith('/')) {
741
+ qrcRoot.chop(1);
742
+ }
743
+ return QDir::cleanPath(qrcRoot + requested);
744
+ }
745
+
746
+ return QString();
747
+ }
748
+
749
+ void AngularHttpBaseServer::handleWebSocketConnected() {
750
+ QTcpSocket* socket = m_wsServer->nextPendingConnection();
751
+ if (socket == nullptr) {
752
+ return;
753
+ }
754
+ detachCurrentClient();
755
+ wireClient(socket);
756
+ emit clientAttached(socket->peerAddress().toString());
757
+ }
758
+
759
+ void AngularHttpBaseServer::wireClient(QTcpSocket* socket) {
760
+ m_client = socket;
761
+ m_wsHandshakeComplete = false;
762
+ m_wsReadBuffer.clear();
763
+ connect(m_client, &QTcpSocket::readyRead, this, &AngularHttpBaseServer::handleWebSocketSocketData);
764
+ connect(m_client, &QTcpSocket::disconnected, this, [this]() {
765
+ if (m_client != nullptr) {
766
+ m_client->deleteLater();
767
+ m_client = nullptr;
768
+ m_wsReadBuffer.clear();
769
+ m_wsHandshakeComplete = false;
770
+ if (m_facade != nullptr) {
771
+ m_facade->setDispatchEnabled(false);
772
+ }
773
+ }
774
+ emit clientDetached();
775
+ });
776
+ }
777
+
778
+ void AngularHttpBaseServer::handleWebSocketSocketData() {
779
+ if (m_client == nullptr) {
780
+ return;
781
+ }
782
+ m_wsReadBuffer.append(m_client->readAll());
783
+ if (!m_wsHandshakeComplete) {
784
+ if (!tryCompleteWebSocketHandshake()) {
785
+ return;
786
+ }
787
+ m_wsHandshakeComplete = true;
788
+ const bool wasDispatchEnabled = m_facade != nullptr && m_facade->dispatchEnabled();
789
+ if (m_facade != nullptr) {
790
+ m_facade->setDispatchEnabled(true);
791
+ }
792
+ sendJsonToClient({
793
+ {QStringLiteral("type"), QStringLiteral("hostReady")},
794
+ });
795
+ if (m_facade != nullptr && wasDispatchEnabled) {
796
+ m_facade->emitOutputSnapshot();
797
+ }
798
+ }
799
+
800
+ QString message;
801
+ while (tryConsumeWebSocketFrame(&message)) {
802
+ const QVariantMap payload = parseJsonPayload(message);
803
+ const QString type = payload.value(QStringLiteral("type")).toString();
804
+ if (m_facade == nullptr || m_client == nullptr) {
805
+ return;
806
+ }
807
+ if (type == QStringLiteral("registerSlot")) {
808
+ m_facade->registerSlot(payload.value(QStringLiteral("service")).toString(), payload.value(QStringLiteral("member")).toString());
809
+ continue;
810
+ }
811
+ if (type == QStringLiteral("call")) {
812
+ const QVariantList args = payload.value(QStringLiteral("args")).toList();
813
+ const QVariant result = m_facade->call(payload.value(QStringLiteral("service")).toString(), payload.value(QStringLiteral("member")).toString(), args);
814
+ sendJsonToClient({
815
+ {QStringLiteral("type"), QStringLiteral("callResult")},
816
+ {QStringLiteral("requestId"), payload.value(QStringLiteral("requestId")).toString()},
817
+ {QStringLiteral("result"), result},
818
+ });
819
+ continue;
820
+ }
821
+ if (type == QStringLiteral("emit")) {
822
+ m_facade->emitMessage(payload.value(QStringLiteral("service")).toString(), payload.value(QStringLiteral("member")).toString(), payload.value(QStringLiteral("args")).toList());
823
+ continue;
824
+ }
825
+ if (type == QStringLiteral("setInput")) {
826
+ m_facade->setInput(payload.value(QStringLiteral("service")).toString(), payload.value(QStringLiteral("member")).toString(), payload.value(QStringLiteral("value")));
827
+ continue;
828
+ }
829
+ if (type == QStringLiteral("resolveSlot")) {
830
+ m_facade->resolveSlot(
831
+ payload.value(QStringLiteral("requestId")).toString(),
832
+ payload.value(QStringLiteral("ok")).toBool(),
833
+ payload.value(QStringLiteral("payload")),
834
+ payload.value(QStringLiteral("error")).toString());
835
+ }
836
+ }
837
+ }
838
+
839
+ bool AngularHttpBaseServer::tryCompleteWebSocketHandshake() {
840
+ const int headerEnd = m_wsReadBuffer.indexOf("\r\n\r\n");
841
+ if (headerEnd < 0) {
842
+ return false;
843
+ }
844
+ const QByteArray header = m_wsReadBuffer.left(headerEnd + 4);
845
+ m_wsReadBuffer.remove(0, headerEnd + 4);
846
+ const QList<QByteArray> lines = header.split('\n');
847
+ QByteArray key;
848
+ for (const QByteArray& rawLine : lines) {
849
+ const QByteArray line = rawLine.trimmed();
850
+ if (line.toLower().startsWith("sec-websocket-key:")) {
851
+ key = line.mid(sizeof("sec-websocket-key:") - 1).trimmed();
852
+ break;
853
+ }
854
+ }
855
+ if (key.isEmpty() || m_client == nullptr) {
856
+ return false;
857
+ }
858
+
859
+ static const QByteArray kMagic("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
860
+ const QByteArray acceptRaw = QCryptographicHash::hash(key + kMagic, QCryptographicHash::Sha1).toBase64();
861
+ QByteArray response;
862
+ response += "HTTP/1.1 101 Switching Protocols\r\n";
863
+ response += "Upgrade: websocket\r\n";
864
+ response += "Connection: Upgrade\r\n";
865
+ response += "Sec-WebSocket-Accept: " + acceptRaw + "\r\n";
866
+ response += "\r\n";
867
+ m_client->write(response);
868
+ return true;
869
+ }
870
+
871
+ bool AngularHttpBaseServer::tryConsumeWebSocketFrame(QString* outMessage) {
872
+ if (m_wsReadBuffer.size() < 2) {
873
+ return false;
874
+ }
875
+ const quint8 b0 = static_cast<quint8>(m_wsReadBuffer.at(0));
876
+ const quint8 b1 = static_cast<quint8>(m_wsReadBuffer.at(1));
877
+ const bool masked = (b1 & 0x80) != 0;
878
+ quint64 payloadLen = static_cast<quint8>(b1 & 0x7F);
879
+ int offset = 2;
880
+ if (payloadLen == 126) {
881
+ if (m_wsReadBuffer.size() < 4) return false;
882
+ payloadLen = (static_cast<quint8>(m_wsReadBuffer.at(2)) << 8) |
883
+ static_cast<quint8>(m_wsReadBuffer.at(3));
884
+ offset = 4;
885
+ } else if (payloadLen == 127) {
886
+ if (m_wsReadBuffer.size() < 10) return false;
887
+ payloadLen = 0;
888
+ for (int i = 0; i < 8; ++i) {
889
+ payloadLen = (payloadLen << 8) | static_cast<quint8>(m_wsReadBuffer.at(2 + i));
890
+ }
891
+ offset = 10;
892
+ }
893
+
894
+ const int maskBytes = masked ? 4 : 0;
895
+ if (m_wsReadBuffer.size() < offset + maskBytes + static_cast<int>(payloadLen)) {
896
+ return false;
897
+ }
898
+
899
+ QByteArray mask;
900
+ if (masked) {
901
+ mask = m_wsReadBuffer.mid(offset, 4);
902
+ }
903
+ QByteArray payload = m_wsReadBuffer.mid(offset + maskBytes, static_cast<int>(payloadLen));
904
+ if (masked) {
905
+ for (int i = 0; i < payload.size(); ++i) {
906
+ payload[i] = payload.at(i) ^ mask.at(i % 4);
907
+ }
908
+ }
909
+ m_wsReadBuffer.remove(0, offset + maskBytes + static_cast<int>(payloadLen));
910
+
911
+ const quint8 opcode = b0 & 0x0F;
912
+ if (opcode == 0x8) {
913
+ detachCurrentClient();
914
+ return false;
915
+ }
916
+ if (opcode != 0x1) {
917
+ return false;
918
+ }
919
+ *outMessage = QString::fromUtf8(payload);
920
+ return true;
921
+ }
922
+
923
+ void AngularHttpBaseServer::detachCurrentClient() {
924
+ if (m_client == nullptr) {
925
+ return;
926
+ }
927
+ disconnect(m_client, nullptr, this, nullptr);
928
+ m_client->close();
929
+ m_client->deleteLater();
930
+ m_client = nullptr;
931
+ m_wsReadBuffer.clear();
932
+ m_wsHandshakeComplete = false;
933
+ if (m_facade != nullptr) {
934
+ m_facade->setDispatchEnabled(false);
935
+ }
936
+ emit clientDetached();
937
+ }
938
+
939
+ void AngularHttpBaseServer::sendJsonToClient(const QVariantMap& payload) {
940
+ if (m_client == nullptr || !m_wsHandshakeComplete) {
941
+ return;
942
+ }
943
+ const QJsonObject obj = QJsonObject::fromVariantMap(payload);
944
+ const QJsonDocument doc(obj);
945
+ const QByteArray body = doc.toJson(QJsonDocument::Compact);
946
+ QByteArray frame;
947
+ frame.append(static_cast<char>(0x81));
948
+ if (body.size() < 126) {
949
+ frame.append(static_cast<char>(body.size()));
950
+ } else if (body.size() <= 0xFFFF) {
951
+ frame.append(static_cast<char>(126));
952
+ frame.append(static_cast<char>((body.size() >> 8) & 0xFF));
953
+ frame.append(static_cast<char>(body.size() & 0xFF));
954
+ } else {
955
+ frame.append(static_cast<char>(127));
956
+ const quint64 len = static_cast<quint64>(body.size());
957
+ for (int i = 7; i >= 0; --i) {
958
+ frame.append(static_cast<char>((len >> (i * 8)) & 0xFF));
959
+ }
960
+ }
961
+ frame.append(body);
962
+ m_client->write(frame);
963
+ }
964
+
965
+ } // namespace ANQST_WEBBASE_NAMESPACE