cutelyst 5.0.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 const 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::WebSocketPhase::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::WebSocketPhase::WebSocketPhaseHeaders:
114 if (!websocket_parse_header(sock, m_postBuffer, io)) {
115 return;
116 }
117 break;
118 case ProtoRequestHttp::WebSocketPhase::WebSocketPhaseSize:
119 if (!websocket_parse_size(sock, m_postBuffer, m_websockets_max_size)) {
120 return;
121 }
122 break;
123 case ProtoRequestHttp::WebSocketPhase::WebSocketPhaseMask:
124 websocket_parse_mask(sock, m_postBuffer, io);
125 break;
126 case ProtoRequestHttp::WebSocketPhase::WebSocketPhasePayload:
127 if (!websocket_parse_payload(sock, m_postBuffer, int(len), io)) {
128 return;
129 }
130 break;
131 default:
132 Q_UNREACHABLE();
133 break;
134 }
135 }
136}
137
138ProtocolData *ProtocolWebSocket::createData(Socket *sock) const
139{
140 Q_UNUSED(sock)
141 return nullptr;
142}
143
144bool ProtocolWebSocket::send_text(Cutelyst::Context *c, Socket *sock, bool singleFrame) const
145{
146 Cutelyst::Request *request = c->request();
147 auto protoRequest = static_cast<ProtoRequestHttp *>(sock->protoData);
148
149 const int msg_size = protoRequest->websocket_message.size();
150 protoRequest->websocket_message.append(protoRequest->websocket_payload);
151
152 QByteArray payload = protoRequest->websocket_payload;
153 if (protoRequest->websocket_start_of_frame != msg_size) {
154 payload = protoRequest->websocket_message.mid(protoRequest->websocket_start_of_frame);
155 }
156
157 auto toUtf16 = QStringDecoder(QStringDecoder::Utf8);
158 const QString frame = toUtf16(payload);
159 const bool failed = toUtf16.hasError();
160
161 if (singleFrame && (failed || (frame.isEmpty() && payload.size()))) {
162 sock->connectionClose();
163 return false;
164 } else if (!failed) {
165 protoRequest->websocket_start_of_frame = protoRequest->websocket_message.size();
166 Q_EMIT request->webSocketTextFrame(
167 frame, protoRequest->websocket_finn_opcode & 0x80, protoRequest->context);
168 }
169
170 if (protoRequest->websocket_finn_opcode & 0x80) {
171 protoRequest->websocket_continue_opcode = 0;
172 if (singleFrame || protoRequest->websocket_payload == protoRequest->websocket_message) {
173 Q_EMIT request->webSocketTextMessage(frame, protoRequest->context);
174 } else {
175 const QString msg = toUtf16(protoRequest->websocket_message);
176 if (toUtf16.hasError()) {
177 sock->connectionClose();
178 return false;
179 }
180 Q_EMIT request->webSocketTextMessage(msg, protoRequest->context);
181 }
182 protoRequest->websocket_message = QByteArray();
183 protoRequest->websocket_payload = QByteArray();
184 }
185
186 return true;
187}
188
189void ProtocolWebSocket::send_binary(Cutelyst::Context *c, Socket *sock, bool singleFrame) const
190{
191 Cutelyst::Request *request = c->request();
192 auto protoRequest = static_cast<ProtoRequestHttp *>(sock->protoData);
193
194 protoRequest->websocket_message.append(protoRequest->websocket_payload);
195
196 const QByteArray frame = protoRequest->websocket_payload;
197 Q_EMIT request->webSocketBinaryFrame(
198 frame, protoRequest->websocket_finn_opcode & 0x80, protoRequest->context);
199
200 if (protoRequest->websocket_finn_opcode & 0x80) {
201 protoRequest->websocket_continue_opcode = 0;
202 if (singleFrame || protoRequest->websocket_payload == protoRequest->websocket_message) {
203 Q_EMIT request->webSocketBinaryMessage(frame, protoRequest->context);
204 } else {
205 Q_EMIT request->webSocketBinaryMessage(protoRequest->websocket_message,
206 protoRequest->context);
207 }
208 protoRequest->websocket_message = QByteArray();
209 protoRequest->websocket_payload = QByteArray();
210 }
211}
212
213void ProtocolWebSocket::send_pong(QIODevice *io, const QByteArray &data) const
214{
215 io->write(ProtocolWebSocket::createWebsocketHeader(ProtoRequestHttp::OpCodePong,
216 quint64(data.size())));
217 io->write(data);
218}
219
220void ProtocolWebSocket::send_closed(const Cutelyst::Context *c, Socket *sock, QIODevice *io) const
221{
222 auto protoRequest = static_cast<ProtoRequestHttp *>(sock->protoData);
223 quint16 closeCode = Cutelyst::Response::CloseCodeMissingStatusCode;
224 QString reason;
225 const bool failed = false; // FIXME
226
227 if (protoRequest->websocket_payload.size() >= 2) {
228 closeCode = net_be16(protoRequest->websocket_payload.data());
229 auto toUtf16 = QStringDecoder(QStringDecoder::Utf8);
230 reason = toUtf16(protoRequest->websocket_payload.mid(2));
231 }
232 Q_EMIT c->request()->webSocketClosed(closeCode, reason);
233
234 if (failed) {
235 reason.clear();
236 closeCode = Cutelyst::Response::CloseCodeProtocolError;
237 } else if (closeCode < 3000 || closeCode > 4999) {
238 switch (closeCode) {
239 case Cutelyst::Response::CloseCodeNormal:
240 case Cutelyst::Response::CloseCodeGoingAway:
241 case Cutelyst::Response::CloseCodeProtocolError:
242 case Cutelyst::Response::CloseCodeDatatypeNotSupported:
243 // case Cutelyst::Response::CloseCodeReserved1004:
244 break;
245 case Cutelyst::Response::CloseCodeMissingStatusCode:
246 if (protoRequest->websocket_payload.isEmpty()) {
247 closeCode = Cutelyst::Response::CloseCodeNormal;
248 } else {
249 closeCode = Cutelyst::Response::CloseCodeProtocolError;
250 }
251 break;
252 // case Cutelyst::Response::CloseCodeAbnormalDisconnection:
253 case Cutelyst::Response::CloseCodeWrongDatatype:
254 case Cutelyst::Response::CloseCodePolicyViolated:
255 case Cutelyst::Response::CloseCodeTooMuchData:
256 case Cutelyst::Response::CloseCodeMissingExtension:
257 case Cutelyst::Response::CloseCodeBadOperation:
258 // case Cutelyst::Response::CloseCodeTlsHandshakeFailed:
259 break;
260 default:
261 reason.clear();
262 closeCode = Cutelyst::Response::CloseCodeProtocolError;
263 break;
264 }
265 }
266
267 const QByteArray reply = ProtocolWebSocket::createWebsocketCloseReply(reason, closeCode);
268 io->write(reply);
269
270 sock->connectionClose();
271}
272
273bool ProtocolWebSocket::websocket_parse_header(Socket *sock, const char *buf, QIODevice *io) const
274{
275 const char byte1 = buf[0];
276 const char byte2 = buf[1];
277
278 auto protoRequest = static_cast<ProtoRequestHttp *>(sock->protoData);
279 protoRequest->websocket_finn_opcode = quint8(byte1);
280 protoRequest->websocket_payload_size = byte2 & 0x7f;
281
282 quint8 opcode = byte1 & 0xf;
283
284 bool websocket_has_mask = byte2 >> 7;
285 if (!websocket_has_mask ||
286 ((opcode == ProtoRequestHttp::OpCodePing || opcode == ProtoRequestHttp::OpCodeClose) &&
287 protoRequest->websocket_payload_size > 125) ||
288 (byte1 & 0x70) ||
289 ((opcode >= ProtoRequestHttp::OpCodeReserved3 &&
290 opcode <= ProtoRequestHttp::OpCodeReserved7) ||
291 (opcode >= ProtoRequestHttp::OpCodeReservedB &&
292 opcode <= ProtoRequestHttp::OpCodeReservedF)) ||
293 (!(byte1 & 0x80) && opcode != ProtoRequestHttp::OpCodeText &&
294 opcode != ProtoRequestHttp::OpCodeBinary && opcode != ProtoRequestHttp::OpCodeContinue) ||
295 (protoRequest->websocket_continue_opcode &&
296 (opcode == ProtoRequestHttp::OpCodeText || opcode == ProtoRequestHttp::OpCodeBinary))) {
297 // RFC errors
298 // client to server MUST have a mask
299 // Control opcode cannot have payload bigger than 125
300 // RSV bytes MUST not be set
301 // reserved opcodes must not be set 3-7
302 // reserved opcodes must not be set B-F
303 // Only Text/Bynary/Coninue opcodes can be fragmented
304 // Continue opcode was set but was NOT followed by CONTINUE
305
306 io->write(ProtocolWebSocket::createWebsocketCloseReply({}, 1002)); // Protocol error
307 sock->connectionClose();
308 return false;
309 }
310
311 if (opcode == ProtoRequestHttp::OpCodeText || opcode == ProtoRequestHttp::OpCodeBinary) {
312 protoRequest->websocket_message = QByteArray();
313 protoRequest->websocket_start_of_frame = 0;
314 if (!(byte1 & 0x80)) {
315 // FINN byte not set, store opcode for continue
316 protoRequest->websocket_continue_opcode = opcode;
317 }
318 }
319
320 if (protoRequest->websocket_payload_size == 126) {
321 protoRequest->websocket_need = 2;
322 protoRequest->websocket_phase = ProtoRequestHttp::WebSocketPhase::WebSocketPhaseSize;
323 } else if (protoRequest->websocket_payload_size == 127) {
324 protoRequest->websocket_need = 8;
325 protoRequest->websocket_phase = ProtoRequestHttp::WebSocketPhase::WebSocketPhaseSize;
326 } else {
327 protoRequest->websocket_need = 4;
328 protoRequest->websocket_phase = ProtoRequestHttp::WebSocketPhase::WebSocketPhaseMask;
329 }
330
331 return true;
332}
333
334bool ProtocolWebSocket::websocket_parse_size(Socket *sock,
335 const char *buf,
336 int websockets_max_message_size) const
337{
338 auto protoRequest = static_cast<ProtoRequestHttp *>(sock->protoData);
339 quint64 size;
340 if (protoRequest->websocket_payload_size == 126) {
341 size = net_be16(buf);
342 } else if (protoRequest->websocket_payload_size == 127) {
343 size = net_be64(buf);
344 } else {
345 qCCritical(C_SERVER_WS) << "BUG error in websocket parser:"
346 << protoRequest->websocket_payload_size;
347 sock->connectionClose();
348 return false;
349 }
350
351 if (size > static_cast<quint64>(websockets_max_message_size)) {
352 qCCritical(C_SERVER_WS) << "Payload size too big" << size << "max allowed"
353 << websockets_max_message_size;
354 sock->connectionClose();
355 return false;
356 }
357 protoRequest->websocket_payload_size = size;
358
359 protoRequest->websocket_need = 4;
360 protoRequest->websocket_phase = ProtoRequestHttp::WebSocketPhase::WebSocketPhaseMask;
361
362 return true;
363}
364
365void ProtocolWebSocket::websocket_parse_mask(Socket *sock, char *buf, QIODevice *io) const
366{
367 auto ptr = reinterpret_cast<const quint32 *>(buf);
368 auto protoRequest = static_cast<ProtoRequestHttp *>(sock->protoData);
369 protoRequest->websocket_mask = *ptr;
370
371 protoRequest->websocket_phase = ProtoRequestHttp::WebSocketPhase::WebSocketPhasePayload;
372 protoRequest->websocket_need = quint32(protoRequest->websocket_payload_size);
373
374 protoRequest->websocket_payload = QByteArray();
375 if (protoRequest->websocket_payload_size == 0) {
376 websocket_parse_payload(sock, buf, 0, io);
377 } else {
378 protoRequest->websocket_payload.reserve(int(protoRequest->websocket_payload_size));
379 }
380}
381
382bool ProtocolWebSocket::websocket_parse_payload(Socket *sock,
383 char *buf,
384 int len,
385 QIODevice *io) const
386{
387 auto protoRequest = static_cast<ProtoRequestHttp *>(sock->protoData);
388 const auto *mask = reinterpret_cast<quint8 *>(&protoRequest->websocket_mask);
389 for (int i = 0, maskIx = protoRequest->websocket_payload.size(); i < len; ++i, ++maskIx) {
390 buf[i] = buf[i] ^ mask[maskIx % 4];
391 }
392
393 protoRequest->websocket_payload.append(buf, len);
394 if (quint64(protoRequest->websocket_payload.size()) < protoRequest->websocket_payload_size) {
395 // need more data
396 protoRequest->websocket_need -= uint(len);
397 return true;
398 }
399
400 protoRequest->websocket_need = 2;
401 protoRequest->websocket_phase = ProtoRequestHttp::WebSocketPhase::WebSocketPhaseHeaders;
402
403 Cutelyst::Request *request = protoRequest->context->request();
404
405 switch (protoRequest->websocket_finn_opcode & 0xf) {
406 case ProtoRequestHttp::OpCodeContinue:
407 switch (protoRequest->websocket_continue_opcode) {
408 case ProtoRequestHttp::OpCodeText:
409 if (!send_text(protoRequest->context, sock, false)) {
410 return false;
411 }
412 break;
413 case ProtoRequestHttp::OpCodeBinary:
414 send_binary(protoRequest->context, sock, false);
415 break;
416 default:
417 qCCritical(C_SERVER_WS)
418 << "Invalid CONTINUE opcode:" << (protoRequest->websocket_finn_opcode & 0xf);
419 sock->connectionClose();
420 return false;
421 }
422 break;
423 case ProtoRequestHttp::OpCodeText:
424 if (!send_text(protoRequest->context, sock, protoRequest->websocket_finn_opcode & 0x80)) {
425 return false;
426 }
427 break;
428 case ProtoRequestHttp::OpCodeBinary:
429 send_binary(protoRequest->context, sock, protoRequest->websocket_finn_opcode & 0x80);
430 break;
431 case ProtoRequestHttp::OpCodeClose:
432 send_closed(protoRequest->context, sock, io);
433 return false;
434 case ProtoRequestHttp::OpCodePing:
435 send_pong(io, protoRequest->websocket_payload.left(125));
436 sock->flush();
437 break;
438 case ProtoRequestHttp::OpCodePong:
439 Q_EMIT request->webSocketPong(protoRequest->websocket_payload, protoRequest->context);
440 break;
441 default:
442 break;
443 }
444
445 return true;
446}
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)
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)
void clear()
bool isEmpty() const const
QByteArray toUtf8() const const