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 
23 Q_LOGGING_CATEGORY(C_SERVER_WS, "cutelyst.server.websocket", QtWarningMsg)
24 
25 using namespace Cutelyst;
26 
27 ProtocolWebSocket::ProtocolWebSocket(Server *wsgi)
28  : Protocol(wsgi)
29  , m_websockets_max_size(wsgi->websocketMaxSize() * 1024)
30 {
31 }
32 
33 ProtocolWebSocket::~ProtocolWebSocket()
34 {
35 }
36 
37 Protocol::Type ProtocolWebSocket::type() const
38 {
39  return Protocol::Type::Http11Websocket;
40 }
41 
42 QByteArray 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 
74 QByteArray 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 
94 void 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 
140 ProtocolData *ProtocolWebSocket::createData(Socket *sock) const
141 {
142  Q_UNUSED(sock)
143  return nullptr;
144 }
145 
146 bool 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 
206 void 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 
230 void 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 
237 void 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 
297 bool 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 
358 bool 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 
389 void 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 
406 bool 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 }
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...
Request request
Definition: context.h:72
QString errorString() const const
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:59
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...
The Cutelyst Context.
Definition: context.h:42
void webSocketClosed(quint16 closeCode, const QString &reason)
Emitted when the websocket receives a close frame, including a close code and a reason, it&#39;s also emitted when the connection closes without the client sending the close frame.
bool isEmpty() const const
qint64 read(char *data, qint64 maxSize)
The Cutelyst namespace holds all public Cutelyst API.
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...
QByteArray mid(qsizetype pos, qsizetype len) const const
virtual qint64 bytesAvailable() const const
QByteArray & append(char ch)
QByteArray left(qsizetype len) const const
char * data()
qint64 write(const char *data, qint64 maxSize)
A request.
Definition: request.h:41
qsizetype size() const const
void webSocketPong(const QByteArray &payload, Cutelyst::Context *c)
Emitted when the websocket receives a pong frame, which might include a payload.
QByteArray toUtf8() const const