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