]> git.netwichtig.de Git - user/henk/code/inspircd.git/blob - src/modules/m_websocket.cpp
Add support for blocking tag messages with the deaf mode.
[user/henk/code/inspircd.git] / src / modules / m_websocket.cpp
1 /*
2  * InspIRCd -- Internet Relay Chat Daemon
3  *
4  *   Copyright (C) 2019 iwalkalone <iwalkalone69@gmail.com>
5  *   Copyright (C) 2017-2020 Sadie Powell <sadie@witchery.services>
6  *   Copyright (C) 2016-2017 Attila Molnar <attilamolnar@hush.com>
7  *
8  * This file is part of InspIRCd.  InspIRCd is free software: you can
9  * redistribute it and/or modify it under the terms of the GNU General Public
10  * License as published by the Free Software Foundation, version 2.
11  *
12  * This program is distributed in the hope that it will be useful, but WITHOUT
13  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
14  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
15  * details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19  */
20
21 /// $CompilerFlags: -Ivendor_directory("utfcpp")
22
23
24 #include "inspircd.h"
25 #include "iohook.h"
26 #include "modules/hash.h"
27
28 #include <utf8.h>
29
30 static const char MagicGUID[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
31 static const char whitespace[] = " \t\r\n";
32 static dynamic_reference_nocheck<HashProvider>* sha1;
33
34 struct WebSocketConfig
35 {
36         typedef std::vector<std::string> OriginList;
37         typedef std::vector<std::string> ProxyRanges;
38
39         // The HTTP origins that can connect to the server.
40         OriginList allowedorigins;
41
42         // The IP ranges which send trustworthy X-Real-IP or X-Forwarded-For headers.
43         ProxyRanges proxyranges;
44
45         // Whether to send as UTF-8 text instead of binary data.
46         bool sendastext;
47 };
48
49 class WebSocketHookProvider : public IOHookProvider
50 {
51  public:
52         WebSocketConfig config;
53         WebSocketHookProvider(Module* mod)
54                 : IOHookProvider(mod, "websocket", IOHookProvider::IOH_UNKNOWN, true)
55         {
56         }
57
58         void OnAccept(StreamSocket* sock, irc::sockets::sockaddrs* client, irc::sockets::sockaddrs* server) CXX11_OVERRIDE;
59
60         void OnConnect(StreamSocket* sock) CXX11_OVERRIDE
61         {
62         }
63 };
64
65 class WebSocketHook : public IOHookMiddle
66 {
67         class HTTPHeaderFinder
68         {
69                 std::string::size_type bpos;
70                 std::string::size_type len;
71
72          public:
73                 bool Find(const std::string& req, const char* header, std::string::size_type headerlen, std::string::size_type maxpos)
74                 {
75                         std::string::size_type keybegin = req.find(header);
76                         if ((keybegin == std::string::npos) || (keybegin > maxpos) || (keybegin == 0) || (req[keybegin-1] != '\n'))
77                                 return false;
78
79                         keybegin += headerlen;
80
81                         bpos = req.find_first_not_of(whitespace, keybegin, sizeof(whitespace)-1);
82                         if ((bpos == std::string::npos) || (bpos > maxpos))
83                                 return false;
84
85                         const std::string::size_type epos = req.find_first_of(whitespace, bpos, sizeof(whitespace)-1);
86                         len = epos - bpos;
87
88                         return true;
89                 }
90
91                 std::string ExtractValue(const std::string& req) const
92                 {
93                         return std::string(req, bpos, len);
94                 }
95         };
96
97         enum OpCode
98         {
99                 OP_CONTINUATION = 0x00,
100                 OP_TEXT = 0x01,
101                 OP_BINARY = 0x02,
102                 OP_CLOSE = 0x08,
103                 OP_PING = 0x09,
104                 OP_PONG = 0x0a
105         };
106
107         enum State
108         {
109                 STATE_HTTPREQ,
110                 STATE_ESTABLISHED
111         };
112
113         static const unsigned char WS_MASKBIT = (1 << 7);
114         static const unsigned char WS_FINBIT = (1 << 7);
115         static const unsigned char WS_PAYLOAD_LENGTH_MAGIC_LARGE = 126;
116         static const unsigned char WS_PAYLOAD_LENGTH_MAGIC_HUGE = 127;
117         static const size_t WS_MAX_PAYLOAD_LENGTH_SMALL = 125;
118         static const size_t WS_MAX_PAYLOAD_LENGTH_LARGE = 65535;
119         static const size_t MAXHEADERSIZE = sizeof(uint64_t) + 2;
120
121         // Clients sending ping or pong frames faster than this are killed
122         static const time_t MINPINGPONGDELAY = 10;
123
124         State state;
125         time_t lastpingpong;
126         WebSocketConfig& config;
127
128         static size_t FillHeader(unsigned char* outbuf, size_t sendlength, OpCode opcode)
129         {
130                 size_t pos = 0;
131                 outbuf[pos++] = WS_FINBIT | opcode;
132
133                 if (sendlength <= WS_MAX_PAYLOAD_LENGTH_SMALL)
134                 {
135                         outbuf[pos++] = sendlength;
136                 }
137                 else if (sendlength <= WS_MAX_PAYLOAD_LENGTH_LARGE)
138                 {
139                         outbuf[pos++] = WS_PAYLOAD_LENGTH_MAGIC_LARGE;
140                         outbuf[pos++] = (sendlength >> 8) & 0xff;
141                         outbuf[pos++] = sendlength & 0xff;
142                 }
143                 else
144                 {
145                         outbuf[pos++] = WS_PAYLOAD_LENGTH_MAGIC_HUGE;
146                         const uint64_t len = sendlength;
147                         for (int i = sizeof(uint64_t)-1; i >= 0; i--)
148                                 outbuf[pos++] = ((len >> i*8) & 0xff);
149                 }
150
151                 return pos;
152         }
153
154         static StreamSocket::SendQueue::Element PrepareSendQElem(size_t size, OpCode opcode)
155         {
156                 unsigned char header[MAXHEADERSIZE];
157                 const size_t n = FillHeader(header, size, opcode);
158
159                 return StreamSocket::SendQueue::Element(reinterpret_cast<const char*>(header), n);
160         }
161
162         int HandleAppData(StreamSocket* sock, std::string& appdataout, bool allowlarge)
163         {
164                 std::string& myrecvq = GetRecvQ();
165                 // Need 1 byte opcode, minimum 1 byte len, 4 bytes masking key
166                 if (myrecvq.length() < 6)
167                         return 0;
168
169                 const std::string& cmyrecvq = myrecvq;
170                 unsigned char len1 = (unsigned char)cmyrecvq[1];
171                 if (!(len1 & WS_MASKBIT))
172                 {
173                         sock->SetError("WebSocket protocol violation: unmasked client frame");
174                         return -1;
175                 }
176
177                 len1 &= ~WS_MASKBIT;
178
179                 // Assume the length is a single byte, if not, update values later
180                 unsigned int len = len1;
181                 unsigned int payloadstartoffset = 6;
182                 const unsigned char* maskkey = reinterpret_cast<const unsigned char*>(&cmyrecvq[2]);
183
184                 if (len1 == WS_PAYLOAD_LENGTH_MAGIC_LARGE)
185                 {
186                         // allowlarge is false for control frames according to the RFC meaning large pings, etc. are not allowed
187                         if (!allowlarge)
188                         {
189                                 sock->SetError("WebSocket protocol violation: large control frame");
190                                 return -1;
191                         }
192
193                         // Large frame, has 2 bytes len after the magic byte indicating the length
194                         // Need 1 byte opcode, 3 bytes len, 4 bytes masking key
195                         if (myrecvq.length() < 8)
196                                 return 0;
197
198                         unsigned char len2 = (unsigned char)cmyrecvq[2];
199                         unsigned char len3 = (unsigned char)cmyrecvq[3];
200                         len = (len2 << 8) | len3;
201
202                         if (len <= WS_MAX_PAYLOAD_LENGTH_SMALL)
203                         {
204                                 sock->SetError("WebSocket protocol violation: non-minimal length encoding used");
205                                 return -1;
206                         }
207
208                         maskkey += 2;
209                         payloadstartoffset += 2;
210                 }
211                 else if (len1 == WS_PAYLOAD_LENGTH_MAGIC_HUGE)
212                 {
213                         sock->SetError("WebSocket: Huge frames are not supported");
214                         return -1;
215                 }
216
217                 if (myrecvq.length() < payloadstartoffset + len)
218                         return 0;
219
220                 unsigned int maskkeypos = 0;
221                 const std::string::iterator endit = myrecvq.begin() + payloadstartoffset + len;
222                 for (std::string::const_iterator i = myrecvq.begin() + payloadstartoffset; i != endit; ++i)
223                 {
224                         const unsigned char c = (unsigned char)*i;
225                         appdataout.push_back(c ^ maskkey[maskkeypos++]);
226                         maskkeypos %= 4;
227                 }
228
229                 myrecvq.erase(myrecvq.begin(), endit);
230                 return 1;
231         }
232
233         int HandlePingPongFrame(StreamSocket* sock, bool isping)
234         {
235                 if (lastpingpong + MINPINGPONGDELAY >= ServerInstance->Time())
236                 {
237                         sock->SetError("WebSocket: Ping/pong flood");
238                         return -1;
239                 }
240
241                 lastpingpong = ServerInstance->Time();
242
243                 std::string appdata;
244                 const int result = HandleAppData(sock, appdata, false);
245                 // If it's a pong stop here regardless of the result so we won't generate a reply
246                 if ((result <= 0) || (!isping))
247                         return result;
248
249                 StreamSocket::SendQueue::Element elem = PrepareSendQElem(appdata.length(), OP_PONG);
250                 elem.append(appdata);
251                 GetSendQ().push_back(elem);
252
253                 SocketEngine::ChangeEventMask(sock, FD_ADD_TRIAL_WRITE);
254                 return 1;
255         }
256
257         int HandleWS(StreamSocket* sock, std::string& destrecvq)
258         {
259                 if (GetRecvQ().empty())
260                         return 0;
261
262                 unsigned char opcode = (unsigned char)GetRecvQ().c_str()[0];
263                 switch (opcode & ~WS_FINBIT)
264                 {
265                         case OP_CONTINUATION:
266                         case OP_TEXT:
267                         case OP_BINARY:
268                         {
269                                 std::string appdata;
270                                 const int result = HandleAppData(sock, appdata, true);
271                                 if (result != 1)
272                                         return result;
273
274                                 // Strip out any CR+LF which may have been erroneously sent.
275                                 for (std::string::const_iterator iter = appdata.begin(); iter != appdata.end(); ++iter)
276                                 {
277                                         if (*iter != '\r' && *iter != '\n')
278                                                 destrecvq.push_back(*iter);
279                                 }
280
281                                 // If we are on the final message of this block append a line terminator.
282                                 if (opcode & WS_FINBIT)
283                                         destrecvq.append("\r\n");
284
285                                 return 1;
286                         }
287
288                         case OP_PING:
289                         {
290                                 return HandlePingPongFrame(sock, true);
291                         }
292
293                         case OP_PONG:
294                         {
295                                 // A pong frame may be sent unsolicited, so we have to handle it.
296                                 // It may carry application data which we need to remove from the recvq as well.
297                                 return HandlePingPongFrame(sock, false);
298                         }
299
300                         case OP_CLOSE:
301                         {
302                                 sock->SetError("Connection closed");
303                                 return -1;
304                         }
305
306                         default:
307                         {
308                                 sock->SetError("WebSocket: Invalid opcode");
309                                 return -1;
310                         }
311                 }
312         }
313
314         void FailHandshake(StreamSocket* sock, const char* httpreply, const char* sockerror)
315         {
316                 GetSendQ().push_back(StreamSocket::SendQueue::Element(httpreply));
317                 sock->DoWrite();
318                 sock->SetError(sockerror);
319         }
320
321         int HandleHTTPReq(StreamSocket* sock)
322         {
323                 std::string& recvq = GetRecvQ();
324                 const std::string::size_type reqend = recvq.find("\r\n\r\n");
325                 if (reqend == std::string::npos)
326                         return 0;
327
328                 bool allowedorigin = false;
329                 HTTPHeaderFinder originheader;
330                 if (originheader.Find(recvq, "Origin:", 7, reqend))
331                 {
332                         const std::string origin = originheader.ExtractValue(recvq);
333                         for (WebSocketConfig::OriginList::const_iterator iter = config.allowedorigins.begin(); iter != config.allowedorigins.end(); ++iter)
334                         {
335                                 if (InspIRCd::Match(origin, *iter, ascii_case_insensitive_map))
336                                 {
337                                         allowedorigin = true;
338                                         break;
339                                 }
340                         }
341                 }
342
343                 if (!allowedorigin)
344                 {
345                         FailHandshake(sock, "HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n", "WebSocket: Received HTTP request from a non-whitelisted origin");
346                         return -1;
347                 }
348
349                 if (!config.proxyranges.empty() && sock->type == StreamSocket::SS_USER)
350                 {
351                         LocalUser* luser = static_cast<UserIOHandler*>(sock)->user;
352                         irc::sockets::sockaddrs realsa(luser->client_sa);
353
354                         HTTPHeaderFinder proxyheader;
355                         if (proxyheader.Find(recvq, "X-Real-IP:", 10, reqend)
356                                 && irc::sockets::aptosa(proxyheader.ExtractValue(recvq), realsa.port(), realsa))
357                         {
358                                 // Nothing to do here.
359                         }
360                         else if (proxyheader.Find(recvq, "X-Forwarded-For:", 16, reqend)
361                                 && irc::sockets::aptosa(proxyheader.ExtractValue(recvq), realsa.port(), realsa))
362                         {
363                                 // Nothing to do here.
364                         }
365
366                         for (WebSocketConfig::ProxyRanges::const_iterator iter = config.proxyranges.begin(); iter != config.proxyranges.end(); ++iter)
367                         {
368                                 if (InspIRCd::MatchCIDR(luser->GetIPString(), *iter, ascii_case_insensitive_map))
369                                 {
370                                         // Give the user their real IP address.
371                                         if (realsa != luser->client_sa)
372                                                 luser->SetClientIP(realsa);
373
374                                         // Error if changing their IP gets them banned.
375                                         if (luser->quitting)
376                                                 return -1;
377                                         break;
378                                 }
379                         }
380                 }
381
382
383                 HTTPHeaderFinder keyheader;
384                 if (!keyheader.Find(recvq, "Sec-WebSocket-Key:", 18, reqend))
385                 {
386                         FailHandshake(sock, "HTTP/1.1 501 Not Implemented\r\nConnection: close\r\n\r\n", "WebSocket: Received HTTP request which is not a websocket upgrade");
387                         return -1;
388                 }
389
390                 if (!*sha1)
391                 {
392                         FailHandshake(sock, "HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n", "WebSocket: SHA-1 provider missing");
393                         return -1;
394                 }
395
396                 state = STATE_ESTABLISHED;
397
398                 std::string key = keyheader.ExtractValue(recvq);
399                 key.append(MagicGUID);
400
401                 std::string reply = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ";
402                 reply.append(BinToBase64((*sha1)->GenerateRaw(key), NULL, '=')).append("\r\n\r\n");
403                 GetSendQ().push_back(StreamSocket::SendQueue::Element(reply));
404
405                 SocketEngine::ChangeEventMask(sock, FD_ADD_TRIAL_WRITE);
406
407                 recvq.erase(0, reqend + 4);
408
409                 return 1;
410         }
411
412  public:
413         WebSocketHook(IOHookProvider* Prov, StreamSocket* sock, WebSocketConfig& cfg)
414                 : IOHookMiddle(Prov)
415                 , state(STATE_HTTPREQ)
416                 , lastpingpong(0)
417                 , config(cfg)
418         {
419                 sock->AddIOHook(this);
420         }
421
422         int OnStreamSocketWrite(StreamSocket* sock, StreamSocket::SendQueue& uppersendq) CXX11_OVERRIDE
423         {
424                 StreamSocket::SendQueue& mysendq = GetSendQ();
425
426                 // Return 1 to allow sending back an error HTTP response
427                 if (state != STATE_ESTABLISHED)
428                         return (mysendq.empty() ? 0 : 1);
429
430                 std::string message;
431                 for (StreamSocket::SendQueue::const_iterator elem = uppersendq.begin(); elem != uppersendq.end(); ++elem)
432                 {
433                         for (StreamSocket::SendQueue::Element::const_iterator chr = elem->begin(); chr != elem->end(); ++chr)
434                         {
435                                 if (*chr == '\n')
436                                 {
437                                         // We have found an entire message. Send it in its own frame.
438                                         if (config.sendastext)
439                                         {
440                                                 // If we send messages as text then we need to ensure they are valid UTF-8.
441                                                 std::string encoded;
442                                                 utf8::replace_invalid(message.begin(), message.end(), std::back_inserter(encoded));
443
444                                                 mysendq.push_back(PrepareSendQElem(encoded.length(), OP_TEXT));
445                                                 mysendq.push_back(encoded);
446                                         }
447                                         else
448                                         {
449                                                 // Otherwise, send the raw message as a binary frame.
450                                                 mysendq.push_back(PrepareSendQElem(message.length(), OP_BINARY));
451                                                 mysendq.push_back(message);
452                                         }
453                                         message.clear();
454                                 }
455                                 else if (*chr != '\r')
456                                 {
457                                         message.push_back(*chr);
458                                 }
459                         }
460                 }
461
462                 // Empty the upper send queue and push whatever is left back onto it.
463                 uppersendq.clear();
464                 if (!message.empty())
465                 {
466                         uppersendq.push_back(message);
467                         return 0;
468                 }
469
470                 return 1;
471         }
472
473         int OnStreamSocketRead(StreamSocket* sock, std::string& destrecvq) CXX11_OVERRIDE
474         {
475                 if (state == STATE_HTTPREQ)
476                 {
477                         int httpret = HandleHTTPReq(sock);
478                         if (httpret <= 0)
479                                 return httpret;
480                 }
481
482                 int wsret;
483                 do
484                 {
485                         wsret = HandleWS(sock, destrecvq);
486                 }
487                 while ((!GetRecvQ().empty()) && (wsret > 0));
488
489                 return wsret;
490         }
491
492         void OnStreamSocketClose(StreamSocket* sock) CXX11_OVERRIDE
493         {
494         }
495 };
496
497 void WebSocketHookProvider::OnAccept(StreamSocket* sock, irc::sockets::sockaddrs* client, irc::sockets::sockaddrs* server)
498 {
499         new WebSocketHook(this, sock, config);
500 }
501
502 class ModuleWebSocket : public Module
503 {
504         dynamic_reference_nocheck<HashProvider> hash;
505         reference<WebSocketHookProvider> hookprov;
506
507  public:
508         ModuleWebSocket()
509                 : hash(this, "hash/sha1")
510                 , hookprov(new WebSocketHookProvider(this))
511         {
512                 sha1 = &hash;
513         }
514
515         void ReadConfig(ConfigStatus& status) CXX11_OVERRIDE
516         {
517                 ConfigTagList tags = ServerInstance->Config->ConfTags("wsorigin");
518                 if (tags.first == tags.second)
519                         throw ModuleException("You have loaded the websocket module but not configured any allowed origins!");
520
521                 WebSocketConfig config;
522                 for (ConfigIter i = tags.first; i != tags.second; ++i)
523                 {
524                         ConfigTag* tag = i->second;
525
526                         // Ensure that we have the <wsorigin:allow> parameter.
527                         const std::string allow = tag->getString("allow");
528                         if (allow.empty())
529                                 throw ModuleException("<wsorigin:allow> is a mandatory field, at " + tag->getTagLocation());
530
531                         config.allowedorigins.push_back(allow);
532                 }
533
534                 ConfigTag* tag = ServerInstance->Config->ConfValue("websocket");
535                 config.sendastext = tag->getBool("sendastext", true);
536
537                 irc::spacesepstream proxyranges(tag->getString("proxyranges"));
538                 for (std::string proxyrange; proxyranges.GetToken(proxyrange); )
539                         config.proxyranges.push_back(proxyrange);
540
541                 // Everything is okay; apply the new config.
542                 hookprov->config = config;
543         }
544
545         void OnCleanup(ExtensionItem::ExtensibleType type, Extensible* item) CXX11_OVERRIDE
546         {
547                 if (type != ExtensionItem::EXT_USER)
548                         return;
549
550                 LocalUser* user = IS_LOCAL(static_cast<User*>(item));
551                 if ((user) && (user->eh.GetModHook(this)))
552                         ServerInstance->Users.QuitUser(user, "WebSocket module unloading");
553         }
554
555         Version GetVersion() CXX11_OVERRIDE
556         {
557                 return Version("Allows WebSocket clients to connect to the IRC server.", VF_VENDOR);
558         }
559 };
560
561 MODULE_INIT(ModuleWebSocket)