/* * InspIRCd -- Internet Relay Chat Daemon * * Copyright (C) 2026 Sadie Powell * * 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 . */ /// $CompilerFlags: require_environment("SYSTEM_UTFCPP" "1") -DUSE_SYSTEM_UTFCPP #ifdef USE_SYSTEM_UTFCPP # include #else # include #endif #include "inspircd.h" #include "modules/isupport.h" #include "modules/ircv3_replies.h" enum { // From ircu. ERR_INPUTTOOLONG = 417 }; class UTF8Serializer final : public ClientProtocol::Serializer { private: // The maximum size of client-originated message tags in an incoming message including the `@`. static constexpr std::string::size_type MAX_CLIENT_MESSAGE_TAG_LENGTH = 4095; // The maximum size of server-originated message tags in an outgoing message including the `@`. static constexpr std::string::size_type MAX_SERVER_MESSAGE_TAG_LENGTH = 4095; static void SerializeTags(const ClientProtocol::TagMap& tags, const ClientProtocol::TagSelection& tagwl, std::string& line); IRCv3::Replies::Fail failrpl; inline static void AppendUTF8(const std::string& in, std::string& out) { utf8::unchecked::replace_invalid(in.begin(), in.end(), std::back_inserter(out)); } inline static size_t TruncateUTF8(const std::string& str, size_t len) { if (str.length() < len) return str.length(); // Skip back to the start of the previous codepoint. while (len && (static_cast(str[len]) & 0xC0) == 0x80) len--; return len; } public: UTF8Serializer(Module* mod) : ClientProtocol::Serializer(mod, "utf8") , failrpl(mod) { } bool Parse(LocalUser* user, const std::string& line, ClientProtocol::ParseOutput& parseoutput) override; ClientProtocol::SerializedMessage Serialize(const ClientProtocol::Message& msg, const ClientProtocol::TagSelection& tagwl) const override; }; bool UTF8Serializer::Parse(LocalUser* user, const std::string& line, ClientProtocol::ParseOutput& parseoutput) { // Work out how long the message can actually be. auto maxline = ServerInstance->Config->Limits.MaxLine - 2; if (line[0] == '@') maxline += MAX_CLIENT_MESSAGE_TAG_LENGTH + 1; irc::tokenstream tokens(line, 0, TruncateUTF8(line, maxline)); if (!utf8::is_valid(line)) { failrpl.Send(user, nullptr, "INVALID_UTF8", "Message rejected, your IRC software MUST use UTF-8 encoding on this network"); user->CommandFloodPenalty += 2000; return false; } // Try to read either the tags or the command name. Unlike with the RFC // serializer we do not allow for broken legacy clients to send preceding // whitespace. std::string token; tokens.GetMiddle(token); if (token.empty()) { // Discourage the user from flooding the server. user->CommandFloodPenalty += 2000; return false; } ServerInstance->Logs.RawIO("USERINPUT", "C[{}] I {}", user->uuid, tokens.GetMessage()); if (token[0] == '@') { // Check that the client tags fit within the client tag space. if (token.length() > MAX_CLIENT_MESSAGE_TAG_LENGTH) { user->WriteNumeric(ERR_INPUTTOOLONG, "Input line was too long"); user->CommandFloodPenalty += 2000; return false; } // Truncate the RFC part of the message if it is too long. size_t maxrfcline = token.length() + ServerInstance->Config->Limits.MaxLine - 1; if (tokens.GetMessage().length() > maxrfcline) tokens.GetMessage().erase(TruncateUTF8(tokens.GetMessage(), maxrfcline)); // Line begins with message tags, parse them. std::string tagval; irc::sepstream ss(token.substr(1), ';'); while (ss.GetToken(token)) { // Two or more tags with the same key must not be sent, but if a client violates that we accept // the first occurrence of duplicate tags and ignore all later occurrences. // // Another option is to reject the message entirely but there is no standard way of doing that. const std::string::size_type p = token.find('='); if (p != std::string::npos) { // Tag has a value tagval.assign(token, p+1, std::string::npos); token.erase(p); } else tagval.clear(); HandleTag(user, token, tagval, parseoutput.tags); } // Try to read the prefix or command name. if (!tokens.GetMiddle(token)) { // Discourage the user from flooding the server. user->CommandFloodPenalty += 2000; return false; } } if (token[0] == ':') { // If this exists then the client sent a prefix as part of their // message. Section 2.3 of RFC 1459 technically says we should only // allow the nick of the client here but in practise everyone just // ignores it so we will copy them. // Try to read the command name. if (!tokens.GetMiddle(token)) { // Discourage the user from flooding the server. user->CommandFloodPenalty += 2000; return false; } } parseoutput.cmd.assign(token); // Build the parameter map. We intentionally do not respect the RFC 1459 // thirteen parameter limit here. while (tokens.GetTrailing(token)) parseoutput.params.push_back(token); return true; } namespace { void CheckTagLength(std::string& line, size_t prevsize, size_t& length, size_t maxlength) { const std::string::size_type diffsize = line.size() - prevsize; if (length + diffsize > maxlength) line.erase(prevsize); else length += diffsize; } } void UTF8Serializer::SerializeTags(const ClientProtocol::TagMap& tags, const ClientProtocol::TagSelection& tagwl, std::string& line) { size_t client_tag_length = 0; size_t server_tag_length = 0; for (ClientProtocol::TagMap::const_iterator i = tags.begin(); i != tags.end(); ++i) { if (!tagwl.IsSelected(tags, i)) continue; const auto prevsize = line.size(); line.push_back(prevsize ? ';' : '@'); AppendUTF8(i->first, line); const auto& val = i->second.value; if (!val.empty()) { line.push_back('='); AppendUTF8(val, line); } // The tags part of the message must not contain more client and server tags than allowed by the // message tags specification. This is complicated by the tag space having separate limits for // both server-originated and client-originated tags. If either of the tag limits is exceeded then // the most recently added tag is removed. if (i->first[0] == '+') CheckTagLength(line, prevsize, client_tag_length, MAX_CLIENT_MESSAGE_TAG_LENGTH); else CheckTagLength(line, prevsize, server_tag_length, MAX_SERVER_MESSAGE_TAG_LENGTH); } if (!line.empty()) line.push_back(' '); } ClientProtocol::SerializedMessage UTF8Serializer::Serialize(const ClientProtocol::Message& msg, const ClientProtocol::TagSelection& tagwl) const { std::string line; SerializeTags(msg.GetTags(), tagwl, line); // Save position for length calculation later const auto rfcmsg_begin = line.size(); if (msg.GetSource()) { line.push_back(':'); AppendUTF8(*msg.GetSource(), line); line.push_back(' '); } AppendUTF8(msg.GetCommand(), line); const auto& params = msg.GetParams(); if (!params.empty()) { for (auto param = params.begin(); param != params.end() - 1; ++param) { line.push_back(' '); AppendUTF8(*param, line); } line.append(" :", 2); AppendUTF8(params.back(), line); } // Truncate if too long auto maxline = ServerInstance->Config->Limits.MaxLine - 2; if (line.length() - rfcmsg_begin > maxline) line.erase(TruncateUTF8(line, rfcmsg_begin + maxline)); line.append("\r\n", 2); return line; } class IRCv3UTF8Only final : public Module , public ISupport::EventListener { private: UTF8Serializer utf8serializer; public: IRCv3UTF8Only() : Module(VF_CORE | VF_VENDOR, "Provides support for the IRCv3 UTF8ONLY specification.") , ISupport::EventListener(this) , utf8serializer(this) { } void Prioritize() override { ServerInstance->Modules.SetPriority(this, I_OnUserInit, PRIORITY_BEFORE, "core_serialize_rfc"); } void OnBuildISupport(ISupport::TokenMap& tokens) override { tokens["CHARSET"] = "utf8"; tokens["UTF8ONLY"]; } void OnCleanup(ExtensionType type, Extensible* item) override { if (type != ExtensionType::USER) return; auto* const user = IS_LOCAL(static_cast(item)); if (user && user->serializer == &utf8serializer) ServerInstance->Users.QuitUser(user, "Protocol serializer module unloading"); } void OnUserInit(LocalUser* user) override { if (!user->serializer) user->serializer = &utf8serializer; } }; MODULE_INIT(IRCv3UTF8Only)