cutelyst 4.3.0
A C++ Web Framework built on top of Qt, using the simple approach of Catalyst (Perl) framework.
protocolwebsocket.cpp
1/*
2 * SPDX-FileCopyrightText: (C) 2017-2018 Daniel Nicoletti <dantti12@gmail.com>
3 * SPDX-License-Identifier: BSD-3-Clause
4 */
5#include "protocolwebsocket.h"
6
7#include "protocolhttp.h"
8#include "server.h"
9#include "socket.h"
10
11#include <Cutelyst/Context>
12#include <Cutelyst/Headers>
13#include <Cutelyst/Response>
14
15#include <QLoggingCategory>
16
17#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
18# include <QTextCodec>
19#else
20# include <QStringConverter>
21#endif
22
23Q_LOGGING_CATEGORY(C_SERVER_WS, "cutelyst.server.websocket", QtWarningMsg)
24
25using namespace Cutelyst;
26
27ProtocolWebSocket::ProtocolWebSocket(Server *wsgi)
28 : Protocol(wsgi)
29 , m_websockets_max_size(wsgi->websocketMaxSize() * 1024)
30{
31}
32
33ProtocolWebSocket::~ProtocolWebSocket()
34{
35}
36
37Protocol::Type ProtocolWebSocket::type() const
38{
39 return Protocol::Type::Http11Websocket;
40}
41
42QByteArray ProtocolWebSocket::createWebsocketHeader(quint8 opcode, quint64 len)
43{
44 QByteArray ret;
45 ret.append(char(0x80 + opcode));
46
47 if (len < 126) {
48 ret.append(static_cast<char>(len));
49 } else if (len <= static_cast<quint16>(0xffff)) {
50 ret.append(char(126));
51
52 quint8 buf[2];
53 buf[1] = quint8(len & 0xff);
54 buf[0] = quint8((len >> 8) & 0xff);
55 ret.append(reinterpret_cast<char *>(buf), 2);
56 } else {
57 ret.append(127);
58
59 quint8 buf[8];
60 buf[7] = quint8(len & 0xff);
61 buf[6] = quint8((len >> 8) & 0xff);
62 buf[5] = quint8((len >> 16) & 0xff);
63 buf[4] = quint8((len >> 24) & 0xff);
64 buf[3] = quint8((len >> 32) & 0xff);
65 buf[2] = quint8((len >> 40) & 0xff);
66 buf[1] = quint8((len >> 48) & 0xff);
67 buf[0] = quint8((len >> 56) & 0xff);
68 ret.append(reinterpret_cast<char *>(buf), 8);
69 }
70
71 return ret;
72}
73
74QByteArray ProtocolWebSocket::createWebsocketCloseReply(const QString &msg, quint16 closeCode)
75{
76 QByteArray payload;
77
78 const QByteArray data = msg.toUtf8().left(123);
79
80 payload = ProtocolWebSocket::createWebsocketHeader(ProtoRequestHttp::OpCodeClose,
81 quint64(data.size() + 2));
82
83 quint8 buf[2];
84 buf[1] = quint8(closeCode & 0xff);
85 buf[0] = quint8((closeCode >> 8) & 0xff);
86 payload.append(reinterpret_cast<char *>(buf), 2);
87
88 // 125 is max payload - 2 of the above bytes
89 payload.append(data);
90
91 return payload;
92}
93
94void ProtocolWebSocket::parse(Socket *sock, QIODevice *io) const
95{
96 qint64 bytesAvailable = io->bytesAvailable();
97 auto request = static_cast<ProtoRequestHttp *>(sock->protoData);
98
99 Q_FOREVER
100 {
101 if (!bytesAvailable || !request->websocket_need ||
102 (bytesAvailable < request->websocket_need &&
103 request->websocket_phase != ProtoRequestHttp::WebSocketPhasePayload)) {
104 // Need more data
105 return;
106 }
107
108 quint32 maxlen = qMin(request->websocket_need, static_cast<quint32>(m_postBufferSize));
109 qint64 len = io->read(m_postBuffer, maxlen);
110 if (len == -1) {
111 qCWarning(C_SERVER_WS) << "Failed to read from socket" << io->errorString();
112 sock->connectionClose();
113 return;
114 }
115 bytesAvailable -= len;
116
117 switch (request->websocket_phase) {
118 case ProtoRequestHttp::WebSocketPhaseHeaders:
119 if (!websocket_parse_header(sock, m_postBuffer, io)) {
120 return;
121 }
122 break;
123 case ProtoRequestHttp::WebSocketPhaseSize:
124 if (!websocket_parse_size(sock, m_postBuffer, m_websockets_max_size)) {
125 return;
126 }
127 break;
128 case ProtoRequestHttp::WebSocketPhaseMask:
129 websocket_parse_mask(sock, m_postBuffer, io);
130 break;
131 case ProtoRequestHttp::WebSocketPhasePayload:
132 if (!websocket_parse_payload(sock, m_postBuffer, int(len), io)) {
133 return;
134 }
135 break;
136 }
137 }
138}
139
140ProtocolData *ProtocolWebSocket::createData(Socket *sock) const
141{
142 Q_UNUSED(sock)
143 return nullptr;
144}
145
146bool ProtocolWebSocket::send_text(Cutelyst::Context *c, Socket *sock, bool singleFrame) const
147{
148 Cutelyst::Request *request = c->request();
149 auto protoRequest = static_cast<ProtoRequestHttp *>(sock->protoData);
150
151 const int msg_size = protoRequest->websocket_message.size();
152 protoRequest->websocket_message.append(protoRequest->websocket_payload);
153
154 QByteArray payload = protoRequest->websocket_payload;
155 if (protoRequest->websocket_start_of_frame != msg_size) {
156 payload = protoRequest->websocket_message.mid(protoRequest->websocket_start_of_frame);
157 }
158
159#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
160 QTextCodec::ConverterState state;
161 const QString frame = m_codec->toUnicode(payload.data(), payload.size(), &state);
162 const bool failed = state.invalidChars || state.remainingChars;
163#else
164 auto toUtf16 = QStringDecoder(QStringDecoder::Utf8);
165 const QString frame = toUtf16(payload);
166 const bool failed = false; // FIXME
167#endif
168 if (singleFrame && (failed || (frame.isEmpty() && payload.size()))) {
169 sock->connectionClose();
170 return false;
171 } else if (!failed) {
172 protoRequest->websocket_start_of_frame = protoRequest->websocket_message.size();
173 Q_EMIT request->webSocketTextFrame(
174 frame, protoRequest->websocket_finn_opcode & 0x80, protoRequest->context);
175 }
176
177 if (protoRequest->websocket_finn_opcode & 0x80) {
178 protoRequest->websocket_continue_opcode = 0;
179 if (singleFrame || protoRequest->websocket_payload == protoRequest->websocket_message) {
180 Q_EMIT request->webSocketTextMessage(frame, protoRequest->context);
181 } else {
182#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
183 QTextCodec::ConverterState stateMsg;
184 const QString msg = m_codec->toUnicode(protoRequest->websocket_message.data(),
185 protoRequest->websocket_message.size(),
186 &stateMsg);
187 const bool failed = stateMsg.invalidChars || stateMsg.remainingChars;
188#else
189 auto toUtf16 = QStringDecoder(QStringDecoder::Utf8);
190 const QString msg = toUtf16(protoRequest->websocket_message);
191 const bool failed = false; // FIXME
192#endif
193 if (failed) {
194 sock->connectionClose();
195 return false;
196 }
197 Q_EMIT request->webSocketTextMessage(msg, protoRequest->context);
198 }
199 protoRequest->websocket_message = QByteArray();
200 protoRequest->websocket_payload = QByteArray();
201 }
202
203 return true;
204}
205
206void ProtocolWebSocket::send_binary(Cutelyst::Context *c, Socket *sock, bool singleFrame) const
207{
208 Cutelyst::Request *request = c->request();
209 auto protoRequest = static_cast<ProtoRequestHttp *>(sock->protoData);
210
211 protoRequest->websocket_message.append(protoRequest->websocket_payload);
212
213 const QByteArray frame = protoRequest->websocket_payload;
214 Q_EMIT request->webSocketBinaryFrame(
215 frame, protoRequest->websocket_finn_opcode & 0x80, protoRequest->context);
216
217 if (protoRequest->websocket_finn_opcode & 0x80) {
218 protoRequest->websocket_continue_opcode = 0;
219 if (singleFrame || protoRequest->websocket_payload == protoRequest->websocket_message) {
220 Q_EMIT request->webSocketBinaryMessage(frame, protoRequest->context);
221 } else {
222 Q_EMIT request->webSocketBinaryMessage(protoRequest->websocket_message,
223 protoRequest->context);
224 }
225 protoRequest->websocket_message = QByteArray();
226 protoRequest->websocket_payload = QByteArray();
227 }
228}
229
230void ProtocolWebSocket::send_pong(QIODevice *io, const QByteArray data) const
231{
232 io->write(ProtocolWebSocket::createWebsocketHeader(ProtoRequestHttp::OpCodePong,
233 quint64(data.size())));
234 io->write(data);
235}
236
237void ProtocolWebSocket::send_closed(Cutelyst::Context *c, Socket *sock, QIODevice *io) const
238{
239 auto protoRequest = static_cast<ProtoRequestHttp *>(sock->protoData);
240 quint16 closeCode = Cutelyst::Response::CloseCodeMissingStatusCode;
241 QString reason;
242#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
243 QTextCodec::ConverterState state;
244 const QString msg = m_codec->toUnicode(
245 protoRequest->websocket_message.data(), protoRequest->websocket_message.size(), &state);
246 const bool failed = state.invalidChars || state.remainingChars;
247#else
248 const bool failed = false; // FIXME
249#endif
250
251 if (protoRequest->websocket_payload.size() >= 2) {
252 closeCode = net_be16(protoRequest->websocket_payload.data());
253 auto toUtf16 = QStringDecoder(QStringDecoder::Utf8);
254 reason = toUtf16(protoRequest->websocket_payload.mid(2));
255 }
256 Q_EMIT c->request()->webSocketClosed(closeCode, reason);
257
258 if (failed) {
259 reason = QString();
260 closeCode = Cutelyst::Response::CloseCodeProtocolError;
261 } else if (closeCode < 3000 || closeCode > 4999) {
262 switch (closeCode) {
263 case Cutelyst::Response::CloseCodeNormal:
264 case Cutelyst::Response::CloseCodeGoingAway:
265 case Cutelyst::Response::CloseCodeProtocolError:
266 case Cutelyst::Response::CloseCodeDatatypeNotSupported:
267 // case Cutelyst::Response::CloseCodeReserved1004:
268 break;
269 case Cutelyst::Response::CloseCodeMissingStatusCode:
270 if (protoRequest->websocket_payload.isEmpty()) {
271 closeCode = Cutelyst::Response::CloseCodeNormal;
272 } else {
273 closeCode = Cutelyst::Response::CloseCodeProtocolError;
274 }
275 break;
276 // case Cutelyst::Response::CloseCodeAbnormalDisconnection:
277 case Cutelyst::Response::CloseCodeWrongDatatype:
278 case Cutelyst::Response::CloseCodePolicyViolated:
279 case Cutelyst::Response::CloseCodeTooMuchData:
280 case Cutelyst::Response::CloseCodeMissingExtension:
281 case Cutelyst::Response::CloseCodeBadOperation:
282 // case Cutelyst::Response::CloseCodeTlsHandshakeFailed:
283 break;
284 default:
285 reason = QString();
286 closeCode = Cutelyst::Response::CloseCodeProtocolError;
287 break;
288 }
289 }
290
291 const QByteArray reply = ProtocolWebSocket::createWebsocketCloseReply(reason, closeCode);
292 io->write(reply);
293
294 sock->connectionClose();
295}
296
297bool ProtocolWebSocket::websocket_parse_header(Socket *sock, const char *buf, QIODevice *io) const
298{
299 const char byte1 = buf[0];
300 const char byte2 = buf[1];
301
302 auto protoRequest = static_cast<ProtoRequestHttp *>(sock->protoData);
303 protoRequest->websocket_finn_opcode = quint8(byte1);
304 protoRequest->websocket_payload_size = byte2 & 0x7f;
305
306 quint8 opcode = byte1 & 0xf;
307
308 bool websocket_has_mask = byte2 >> 7;
309 if (!websocket_has_mask ||
310 ((opcode == ProtoRequestHttp::OpCodePing || opcode == ProtoRequestHttp::OpCodeClose) &&
311 protoRequest->websocket_payload_size > 125) ||
312 (byte1 & 0x70) ||
313 ((opcode >= ProtoRequestHttp::OpCodeReserved3 &&
314 opcode <= ProtoRequestHttp::OpCodeReserved7) ||
315 (opcode >= ProtoRequestHttp::OpCodeReservedB &&
316 opcode <= ProtoRequestHttp::OpCodeReservedF)) ||
317 (!(byte1 & 0x80) && opcode != ProtoRequestHttp::OpCodeText &&
318 opcode != ProtoRequestHttp::OpCodeBinary && opcode != ProtoRequestHttp::OpCodeContinue) ||
319 (protoRequest->websocket_continue_opcode &&
320 (opcode == ProtoRequestHttp::OpCodeText || opcode == ProtoRequestHttp::OpCodeBinary))) {
321 // RFC errors
322 // client to server MUST have a mask
323 // Control opcode cannot have payload bigger than 125
324 // RSV bytes MUST not be set
325 // reserved opcodes must not be set 3-7
326 // reserved opcodes must not be set B-F
327 // Only Text/Bynary/Coninue opcodes can be fragmented
328 // Continue opcode was set but was NOT followed by CONTINUE
329
330 io->write(ProtocolWebSocket::createWebsocketCloseReply(QString(), 1002)); // Protocol error
331 sock->connectionClose();
332 return false;
333 }
334
335 if (opcode == ProtoRequestHttp::OpCodeText || opcode == ProtoRequestHttp::OpCodeBinary) {
336 protoRequest->websocket_message = QByteArray();
337 protoRequest->websocket_start_of_frame = 0;
338 if (!(byte1 & 0x80)) {
339 // FINN byte not set, store opcode for continue
340 protoRequest->websocket_continue_opcode = opcode;
341 }
342 }
343
344 if (protoRequest->websocket_payload_size == 126) {
345 protoRequest->websocket_need = 2;
346 protoRequest->websocket_phase = ProtoRequestHttp::WebSocketPhaseSize;
347 } else if (protoRequest->websocket_payload_size == 127) {
348 protoRequest->websocket_need = 8;
349 protoRequest->websocket_phase = ProtoRequestHttp::WebSocketPhaseSize;
350 } else {
351 protoRequest->websocket_need = 4;
352 protoRequest->websocket_phase = ProtoRequestHttp::WebSocketPhaseMask;
353 }
354
355 return true;
356}
357
358bool ProtocolWebSocket::websocket_parse_size(Socket *sock,
359 const char *buf,
360 int websockets_max_message_size) const
361{
362 auto protoRequest = static_cast<ProtoRequestHttp *>(sock->protoData);
363 quint64 size;
364 if (protoRequest->websocket_payload_size == 126) {
365 size = net_be16(buf);
366 } else if (protoRequest->websocket_payload_size == 127) {
367 size = net_be64(buf);
368 } else {
369 qCCritical(C_SERVER_WS) << "BUG error in websocket parser:"
370 << protoRequest->websocket_payload_size;
371 sock->connectionClose();
372 return false;
373 }
374
375 if (size > static_cast<quint64>(websockets_max_message_size)) {
376 qCCritical(C_SERVER_WS) << "Payload size too big" << size << "max allowed"
377 << websockets_max_message_size;
378 sock->connectionClose();
379 return false;
380 }
381 protoRequest->websocket_payload_size = size;
382
383 protoRequest->websocket_need = 4;
384 protoRequest->websocket_phase = ProtoRequestHttp::WebSocketPhaseMask;
385
386 return true;
387}
388
389void ProtocolWebSocket::websocket_parse_mask(Socket *sock, char *buf, QIODevice *io) const
390{
391 auto ptr = reinterpret_cast<const quint32 *>(buf);
392 auto protoRequest = static_cast<ProtoRequestHttp *>(sock->protoData);
393 protoRequest->websocket_mask = *ptr;
394
395 protoRequest->websocket_phase = ProtoRequestHttp::WebSocketPhasePayload;
396 protoRequest->websocket_need = quint32(protoRequest->websocket_payload_size);
397
398 protoRequest->websocket_payload = QByteArray();
399 if (protoRequest->websocket_payload_size == 0) {
400 websocket_parse_payload(sock, buf, 0, io);
401 } else {
402 protoRequest->websocket_payload.reserve(int(protoRequest->websocket_payload_size));
403 }
404}
405
406bool ProtocolWebSocket::websocket_parse_payload(Socket *sock,
407 char *buf,
408 int len,
409 QIODevice *io) const
410{
411 auto protoRequest = static_cast<ProtoRequestHttp *>(sock->protoData);
412 auto mask = reinterpret_cast<quint8 *>(&protoRequest->websocket_mask);
413 for (int i = 0, maskIx = protoRequest->websocket_payload.size(); i < len; ++i, ++maskIx) {
414 buf[i] = buf[i] ^ mask[maskIx % 4];
415 }
416
417 protoRequest->websocket_payload.append(buf, len);
418 if (quint64(protoRequest->websocket_payload.size()) < protoRequest->websocket_payload_size) {
419 // need more data
420 protoRequest->websocket_need -= uint(len);
421 return true;
422 }
423
424 protoRequest->websocket_need = 2;
425 protoRequest->websocket_phase = ProtoRequestHttp::WebSocketPhaseHeaders;
426
427 Cutelyst::Request *request = protoRequest->context->request();
428
429 switch (protoRequest->websocket_finn_opcode & 0xf) {
430 case ProtoRequestHttp::OpCodeContinue:
431 switch (protoRequest->websocket_continue_opcode) {
432 case ProtoRequestHttp::OpCodeText:
433 if (!send_text(protoRequest->context, sock, false)) {
434 return false;
435 }
436 break;
437 case ProtoRequestHttp::OpCodeBinary:
438 send_binary(protoRequest->context, sock, false);
439 break;
440 default:
441 qCCritical(C_SERVER_WS)
442 << "Invalid CONTINUE opcode:" << (protoRequest->websocket_finn_opcode & 0xf);
443 sock->connectionClose();
444 return false;
445 }
446 break;
447 case ProtoRequestHttp::OpCodeText:
448 if (!send_text(protoRequest->context, sock, protoRequest->websocket_finn_opcode & 0x80)) {
449 return false;
450 }
451 break;
452 case ProtoRequestHttp::OpCodeBinary:
453 send_binary(protoRequest->context, sock, protoRequest->websocket_finn_opcode & 0x80);
454 break;
455 case ProtoRequestHttp::OpCodeClose:
456 send_closed(protoRequest->context, sock, io);
457 return false;
458 case ProtoRequestHttp::OpCodePing:
459 send_pong(io, protoRequest->websocket_payload.left(125));
460 sock->flush();
461 break;
462 case ProtoRequestHttp::OpCodePong:
463 Q_EMIT request->webSocketPong(protoRequest->websocket_payload, protoRequest->context);
464 break;
465 default:
466 break;
467 }
468
469 return true;
470}
The Cutelyst Context.
Definition context.h:42
Request * request
Definition context.h:71
A request.
Definition request.h:42
void webSocketBinaryMessage(const QByteArray &message, Cutelyst::Context *c)
Emitted when the websocket receives a binary message, this accounts for all binary frames till the la...
void webSocketBinaryFrame(const QByteArray &message, bool isLastFrame, Cutelyst::Context *c)
Emitted when the websocket receives a binary frame, this is usefull for parsing big chunks of data wi...
void webSocketTextFrame(const QString &message, bool isLastFrame, Cutelyst::Context *c)
Emitted when the websocket receives a text frame, this is usefull for parsing big chunks of data with...
void webSocketPong(const QByteArray &payload, Cutelyst::Context *c)
Emitted when the websocket receives a pong frame, which might include a payload.
void webSocketClosed(quint16 closeCode, const QString &reason)
Emitted when the websocket receives a close frame, including a close code and a reason,...
void webSocketTextMessage(const QString &message, Cutelyst::Context *c)
Emitted when the websocket receives a text message, this accounts for all text frames till the last o...
Implements a web server.
Definition server.h:60
The Cutelyst namespace holds all public Cutelyst API.
QByteArray & append(QByteArrayView data)
char * data()
QByteArray left(qsizetype len) const const
QByteArray mid(qsizetype pos, qsizetype len) const const
qsizetype size() const const
virtual qint64 bytesAvailable() const const
QString errorString() const const
QByteArray read(qint64 maxSize)
qint64 write(const QByteArray &data)
bool isEmpty() const const
QByteArray toUtf8() const const