cutelyst 5.0.1
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 *server)
23 : Protocol(server)
24 , m_websockets_max_size(server->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}
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)
void clear()
bool isEmpty() const const
QByteArray toUtf8() const const