aboutsummaryrefslogtreecommitdiff
path: root/src/modules/m_websocket.cpp
diff options
context:
space:
mode:
authorGravatar Sadie Powell2024-07-17 00:06:50 +0100
committerGravatar Sadie Powell2024-07-17 00:06:50 +0100
commit889d521e05f2e922683d48c74eb00290e7a2f548 (patch)
treeb4c56bd4cdf0881601c7e4d6ff571e491af10bf4 /src/modules/m_websocket.cpp
parentUse std::endian from C++20 in the sha1 module. (diff)
Shuffle the modules about a bit.
Diffstat (limited to 'src/modules/m_websocket.cpp')
-rw-r--r--src/modules/m_websocket.cpp692
1 files changed, 0 insertions, 692 deletions
diff --git a/src/modules/m_websocket.cpp b/src/modules/m_websocket.cpp
deleted file mode 100644
index 8119f30c6..000000000
--- a/src/modules/m_websocket.cpp
+++ /dev/null
@@ -1,692 +0,0 @@
-/*
- * InspIRCd -- Internet Relay Chat Daemon
- *
- * Copyright (C) 2021 Dominic Hamon
- * Copyright (C) 2019 iwalkalone <iwalkalone69@gmail.com>
- * Copyright (C) 2018-2024 Sadie Powell <sadie@witchery.services>
- * Copyright (C) 2016 Attila Molnar <attilamolnar@hush.com>
- *
- * This file is part of InspIRCd. InspIRCd is free software: you can
- * redistribute it and/or modify it under the terms of the GNU General Public
- * License as published by the Free Software Foundation, version 2.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
- * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
- * details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-
-#include "inspircd.h"
-#include "iohook.h"
-#include "modules/hash.h"
-#include "utility/string.h"
-
-#define UTF_CPP_CPLUSPLUS 199711L
-#include <utfcpp/unchecked.h>
-
-static constexpr char MagicGUID[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
-static constexpr char newline[] = "\r\n";
-static constexpr char whitespace[] = " \t";
-static dynamic_reference_nocheck<HashProvider>* sha1;
-
-struct WebSocketConfig final
-{
- enum DefaultMode
- {
- // Reject connections if a subprotocol is not requested.
- DM_REJECT,
-
- // Use binary frames if a subprotocol is not requested.
- DM_BINARY,
-
- // Use UTF-8 text frames if a subprotocol is not requested.
- DM_TEXT
- };
-
- typedef std::vector<std::string> OriginList;
- typedef std::vector<std::string> ProxyRanges;
-
- // The HTTP origins that can connect to the server.
- OriginList allowedorigins;
-
- // The method to use if a subprotocol is not negotiated.
- DefaultMode defaultmode;
-
- // The IP ranges which send trustworthy X-Real-IP or X-Forwarded-For headers.
- ProxyRanges proxyranges;
-
- // Whether to allow connections from clients that do not send an Origin header.
- bool allowmissingorigin;
-
- // Whether to send WebSocket ping messages instead of IRC ping messages.
- bool nativeping;
-};
-
-class WebSocketHookProvider final
- : public IOHookProvider
-{
-public:
- WebSocketConfig config;
- WebSocketHookProvider(Module* mod)
- : IOHookProvider(mod, "websocket", IOHookProvider::IOH_UNKNOWN, true)
- {
- }
-
- void OnAccept(StreamSocket* sock, const irc::sockets::sockaddrs& client, const irc::sockets::sockaddrs& server) override;
-
- void OnConnect(StreamSocket* sock) override
- {
- }
-};
-
-class WebSocketHook final
- : public IOHookMiddle
-{
- class HTTPHeaderFinder final
- {
- private:
- std::string::size_type bpos;
- std::string::size_type len;
-
- public:
- bool Find(const std::string& req, const char* header, std::string::size_type headerlen, std::string::size_type maxpos)
- {
- // Skip the GET /wibble HTTP/1.1 line.
- size_t startpos = req.find(newline) + sizeof(newline) - 1;
- while (startpos < maxpos)
- {
- size_t endpos = req.find(newline, startpos);
- if (strncasecmp(req.c_str() + startpos, header, headerlen) != 0)
- {
- startpos = endpos + sizeof(newline) - 1;
- continue; // Incorrect header.
- }
-
- bpos = req.find_first_not_of(whitespace, startpos + headerlen);
- if (bpos >= endpos)
- return false; // Empty value.
-
- size_t epos = std::min(req.find_last_not_of(whitespace), endpos);
- if (epos < bpos)
- return false; // Should never happen.
-
- len = epos - bpos;
- return true;
- }
-
- // No header found.
- return false;
- }
-
- std::string ExtractValue(const std::string& req) const
- {
- return std::string(req, bpos, len);
- }
- };
-
- enum CloseCode
- {
- CLOSE_PROTOCOL_ERROR = 1002,
- CLOSE_POLICY_VIOLATION = 1008,
- CLOSE_TOO_LARGE = 1009
- };
-
- enum OpCode
- {
- OP_CONTINUATION = 0x00,
- OP_TEXT = 0x01,
- OP_BINARY = 0x02,
- OP_CLOSE = 0x08,
- OP_PING = 0x09,
- OP_PONG = 0x0a
- };
-
- enum State
- {
- STATE_HTTPREQ,
- STATE_ESTABLISHED
- };
-
- static constexpr unsigned char WS_MASKBIT = (1 << 7);
- static constexpr unsigned char WS_FINBIT = (1 << 7);
- static constexpr unsigned char WS_PAYLOAD_LENGTH_MAGIC_LARGE = 126;
- static constexpr unsigned char WS_PAYLOAD_LENGTH_MAGIC_HUGE = 127;
- static constexpr size_t WS_MAX_PAYLOAD_LENGTH_SMALL = 125;
- static constexpr size_t WS_MAX_PAYLOAD_LENGTH_LARGE = 65535;
- static constexpr size_t MAXHEADERSIZE = sizeof(uint64_t) + 2;
-
- // Clients sending ping or pong frames faster than this are killed
- static constexpr time_t MINPINGPONGDELAY = 10;
-
- State state = STATE_HTTPREQ;
- time_t lastpingpong = 0;
- WebSocketConfig& config;
- bool sendastext;
-
- static size_t FillHeader(unsigned char* outbuf, size_t sendlength, OpCode opcode)
- {
- size_t pos = 0;
- outbuf[pos++] = WS_FINBIT | opcode;
-
- if (sendlength <= WS_MAX_PAYLOAD_LENGTH_SMALL)
- {
- outbuf[pos++] = sendlength;
- }
- else if (sendlength <= WS_MAX_PAYLOAD_LENGTH_LARGE)
- {
- outbuf[pos++] = WS_PAYLOAD_LENGTH_MAGIC_LARGE;
- outbuf[pos++] = (sendlength >> 8) & 0xff;
- outbuf[pos++] = sendlength & 0xff;
- }
- else
- {
- outbuf[pos++] = WS_PAYLOAD_LENGTH_MAGIC_HUGE;
- const uint64_t len = sendlength;
- for (int i = sizeof(uint64_t)-1; i >= 0; i--)
- outbuf[pos++] = ((len >> i*8) & 0xff);
- }
-
- return pos;
- }
-
- static StreamSocket::SendQueue::Element PrepareSendQElem(size_t size, OpCode opcode)
- {
- unsigned char header[MAXHEADERSIZE];
- const size_t n = FillHeader(header, size, opcode);
-
- return StreamSocket::SendQueue::Element(reinterpret_cast<const char*>(header), n);
- }
-
- int HandleAppData(StreamSocket* sock, std::string& appdataout, bool allowlarge)
- {
- std::string& myrecvq = GetRecvQ();
- // Need 1 byte opcode, minimum 1 byte len, 4 bytes masking key
- if (myrecvq.length() < 6)
- return 0;
-
- const std::string& cmyrecvq = myrecvq;
- unsigned char len1 = (unsigned char)cmyrecvq[1];
- if (!(len1 & WS_MASKBIT))
- {
- CloseConnection(sock, CLOSE_PROTOCOL_ERROR, "WebSocket protocol violation: unmasked client frame");
- return -1;
- }
-
- len1 &= ~WS_MASKBIT;
-
- // Assume the length is a single byte, if not, update values later
- unsigned int len = len1;
- unsigned int payloadstartoffset = 6;
- const unsigned char* maskkey = reinterpret_cast<const unsigned char*>(&cmyrecvq[2]);
-
- if (len1 == WS_PAYLOAD_LENGTH_MAGIC_LARGE)
- {
- // allowlarge is false for control frames according to the RFC meaning large pings, etc. are not allowed
- if (!allowlarge)
- {
- CloseConnection(sock, CLOSE_PROTOCOL_ERROR, "WebSocket protocol violation: large control frame");
- return -1;
- }
-
- // Large frame, has 2 bytes len after the magic byte indicating the length
- // Need 1 byte opcode, 3 bytes len, 4 bytes masking key
- if (myrecvq.length() < 8)
- return 0;
-
- unsigned char len2 = (unsigned char)cmyrecvq[2];
- unsigned char len3 = (unsigned char)cmyrecvq[3];
- len = (len2 << 8) | len3;
-
- if (len <= WS_MAX_PAYLOAD_LENGTH_SMALL)
- {
- CloseConnection(sock, CLOSE_PROTOCOL_ERROR, "WebSocket protocol violation: non-minimal length encoding used");
- return -1;
- }
-
- maskkey += 2;
- payloadstartoffset += 2;
- }
- else if (len1 == WS_PAYLOAD_LENGTH_MAGIC_HUGE)
- {
- CloseConnection(sock, CLOSE_TOO_LARGE, "WebSocket: Huge frames are not supported");
- return -1;
- }
-
- if (myrecvq.length() < payloadstartoffset + len)
- return 0;
-
- unsigned int maskkeypos = 0;
- const std::string::iterator endit = myrecvq.begin() + payloadstartoffset + len;
- for (std::string::const_iterator i = myrecvq.begin() + payloadstartoffset; i != endit; ++i)
- {
- const unsigned char c = (unsigned char)*i;
- appdataout.push_back(c ^ maskkey[maskkeypos++]);
- maskkeypos %= 4;
- }
-
- myrecvq.erase(myrecvq.begin(), endit);
- return 1;
- }
-
- int HandlePingPongFrame(StreamSocket* sock, bool isping)
- {
- if (lastpingpong + MINPINGPONGDELAY >= ServerInstance->Time())
- {
- CloseConnection(sock, CLOSE_POLICY_VIOLATION, "WebSocket: Ping/pong flood");
- return -1;
- }
-
- lastpingpong = ServerInstance->Time();
-
- std::string appdata;
- const int result = HandleAppData(sock, appdata, false);
- if (result <= 0)
- return result;
-
- if (isping)
- {
- StreamSocket::SendQueue::Element elem = PrepareSendQElem(appdata.length(), OP_PONG);
- elem.append(appdata);
- GetSendQ().push_back(elem);
-
- SocketEngine::ChangeEventMask(sock, FD_ADD_TRIAL_WRITE);
- }
- else if (sock->type == StreamSocket::SS_USER && config.nativeping)
- {
- // Pong reply on user socket; reset their idle time.
- UserIOHandler* ioh = static_cast<UserIOHandler*>(sock);
- ioh->user->lastping = 1;
- }
- return 1;
- }
-
- int HandleWS(StreamSocket* sock, std::string& destrecvq)
- {
- if (GetRecvQ().empty())
- return 0;
-
- unsigned char opcode = (unsigned char)GetRecvQ()[0];
- switch (opcode & ~WS_FINBIT)
- {
- case OP_CONTINUATION:
- case OP_TEXT:
- case OP_BINARY:
- {
- std::string appdata;
- const int result = HandleAppData(sock, appdata, true);
- if (result != 1)
- return result;
-
- // Strip out any CR+LF which may have been erroneously sent.
- for (const auto chr : appdata)
- {
- if (chr != '\r' && chr != '\n')
- destrecvq.push_back(chr);
- }
-
- // If we are on the final message of this block append a line terminator.
- if (opcode & WS_FINBIT)
- destrecvq.append("\r\n");
-
- return 1;
- }
-
- case OP_PING:
- {
- return HandlePingPongFrame(sock, true);
- }
-
- case OP_PONG:
- {
- // A pong frame may be sent unsolicited, so we have to handle it.
- // It may carry application data which we need to remove from the recvq as well.
- return HandlePingPongFrame(sock, false);
- }
-
- case OP_CLOSE:
- {
- sock->SetError("Connection closed");
- return -1;
- }
-
- default:
- {
- CloseConnection(sock, CLOSE_PROTOCOL_ERROR, "WebSocket: Invalid opcode");
- return -1;
- }
- }
- }
-
- void CloseConnection(StreamSocket* sock, CloseCode closecode, const std::string& reason)
- {
- uint16_t netcode = htons(closecode);
- std::string packedcode;
- packedcode.push_back(netcode & 0x00FF);
- packedcode.push_back(netcode >> 8);
-
- GetSendQ().push_back(PrepareSendQElem(reason.length() + 2, OP_CLOSE));
- GetSendQ().push_back(packedcode);
- GetSendQ().push_back(reason);
- sock->DoWrite();
- sock->SetError(reason);
- }
-
- void FailHandshake(StreamSocket* sock, const char* httpreply, const char* sockerror)
- {
- GetSendQ().push_back(StreamSocket::SendQueue::Element(httpreply));
- GetSendQ().push_back(StreamSocket::SendQueue::Element(sockerror));
- sock->DoWrite();
- sock->SetError(sockerror);
- }
-
- int HandleHTTPReq(StreamSocket* sock)
- {
- std::string& recvq = GetRecvQ();
- const std::string::size_type reqend = recvq.find("\r\n\r\n");
- if (reqend == std::string::npos)
- return 0;
-
- bool allowedorigin = false;
- HTTPHeaderFinder originheader;
- if (originheader.Find(recvq, "Origin:", 7, reqend))
- {
- const std::string origin = originheader.ExtractValue(recvq);
- for (const auto& cfgorigin : config.allowedorigins)
- {
- if (InspIRCd::Match(origin, cfgorigin, ascii_case_insensitive_map))
- {
- allowedorigin = true;
- break;
- }
- }
- }
- else if (config.allowmissingorigin)
- {
- // This is a non-web WebSocket connection.
- allowedorigin = true;
- }
- else
- {
- 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");
- return -1;
- }
-
- if (!allowedorigin)
- {
- FailHandshake(sock, "HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n", "WebSocket: Received HTTP request from a non-whitelisted origin");
- return -1;
- }
-
- if (!config.proxyranges.empty() && sock->type == StreamSocket::SS_USER)
- {
- LocalUser* luser = static_cast<UserIOHandler*>(sock)->user;
- irc::sockets::sockaddrs realsa(luser->client_sa);
-
- HTTPHeaderFinder proxyheader;
- if (proxyheader.Find(recvq, "X-Real-IP:", 10, reqend) || proxyheader.Find(recvq, "X-Forwarded-For:", 16, reqend))
- {
- // Attempt to parse the proxy HTTP header.
- if (!realsa.from_ip_port(proxyheader.ExtractValue(recvq), realsa.port()))
- {
- // The proxy header value contains a malformed value.
- FailHandshake(sock, "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n", "WebSocket: Received a proxied HTTP request that sent a malformed real IP address");
- return -1;
- }
- }
- else
- {
- // The proxy header is missing.
- FailHandshake(sock, "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n", "WebSocket: Received a proxied HTTP request that did not send a real IP address header");
- return -1;
- }
- for (const auto& proxyrange : config.proxyranges)
- {
- if (InspIRCd::MatchCIDR(luser->GetAddress(), proxyrange, ascii_case_insensitive_map))
- {
- // Give the user their real IP address.
- if (realsa != luser->client_sa)
- luser->ChangeRemoteAddress(realsa);
-
- // Error if changing their IP gets them banned.
- if (luser->quitting)
- return -1;
- break;
- }
- }
- }
-
- std::string selectedproto;
- HTTPHeaderFinder protocolheader;
- if (protocolheader.Find(recvq, "Sec-WebSocket-Protocol:", 23, reqend))
- {
- irc::commasepstream protostream(protocolheader.ExtractValue(recvq));
- for (std::string proto; protostream.GetToken(proto); )
- {
- std::erase_if(proto, ::isspace);
-
- bool is_binary = insp::equalsci(proto, "binary.inspircd.org");
- bool is_text = insp::equalsci(proto, "text.inspircd.org");
-
- if (is_binary || is_text)
- {
- selectedproto = std::move(proto);
- sendastext = is_text;
- break;
- }
- }
- }
-
- if (selectedproto.empty() && config.defaultmode == WebSocketConfig::DM_REJECT)
- {
- FailHandshake(sock, "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n", "WebSocket: Received HTTP request that did not send the Sec-WebSocket-Protocol header");
- return -1;
- }
-
- HTTPHeaderFinder keyheader;
- if (!keyheader.Find(recvq, "Sec-WebSocket-Key:", 18, reqend))
- {
- 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");
- return -1;
- }
-
- if (!*sha1)
- {
- FailHandshake(sock, "HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n", "WebSocket: SHA-1 provider missing");
- return -1;
- }
-
- state = STATE_ESTABLISHED;
-
- std::string key = keyheader.ExtractValue(recvq);
- key.append(MagicGUID);
-
- std::string reply = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ";
- reply.append(Base64::Encode((*sha1)->GenerateRaw(key), nullptr, '=')).append(newline);
- if (!selectedproto.empty())
- reply.append("Sec-WebSocket-Protocol: ").append(selectedproto).append(newline);
- reply.append(newline);
- GetSendQ().push_back(StreamSocket::SendQueue::Element(reply));
-
- SocketEngine::ChangeEventMask(sock, FD_ADD_TRIAL_WRITE);
-
- recvq.erase(0, reqend + 4);
-
- return 1;
- }
-
-public:
- WebSocketHook(const std::shared_ptr<IOHookProvider>& Prov, StreamSocket* sock, WebSocketConfig& cfg)
- : IOHookMiddle(Prov)
- , config(cfg)
- , sendastext(config.defaultmode != WebSocketConfig::DM_BINARY)
- {
- sock->AddIOHook(this);
- }
-
- bool IsHookReady() const override
- {
- return state == STATE_ESTABLISHED;
- }
-
- ssize_t OnStreamSocketWrite(StreamSocket* sock, StreamSocket::SendQueue& uppersendq) override
- {
- StreamSocket::SendQueue& mysendq = GetSendQ();
-
- // Return 1 to allow sending back an error HTTP response
- if (state != STATE_ESTABLISHED)
- return (mysendq.empty() ? 0 : 1);
-
- std::string message;
- for (const auto& elem : uppersendq)
- {
- for (const auto chr : elem)
- {
- if (chr == '\n')
- {
- // We have found an entire message. Send it in its own frame.
- if (sendastext)
- {
- // If we send messages as text then we need to ensure they are valid UTF-8.
- std::string encoded;
- utf8::unchecked::replace_invalid(message.begin(), message.end(), std::back_inserter(encoded));
-
- mysendq.push_back(PrepareSendQElem(encoded.length(), OP_TEXT));
- mysendq.push_back(encoded);
- }
- else
- {
- // Otherwise, send the raw message as a binary frame.
- mysendq.push_back(PrepareSendQElem(message.length(), OP_BINARY));
- mysendq.push_back(message);
- }
- message.clear();
- }
- else if (chr != '\r')
- {
- message.push_back(chr);
- }
- }
- }
-
- // Empty the upper send queue and push whatever is left back onto it.
- uppersendq.clear();
- if (!message.empty())
- {
- uppersendq.push_back(message);
- return 0;
- }
-
- return 1;
- }
-
- ssize_t OnStreamSocketRead(StreamSocket* sock, std::string& destrecvq) override
- {
- if (state == STATE_HTTPREQ)
- {
- int httpret = HandleHTTPReq(sock);
- if (httpret <= 0)
- return httpret;
- }
-
- int wsret;
- do
- {
- wsret = HandleWS(sock, destrecvq);
- }
- while ((!GetRecvQ().empty()) && (wsret > 0));
-
- return wsret;
- }
-
- bool Ping() override
- {
- if (!config.nativeping)
- return false;
-
- StreamSocket::SendQueue& mysendq = GetSendQ();
-
- const std::string& message = ServerInstance->Config->GetServerName();
- mysendq.push_back(PrepareSendQElem(message.length(), OP_PING));
- mysendq.push_back(message);
-
- return true;
- }
-};
-
-void WebSocketHookProvider::OnAccept(StreamSocket* sock, const irc::sockets::sockaddrs& client, const irc::sockets::sockaddrs& server)
-{
- new WebSocketHook(shared_from_this(), sock, config);
-}
-
-class ModuleWebSocket final
- : public Module
-{
-private:
- dynamic_reference_nocheck<HashProvider> hash;
- std::shared_ptr<WebSocketHookProvider> hookprov;
-
-public:
- ModuleWebSocket()
- : Module(VF_VENDOR, "Allows WebSocket clients to connect to the IRC server.")
- , hash(this, "hash/sha1")
- , hookprov(std::make_shared<WebSocketHookProvider>(this))
- {
- sha1 = &hash;
- }
-
- void ReadConfig(ConfigStatus& status) override
- {
- auto tags = ServerInstance->Config->ConfTags("wsorigin");
- if (tags.empty())
- throw ModuleException(this, "You have loaded the websocket module but not configured any allowed origins!");
-
- WebSocketConfig config;
- for (const auto& [_, tag] : tags)
- {
- // Ensure that we have the <wsorigin:allow> parameter.
- const std::string allow = tag->getString("allow");
- if (allow.empty())
- throw ModuleException(this, "<wsorigin:allow> is a mandatory field, at " + tag->source.str());
-
- config.allowedorigins.push_back(allow);
- }
-
- const auto& tag = ServerInstance->Config->ConfValue("websocket");
-
- const std::string defaultmodestr = tag->getString("defaultmode", "text", 1);
- if (insp::equalsci(defaultmodestr, "reject"))
- config.defaultmode = WebSocketConfig::DM_REJECT;
- else if (insp::equalsci(defaultmodestr, "binary"))
- config.defaultmode = WebSocketConfig::DM_BINARY;
- else if (insp::equalsci(defaultmodestr, "text"))
- config.defaultmode = WebSocketConfig::DM_TEXT;
- else
- throw ModuleException(this, defaultmodestr + " is an invalid value for <websocket:defaultmode>; acceptable values are 'binary', 'text' and 'reject', at " + tag->source.str());
-
- irc::spacesepstream proxyranges(tag->getString("proxyranges"));
- for (std::string proxyrange; proxyranges.GetToken(proxyrange); )
- config.proxyranges.push_back(proxyrange);
-
- config.allowmissingorigin = tag->getBool("allowmissingorigin", true);
- config.nativeping = tag->getBool("nativeping", true);
-
- // Everything is okay; apply the new config.
- hookprov->config = config;
- }
-
- void OnCleanup(ExtensionType type, Extensible* item) override
- {
- if (type != ExtensionType::USER)
- return;
-
- LocalUser* user = IS_LOCAL(static_cast<User*>(item));
- if ((user) && (user->eh.GetModHook(this)))
- ServerInstance->Users.QuitUser(user, "WebSocket module unloading");
- }
-};
-
-MODULE_INIT(ModuleWebSocket)