2 * InspIRCd -- Internet Relay Chat Daemon
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>
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.
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
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/>.
21 /// $CompilerFlags: -Ivendor_directory("utfcpp")
26 #include "modules/hash.h"
28 #define UTF_CPP_CPLUSPLUS 199711L
29 #include <unchecked.h>
31 static const char MagicGUID[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
32 static const char whitespace[] = " \t\r\n";
33 static dynamic_reference_nocheck<HashProvider>* sha1;
35 struct WebSocketConfig
37 typedef std::vector<std::string> OriginList;
38 typedef std::vector<std::string> ProxyRanges;
40 // The HTTP origins that can connect to the server.
41 OriginList allowedorigins;
43 // The IP ranges which send trustworthy X-Real-IP or X-Forwarded-For headers.
44 ProxyRanges proxyranges;
46 // Whether to send as UTF-8 text instead of binary data.
50 class WebSocketHookProvider : public IOHookProvider
53 WebSocketConfig config;
54 WebSocketHookProvider(Module* mod)
55 : IOHookProvider(mod, "websocket", IOHookProvider::IOH_UNKNOWN, true)
59 void OnAccept(StreamSocket* sock, irc::sockets::sockaddrs* client, irc::sockets::sockaddrs* server) CXX11_OVERRIDE;
61 void OnConnect(StreamSocket* sock) CXX11_OVERRIDE
66 class WebSocketHook : public IOHookMiddle
68 class HTTPHeaderFinder
70 std::string::size_type bpos;
71 std::string::size_type len;
74 bool Find(const std::string& req, const char* header, std::string::size_type headerlen, std::string::size_type maxpos)
76 std::string::size_type keybegin = req.find(header);
77 if ((keybegin == std::string::npos) || (keybegin > maxpos) || (keybegin == 0) || (req[keybegin-1] != '\n'))
80 keybegin += headerlen;
82 bpos = req.find_first_not_of(whitespace, keybegin, sizeof(whitespace)-1);
83 if ((bpos == std::string::npos) || (bpos > maxpos))
86 const std::string::size_type epos = req.find_first_of(whitespace, bpos, sizeof(whitespace)-1);
92 std::string ExtractValue(const std::string& req) const
94 return std::string(req, bpos, len);
100 OP_CONTINUATION = 0x00,
114 static const unsigned char WS_MASKBIT = (1 << 7);
115 static const unsigned char WS_FINBIT = (1 << 7);
116 static const unsigned char WS_PAYLOAD_LENGTH_MAGIC_LARGE = 126;
117 static const unsigned char WS_PAYLOAD_LENGTH_MAGIC_HUGE = 127;
118 static const size_t WS_MAX_PAYLOAD_LENGTH_SMALL = 125;
119 static const size_t WS_MAX_PAYLOAD_LENGTH_LARGE = 65535;
120 static const size_t MAXHEADERSIZE = sizeof(uint64_t) + 2;
122 // Clients sending ping or pong frames faster than this are killed
123 static const time_t MINPINGPONGDELAY = 10;
127 WebSocketConfig& config;
129 static size_t FillHeader(unsigned char* outbuf, size_t sendlength, OpCode opcode)
132 outbuf[pos++] = WS_FINBIT | opcode;
134 if (sendlength <= WS_MAX_PAYLOAD_LENGTH_SMALL)
136 outbuf[pos++] = sendlength;
138 else if (sendlength <= WS_MAX_PAYLOAD_LENGTH_LARGE)
140 outbuf[pos++] = WS_PAYLOAD_LENGTH_MAGIC_LARGE;
141 outbuf[pos++] = (sendlength >> 8) & 0xff;
142 outbuf[pos++] = sendlength & 0xff;
146 outbuf[pos++] = WS_PAYLOAD_LENGTH_MAGIC_HUGE;
147 const uint64_t len = sendlength;
148 for (int i = sizeof(uint64_t)-1; i >= 0; i--)
149 outbuf[pos++] = ((len >> i*8) & 0xff);
155 static StreamSocket::SendQueue::Element PrepareSendQElem(size_t size, OpCode opcode)
157 unsigned char header[MAXHEADERSIZE];
158 const size_t n = FillHeader(header, size, opcode);
160 return StreamSocket::SendQueue::Element(reinterpret_cast<const char*>(header), n);
163 int HandleAppData(StreamSocket* sock, std::string& appdataout, bool allowlarge)
165 std::string& myrecvq = GetRecvQ();
166 // Need 1 byte opcode, minimum 1 byte len, 4 bytes masking key
167 if (myrecvq.length() < 6)
170 const std::string& cmyrecvq = myrecvq;
171 unsigned char len1 = (unsigned char)cmyrecvq[1];
172 if (!(len1 & WS_MASKBIT))
174 sock->SetError("WebSocket protocol violation: unmasked client frame");
180 // Assume the length is a single byte, if not, update values later
181 unsigned int len = len1;
182 unsigned int payloadstartoffset = 6;
183 const unsigned char* maskkey = reinterpret_cast<const unsigned char*>(&cmyrecvq[2]);
185 if (len1 == WS_PAYLOAD_LENGTH_MAGIC_LARGE)
187 // allowlarge is false for control frames according to the RFC meaning large pings, etc. are not allowed
190 sock->SetError("WebSocket protocol violation: large control frame");
194 // Large frame, has 2 bytes len after the magic byte indicating the length
195 // Need 1 byte opcode, 3 bytes len, 4 bytes masking key
196 if (myrecvq.length() < 8)
199 unsigned char len2 = (unsigned char)cmyrecvq[2];
200 unsigned char len3 = (unsigned char)cmyrecvq[3];
201 len = (len2 << 8) | len3;
203 if (len <= WS_MAX_PAYLOAD_LENGTH_SMALL)
205 sock->SetError("WebSocket protocol violation: non-minimal length encoding used");
210 payloadstartoffset += 2;
212 else if (len1 == WS_PAYLOAD_LENGTH_MAGIC_HUGE)
214 sock->SetError("WebSocket: Huge frames are not supported");
218 if (myrecvq.length() < payloadstartoffset + len)
221 unsigned int maskkeypos = 0;
222 const std::string::iterator endit = myrecvq.begin() + payloadstartoffset + len;
223 for (std::string::const_iterator i = myrecvq.begin() + payloadstartoffset; i != endit; ++i)
225 const unsigned char c = (unsigned char)*i;
226 appdataout.push_back(c ^ maskkey[maskkeypos++]);
230 myrecvq.erase(myrecvq.begin(), endit);
234 int HandlePingPongFrame(StreamSocket* sock, bool isping)
236 if (lastpingpong + MINPINGPONGDELAY >= ServerInstance->Time())
238 sock->SetError("WebSocket: Ping/pong flood");
242 lastpingpong = ServerInstance->Time();
245 const int result = HandleAppData(sock, appdata, false);
246 // If it's a pong stop here regardless of the result so we won't generate a reply
247 if ((result <= 0) || (!isping))
250 StreamSocket::SendQueue::Element elem = PrepareSendQElem(appdata.length(), OP_PONG);
251 elem.append(appdata);
252 GetSendQ().push_back(elem);
254 SocketEngine::ChangeEventMask(sock, FD_ADD_TRIAL_WRITE);
258 int HandleWS(StreamSocket* sock, std::string& destrecvq)
260 if (GetRecvQ().empty())
263 unsigned char opcode = (unsigned char)GetRecvQ().c_str()[0];
264 switch (opcode & ~WS_FINBIT)
266 case OP_CONTINUATION:
271 const int result = HandleAppData(sock, appdata, true);
275 // Strip out any CR+LF which may have been erroneously sent.
276 for (std::string::const_iterator iter = appdata.begin(); iter != appdata.end(); ++iter)
278 if (*iter != '\r' && *iter != '\n')
279 destrecvq.push_back(*iter);
282 // If we are on the final message of this block append a line terminator.
283 if (opcode & WS_FINBIT)
284 destrecvq.append("\r\n");
291 return HandlePingPongFrame(sock, true);
296 // A pong frame may be sent unsolicited, so we have to handle it.
297 // It may carry application data which we need to remove from the recvq as well.
298 return HandlePingPongFrame(sock, false);
303 sock->SetError("Connection closed");
309 sock->SetError("WebSocket: Invalid opcode");
315 void FailHandshake(StreamSocket* sock, const char* httpreply, const char* sockerror)
317 GetSendQ().push_back(StreamSocket::SendQueue::Element(httpreply));
319 sock->SetError(sockerror);
322 int HandleHTTPReq(StreamSocket* sock)
324 std::string& recvq = GetRecvQ();
325 const std::string::size_type reqend = recvq.find("\r\n\r\n");
326 if (reqend == std::string::npos)
329 bool allowedorigin = false;
330 HTTPHeaderFinder originheader;
331 if (originheader.Find(recvq, "Origin:", 7, reqend))
333 const std::string origin = originheader.ExtractValue(recvq);
334 for (WebSocketConfig::OriginList::const_iterator iter = config.allowedorigins.begin(); iter != config.allowedorigins.end(); ++iter)
336 if (InspIRCd::Match(origin, *iter, ascii_case_insensitive_map))
338 allowedorigin = true;
345 FailHandshake(sock, "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n", "WebSocket: Received HTTP request that did not send the Origin header");
351 FailHandshake(sock, "HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n", "WebSocket: Received HTTP request from a non-whitelisted origin");
355 if (!config.proxyranges.empty() && sock->type == StreamSocket::SS_USER)
357 LocalUser* luser = static_cast<UserIOHandler*>(sock)->user;
358 irc::sockets::sockaddrs realsa(luser->client_sa);
360 HTTPHeaderFinder proxyheader;
361 if (proxyheader.Find(recvq, "X-Real-IP:", 10, reqend)
362 && irc::sockets::aptosa(proxyheader.ExtractValue(recvq), realsa.port(), realsa))
364 // Nothing to do here.
366 else if (proxyheader.Find(recvq, "X-Forwarded-For:", 16, reqend)
367 && irc::sockets::aptosa(proxyheader.ExtractValue(recvq), realsa.port(), realsa))
369 // Nothing to do here.
372 for (WebSocketConfig::ProxyRanges::const_iterator iter = config.proxyranges.begin(); iter != config.proxyranges.end(); ++iter)
374 if (InspIRCd::MatchCIDR(luser->GetIPString(), *iter, ascii_case_insensitive_map))
376 // Give the user their real IP address.
377 if (realsa != luser->client_sa)
378 luser->SetClientIP(realsa);
380 // Error if changing their IP gets them banned.
389 HTTPHeaderFinder keyheader;
390 if (!keyheader.Find(recvq, "Sec-WebSocket-Key:", 18, reqend))
392 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");
398 FailHandshake(sock, "HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n", "WebSocket: SHA-1 provider missing");
402 state = STATE_ESTABLISHED;
404 std::string key = keyheader.ExtractValue(recvq);
405 key.append(MagicGUID);
407 std::string reply = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ";
408 reply.append(BinToBase64((*sha1)->GenerateRaw(key), NULL, '=')).append("\r\n\r\n");
409 GetSendQ().push_back(StreamSocket::SendQueue::Element(reply));
411 SocketEngine::ChangeEventMask(sock, FD_ADD_TRIAL_WRITE);
413 recvq.erase(0, reqend + 4);
419 WebSocketHook(IOHookProvider* Prov, StreamSocket* sock, WebSocketConfig& cfg)
421 , state(STATE_HTTPREQ)
425 sock->AddIOHook(this);
428 int OnStreamSocketWrite(StreamSocket* sock, StreamSocket::SendQueue& uppersendq) CXX11_OVERRIDE
430 StreamSocket::SendQueue& mysendq = GetSendQ();
432 // Return 1 to allow sending back an error HTTP response
433 if (state != STATE_ESTABLISHED)
434 return (mysendq.empty() ? 0 : 1);
437 for (StreamSocket::SendQueue::const_iterator elem = uppersendq.begin(); elem != uppersendq.end(); ++elem)
439 for (StreamSocket::SendQueue::Element::const_iterator chr = elem->begin(); chr != elem->end(); ++chr)
443 // We have found an entire message. Send it in its own frame.
444 if (config.sendastext)
446 // If we send messages as text then we need to ensure they are valid UTF-8.
448 utf8::unchecked::replace_invalid(message.begin(), message.end(), std::back_inserter(encoded));
450 mysendq.push_back(PrepareSendQElem(encoded.length(), OP_TEXT));
451 mysendq.push_back(encoded);
455 // Otherwise, send the raw message as a binary frame.
456 mysendq.push_back(PrepareSendQElem(message.length(), OP_BINARY));
457 mysendq.push_back(message);
461 else if (*chr != '\r')
463 message.push_back(*chr);
468 // Empty the upper send queue and push whatever is left back onto it.
470 if (!message.empty())
472 uppersendq.push_back(message);
479 int OnStreamSocketRead(StreamSocket* sock, std::string& destrecvq) CXX11_OVERRIDE
481 if (state == STATE_HTTPREQ)
483 int httpret = HandleHTTPReq(sock);
491 wsret = HandleWS(sock, destrecvq);
493 while ((!GetRecvQ().empty()) && (wsret > 0));
498 void OnStreamSocketClose(StreamSocket* sock) CXX11_OVERRIDE
503 void WebSocketHookProvider::OnAccept(StreamSocket* sock, irc::sockets::sockaddrs* client, irc::sockets::sockaddrs* server)
505 new WebSocketHook(this, sock, config);
508 class ModuleWebSocket : public Module
510 dynamic_reference_nocheck<HashProvider> hash;
511 reference<WebSocketHookProvider> hookprov;
515 : hash(this, "hash/sha1")
516 , hookprov(new WebSocketHookProvider(this))
521 void ReadConfig(ConfigStatus& status) CXX11_OVERRIDE
523 ConfigTagList tags = ServerInstance->Config->ConfTags("wsorigin");
524 if (tags.first == tags.second)
525 throw ModuleException("You have loaded the websocket module but not configured any allowed origins!");
527 WebSocketConfig config;
528 for (ConfigIter i = tags.first; i != tags.second; ++i)
530 ConfigTag* tag = i->second;
532 // Ensure that we have the <wsorigin:allow> parameter.
533 const std::string allow = tag->getString("allow");
535 throw ModuleException("<wsorigin:allow> is a mandatory field, at " + tag->getTagLocation());
537 config.allowedorigins.push_back(allow);
540 ConfigTag* tag = ServerInstance->Config->ConfValue("websocket");
541 config.sendastext = tag->getBool("sendastext", true);
543 irc::spacesepstream proxyranges(tag->getString("proxyranges"));
544 for (std::string proxyrange; proxyranges.GetToken(proxyrange); )
545 config.proxyranges.push_back(proxyrange);
547 // Everything is okay; apply the new config.
548 hookprov->config = config;
551 void OnCleanup(ExtensionItem::ExtensibleType type, Extensible* item) CXX11_OVERRIDE
553 if (type != ExtensionItem::EXT_USER)
556 LocalUser* user = IS_LOCAL(static_cast<User*>(item));
557 if ((user) && (user->eh.GetModHook(this)))
558 ServerInstance->Users.QuitUser(user, "WebSocket module unloading");
561 Version GetVersion() CXX11_OVERRIDE
563 return Version("Allows WebSocket clients to connect to the IRC server.", VF_VENDOR);
567 MODULE_INIT(ModuleWebSocket)