diff options
| author | 2024-07-17 00:06:50 +0100 | |
|---|---|---|
| committer | 2024-07-17 00:06:50 +0100 | |
| commit | 889d521e05f2e922683d48c74eb00290e7a2f548 (patch) | |
| tree | b4c56bd4cdf0881601c7e4d6ff571e491af10bf4 /modules/httpd.cpp | |
| parent | Use std::endian from C++20 in the sha1 module. (diff) | |
Shuffle the modules about a bit.
Diffstat (limited to 'modules/httpd.cpp')
| -rw-r--r-- | modules/httpd.cpp | 471 |
1 files changed, 471 insertions, 0 deletions
diff --git a/modules/httpd.cpp b/modules/httpd.cpp new file mode 100644 index 000000000..de80b63ed --- /dev/null +++ b/modules/httpd.cpp @@ -0,0 +1,471 @@ +/* + * InspIRCd -- Internet Relay Chat Daemon + * + * Copyright (C) 2021 Dominic Hamon + * Copyright (C) 2019 linuxdaemon <linuxdaemon.irc@gmail.com> + * Copyright (C) 2018 edef <edef@edef.eu> + * Copyright (C) 2013-2014, 2017, 2019-2023 Sadie Powell <sadie@witchery.services> + * Copyright (C) 2012-2016 Attila Molnar <attilamolnar@hush.com> + * Copyright (C) 2012 Robby <robby@chatbelgie.be> + * Copyright (C) 2009 Daniel De Graaf <danieldg@inspircd.org> + * Copyright (C) 2008 Robin Burchell <robin+git@viroteck.net> + * Copyright (C) 2007 John Brooks <john@jbrooks.io> + * Copyright (C) 2007 Dennis Friis <peavey@inspircd.org> + * Copyright (C) 2006, 2008 Craig Edwards <brain@inspircd.org> + * + * 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/httpd.h" +#include "utility/string.h" + +#ifdef __GNUC__ +# pragma GCC diagnostic push +#endif + +// Fix warnings about shadowing in http_parser. +#ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wshadow" +#endif + +#include <http_parser/http_parser.c> + +#ifdef __GNUC__ +# pragma GCC diagnostic pop +#endif + +class ModuleHttpServer; + +static ModuleHttpServer* HttpModule; +static insp::intrusive_list<HttpServerSocket> sockets; +static Events::ModuleEventProvider* aclevprov; +static Events::ModuleEventProvider* reqevprov; +static http_parser_settings parser_settings; + +/** A socket used for HTTP transport + */ +class HttpServerSocket final + : public BufferedSocket + , public Timer + , public insp::intrusive_list_node<HttpServerSocket> +{ +private: + friend class ModuleHttpServer; + + http_parser parser; + http_parser_url url; + std::string ip; + std::string uri; + HTTPHeaders headers; + std::string body; + size_t total_buffers; + int status_code = 0; + + /** True if this object is in the cull list + */ + bool waitingcull = false; + bool messagecomplete = false; + + bool Tick() override + { + if (!messagecomplete) + { + ServerInstance->Logs.Debug(MODNAME, "HTTP socket {} timed out", GetFd()); + Close(); + return false; + } + + return true; + } + + template<int (HttpServerSocket::*f)()> + static int Callback(http_parser* p) + { + HttpServerSocket* sock = static_cast<HttpServerSocket*>(p->data); + return (sock->*f)(); + } + + template<int (HttpServerSocket::*f)(const char*, size_t)> + static int DataCallback(http_parser* p, const char* buf, size_t len) + { + HttpServerSocket* sock = static_cast<HttpServerSocket*>(p->data); + return (sock->*f)(buf, len); + } + + static void ConfigureParser() + { + http_parser_settings_init(&parser_settings); + parser_settings.on_message_begin = Callback<&HttpServerSocket::OnMessageBegin>; + parser_settings.on_url = DataCallback<&HttpServerSocket::OnUrl>; + parser_settings.on_header_field = DataCallback<&HttpServerSocket::OnHeaderField>; + parser_settings.on_header_value = DataCallback<&HttpServerSocket::OnHeaderValue>; + parser_settings.on_headers_complete = Callback<&HttpServerSocket::OnHeadersComplete>; + parser_settings.on_body = DataCallback<&HttpServerSocket::OnBody>; + parser_settings.on_message_complete = Callback<&HttpServerSocket::OnMessageComplete>; + } + + int OnMessageBegin() + { + uri.clear(); + header_state = HEADER_NONE; + body.clear(); + total_buffers = 0; + return 0; + } + + bool AcceptData(size_t len) + { + total_buffers += len; + return total_buffers < 8192; + } + + int OnUrl(const char* buf, size_t len) + { + if (!AcceptData(len)) + { + status_code = HTTP_STATUS_URI_TOO_LONG; + return -1; + } + uri.append(buf, len); + return 0; + } + + enum { HEADER_NONE, HEADER_FIELD, HEADER_VALUE } header_state; + std::string header_field; + std::string header_value; + + void OnHeaderComplete() + { + headers.SetHeader(header_field, header_value); + header_field.clear(); + header_value.clear(); + } + + int OnHeaderField(const char* buf, size_t len) + { + if (header_state == HEADER_VALUE) + OnHeaderComplete(); + header_state = HEADER_FIELD; + if (!AcceptData(len)) + { + status_code = HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE; + return -1; + } + header_field.append(buf, len); + return 0; + } + + int OnHeaderValue(const char* buf, size_t len) + { + header_state = HEADER_VALUE; + if (!AcceptData(len)) + { + status_code = HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE; + return -1; + } + header_value.append(buf, len); + return 0; + } + + int OnHeadersComplete() + { + if (header_state != HEADER_NONE) + OnHeaderComplete(); + return 0; + } + + int OnBody(const char* buf, size_t len) + { + if (!AcceptData(len)) + { + status_code = HTTP_STATUS_PAYLOAD_TOO_LARGE; + return -1; + } + body.append(buf, len); + return 0; + } + + int OnMessageComplete() + { + messagecomplete = true; + ServeData(); + return 0; + } + +public: + HttpServerSocket(int newfd, const std::string& IP, ListenSocket* via, const irc::sockets::sockaddrs& client, const irc::sockets::sockaddrs& server, unsigned long timeoutsec) + : BufferedSocket(newfd) + , Timer(timeoutsec, false) + , ip(IP) + { + if ((!via->iohookprovs.empty()) && (via->iohookprovs.back())) + { + via->iohookprovs.back()->OnAccept(this, client, server); + // IOHook may have errored + if (!GetError().empty()) + { + ServerInstance->Logs.Debug(MODNAME, "HTTP socket {} encountered a hook error: {}", + GetFd(), GetError()); + Close(); + return; + } + } + + parser.data = this; + http_parser_init(&parser, HTTP_REQUEST); + ServerInstance->Timers.AddTimer(this); + } + + ~HttpServerSocket() override + { + sockets.erase(this); + } + + void Close() override + { + if (waitingcull || !HasFd()) + return; + + waitingcull = true; + BufferedSocket::Close(); + ServerInstance->GlobalCulls.AddItem(this); + } + + void OnError(BufferedSocketError err) override + { + ServerInstance->Logs.Debug(MODNAME, "HTTP socket {} encountered an error: {} - {}", + GetFd(), (int)err, GetError()); + Close(); + } + + void SendHTTPError(unsigned int response, const char* errstr = nullptr) + { + if (!errstr) + errstr = http_status_str((http_status)response); + + ServerInstance->Logs.Debug(MODNAME, "Sending HTTP error {}: {}", response, errstr); + static HTTPHeaders empty; + std::string data = fmt::format( + "<html><head></head><body style='font-family: sans-serif; text-align: center'>" + "<h1 style='font-size: 48pt'>Error {}</h1><h2 style='font-size: 24pt'>{}</h2><hr>" + "<small>Powered by <a href='https://www.inspircd.org'>InspIRCd</a></small></body></html>", + response, errstr); + + Page(data, response, &empty); + } + + void SendHeaders(unsigned long size, unsigned int response, HTTPHeaders& rheaders) + { + WriteData(fmt::format("HTTP/{}.{} {} {}\r\n", parser.http_major ? parser.http_major : 1, parser.http_major ? parser.http_minor : 1, response, http_status_str((http_status)response))); + + rheaders.CreateHeader("Date", Time::ToString(ServerInstance->Time(), "%a, %d %b %Y %H:%M:%S GMT", true)); + rheaders.CreateHeader("Server", INSPIRCD_BRANCH); + rheaders.SetHeader("Content-Length", ConvToStr(size)); + + if (size) + rheaders.CreateHeader("Content-Type", "text/html"); + else + rheaders.RemoveHeader("Content-Type"); + + /* Supporting Connection: keep-alive causes a whole world of hurt synchronizing timeouts, + * so remove it, its not essential for what we need. + */ + rheaders.SetHeader("Connection", "Close"); + + WriteData(rheaders.GetFormattedHeaders()); + WriteData("\r\n"); + } + + void OnDataReady() override + { + if (parser.upgrade || HTTP_PARSER_ERRNO(&parser)) + return; + http_parser_execute(&parser, &parser_settings, recvq.data(), recvq.size()); + if (parser.upgrade) + SendHTTPError(status_code ? status_code : 400); + else if (HTTP_PARSER_ERRNO(&parser)) + SendHTTPError(status_code ? status_code : 400, http_errno_description((http_errno)parser.http_errno)); + } + + void ServeData() + { + std::string method = http_method_str(static_cast<http_method>(parser.method)); + HTTPRequestURI parsed; + ParseURI(uri, parsed); + HTTPRequest acl(method, parsed, &headers, this, ip, body); + ModResult res = aclevprov->FirstResult(&HTTPACLEventListener::OnHTTPACLCheck, acl); + if (res != MOD_RES_DENY) + { + HTTPRequest request(method, parsed, &headers, this, ip, body); + res = reqevprov->FirstResult(&HTTPRequestEventListener::OnHTTPRequest, request); + if (res == MOD_RES_PASSTHRU) + { + SendHTTPError(404); + } + } + } + + void Page(const std::string& s, unsigned int response, HTTPHeaders* hheaders) + { + SendHeaders(s.length(), response, *hheaders); + WriteData(s); + BufferedSocket::Close(true); + } + + void Page(std::stringstream* n, unsigned int response, HTTPHeaders* hheaders) + { + Page(n->str(), response, hheaders); + } + + bool ParseURI(const std::string& uristr, HTTPRequestURI& out) + { + http_parser_url_init(&url); + if (http_parser_parse_url(uristr.c_str(), uristr.size(), 0, &url) != 0) + return false; + + if (url.field_set & (1 << UF_PATH)) + { + // Normalise the path. + std::vector<std::string> pathsegments; + irc::sepstream pathstream(uri.substr(url.field_data[UF_PATH].off, url.field_data[UF_PATH].len), '/'); + for (std::string pathsegment; pathstream.GetToken(pathsegment); ) + { + if (pathsegment == ".") + { + // Stay at the current level. + continue; + } + + if (pathsegment == "..") + { + // Traverse up to the previous level. + if (!pathsegments.empty()) + pathsegments.pop_back(); + continue; + } + + pathsegments.push_back(pathsegment); + } + + out.path.reserve(url.field_data[UF_PATH].len); + out.path.append("/").append(insp::join(pathsegments, '/')); + } + + if (url.field_set & (1 << UF_FRAGMENT)) + out.fragment = uri.substr(url.field_data[UF_FRAGMENT].off, url.field_data[UF_FRAGMENT].len); + + std::string param_str; + if (url.field_set & (1 << UF_QUERY)) + param_str = uri.substr(url.field_data[UF_QUERY].off, url.field_data[UF_QUERY].len); + + irc::sepstream param_stream(param_str, '&'); + std::string token; + std::string::size_type eq_pos; + while (param_stream.GetToken(token)) + { + eq_pos = token.find('='); + if (eq_pos == std::string::npos) + { + out.query_params.emplace(token, ""); + } + else + { + out.query_params.emplace(token.substr(0, eq_pos), token.substr(eq_pos + 1)); + } + } + return true; + } +}; + +class HTTPdAPIImpl final + : public HTTPdAPIBase +{ +public: + HTTPdAPIImpl(Module* parent) + : HTTPdAPIBase(parent) + { + } + + void SendResponse(HTTPDocumentResponse& resp) override + { + resp.src.sock->Page(resp.document, resp.responsecode, &resp.headers); + } +}; + +class ModuleHttpServer final + : public Module +{ +private: + HTTPdAPIImpl APIImpl; + unsigned long timeoutsec; + Events::ModuleEventProvider acleventprov; + Events::ModuleEventProvider reqeventprov; + +public: + ModuleHttpServer() + : Module(VF_VENDOR, "Allows the server administrator to serve various useful resources over HTTP.") + , APIImpl(this) + , acleventprov(this, "event/http-acl") + , reqeventprov(this, "event/http-request") + { + aclevprov = &acleventprov; + reqevprov = &reqeventprov; + HttpServerSocket::ConfigureParser(); + } + + void init() override + { + HttpModule = this; + } + + void ReadConfig(ConfigStatus& status) override + { + const auto& tag = ServerInstance->Config->ConfValue("httpd"); + timeoutsec = tag->getDuration("timeout", 10, 1); + } + + ModResult OnAcceptConnection(int nfd, ListenSocket* from, const irc::sockets::sockaddrs& client, const irc::sockets::sockaddrs& server) override + { + if (!insp::equalsci(from->bind_tag->getString("type"), "httpd")) + return MOD_RES_PASSTHRU; + + sockets.push_front(new HttpServerSocket(nfd, client.addr(), from, client, server, timeoutsec)); + return MOD_RES_ALLOW; + } + + void OnUnloadModule(Module* mod) override + { + for (insp::intrusive_list<HttpServerSocket>::const_iterator i = sockets.begin(); i != sockets.end(); ) + { + HttpServerSocket* sock = *i; + ++i; + if (sock->GetModHook(mod)) + { + sock->Cull(); + delete sock; + } + } + } + + Cullable::Result Cull() override + { + for (auto* sock : sockets) + sock->Close(); + return Module::Cull(); + } +}; + +MODULE_INIT(ModuleHttpServer) |
