Compare commits

..

75 Commits

Author SHA1 Message Date
7793e8b02c simpler initialization 2025-03-09 13:52:46 -07:00
04a092b9a3 Clean up the reply codes 2025-02-08 11:08:55 -08:00
4fc8d4d49c join channels that we're announcing to 2025-02-07 17:34:03 -08:00
44ef4c0689 client should clean up 2025-02-06 20:52:42 -08:00
847a149e12 consolidate connection 2025-02-05 09:47:52 -08:00
2b4bb1f071 unify with snowcone 2025-02-05 09:24:47 -08:00
5f2439e5af consolidate command interface 2025-02-03 09:35:50 -08:00
5aec8397bb initial formatter framework 2025-02-02 19:46:11 -08:00
39a4d84a54 add commands to add/drop access 2025-02-02 19:20:15 -08:00
5cfb47ce92 refactor 2025-02-02 16:20:52 -08:00
53771396ca rename 2025-02-02 15:02:08 -08:00
178d7dfcfe implement a few commands 2025-02-02 14:56:34 -08:00
8c9678708b checkpoint initial event processing 2025-02-02 12:40:55 -08:00
4c119c6138 initial webhook infrastructure 2025-02-01 20:57:57 -08:00
1a6ec835ed make a myirc namespace 2025-02-01 11:04:33 -08:00
8324a496b6 initial rate limit support 2025-01-31 16:14:13 -08:00
68429bc1e4 pull sasl config out of registration 2025-01-31 09:36:08 -08:00
7728bc6aee use shared-ptr more consistently 2025-01-31 08:38:14 -08:00
eb01b304e3 implement ecdsa 2025-01-30 16:39:23 -08:00
15c48ab1dc fdrop snotes 2025-01-30 13:36:18 -08:00
206b4c9d89 operspy who 2025-01-30 12:55:40 -08:00
0e708e72f8 clean up ref use 2025-01-30 11:56:03 -08:00
c3650ba38d fix move semantics of Ref 2025-01-30 11:47:26 -08:00
281937e2c5 split up driver and library 2025-01-30 09:28:28 -08:00
5218ea0892 snotes 2025-01-30 08:27:57 -08:00
0e88f3bd7a more snotes 2025-01-29 20:43:03 -08:00
8d544e31de openssl utils 2025-01-29 18:41:28 -08:00
Eric Mertens
5f32505b93 add KILL 2025-01-29 16:32:08 -08:00
Eric Mertens
a9efb96837 more send commands 2025-01-29 15:17:19 -08:00
Eric Mertens
bdf7202e7d missing commands 2025-01-29 11:13:20 -08:00
Eric Mertens
ef223f9cc1 Treat ERR_NOMOTD as registration completion 2025-01-29 10:54:48 -08:00
Eric Mertens
5801a5404a all Ref to support uprefable types 2025-01-29 09:54:17 -08:00
f5b49ebf66 spambot snote 2025-01-28 22:54:55 -08:00
b0f254eb13 two more snotes 2025-01-28 21:43:44 -08:00
9f49baa6ad set challenge slot 2025-01-28 20:42:42 -08:00
1aa56453cc fixup challenge 2025-01-28 20:01:51 -08:00
Eric Mertens
72b2756f34 add openssl_errors.hpp 2025-01-28 19:05:00 -08:00
Eric Mertens
40bd9186da add ref.hpp 2025-01-28 19:04:36 -08:00
Eric Mertens
de19233dd7 challenge.cpp 2025-01-28 19:02:30 -08:00
Eric Mertens
21090f05ab error handling in challenge 2025-01-28 17:15:13 -08:00
763bcffe23 add all the reply argument counts 2025-01-27 20:02:31 -08:00
e7aba11d05 support client certificates 2025-01-27 18:55:19 -08:00
41b1148005 add on_chat layer 2025-01-27 17:46:07 -08:00
Eric Mertens
7cd92ececb unregister caps_slot_ on event 2025-01-27 17:04:31 -08:00
Eric Mertens
a9d6eb3811 push cap logic into client 2025-01-27 11:08:16 -08:00
Eric Mertens
b9d88bbd0c consolidate/simplify Registration's event loop 2025-01-27 09:30:09 -08:00
eb7b27ebe3 disconnect registered slots on trigger 2025-01-26 19:44:35 -08:00
cc06429a69 fix oper tag 2025-01-26 19:43:17 -08:00
a49389d508 explicitly tear down slots 2025-01-26 19:35:56 -08:00
135f5aa47d Move more connection logic out of main 2025-01-26 14:51:44 -08:00
bb7f09f2e9 make disconnect signal reliable 2025-01-26 14:43:26 -08:00
ebe884e9d5 copy over snowcone's stream 2025-01-26 14:38:13 -08:00
8be5332692 initial command logic 2025-01-25 21:24:33 -08:00
093515c3ec mass reformat 2025-01-25 15:45:31 -08:00
fd4612d385 cleanup 2025-01-25 14:12:37 -08:00
4d298b7eec more sasl 2025-01-25 12:25:38 -08:00
Eric Mertens
1a1deb03b7 remove the "write_timer" 2025-01-24 16:57:34 -08:00
Eric Mertens
3b48ff7c7e checkpoint 2025-01-24 14:48:15 -08:00
37721092db consolidate more logic into Connection 2025-01-23 21:23:32 -08:00
Eric Mertens
553d261d73 coroutine checkpoint 2025-01-23 12:46:52 -08:00
d92c6fee21 add coroutine experiment 2025-01-23 00:47:05 -08:00
7665f4c0f5 migrate to signals2 2025-01-22 23:49:48 -08:00
d11412e73f checkpoint 2025-01-22 20:33:17 -08:00
efb49b8708 redundant checks 2024-03-03 12:53:59 -08:00
0aab173d94 comments 2024-03-03 12:27:36 -08:00
Eric Mertens
f59049187e extra snote 2023-11-29 14:52:57 -08:00
Eric Mertens
7999a0672b extra snote pattern 2023-11-29 13:54:34 -08:00
Eric Mertens
7e4346a50e initial logging 2023-11-29 13:13:48 -08:00
Eric Mertens
5f0eb57e83 add and remove priv commands 2023-11-29 11:07:24 -08:00
Eric Mertens
5b19afa0a4 make privthread file configurable 2023-11-29 09:44:36 -08:00
7eb725fd5b add files 2023-11-29 09:07:20 -08:00
7ea1d8c322 add privthread 2023-11-29 08:56:58 -08:00
Eric Mertens
7458c8278c lazy snote matching 2023-11-28 11:34:27 -08:00
1c3b9eb50f add whowas 2023-11-28 07:58:22 -08:00
f33bc5cc87 revise the includes 2023-11-27 19:09:45 -08:00
71 changed files with 5569 additions and 2806 deletions

232
.clang-format Normal file
View File

@@ -0,0 +1,232 @@
---
Language: Cpp
# BasedOnStyle: WebKit
AccessModifierOffset: -4
AlignAfterOpenBracket: BlockIndent
AlignArrayOfStructures: None
AlignConsecutiveAssignments:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
AlignFunctionPointers: false
PadOperators: true
AlignConsecutiveBitFields:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
AlignFunctionPointers: false
PadOperators: false
AlignConsecutiveDeclarations:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
AlignFunctionPointers: false
PadOperators: false
AlignConsecutiveMacros:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
AlignFunctionPointers: false
PadOperators: false
AlignConsecutiveShortCaseStatements:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCaseColons: false
AlignEscapedNewlines: Right
AlignOperands: DontAlign
AlignTrailingComments:
Kind: Never
OverEmptyLines: 0
AllowAllArgumentsOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowBreakBeforeNoexceptSpecifier: Never
AllowShortBlocksOnASingleLine: Empty
AllowShortCaseLabelsOnASingleLine: false
AllowShortCompoundRequirementOnASingleLine: true
AllowShortEnumsOnASingleLine: true
AllowShortFunctionsOnASingleLine: All
AllowShortIfStatementsOnASingleLine: Never
AllowShortLambdasOnASingleLine: All
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: MultiLine
AttributeMacros:
- __capability
BinPackArguments: true
BinPackParameters: true
BitFieldColonSpacing: Both
BraceWrapping:
AfterCaseLabel: false
AfterClass: true
AfterControlStatement: Always
AfterEnum: true
AfterExternBlock: false
AfterFunction: true
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: true
AfterUnion: true
BeforeCatch: true
BeforeElse: true
BeforeLambdaBody: false
BeforeWhile: true
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakAdjacentStringLiterals: true
BreakAfterAttributes: Leave
BreakAfterJavaFieldAnnotations: false
BreakArrays: true
BreakBeforeBinaryOperators: All
BreakBeforeConceptDeclarations: Always
BreakBeforeBraces: Custom
BreakBeforeInlineASMColon: OnlyMultiline
BreakBeforeTernaryOperators: true
BreakConstructorInitializers: BeforeComma
BreakInheritanceList: BeforeColon
BreakStringLiterals: true
ColumnLimit: 0
CommentPragmas: "^ IWYU pragma:"
CompactNamespaces: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DerivePointerAlignment: false
DisableFormat: false
EmptyLineAfterAccessModifier: Never
EmptyLineBeforeAccessModifier: LogicalBlock
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: false
ForEachMacros: []
IfMacros: []
IncludeBlocks: Preserve
IncludeCategories:
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
Priority: 2
SortPriority: 0
CaseSensitive: false
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
Priority: 3
SortPriority: 0
CaseSensitive: false
- Regex: ".*"
Priority: 1
SortPriority: 0
CaseSensitive: false
IncludeIsMainRegex: "(Test)?$"
IncludeIsMainSourceRegex: ""
IndentAccessModifiers: false
IndentCaseBlocks: false
IndentCaseLabels: false
IndentExternBlock: AfterExternBlock
IndentGotoLabels: true
IndentPPDirectives: None
IndentRequiresClause: true
IndentWidth: 4
IndentWrappedFunctionNames: false
InsertBraces: false
InsertNewlineAtEOF: false
InsertTrailingCommas: None
IntegerLiteralSeparator:
Binary: 0
BinaryMinDigits: 0
Decimal: 0
DecimalMinDigits: 0
Hex: 0
HexMinDigits: 0
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: true
KeepEmptyLinesAtEOF: false
LambdaBodyIndentation: Signature
LineEnding: LF
MacroBlockBegin: ""
MacroBlockEnd: ""
MaxEmptyLinesToKeep: 1
NamespaceIndentation: Inner
ObjCBinPackProtocolList: Auto
ObjCBlockIndentWidth: 4
ObjCBreakBeforeNestedBlockParam: true
ObjCSpaceAfterProperty: true
ObjCSpaceBeforeProtocolList: true
PackConstructorInitializers: BinPack
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 19
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakOpenParenthesis: 0
PenaltyBreakScopeResolution: 500
PenaltyBreakString: 1000
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 1000000
PenaltyIndentedWhitespace: 0
PenaltyReturnTypeOnItsOwnLine: 60
PointerAlignment: Right
PPIndentWidth: -1
QualifierAlignment: Left
ReferenceAlignment: Pointer
ReflowComments: true
RemoveBracesLLVM: false
RemoveParentheses: Leave
RemoveSemicolon: false
RequiresClausePosition: OwnLine
RequiresExpressionIndentation: OuterScope
SeparateDefinitionBlocks: Leave
ShortNamespaceLines: 1
SkipMacroDefinitionBody: false
SortIncludes: CaseSensitive
SortJavaStaticImport: Before
SortUsingDeclarations: LexicographicNumeric
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceAroundPointerQualifiers: Default
SpaceBeforeAssignmentOperators: true
SpaceBeforeCaseColon: false
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeJsonColon: false
SpaceBeforeParens: ControlStatements
SpaceBeforeParensOptions:
AfterControlStatements: true
AfterForeachMacros: true
AfterFunctionDefinitionName: false
AfterFunctionDeclarationName: false
AfterIfMacros: true
AfterOverloadedOperator: false
AfterPlacementOperator: true
AfterRequiresInClause: false
AfterRequiresInExpression: false
BeforeNonEmptyParentheses: false
SpaceBeforeRangeBasedForLoopColon: true
SpaceBeforeSquareBrackets: false
SpaceInEmptyBlock: true
SpacesBeforeTrailingComments: 1
SpacesInAngles: Never
SpacesInContainerLiterals: true
SpacesInLineCommentPrefix:
Minimum: 1
Maximum: -1
SpacesInParens: Never
SpacesInParensOptions:
InCStyleCasts: false
InConditionalStatements: false
InEmptyParentheses: false
Other: false
SpacesInSquareBrackets: false
Standard: Latest
StatementAttributeLikeMacros: []
StatementMacros: []
TabWidth: 8
UseTab: Never
VerilogBreakBetweenInstancePorts: true
WhitespaceSensitiveMacros: []

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
/out
/config.toml
/privs.toml
/.ccls
/archive
/.vscode

View File

@@ -1,46 +1,32 @@
cmake_minimum_required(VERSION 3.13)
set(CMAKE_C_STANDARD 11)
cmake_minimum_required(VERSION 3.25)
set(CMAKE_CXX_STANDARD 20)
project(xbot
VERSION 1
LANGUAGES C CXX
LANGUAGES CXX
)
find_package(Boost REQUIRED)
find_package(PkgConfig REQUIRED)
find_package(OpenSSL REQUIRED)
pkg_check_modules(LIBHS libhs REQUIRED IMPORTED_TARGET)
set(BOOST_INCLUDE_LIBRARIES asio log signals2 endian beast json)
set(BOOST_ENABLE_CMAKE ON)
include(FetchContent)
FetchContent_Declare(
tomlplusplus
GIT_REPOSITORY https://github.com/marzer/tomlplusplus.git
GIT_TAG v3.4.0
Boost
URL https://github.com/boostorg/boost/releases/download/boost-1.87.0/boost-1.87.0-cmake.tar.xz
URL_HASH SHA1=4ec86f884ffb57ce7f6a6c6eb05b1af247aba0ac
)
FetchContent_Declare(
tomlplusplus
GIT_REPOSITORY https://github.com/marzer/tomlplusplus.git
GIT_TAG v3.4.0
)
FetchContent_MakeAvailable(tomlplusplus)
FetchContent_MakeAvailable(Boost)
FetchContent_Declare(
eventpp
GIT_REPOSITORY https://github.com/wqking/eventpp.git
GIT_TAG v0.1.3
)
FetchContent_MakeAvailable(eventpp)
add_custom_command(
OUTPUT irc_commands.inc
COMMAND
gperf
-C -Z IrcCommandHash -K text -L C++ -t
--output-file irc_commands.inc
${CMAKE_CURRENT_SOURCE_DIR}/irc_commands.gperf
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/irc_commands.gperf
VERBATIM)
add_executable(xbot
main.cpp irc_commands.inc ircmsg.cpp settings.cpp connection.cpp
snote_thread.cpp watchdog_thread.cpp write_irc.cpp
ping_thread.cpp irc_parse_thread.cpp registration_thread.cpp
self_thread.cpp command_thread.cpp)
target_include_directories(xbot PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(xbot PRIVATE Boost::headers tomlplusplus_tomlplusplus eventpp PkgConfig::LIBHS)
add_subdirectory(mybase64)
add_subdirectory(mysocks5)
add_subdirectory(myirc)
add_subdirectory(driver)

View File

@@ -1,8 +1,8 @@
{
"version": 2,
"version": 6,
"configurePresets": [
{
"name": "arm-mac",
"name": "default",
"displayName": "Configure preset using toolchain file",
"description": "Sets Ninja generator, build and install directory",
"generator": "Ninja",
@@ -17,8 +17,8 @@
],
"buildPresets": [
{
"name": "arm-mac",
"configurePreset": "arm-mac"
"name": "default",
"configurePreset": "default"
}
]
}

View File

@@ -1,52 +0,0 @@
#include "command_thread.hpp"
#include "connection.hpp"
#include "irc_parse_thread.hpp"
#include <iostream>
auto CommandThread::start(Connection& connection) -> void
{
connection.add_listener<IrcMsgEvent>([&connection](IrcMsgEvent& event)
{
if (IrcCommand::PRIVMSG != event.command) return;
auto const message = event.irc.args[1];
if (message.empty()) return;
if ('!' != message.front()) return;
CommandEvent command_event;
{
auto const cmdend = message.find(' ');
if (std::string_view::npos == cmdend)
{
command_event.command = message.substr(1);
}
else
{
command_event.command = message.substr(1, cmdend - 1);
command_event.arg = message.substr(cmdend + 1);
}
}
{
auto const nickend = event.irc.source.find('!');
if (std::string_view::npos != nickend)
{
command_event.nick = event.irc.source.substr(0, nickend);
}
}
for (auto const& [k,v] : event.irc.tags)
{
if ("solanum.chat/oper" == k)
{
command_event.oper = v;
}
else if ("account" == k)
{
command_event.account = v;
}
}
connection.dispatch(command_event);
});
}

View File

@@ -1,21 +0,0 @@
#pragma once
#include "event.hpp"
#include <string>
class Connection;
struct CommandEvent : Event
{
std::string_view oper;
std::string_view account;
std::string_view nick;
std::string_view command;
std::string_view arg;
};
struct CommandThread
{
static auto start(Connection&) -> void;
};

View File

@@ -1,107 +0,0 @@
#include "connection.hpp"
Connection::Connection(boost::asio::io_context & io)
: stream_{io}
, write_timer_{io, std::chrono::steady_clock::time_point::max()}
{
}
auto Connection::writer_() -> void
{
std::vector<boost::asio::const_buffer> buffers;
buffers.reserve(write_strings_.size());
for (auto const& elt : write_strings_)
{
buffers.push_back(boost::asio::buffer(elt));
}
boost::asio::async_write(
stream_,
buffers,
[weak = weak_from_this()
,strings = std::move(write_strings_)
](boost::system::error_code const& error, std::size_t)
{
if (not error)
{
if (auto self = weak.lock())
{
self->writer();
}
}
});
write_strings_.clear();
}
auto Connection::writer() -> void
{
if (write_strings_.empty())
{
write_timer_.async_wait([weak = weak_from_this()](auto){
if (auto self = weak.lock())
{
if (not self->write_strings_.empty())
{
self->writer_();
}
}
});
}
else
{
writer_();
}
}
auto Connection::connect(
boost::asio::io_context & io,
std::string host,
std::string port
) -> boost::asio::awaitable<void>
{
using namespace std::placeholders;
// keep connection alive while coroutine is active
auto const self = shared_from_this();
{
auto resolver = boost::asio::ip::tcp::resolver{io};
auto const endpoints = co_await resolver.async_resolve(host, port, boost::asio::use_awaitable);
auto const endpoint = co_await boost::asio::async_connect(stream_, endpoints, boost::asio::use_awaitable);
make_event<ConnectEvent>();
}
self->writer();
for(LineBuffer buffer{32'768};;)
{
boost::system::error_code error;
auto const n = co_await stream_.async_read_some(buffer.get_buffer(), boost::asio::redirect_error(boost::asio::use_awaitable, error));
if (error)
{
break;
}
buffer.add_bytes(n, [this](char * line) {
std::cout << "RECV: " << line << std::endl;
make_event<LineEvent>(line);
});
}
make_event<DisconnectEvent>();
}
auto Connection::write_line(std::string message) -> void
{
std::cout << "SEND: " << message << std::endl;
message += "\r\n";
auto const need_cancel = write_strings_.empty();
write_strings_.push_back(std::move(message));
if (need_cancel)
{
write_timer_.cancel_one();
}
}
auto Connection::close() -> void
{
stream_.close();
}

View File

@@ -1,110 +0,0 @@
#pragma once
#include "event.hpp"
#include "linebuffer.hpp"
#include "settings.hpp"
#include <eventpp/eventdispatcher.h>
#include <eventpp/utilities/argumentadapter.h>
#include <boost/asio.hpp>
#include <chrono>
#include <functional>
#include <concepts>
#include <iostream>
#include <list>
#include <memory>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <variant>
#include <vector>
#include <typeinfo>
#include <typeindex>
struct ConnectEvent : Event
{
};
struct DisconnectEvent : Event
{
};
struct LineEvent : Event
{
explicit LineEvent(char * line) : line{line} {}
char * line;
};
class Connection : public std::enable_shared_from_this<Connection>
{
using EventDispatcher = eventpp::EventDispatcher<std::type_index, void(Event&)>;
public:
template <typename T>
class Handle
{
EventDispatcher::Handle handle;
Handle(EventDispatcher::Handle handle) : handle{handle} {}
public:
Handle() : handle{} {}
friend Connection;
};
private:
boost::asio::ip::tcp::socket stream_;
boost::asio::steady_timer write_timer_;
std::list<std::string> write_strings_;
EventDispatcher dispatcher_;
auto writer() -> void;
auto writer_() -> void;
public:
Connection(boost::asio::io_context & io);
template <typename T, typename F>
auto add_listener(F f) -> Handle<T>
{
return Handle<T>{dispatcher_.appendListener(
typeid(T),
eventpp::argumentAdapter<void(T&)>(f)
)};
}
template <typename T>
auto remove_listener(Handle<T> handle) -> void
{
dispatcher_.removeListener(typeid(T), handle.handle);
}
template <typename T>
auto dispatch(T& event) -> void
{
dispatcher_.dispatch(typeid(T), event);
}
auto get_executor() -> boost::asio::any_io_executor {
return stream_.get_executor();
}
template <typename T, typename... Args>
auto make_event(Args&& ... args) {
auto event = T{std::forward<Args>(args)...};
dispatch<T>(event);
}
/// Write bytes into the socket. Messages should be properly newline terminated.
auto write_line(std::string message) -> void;
auto connect(
boost::asio::io_context & io,
std::string host,
std::string port
) -> boost::asio::awaitable<void>;
auto close() -> void;
};

13
driver/CMakeLists.txt Normal file
View File

@@ -0,0 +1,13 @@
add_executable(xbot
main.cpp
settings.cpp
web.cpp
)
target_link_libraries(xbot PRIVATE
myirc
OpenSSL::SSL
Boost::signals2 Boost::log Boost::asio Boost::beast Boost::json
tomlplusplus_tomlplusplus
PkgConfig::LIBHS
mysocks5 mybase64)

133
driver/main.cpp Normal file
View File

@@ -0,0 +1,133 @@
#include "settings.hpp"
#include "web.hpp"
#include "myirc/bot.hpp"
#include "myirc/challenge.hpp"
#include "myirc/client.hpp"
#include "myirc/connection.hpp"
#include "myirc/openssl_utils.hpp"
#include "myirc/registration.hpp"
#include "myirc/sasl_mechanism.hpp"
#include "myirc/ref.hpp"
#include "myirc/irc_coroutine.hpp"
#include <boost/asio.hpp>
#include <boost/log/trivial.hpp>
#include <boost/log/expressions.hpp>
#include <openssl/pem.h>
#include <fstream>
#include <memory>
using namespace std::literals;
using myirc::Bot;
using myirc::Client;
using myirc::Connection;
using myirc::Registration;
using myirc::Challenge;
using myirc::Ref;
static auto start_irc(
boost::asio::io_context &io,
const Settings &settings,
std::shared_ptr<Webhooks> webhook
) -> void
{
Ref<X509> tls_cert;
if (settings.use_tls && not settings.tls_cert_file.empty())
{
tls_cert = myirc::cert_from_file(settings.tls_cert_file);
}
Ref<EVP_PKEY> tls_key;
if (settings.use_tls && not settings.tls_key_file.empty())
{
tls_key = myirc::key_from_file(settings.tls_key_file, settings.tls_key_password);
}
const auto connection = std::make_shared<Connection>(io);
const auto client = Client::start(connection);
const auto bot = Bot::start(client);
Registration::start({
.nickname = settings.nickname,
.realname = settings.realname,
.username = settings.username,
.password = settings.password,
.sasl_mechanism = configure_sasl(settings),
}, client);
// Configure CHALLENGE on registration if applicable
if (not settings.challenge_username.empty() && not settings.challenge_key_file.empty()) {
if (auto key = myirc::key_from_file(settings.challenge_key_file, settings.challenge_key_password)) {
client->sig_registered.connect([&settings, client, key = std::move(key)]() {
Challenge::start(client, settings.challenge_username, key);
});
}
}
client->sig_registered.connect([client, webhook]() {
webhook->set_client(client);
});
// On disconnect reconnect in 5 seconds
// connection is captured in the disconnect handler so it can keep itself alive
connection->sig_disconnect.connect(
[&io, &settings, connection, webhook](auto) {
webhook->clear_client();
auto timer = std::make_shared<boost::asio::steady_timer>(io);
timer->expires_after(5s);
timer->async_wait([&io, &settings, timer, webhook](auto) { start_irc(io, settings, webhook); });
}
);
// Dispatch commands to the webhook logic
bot->sig_command.connect([webhook, connection](const Bot::Command &cmd) {
auto cursor = webhook_commands.find(std::string{cmd.command});
if (cursor != webhook_commands.end()) {
try {
cursor->second(webhook, cmd);
} catch (const std::exception &e) {
BOOST_LOG_TRIVIAL(error) << "Command handler failed: " << e.what();
}
}
});
connection->start({
.tls = settings.use_tls,
.host = settings.host,
.port = settings.service,
.verify = settings.tls_hostname,
.client_cert = std::move(tls_cert),
.client_key = std::move(tls_key),
});
}
static auto get_settings(const char * const filename) -> Settings
{
if (auto config_stream = std::ifstream{filename})
{
return Settings::from_stream(config_stream);
}
else
{
BOOST_LOG_TRIVIAL(error) << "Unable to open configuration";
std::exit(1);
}
}
auto main(int argc, char *argv[]) -> int
{
//boost::log::core::get()->set_filter(boost::log::trivial::severity >= boost::log::trivial::warning);
if (argc != 3) {
BOOST_LOG_TRIVIAL(error) << "Bad arguments";
return 1;
}
const auto settings = get_settings(argv[1]);
auto io = boost::asio::io_context{};
auto webhooks = start_webhook(io, argv[2]);
start_irc(io, settings, webhooks);
io.run();
}

61
driver/settings.cpp Normal file
View File

@@ -0,0 +1,61 @@
#include "settings.hpp"
#include <myirc/openssl_utils.hpp>
#define TOML_ENABLE_FORMATTERS 0
#include <toml++/toml.hpp>
auto Settings::from_stream(std::istream &in) -> Settings
{
const auto config = toml::parse(in);
return Settings{
.host = config["host"].value_or(std::string{}),
.service = config["port"].value_or(std::uint16_t{6667}),
.password = config["password"].value_or(std::string{}),
.username = config["username"].value_or(std::string{}),
.realname = config["realname"].value_or(std::string{}),
.nickname = config["nickname"].value_or(std::string{}),
.sasl_mechanism = config["sasl_mechanism"].value_or(std::string{}),
.sasl_authcid = config["sasl_authcid"].value_or(std::string{}),
.sasl_authzid = config["sasl_authzid"].value_or(std::string{}),
.sasl_password = config["sasl_password"].value_or(std::string{}),
.sasl_key_file = config["sasl_key_file"].value_or(std::string{}),
.sasl_key_password = config["sasl_key_password"].value_or(std::string{}),
.tls_hostname = config["tls_hostname"].value_or(std::string{}),
.tls_cert_file = config["tls_cert_file"].value_or(std::string{}),
.tls_key_file = config["tls_key_file"].value_or(std::string{}),
.tls_key_password = config["tls_key_password"].value_or(std::string{}),
.challenge_username = config["challenge_username"].value_or(std::string{}),
.challenge_key_file = config["challenge_key_file"].value_or(std::string{}),
.challenge_key_password = config["challenge_key_password"].value_or(std::string{}),
.use_tls = config["use_tls"].value_or(false),
};
}
auto configure_sasl(const Settings &settings) -> std::unique_ptr<myirc::SaslMechanism>
{
if (settings.sasl_mechanism == "PLAIN" &&
not settings.sasl_authcid.empty()
) {
return std::make_unique<myirc::SaslPlain>(
settings.sasl_authcid,
settings.sasl_authzid,
settings.sasl_password);
} else if (settings.sasl_mechanism == "EXTERNAL") {
return std::make_unique<myirc::SaslExternal>(settings.sasl_authzid);
} else if (
settings.sasl_mechanism == "ECDSA" &&
not settings.sasl_authcid.empty() &&
not settings.sasl_key_file.empty()
) {
if (auto sasl_key = myirc::key_from_file(settings.sasl_key_file, settings.sasl_key_password))
return std::make_unique<myirc::SaslEcdsa>(
settings.sasl_authcid,
settings.sasl_authzid,
std::move(sasl_key));
}
return nullptr;
}

38
driver/settings.hpp Normal file
View File

@@ -0,0 +1,38 @@
#pragma once
#include <myirc/sasl_mechanism.hpp>
#include <istream>
#include <string>
struct Settings
{
std::string host;
std::uint16_t service;
std::string password;
std::string username;
std::string realname;
std::string nickname;
std::string sasl_mechanism;
std::string sasl_authcid;
std::string sasl_authzid;
std::string sasl_password;
std::string sasl_key_file;
std::string sasl_key_password;
std::string tls_hostname;
std::string tls_cert_file;
std::string tls_key_file;
std::string tls_key_password;
std::string challenge_username;
std::string challenge_key_file;
std::string challenge_key_password;
bool use_tls;
static auto from_stream(std::istream &in) -> Settings;
};
auto configure_sasl(const Settings &settings) -> std::unique_ptr<myirc::SaslMechanism>;

624
driver/web.cpp Normal file
View File

@@ -0,0 +1,624 @@
#include "web.hpp"
#include <boost/beast.hpp>
#include <boost/json.hpp>
#include <boost/log/trivial.hpp>
#include <boost/system/system_error.hpp>
#include <openssl/hmac.h>
#include <chrono>
#include <fstream>
#include <sstream>
#include <vector>
namespace beast = boost::beast; // from <boost/beast.hpp>
namespace http = beast::http; // from <boost/beast/http.hpp>
namespace net = boost::asio; // from <boost/asio.hpp>
using tcp = net::ip::tcp; // from <boost/asio/ip/tcp.hpp>
using namespace std::literals;
namespace {
std::map<std::string, void(*)(std::shared_ptr<Webhooks>, const ProjectSettings &, std::string_view, const boost::json::object &)> formatters {
{"push", [](std::shared_ptr<Webhooks> webhooks, const ProjectSettings &project, std::string_view full_name, const boost::json::object &body) {
webhooks->send_notice(project.channel, "push");
}},
};
// Used as the completion handler for coroutines in this module to print
// failure reasons to the log.
auto report_error(std::exception_ptr eptr) -> void
{
if (eptr)
{
try
{
std::rethrow_exception(eptr);
}
catch (const std::exception &e)
{
BOOST_LOG_TRIVIAL(error) << "HTTP coroutine failed: " << e.what();
}
}
}
// Construct a simple, empty reply using the given status code.
auto simple_response(
http::status status,
unsigned version,
bool keep_alive
) -> http::message_generator
{
http::response<http::string_body> res{status, version};
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
res.keep_alive(keep_alive);
res.content_length(0);
return res;
}
// Compute the expected signature string for the POST body.
auto compute_signature(const std::string_view secret, const std::string_view body) -> std::string
{
unsigned int digest_length = EVP_MAX_MD_SIZE;
unsigned char digest[EVP_MAX_MD_SIZE];
HMAC(EVP_sha256(), secret.data(), static_cast<int>(secret.size()), reinterpret_cast<const unsigned char *>(body.data()), body.size(), digest, &digest_length);
std::stringstream ss;
ss << "sha256=";
ss << std::hex << std::setfill('0');
for (unsigned int i = 0; i < digest_length; i++)
{
ss << std::setw(2) << static_cast<int>(digest[i]);
}
return ss.str();
}
// This event is ready to actually announce
auto announce_event(
std::shared_ptr<Webhooks> self,
const ProjectSettings &project,
const std::string full_name,
const std::string_view event_name,
const boost::json::value event
) -> void {
const auto message = std::string{event_name} + " on " + full_name;
self->send_notice(project.channel, std::move(message));
}
// Determine if this event should be announced
auto process_event(
std::shared_ptr<Webhooks> self,
const std::string_view notify_user,
const std::string event_name,
const boost::json::value &json
) -> void
{
auto &event = json.as_object();
// Determine the project name. Repositories use: user/project. Organization events use: organization
std::string full_name;
if (event.contains("repository"))
{
full_name = std::string{event.at("repository").as_object().at("full_name").as_string()};
}
else if (event.contains("organization"))
{
full_name = std::string{event.at("organization").as_object().at("login").as_string()};
}
else
{
BOOST_LOG_TRIVIAL(warning) << "No repository or organization detected";
return;
}
const auto &settings = self->settings_.projects.at(full_name);
// Ensure that this sender is authorized to send for this project
if (settings.credential_name != notify_user)
{
BOOST_LOG_TRIVIAL(warning) << "Credential mismatch for " << full_name << " wanted: " << settings.credential_name << " got: " << notify_user;
return;
}
if (not settings.enabled || not settings.events.contains(event_name))
{
// quietly ignore events we don't care about
return;
}
auto formatter_cursor = formatters.find(event_name);
if (formatter_cursor != formatters.end()) {
formatter_cursor->second(self, settings, full_name, event);
}
}
// Process the HTTP request validating its structure and signature.
template <class Body, class Allocator>
auto handle_request(
std::shared_ptr<Webhooks> self,
http::request<Body, http::basic_fields<Allocator>> &&req
) -> http::message_generator
{
BOOST_LOG_TRIVIAL(info) << "HTTP request " << req.method_string() << " " << req.target();
if (not req.target().starts_with("/notify/"))
{
BOOST_LOG_TRIVIAL(warning) << "HTTP Bad target: " << req.target();
return simple_response(http::status::not_found, req.version(), req.keep_alive());
}
std::string notify_user = req.target().substr(8);
if (req.method() != http::verb::post)
{
BOOST_LOG_TRIVIAL(warning) << "HTTP Bad method: " << req.method_string();
return simple_response(http::status::method_not_allowed, req.version(), req.keep_alive());
}
const auto event = req["x-github-event"];
const auto signature = req["x-hub-signature-256"];
if (event.empty() || signature.empty())
{
BOOST_LOG_TRIVIAL(warning) << "HTTP Missing headers";
return simple_response(http::status::bad_request, req.version(), req.keep_alive());
}
const auto credential_cursor = self->settings_.credentials.find(notify_user);
if (credential_cursor == self->settings_.credentials.end())
{
BOOST_LOG_TRIVIAL(warning) << "HTTP Unknown user: " << notify_user;
return simple_response(http::status::unauthorized, req.version(), req.keep_alive());
}
const auto &secret = credential_cursor->second;
const auto expected_signature = compute_signature(secret, req.body());
if (signature != expected_signature)
{
BOOST_LOG_TRIVIAL(warning) << "HTTP Bad signature: " << signature << " expected: " << expected_signature;
return simple_response(http::status::unauthorized, req.version(), req.keep_alive());
}
try
{
process_event(self, notify_user, event, boost::json::parse(req.body()));
}
catch (const boost::system::system_error &e)
{
BOOST_LOG_TRIVIAL(error) << "HTTP Failed to process event: " << e.what();
return simple_response(http::status::internal_server_error, req.version(), req.keep_alive());
}
return simple_response(http::status::ok, req.version(), req.keep_alive());
}
// Repeatedly read HTTP requests off a socket and reply to them
auto read_loop(tcp::socket socket, std::shared_ptr<Webhooks> self) -> boost::asio::awaitable<void>
{
beast::tcp_stream stream{std::move(socket)};
beast::flat_buffer buffer;
http::request<http::string_body> req;
bool keep_alive = true;
while (keep_alive)
{
req.clear();
stream.expires_after(30s);
boost::system::error_code ec;
co_await http::async_read(stream, buffer, req, net::redirect_error(net::use_awaitable, ec));
if (ec == http::error::end_of_stream)
{
break;
}
else if (ec)
{
throw boost::system::system_error{ec};
}
keep_alive = req.keep_alive();
auto msg = handle_request(self, std::move(req));
co_await beast::async_write(stream, std::move(msg), net::use_awaitable);
}
stream.socket().shutdown(tcp::socket::shutdown_both);
}
// Repeatedly accept new connections on a listening socket
auto accept_loop(
tcp::acceptor acceptor,
std::shared_ptr<Webhooks> self
) -> boost::asio::awaitable<void>
{
for (;;)
{
auto socket = co_await acceptor.async_accept(net::use_awaitable);
boost::asio::co_spawn(
acceptor.get_executor(),
read_loop(std::move(socket), self),
report_error
);
}
}
// Launch the listening sockets
auto spawn_webhook(
boost::asio::io_context &io,
const std::shared_ptr<Webhooks> webhook
) -> boost::asio::awaitable<void>
{
tcp::resolver resolver{io};
auto results = co_await resolver.async_resolve(webhook->settings_.host, webhook->settings_.service, tcp::resolver::passive, boost::asio::use_awaitable);
for (auto &&result : results)
{
const auto endpoint = result.endpoint();
BOOST_LOG_TRIVIAL(info) << "HTTP: Listening on " << endpoint;
tcp::acceptor acceptor{io};
acceptor.open(endpoint.protocol());
acceptor.set_option(net::socket_base::reuse_address(true));
acceptor.bind(endpoint);
acceptor.listen(net::socket_base::max_listen_connections);
boost::asio::co_spawn(io, accept_loop(std::move(acceptor), webhook), report_error);
}
}
} // namespace
auto start_webhook(
boost::asio::io_context &io,
const char *webhook_settings_filename
) -> std::shared_ptr<Webhooks>
{
auto webhook = std::make_shared<Webhooks>(webhook_settings_filename);
webhook->load_settings();
boost::asio::co_spawn(io, spawn_webhook(io, webhook), report_error);
return webhook;
}
auto Webhooks::load_settings() -> void
{
std::ifstream webhook_settings_file{settings_filename_};
if (!webhook_settings_file)
{
BOOST_LOG_TRIVIAL(error) << "Unable to open webhook settings file";
}
auto webhook_settings = toml::parse(webhook_settings_file);
settings_ = WebhookSettings::from_toml(webhook_settings);
}
auto Webhooks::save_settings() const -> void
{
std::ofstream webhook_settings_file{settings_filename_};
if (!webhook_settings_file)
{
BOOST_LOG_TRIVIAL(error) << "Unable to open webhook settings file";
return;
}
webhook_settings_file << settings_.to_toml() << "\n";
}
auto ProjectSettings::from_toml(const toml::table &v) -> ProjectSettings
{
ProjectSettings result;
result.channel = v["channel"].value_or(""s);
result.credential_name = v["credential_name"].value_or(""s);
result.enabled = v["enabled"].value_or(false);
if (const auto events = v["events"].as_array())
{
for (const auto &event : *events)
{
result.events.insert(event.value_or(""s));
}
}
if (const auto accounts = v["authorized_accounts"].as_array())
{
for (const auto &account : *accounts)
{
result.authorized_accounts.insert(account.value_or(""s));
}
}
return result;
}
auto ProjectSettings::to_toml() const -> toml::table
{
toml::array events_array;
for (const auto &event : events)
{
events_array.emplace_back(event);
}
toml::array authorized_accounts_array;
for (const auto &account : authorized_accounts)
{
authorized_accounts_array.emplace_back(account);
}
return toml::table{
{"channel", channel},
{"credential_name", credential_name},
{"enabled", enabled},
{"events", std::move(events_array)},
{"authorized_accounts", std::move(authorized_accounts_array)}
};
}
auto WebhookSettings::from_toml(const toml::table &v) -> WebhookSettings
{
WebhookSettings result;
result.host = v["host"].value_or(""s);
result.service = v["service"].value_or("http"s);
if (const auto credentials = v["credentials"].as_array())
{
for (const auto &credential : *credentials)
{
if (auto credential_table = credential.as_table())
{
result.credentials.emplace(
(*credential_table)["name"].value_or(""s),
(*credential_table)["key"].value_or(""s)
);
}
}
}
if (const auto projects = v["projects"].as_array())
{
for (const auto &project : *projects)
{
if (auto project_table = project.as_table())
{
result.projects.emplace(
(*project_table)["name"].value_or(""s),
ProjectSettings::from_toml(*project_table)
);
}
}
}
return result;
}
auto WebhookSettings::to_toml() const -> toml::table
{
toml::array credential_tables;
for (const auto &[name, key] : credentials)
{
credential_tables.emplace_back(toml::table{
{"name", name},
{"key", key}
});
}
toml::array project_tables;
for (const auto &[name, project] : projects)
{
auto tab = project.to_toml();
tab.emplace("name", name);
project_tables.emplace_back(std::move(tab));
}
return toml::table{
{"host", host},
{"service", service},
{"credentials", std::move(credential_tables)},
{"projects", std::move(project_tables)}
};
}
// Either emit the event now or save it until a connection is set
auto Webhooks::send_notice(std::string_view target, std::string message) -> void
{
if (client_)
{
client_->send_notice(target, message);
}
}
auto Webhooks::refresh_channels() const -> void
{
if (not client_) return;
std::stringstream ss;
std::set<std::string_view> added;
bool first = true;
for (auto &&[_, project] : settings_.projects) {
auto &channel = project.channel;
if (!channel.empty() &&
!client_->is_on_channel(channel) &&
added.insert(channel).second)
{
if (first) {
first = false;
} else {
ss << ",";
}
ss << channel;
}
}
if (not first) {
client_->send_join(ss.str());
}
}
auto Webhooks::set_client(std::shared_ptr<myirc::Client> client) -> void
{
client_ = std::move(client);
refresh_channels();
}
auto Webhooks::clear_client() -> void
{
client_.reset();
}
static auto reply_to(std::shared_ptr<Webhooks> webhooks, const myirc::Bot::Command &cmd, std::string message) -> void
{
if (cmd.target.starts_with("#"))
{
webhooks->send_notice(cmd.target, std::move(message));
}
else
{
webhooks->send_notice(cmd.nick(), std::move(message));
}
}
// Operators are authorized for all projects otherwise nickserv account names can be added to individual projects.
static auto authorized_for_project(
const myirc::Bot::Command &cmd,
const ProjectSettings &project,
const std::string_view nick
) -> bool
{
return !cmd.oper.empty() || project.authorized_accounts.contains(std::string{nick});
}
std::map<std::string, void (*)(std::shared_ptr<Webhooks>, const myirc::Bot::Command &)> webhook_commands{
{"announce", [](std::shared_ptr<Webhooks> webhooks, const myirc::Bot::Command &cmd) {
std::istringstream iss{std::string{cmd.arguments}};
std::string name, mode;
if (iss >> name >> mode)
{
auto &project = webhooks->settings_.projects.at(name);
if (not authorized_for_project(cmd, project, cmd.account))
{
return;
}
if (mode == "on") {
project.enabled = true;
reply_to(webhooks, cmd, "Enabled project " + name);
} else if (mode == "off") {
project.enabled = false;
reply_to(webhooks, cmd, "Disabled project " + name);
} else {
return;
}
webhooks->save_settings();
}
}},
{"event", [](std::shared_ptr<Webhooks> webhooks, const myirc::Bot::Command &cmd) {
std::istringstream iss{std::string{cmd.arguments}};
std::string name, mode;
if (iss >> name >> mode)
{
auto &project = webhooks->settings_.projects.at(name);
if (not authorized_for_project(cmd, project, cmd.account))
{
return;
}
if (mode == "list") {
std::stringstream ss;
ss << "Events for " << name << ":";
for (auto &&event : project.events) {
ss << " " << event;
}
reply_to(webhooks, cmd, ss.str());
return;
}
unsigned n_added = 0, n_removed = 0, n_skipped = 0, n_unknown = 0;
if (mode == "add") {
while (iss >> name) {
if (formatters.contains(name)) {
const auto [_, added] = project.events.insert(name);
if (added) { n_added++; } else { n_skipped++; }
} else {
n_unknown++;
}
}
} else if (mode == "del") {
while (iss >> name) {
if (formatters.contains(name)) {
const auto removed = project.events.erase(name);
if (removed) { n_removed++; } else { n_skipped++; }
} else {
n_unknown++;
}
}
}
webhooks->save_settings();
std::stringstream ss;
ss << "Events updated:";
if (n_added) { ss << " added " << n_added; }
if (n_removed) { ss << " removed " << n_removed; }
if (n_skipped) { ss << " skipped " << n_skipped; }
if (n_unknown) { ss << " unknown " << n_unknown; }
reply_to(webhooks, cmd, ss.str());
}
}},
{"auth", [](std::shared_ptr<Webhooks> webhooks, const myirc::Bot::Command &cmd) {
if (cmd.oper.empty())
{
return;
}
std::istringstream iss{std::string{cmd.arguments}};
std::string name, mode;
if (iss >> name >> mode)
{
auto &project = webhooks->settings_.projects.at(name);
if (mode == "list") {
std::stringstream ss;
ss << "Authorized accounts:";
for (auto &&event : project.authorized_accounts) {
ss << " " << event;
}
reply_to(webhooks, cmd, ss.str());
return;
}
unsigned n_added = 0, n_removed = 0, n_skipped = 0;
if (mode == "add") {
while (iss >> name) {
const auto [_, added] = project.authorized_accounts.insert(name);
if (added) { n_added++; } else { n_skipped++; }
}
} else if (mode == "del") {
while (iss >> name) {
const auto removed = project.authorized_accounts.erase(name);
if (removed) { n_removed++; } else { n_skipped++; }
}
}
webhooks->save_settings();
std::stringstream ss;
ss << "Authorized accounts updated:";
if (n_added) { ss << " added " << n_added; }
if (n_removed) { ss << " removed " << n_removed; }
if (n_skipped) { ss << " skipped " << n_skipped; }
reply_to(webhooks, cmd, ss.str());
}
}},
{"setchannel", [](std::shared_ptr<Webhooks> webhooks, const myirc::Bot::Command &cmd) {
if (cmd.oper.empty())
{
return;
}
std::istringstream iss{std::string{cmd.arguments}};
std::string name, channel;
if (iss >> name >> channel)
{
auto &project = webhooks->settings_.projects.at(name);
project.channel = channel;
webhooks->save_settings();
reply_to(webhooks, cmd, "Channel assigned");
webhooks->refresh_channels();
}
}},
{"rehash", [](std::shared_ptr<Webhooks> webhooks, const myirc::Bot::Command &cmd) {
if (cmd.oper.empty())
{
return;
}
webhooks->load_settings();
reply_to(webhooks, cmd, "Rehashed");
}},
};

76
driver/web.hpp Normal file
View File

@@ -0,0 +1,76 @@
#pragma once
#include <myirc/bot.hpp>
#include <myirc/client.hpp>
#include <toml++/toml.hpp>
#include <boost/asio.hpp>
#include <boost/signals2.hpp>
#include <memory>
#include <map>
#include <set>
struct ProjectSettings {
// *** Administrative settings ***
// IRC channel to announce to
std::string channel;
// name extracted from notify/$user
std::string credential_name;
// Authorized accounts can edit the event list
std::set<std::string> authorized_accounts;
// *** User settings ***
// Events to announce
std::set<std::string> events;
// Whether to announce events
bool enabled;
auto to_toml() const -> toml::table;
static auto from_toml(const toml::table &v) -> ProjectSettings;
};
struct WebhookSettings {
std::string host;
std::string service;
std::map<std::string, std::string> credentials;
std::map<std::string, ProjectSettings> projects;
auto to_toml() const -> toml::table;
static auto from_toml(const toml::table &v) -> WebhookSettings;
};
class Webhooks {
// IRC connection to announce on; could be empty
std::shared_ptr<myirc::Client> client_;
const char * settings_filename_;
public:
WebhookSettings settings_;
Webhooks(const char * settings_filename)
: settings_filename_{settings_filename}
{
}
// Either emit the event now or save it until a connection is set
auto send_notice(std::string_view, std::string) -> void;
auto set_client(std::shared_ptr<myirc::Client> client) -> void;
auto clear_client() -> void;
auto save_settings() const -> void;
auto load_settings() -> void;
auto refresh_channels() const -> void;
};
auto start_webhook(boost::asio::io_context &io, const char *) -> std::shared_ptr<Webhooks>;
extern std::map<std::string, void(*)(std::shared_ptr<Webhooks>, const myirc::Bot::Command &)> webhook_commands;

View File

@@ -1,11 +0,0 @@
#pragma once
#include "ircmsg.hpp"
#include <memory>
#include <vector>
struct Event {
virtual ~Event() = default;
bool handled_ = false;
};

View File

@@ -1,278 +0,0 @@
struct RecognizedCommand {
char const* text;
IrcCommand command;
std::size_t min_args;
std::size_t max_args;
};
%%
001, IrcCommand::RPL_WELCOME, 2, 2
002, IrcCommand::RPL_YOURHOST, 2, 2
003, IrcCommand::RPL_CREATED, 2, 2
004, IrcCommand::RPL_MYINFO, 5, 5
005, IrcCommand::RPL_ISUPPORT, 2, 15
008, IrcCommand::RPL_SNOMASK, 3, 3
010, IrcCommand::RPL_REDIR, 4, 4
015, IrcCommand::RPL_MAP, 2, 2
017, IrcCommand::RPL_MAPEND, 2, 2
043, IrcCommand::RPL_SAVENICK, 3, 3
200, IrcCommand::RPL_TRACELINK, 5, 5
201, IrcCommand::RPL_TRACECONNECTING, 4, 4
202, IrcCommand::RPL_TRACEHANDSHAKE, 4, 4
203, IrcCommand::RPL_TRACEUNKNOWN, 6, 6
204, IrcCommand::RPL_TRACEOPERATOR
205, IrcCommand::RPL_TRACEUSER
206, IrcCommand::RPL_TRACESERVER
208, IrcCommand::RPL_TRACENEWTYPE
209, IrcCommand::RPL_TRACECLASS
211, IrcCommand::RPL_STATSLINKINFO
212, IrcCommand::RPL_STATSCOMMANDS
213, IrcCommand::RPL_STATSCLINE
214, IrcCommand::RPL_STATSNLINE
215, IrcCommand::RPL_STATSILINE, 8, 8
216, IrcCommand::RPL_STATSKLINE
217, IrcCommand::RPL_STATSQLINE
218, IrcCommand::RPL_STATSYLINE
219, IrcCommand::RPL_ENDOFSTATS
220, IrcCommand::RPL_STATSPLINE
221, IrcCommand::RPL_UMODEIS, 2, 2
224, IrcCommand::RPL_STATSFLINE
225, IrcCommand::RPL_STATSDLINE
234, IrcCommand::RPL_SERVLIST
235, IrcCommand::RPL_SERVLISTEND
241, IrcCommand::RPL_STATSLLINE
242, IrcCommand::RPL_STATSUPTIME
243, IrcCommand::RPL_STATSOLINE
244, IrcCommand::RPL_STATSHLINE
245, IrcCommand::RPL_STATSSLINE
247, IrcCommand::RPL_STATSXLINE
248, IrcCommand::RPL_STATSULINE
249, IrcCommand::RPL_STATSDEBUG
250, IrcCommand::RPL_STATSCONN
251, IrcCommand::RPL_LUSERCLIENT
252, IrcCommand::RPL_LUSEROP
253, IrcCommand::RPL_LUSERUNKNOWN
254, IrcCommand::RPL_LUSERCHANNELS
255, IrcCommand::RPL_LUSERME
256, IrcCommand::RPL_ADMINME, 3, 3
257, IrcCommand::RPL_ADMINLOC1, 2, 2
258, IrcCommand::RPL_ADMINLOC2, 2, 2
259, IrcCommand::RPL_ADMINEMAIL, 2, 2
261, IrcCommand::RPL_TRACELOG
262, IrcCommand::RPL_ENDOFTRACE, 3, 3
263, IrcCommand::RPL_LOAD2HI
265, IrcCommand::RPL_LOCALUSERS, 4, 4
266, IrcCommand::RPL_GLOBALUSERS, 4, 4
270, IrcCommand::RPL_PRIVS, 3, 3
276, IrcCommand::RPL_WHOISCERTFP, 3, 3
281, IrcCommand::RPL_ACCEPTLIST, 1, 15
282, IrcCommand::RPL_ENDOFACCEPT, 2, 2
300, IrcCommand::RPL_NONE
301, IrcCommand::RPL_AWAY
302, IrcCommand::RPL_USERHOST
303, IrcCommand::RPL_ISON, 2, 2
304, IrcCommand::RPL_TEXT
305, IrcCommand::RPL_UNAWAY
306, IrcCommand::RPL_NOWAWAY
310, IrcCommand::RPL_WHOISHELPOP
311, IrcCommand::RPL_WHOISUSER, 6, 6
312, IrcCommand::RPL_WHOISSERVER, 4, 4
313, IrcCommand::RPL_WHOISOPERATOR, 3, 3
314, IrcCommand::RPL_WHOWASUSER
369, IrcCommand::RPL_ENDOFWHOWAS
316, IrcCommand::RPL_WHOISCHANOP
317, IrcCommand::RPL_WHOISIDLE, 5, 5
318, IrcCommand::RPL_ENDOFWHOIS, 3, 3
319, IrcCommand::RPL_WHOISCHANNELS, 3, 3
320, IrcCommand::RPL_WHOISSPECIAL, 3, 3
321, IrcCommand::RPL_LISTSTART, 3, 3
322, IrcCommand::RPL_LIST, 4, 4
323, IrcCommand::RPL_LISTEND, 2, 2
324, IrcCommand::RPL_CHANNELMODEIS, 3, 3
325, IrcCommand::RPL_CHANNELMLOCK
328, IrcCommand::RPL_CHANNELURL
329, IrcCommand::RPL_CREATIONTIME, 3, 3
330, IrcCommand::RPL_WHOISLOGGEDIN, 4, 4
331, IrcCommand::RPL_NOTOPIC, 3, 3
332, IrcCommand::RPL_TOPIC, 3, 3
333, IrcCommand::RPL_TOPICWHOTIME, 4, 4
338, IrcCommand::RPL_WHOISACTUALLY
341, IrcCommand::RPL_INVITING
342, IrcCommand::RPL_SUMMONING
346, IrcCommand::RPL_INVITELIST
347, IrcCommand::RPL_ENDOFINVITELIST
348, IrcCommand::RPL_EXCEPTLIST
349, IrcCommand::RPL_ENDOFEXCEPTLIST, 3, 3
351, IrcCommand::RPL_VERSION, 4, 4
352, IrcCommand::RPL_WHOREPLY
354, IrcCommand::RPL_WHOSPCRPL
315, IrcCommand::RPL_ENDOFWHO
353, IrcCommand::RPL_NAMREPLY, 4, 4
360, IrcCommand::RPL_WHOWASREAL
366, IrcCommand::RPL_ENDOFNAMES, 3, 3
361, IrcCommand::RPL_KILLDONE
362, IrcCommand::RPL_CLOSING
363, IrcCommand::RPL_CLOSEEND
364, IrcCommand::RPL_LINKS, 4, 4
365, IrcCommand::RPL_ENDOFLINKS, 3, 3
367, IrcCommand::RPL_BANLIST, 5, 5
368, IrcCommand::RPL_ENDOFBANLIST, 3, 3
371, IrcCommand::RPL_INFO, 2, 2
372, IrcCommand::RPL_MOTD, 2, 2
373, IrcCommand::RPL_INFOSTART
374, IrcCommand::RPL_ENDOFINFO, 2, 2
375, IrcCommand::RPL_MOTDSTART, 2, 2
376, IrcCommand::RPL_ENDOFMOTD, 2, 2
378, IrcCommand::RPL_WHOISHOST
381, IrcCommand::RPL_YOUREOPER, 2, 2
382, IrcCommand::RPL_REHASHING
384, IrcCommand::RPL_MYPORTIS
385, IrcCommand::RPL_NOTOPERANYMORE
386, IrcCommand::RPL_RSACHALLENGE
391, IrcCommand::RPL_TIME, 3, 3
392, IrcCommand::RPL_USERSSTART
393, IrcCommand::RPL_USERS
394, IrcCommand::RPL_ENDOFUSERS
395, IrcCommand::RPL_NOUSERS
396, IrcCommand::RPL_HOSTHIDDEN
401, IrcCommand::ERR_NOSUCHNICK, 3, 3
402, IrcCommand::ERR_NOSUCHSERVER, 3, 3
403, IrcCommand::ERR_NOSUCHCHANNEL
404, IrcCommand::ERR_CANNOTSENDTOCHAN
405, IrcCommand::ERR_TOOMANYCHANNELS
406, IrcCommand::ERR_WASNOSUCHNICK
407, IrcCommand::ERR_TOOMANYTARGETS
409, IrcCommand::ERR_NOORIGIN
410, IrcCommand::ERR_INVALIDCAPCMD
411, IrcCommand::ERR_NORECIPIENT
412, IrcCommand::ERR_NOTEXTTOSEND
413, IrcCommand::ERR_NOTOPLEVEL
414, IrcCommand::ERR_WILDTOPLEVEL
415, IrcCommand::ERR_MSGNEEDREGGEDNICK
416, IrcCommand::ERR_TOOMANYMATCHES
421, IrcCommand::ERR_UNKNOWNCOMMAND
422, IrcCommand::ERR_NOMOTD, 2, 2
423, IrcCommand::ERR_NOADMININFO
424, IrcCommand::ERR_FILEERROR
431, IrcCommand::ERR_NONICKNAMEGIVEN
432, IrcCommand::ERR_ERRONEUSNICKNAME
433, IrcCommand::ERR_NICKNAMEINUSE, 3, 3
435, IrcCommand::ERR_BANNICKCHANGE
436, IrcCommand::ERR_NICKCOLLISION
437, IrcCommand::ERR_UNAVAILRESOURCE
438, IrcCommand::ERR_NICKTOOFAST
440, IrcCommand::ERR_SERVICESDOWN
441, IrcCommand::ERR_USERNOTINCHANNEL
442, IrcCommand::ERR_NOTONCHANNEL, 3, 3
443, IrcCommand::ERR_USERONCHANNEL
444, IrcCommand::ERR_NOLOGIN
445, IrcCommand::ERR_SUMMONDISABLED
446, IrcCommand::ERR_USERSDISABLED
451, IrcCommand::ERR_NOTREGISTERED
456, IrcCommand::ERR_ACCEPTFULL
457, IrcCommand::ERR_ACCEPTEXIST
458, IrcCommand::ERR_ACCEPTNOT
461, IrcCommand::ERR_NEEDMOREPARAMS
462, IrcCommand::ERR_ALREADYREGISTRED
463, IrcCommand::ERR_NOPERMFORHOST
464, IrcCommand::ERR_PASSWDMISMATCH
465, IrcCommand::ERR_YOUREBANNEDCREEP
466, IrcCommand::ERR_YOUWILLBEBANNED
467, IrcCommand::ERR_KEYSET
470, IrcCommand::ERR_LINKCHANNEL
471, IrcCommand::ERR_CHANNELISFULL
472, IrcCommand::ERR_UNKNOWNMODE
473, IrcCommand::ERR_INVITEONLYCHAN
474, IrcCommand::ERR_BANNEDFROMCHAN
475, IrcCommand::ERR_BADCHANNELKEY
476, IrcCommand::ERR_BADCHANMASK
477, IrcCommand::ERR_NEEDREGGEDNICK
478, IrcCommand::ERR_BANLISTFULL
479, IrcCommand::ERR_BADCHANNAME
480, IrcCommand::ERR_THROTTLE
481, IrcCommand::ERR_NOPRIVILEGES
482, IrcCommand::ERR_CHANOPRIVSNEEDED
483, IrcCommand::ERR_CANTKILLSERVER
484, IrcCommand::ERR_ISCHANSERVICE
485, IrcCommand::ERR_BANNEDNICK
486, IrcCommand::ERR_NONONREG
489, IrcCommand::ERR_VOICENEEDED
491, IrcCommand::ERR_NOOPERHOST
492, IrcCommand::ERR_CANNOTSENDTOUSER
494, IrcCommand::ERR_OWNMODE
501, IrcCommand::ERR_UMODEUNKNOWNFLAG
502, IrcCommand::ERR_USERSDONTMATCH, 2, 2
503, IrcCommand::ERR_GHOSTEDCLIENT
504, IrcCommand::ERR_USERNOTONSERV
513, IrcCommand::ERR_WRONGPONG
517, IrcCommand::ERR_DISABLED
524, IrcCommand::ERR_HELPNOTFOUND
670, IrcCommand::RPL_STARTTLS
671, IrcCommand::RPL_WHOISSECURE, 3, 3
691, IrcCommand::ERR_STARTTLS
702, IrcCommand::RPL_MODLIST
703, IrcCommand::RPL_ENDOFMODLIST
704, IrcCommand::RPL_HELPSTART
705, IrcCommand::RPL_HELPTXT
706, IrcCommand::RPL_ENDOFHELP
707, IrcCommand::ERR_TARGCHANGE
708, IrcCommand::RPL_ETRACEFULL, 10, 10
709, IrcCommand::RPL_ETRACE
710, IrcCommand::RPL_KNOCK
711, IrcCommand::RPL_KNOCKDLVR
712, IrcCommand::ERR_TOOMANYKNOCK
713, IrcCommand::ERR_CHANOPEN
714, IrcCommand::ERR_KNOCKONCHAN
715, IrcCommand::ERR_KNOCKDISABLED
716, IrcCommand::ERR_TARGUMODEG
717, IrcCommand::RPL_TARGNOTIFY
718, IrcCommand::RPL_UMODEGMSG
720, IrcCommand::RPL_OMOTDSTART
721, IrcCommand::RPL_OMOTD
722, IrcCommand::RPL_ENDOFOMOTD
723, IrcCommand::ERR_NOPRIVS
724, IrcCommand::RPL_TESTMASK
725, IrcCommand::RPL_TESTLINE
726, IrcCommand::RPL_NOTESTLINE
727, IrcCommand::RPL_TESTMASKGECO, 6, 6
728, IrcCommand::RPL_QUIETLIST, 6, 6
729, IrcCommand::RPL_ENDOFQUIETLIS, 4, 4
730, IrcCommand::RPL_MONONLINE
731, IrcCommand::RPL_MONOFFLINE
732, IrcCommand::RPL_MONLIST
733, IrcCommand::RPL_ENDOFMONLIS, 2, 2
734, IrcCommand::ERR_MONLISTFULL
740, IrcCommand::RPL_RSACHALLENGE2, 2, 2
741, IrcCommand::RPL_ENDOFRSACHALLENGE2, 2, 2
742, IrcCommand::ERR_MLOCKRESTRICTE
743, IrcCommand::ERR_INVALIDBAN
744, IrcCommand::ERR_TOPICLOCK
750, IrcCommand::RPL_SCANMATCHED
751, IrcCommand::RPL_SCANUMODES
900, IrcCommand::RPL_LOGGEDIN
901, IrcCommand::RPL_LOGGEDOUT
902, IrcCommand::ERR_NICKLOCKED
903, IrcCommand::RPL_SASLSUCCESS
904, IrcCommand::ERR_SASLFAIL
905, IrcCommand::ERR_SASLTOOLONG
906, IrcCommand::ERR_SASLABORTED
907, IrcCommand::ERR_SASLALREADY
908, IrcCommand::RPL_SASLMECHS
ACCOUNT, IrcCommand::ACCOUNT, 1, 1
AUTHENTICATE, IrcCommand::AUTHENTICATE, 1, 1
AWAY, IrcCommand::AWAY, 0, 1
BATCH, IrcCommand::BATCH
BOUNCER, IrcCommand::BOUNCER
CAP, IrcCommand::CAP, 1, 15
CHGHOST, IrcCommand::CHGHOST, 2, 2
ERROR, IrcCommand::ERROR, 1, 1
JOIN, IrcCommand::JOIN, 1, 3
KICK, IrcCommand::KICK, 3, 3
MODE, IrcCommand::MODE, 2, 15
NICK, IrcCommand::NICK, 1, 1
NOTICE, IrcCommand::NOTICE, 2, 2
PART, IrcCommand::PART, 1, 2
PING, IrcCommand::PING, 1, 1
PRIVMSG, IrcCommand::PRIVMSG, 2, 2
QUIT, IrcCommand::QUIT, 1, 1
SETNAME, IrcCommand::SETNAME, 1, 1
TOPIC, IrcCommand::TOPIC, 2, 2

View File

@@ -1,857 +0,0 @@
/* C++ code produced by gperf version 3.1 */
/* Command-line: /usr/local/bin/gperf -IC -Z IrcCommandHash -K text -L C++ --output-file irc_commands.hpp -t irc_commands.gperf */
/* Computed positions: -k'1-3' */
#if !((' ' == 32) && ('!' == 33) && ('"' == 34) && ('#' == 35) \
&& ('%' == 37) && ('&' == 38) && ('\'' == 39) && ('(' == 40) \
&& (')' == 41) && ('*' == 42) && ('+' == 43) && (',' == 44) \
&& ('-' == 45) && ('.' == 46) && ('/' == 47) && ('0' == 48) \
&& ('1' == 49) && ('2' == 50) && ('3' == 51) && ('4' == 52) \
&& ('5' == 53) && ('6' == 54) && ('7' == 55) && ('8' == 56) \
&& ('9' == 57) && (':' == 58) && (';' == 59) && ('<' == 60) \
&& ('=' == 61) && ('>' == 62) && ('?' == 63) && ('A' == 65) \
&& ('B' == 66) && ('C' == 67) && ('D' == 68) && ('E' == 69) \
&& ('F' == 70) && ('G' == 71) && ('H' == 72) && ('I' == 73) \
&& ('J' == 74) && ('K' == 75) && ('L' == 76) && ('M' == 77) \
&& ('N' == 78) && ('O' == 79) && ('P' == 80) && ('Q' == 81) \
&& ('R' == 82) && ('S' == 83) && ('T' == 84) && ('U' == 85) \
&& ('V' == 86) && ('W' == 87) && ('X' == 88) && ('Y' == 89) \
&& ('Z' == 90) && ('[' == 91) && ('\\' == 92) && (']' == 93) \
&& ('^' == 94) && ('_' == 95) && ('a' == 97) && ('b' == 98) \
&& ('c' == 99) && ('d' == 100) && ('e' == 101) && ('f' == 102) \
&& ('g' == 103) && ('h' == 104) && ('i' == 105) && ('j' == 106) \
&& ('k' == 107) && ('l' == 108) && ('m' == 109) && ('n' == 110) \
&& ('o' == 111) && ('p' == 112) && ('q' == 113) && ('r' == 114) \
&& ('s' == 115) && ('t' == 116) && ('u' == 117) && ('v' == 118) \
&& ('w' == 119) && ('x' == 120) && ('y' == 121) && ('z' == 122) \
&& ('{' == 123) && ('|' == 124) && ('}' == 125) && ('~' == 126))
/* The character set is not based on ISO-646. */
#error "gperf generated tables don't work with this execution character set. Please report a bug to <bug-gperf@gnu.org>."
#endif
#line 1 "irc_commands.gperf"
struct RecognizedCommand {
char * text;
IrcCommand command;
};
#include <string.h>
#define TOTAL_KEYWORDS 270
#define MIN_WORD_LENGTH 3
#define MAX_WORD_LENGTH 12
#define MIN_HASH_VALUE 4
#define MAX_HASH_VALUE 906
/* maximum key range = 903, duplicates = 0 */
class IrcCommandHash
{
private:
static inline unsigned int hash (const char *str, size_t len);
public:
static const struct RecognizedCommand *in_word_set (const char *str, size_t len);
};
inline unsigned int
IrcCommandHash::hash (const char *str, size_t len)
{
static const unsigned short asso_values[] =
{
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
907, 907, 907, 907, 907, 907, 907, 907, 55, 235,
203, 333, 93, 388, 35, 51, 15, 5, 0, 205,
20, 85, 98, 10, 100, 3, 435, 165, 500, 106,
493, 211, 30, 0, 907, 0, 907, 907, 907, 0,
20, 5, 5, 10, 0, 25, 0, 0, 907, 907,
0, 907, 0, 907, 0, 907, 0, 907, 20, 0,
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
907, 907, 907, 907, 907, 907, 907, 907, 907, 907
};
return len + asso_values[static_cast<unsigned char>(str[2]+14)] + asso_values[static_cast<unsigned char>(str[1])] + asso_values[static_cast<unsigned char>(str[0]+6)];
}
const struct RecognizedCommand *
IrcCommandHash::in_word_set (const char *str, size_t len)
{
static const struct RecognizedCommand wordlist[] =
{
{""}, {""}, {""}, {""},
#line 272 "irc_commands.gperf"
{"PING", IrcCommand::PING},
#line 275 "irc_commands.gperf"
{"TOPIC", IrcCommand::TOPIC},
#line 264 "irc_commands.gperf"
{"CAP", IrcCommand::CAP},
#line 271 "irc_commands.gperf"
{"PART", IrcCommand::PART},
{""},
#line 269 "irc_commands.gperf"
{"NICK", IrcCommand::NICK},
#line 265 "irc_commands.gperf"
{"ERROR", IrcCommand::ERROR},
{""},
#line 273 "irc_commands.gperf"
{"PRIVMSG", IrcCommand::PRIVMSG},
{""},
#line 267 "irc_commands.gperf"
{"KICK", IrcCommand::KICK},
{""},
#line 133 "irc_commands.gperf"
{"393", IrcCommand::RPL_USERS},
{""},
#line 199 "irc_commands.gperf"
{"491", IrcCommand::ERR_NOOPERHOST},
#line 268 "irc_commands.gperf"
{"MODE", IrcCommand::MODE},
{""},
#line 194 "irc_commands.gperf"
{"483", IrcCommand::ERR_CANTKILLSERVER},
{""},
#line 131 "irc_commands.gperf"
{"391", IrcCommand::RPL_TIME},
#line 266 "irc_commands.gperf"
{"JOIN", IrcCommand::JOIN},
{""},
#line 270 "irc_commands.gperf"
{"NOTICE", IrcCommand::NOTICE},
{""},
#line 192 "irc_commands.gperf"
{"481", IrcCommand::ERR_NOPRIVILEGES},
#line 274 "irc_commands.gperf"
{"QUIT", IrcCommand::QUIT},
{""}, {""}, {""},
#line 126 "irc_commands.gperf"
{"381", IrcCommand::RPL_YOUREOPER},
{""}, {""}, {""},
#line 263 "irc_commands.gperf"
{"BOUNCER", IrcCommand::BOUNCER},
#line 211 "irc_commands.gperf"
{"691", IrcCommand::ERR_STARTTLS},
{""}, {""},
#line 176 "irc_commands.gperf"
{"463", IrcCommand::ERR_NOPERMFORHOST},
{""},
#line 66 "irc_commands.gperf"
{"281", IrcCommand::RPL_ACCEPTLIST},
{""}, {""},
#line 114 "irc_commands.gperf"
{"363", IrcCommand::RPL_CLOSEEND},
{""},
#line 174 "irc_commands.gperf"
{"461", IrcCommand::ERR_NEEDMOREPARAMS},
{""}, {""}, {""}, {""},
#line 112 "irc_commands.gperf"
{"361", IrcCommand::RPL_KILLDONE},
{""}, {""},
#line 61 "irc_commands.gperf"
{"263", IrcCommand::RPL_LOAD2HI},
#line 184 "irc_commands.gperf"
{"473", IrcCommand::ERR_INVITEONLYCHAN},
#line 262 "irc_commands.gperf"
{"BATCH", IrcCommand::BATCH},
{""}, {""},
#line 139 "irc_commands.gperf"
{"403", IrcCommand::ERR_NOSUCHCHANNEL},
#line 121 "irc_commands.gperf"
{"373", IrcCommand::RPL_INFOSTART},
#line 59 "irc_commands.gperf"
{"261", IrcCommand::RPL_TRACELOG},
#line 182 "irc_commands.gperf"
{"471", IrcCommand::ERR_CHANNELISFULL},
{""},
#line 71 "irc_commands.gperf"
{"303", IrcCommand::RPL_ISON},
{""},
#line 137 "irc_commands.gperf"
{"401", IrcCommand::ERR_NOSUCHNICK},
#line 119 "irc_commands.gperf"
{"371", IrcCommand::RPL_INFO},
{""},
#line 254 "irc_commands.gperf"
{"903", IrcCommand::RPL_SASLSUCCESS},
{""},
#line 69 "irc_commands.gperf"
{"301", IrcCommand::RPL_AWAY},
{""}, {""},
#line 20 "irc_commands.gperf"
{"203", IrcCommand::RPL_TRACEUNKNOWN},
{""},
#line 252 "irc_commands.gperf"
{"901", IrcCommand::RPL_LOGGEDOUT},
{""}, {""}, {""}, {""},
#line 18 "irc_commands.gperf"
{"201", IrcCommand::RPL_TRACECONNECTING},
#line 210 "irc_commands.gperf"
{"671", IrcCommand::RPL_WHOISSECURE},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
{""}, {""},
#line 8 "irc_commands.gperf"
{"003", IrcCommand::RPL_CREATED},
{""}, {""},
#line 166 "irc_commands.gperf"
{"443", IrcCommand::ERR_USERONCHANNEL},
{""}, {""}, {""},
#line 6 "irc_commands.gperf"
{"001", IrcCommand::RPL_WELCOME},
{""}, {""},
#line 164 "irc_commands.gperf"
{"441", IrcCommand::ERR_USERNOTINCHANNEL},
{""},
#line 200 "irc_commands.gperf"
{"492", IrcCommand::ERR_CANNOTSENDTOUSER},
{""}, {""},
#line 99 "irc_commands.gperf"
{"341", IrcCommand::RPL_INVITING},
{""},
#line 132 "irc_commands.gperf"
{"392", IrcCommand::RPL_USERSSTART},
#line 43 "irc_commands.gperf"
{"243", IrcCommand::RPL_STATSOLINE},
{""},
#line 191 "irc_commands.gperf"
{"480", IrcCommand::ERR_THROTTLE},
{""},
#line 193 "irc_commands.gperf"
{"482", IrcCommand::ERR_CHANOPRIVSNEEDED},
{""}, {""},
#line 41 "irc_commands.gperf"
{"241", IrcCommand::RPL_STATSLLINE},
{""},
#line 127 "irc_commands.gperf"
{"382", IrcCommand::RPL_REHASHING},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
#line 67 "irc_commands.gperf"
{"282", IrcCommand::RPL_ENDOFACCEPT},
#line 16 "irc_commands.gperf"
{"043", IrcCommand::RPL_SAVENICK},
{""}, {""}, {""},
#line 175 "irc_commands.gperf"
{"462", IrcCommand::ERR_ALREADYREGISTRED},
{""}, {""},
#line 110 "irc_commands.gperf"
{"360", IrcCommand::RPL_WHOWASREAL},
{""},
#line 113 "irc_commands.gperf"
{"362", IrcCommand::RPL_CLOSING},
#line 180 "irc_commands.gperf"
{"467", IrcCommand::ERR_KEYSET},
{""},
#line 213 "irc_commands.gperf"
{"703", IrcCommand::RPL_ENDOFMODLIST},
{""}, {""},
#line 117 "irc_commands.gperf"
{"367", IrcCommand::RPL_BANLIST},
{""}, {""},
#line 181 "irc_commands.gperf"
{"470", IrcCommand::ERR_LINKCHANNEL},
#line 60 "irc_commands.gperf"
{"262", IrcCommand::RPL_ENDOFTRACE},
#line 183 "irc_commands.gperf"
{"472", IrcCommand::ERR_UNKNOWNMODE},
{""}, {""}, {""},
#line 138 "irc_commands.gperf"
{"402", IrcCommand::ERR_NOSUCHSERVER},
#line 120 "irc_commands.gperf"
{"372", IrcCommand::RPL_MOTD},
#line 188 "irc_commands.gperf"
{"477", IrcCommand::ERR_NEEDREGGEDNICK},
#line 68 "irc_commands.gperf"
{"300", IrcCommand::RPL_NONE},
{""},
#line 70 "irc_commands.gperf"
{"302", IrcCommand::RPL_USERHOST},
#line 143 "irc_commands.gperf"
{"407", IrcCommand::ERR_TOOMANYTARGETS},
{""},
#line 251 "irc_commands.gperf"
{"900", IrcCommand::RPL_LOGGEDIN},
#line 64 "irc_commands.gperf"
{"270", IrcCommand::RPL_PRIVS},
#line 253 "irc_commands.gperf"
{"902", IrcCommand::ERR_NICKLOCKED},
{""}, {""},
#line 17 "irc_commands.gperf"
{"200", IrcCommand::RPL_TRACELINK},
#line 209 "irc_commands.gperf"
{"670", IrcCommand::RPL_STARTTLS},
#line 19 "irc_commands.gperf"
{"202", IrcCommand::RPL_TRACEHANDSHAKE},
#line 258 "irc_commands.gperf"
{"907", IrcCommand::ERR_SASLALREADY},
{""}, {""}, {""},
#line 135 "irc_commands.gperf"
{"395", IrcCommand::RPL_NOUSERS},
{""}, {""}, {""}, {""},
#line 196 "irc_commands.gperf"
{"485", IrcCommand::ERR_BANNEDNICK},
#line 247 "irc_commands.gperf"
{"743", IrcCommand::ERR_INVALIDBAN},
{""}, {""}, {""},
#line 129 "irc_commands.gperf"
{"385", IrcCommand::RPL_NOTOPERANYMORE},
{""}, {""},
#line 245 "irc_commands.gperf"
{"741", IrcCommand::RPL_ENDOFRSACHALLENGE2},
{""},
#line 7 "irc_commands.gperf"
{"002", IrcCommand::RPL_YOURHOST},
#line 163 "irc_commands.gperf"
{"440", IrcCommand::ERR_SERVICESDOWN},
{""},
#line 165 "irc_commands.gperf"
{"442", IrcCommand::ERR_NOTONCHANNEL},
{""}, {""}, {""}, {""},
#line 100 "irc_commands.gperf"
{"342", IrcCommand::RPL_SUMMONING},
{""},
#line 178 "irc_commands.gperf"
{"465", IrcCommand::ERR_YOUREBANNEDCREEP},
{""}, {""}, {""},
#line 102 "irc_commands.gperf"
{"347", IrcCommand::RPL_ENDOFINVITELIST},
#line 116 "irc_commands.gperf"
{"365", IrcCommand::RPL_ENDOFLINKS},
#line 154 "irc_commands.gperf"
{"423", IrcCommand::ERR_NOADMININFO},
{""},
#line 42 "irc_commands.gperf"
{"242", IrcCommand::RPL_STATSUPTIME},
{""}, {""},
#line 88 "irc_commands.gperf"
{"323", IrcCommand::RPL_LISTEND},
#line 261 "irc_commands.gperf"
{"AWAY", IrcCommand::AWAY},
#line 152 "irc_commands.gperf"
{"421", IrcCommand::ERR_UNKNOWNCOMMAND},
#line 46 "irc_commands.gperf"
{"247", IrcCommand::RPL_STATSXLINE},
#line 62 "irc_commands.gperf"
{"265", IrcCommand::RPL_LOCALUSERS},
#line 186 "irc_commands.gperf"
{"475", IrcCommand::ERR_BADCHANNELKEY},
{""},
#line 86 "irc_commands.gperf"
{"321", IrcCommand::RPL_LISTSTART},
{""},
#line 141 "irc_commands.gperf"
{"405", IrcCommand::ERR_TOOMANYCHANNELS},
#line 123 "irc_commands.gperf"
{"375", IrcCommand::RPL_MOTDSTART},
{""}, {""}, {""},
#line 73 "irc_commands.gperf"
{"305", IrcCommand::RPL_UNAWAY},
#line 198 "irc_commands.gperf"
{"489", IrcCommand::ERR_VOICENEEDED},
{""},
#line 36 "irc_commands.gperf"
{"221", IrcCommand::RPL_UMODEIS},
{""},
#line 256 "irc_commands.gperf"
{"905", IrcCommand::ERR_SASLTOOLONG},
{""}, {""}, {""}, {""},
#line 22 "irc_commands.gperf"
{"205", IrcCommand::RPL_TRACEUSER},
{""}, {""},
#line 148 "irc_commands.gperf"
{"413", IrcCommand::ERR_NOTOPLEVEL},
{""},
#line 212 "irc_commands.gperf"
{"702", IrcCommand::RPL_MODLIST},
{""}, {""},
#line 78 "irc_commands.gperf"
{"313", IrcCommand::RPL_WHOISOPERATOR},
{""},
#line 146 "irc_commands.gperf"
{"411", IrcCommand::ERR_NORECIPIENT},
#line 217 "irc_commands.gperf"
{"707", IrcCommand::ERR_TARGCHANGE},
{""}, {""}, {""},
#line 76 "irc_commands.gperf"
{"311", IrcCommand::RPL_WHOISUSER},
#line 80 "irc_commands.gperf"
{"369", IrcCommand::RPL_ENDOFWHOWAS},
{""},
#line 28 "irc_commands.gperf"
{"213", IrcCommand::RPL_STATSCLINE},
{""},
#line 10 "irc_commands.gperf"
{"005", IrcCommand::RPL_ISUPPORT},
{""}, {""},
#line 168 "irc_commands.gperf"
{"445", IrcCommand::ERR_SUMMONDISABLED},
{""},
#line 26 "irc_commands.gperf"
{"211", IrcCommand::RPL_STATSLINKINFO},
{""},
#line 190 "irc_commands.gperf"
{"479", IrcCommand::ERR_BADCHANNAME},
#line 204 "irc_commands.gperf"
{"503", IrcCommand::ERR_GHOSTEDCLIENT},
{""},
#line 260 "irc_commands.gperf"
{"AUTHENTICATE", IrcCommand::AUTHENTICATE},
#line 144 "irc_commands.gperf"
{"409", IrcCommand::ERR_NOORIGIN},
{""}, {""}, {""},
#line 202 "irc_commands.gperf"
{"501", IrcCommand::ERR_UMODEUNKNOWNFLAG},
{""}, {""},
#line 45 "irc_commands.gperf"
{"245", IrcCommand::RPL_STATSSLINE},
{""}, {""},
#line 244 "irc_commands.gperf"
{"740", IrcCommand::RPL_RSACHALLENGE2},
{""},
#line 246 "irc_commands.gperf"
{"742", IrcCommand::ERR_MLOCKRESTRICTE},
{""}, {""},
#line 25 "irc_commands.gperf"
{"209", IrcCommand::RPL_TRACECLASS},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
#line 232 "irc_commands.gperf"
{"723", IrcCommand::ERR_NOPRIVS},
{""}, {""}, {""}, {""}, {""}, {""},
#line 230 "irc_commands.gperf"
{"721", IrcCommand::RPL_OMOTD},
{""}, {""}, {""}, {""},
#line 153 "irc_commands.gperf"
{"422", IrcCommand::ERR_NOMOTD},
{""},
#line 215 "irc_commands.gperf"
{"705", IrcCommand::RPL_HELPTXT},
#line 85 "irc_commands.gperf"
{"320", IrcCommand::RPL_WHOISSPECIAL},
{""},
#line 87 "irc_commands.gperf"
{"322", IrcCommand::RPL_LIST},
#line 104 "irc_commands.gperf"
{"349", IrcCommand::RPL_ENDOFEXCEPTLIST},
{""}, {""}, {""}, {""}, {""}, {""},
#line 35 "irc_commands.gperf"
{"220", IrcCommand::RPL_STATSPLINE},
{""}, {""},
#line 48 "irc_commands.gperf"
{"249", IrcCommand::RPL_STATSDEBUG},
{""}, {""}, {""},
#line 223 "irc_commands.gperf"
{"713", IrcCommand::ERR_CHANOPEN},
{""}, {""}, {""}, {""}, {""}, {""},
#line 221 "irc_commands.gperf"
{"711", IrcCommand::RPL_KNOCKDLVR},
{""}, {""},
#line 145 "irc_commands.gperf"
{"410", IrcCommand::ERR_INVALIDCAPCMD},
{""},
#line 147 "irc_commands.gperf"
{"412", IrcCommand::ERR_NOTEXTTOSEND},
#line 158 "irc_commands.gperf"
{"433", IrcCommand::ERR_NICKNAMEINUSE},
{""},
#line 75 "irc_commands.gperf"
{"310", IrcCommand::RPL_WHOISHELPOP},
{""},
#line 77 "irc_commands.gperf"
{"312", IrcCommand::RPL_WHOISSERVER},
#line 96 "irc_commands.gperf"
{"333", IrcCommand::RPL_TOPICWHOTIME},
{""},
#line 156 "irc_commands.gperf"
{"431", IrcCommand::ERR_NONICKNAMEGIVEN},
{""}, {""},
#line 82 "irc_commands.gperf"
{"317", IrcCommand::RPL_WHOISIDLE},
{""},
#line 94 "irc_commands.gperf"
{"331", IrcCommand::RPL_NOTOPIC},
{""},
#line 27 "irc_commands.gperf"
{"212", IrcCommand::RPL_STATSCOMMANDS},
#line 219 "irc_commands.gperf"
{"709", IrcCommand::RPL_ETRACE},
{""}, {""}, {""}, {""},
#line 32 "irc_commands.gperf"
{"217", IrcCommand::RPL_STATSQLINE},
{""}, {""}, {""},
#line 203 "irc_commands.gperf"
{"502", IrcCommand::ERR_USERSDONTMATCH},
{""}, {""}, {""}, {""}, {""}, {""}, {""},
#line 12 "irc_commands.gperf"
{"010", IrcCommand::RPL_REDIR},
{""}, {""}, {""}, {""},
#line 90 "irc_commands.gperf"
{"325", IrcCommand::RPL_CHANNELMLOCK},
{""}, {""},
#line 15 "irc_commands.gperf"
{"017", IrcCommand::RPL_MAPEND},
{""}, {""}, {""}, {""}, {""}, {""},
#line 38 "irc_commands.gperf"
{"225", IrcCommand::RPL_STATSDLINE},
{""}, {""},
#line 229 "irc_commands.gperf"
{"720", IrcCommand::RPL_OMOTDSTART},
{""},
#line 231 "irc_commands.gperf"
{"722", IrcCommand::RPL_ENDOFOMOTD},
{""}, {""}, {""}, {""}, {""},
#line 236 "irc_commands.gperf"
{"727", IrcCommand::RPL_TESTMASKGECO},
{""},
#line 109 "irc_commands.gperf"
{"353", IrcCommand::RPL_NAMREPLY},
{""},
#line 170 "irc_commands.gperf"
{"451", IrcCommand::ERR_NOTREGISTERED},
{""},
#line 150 "irc_commands.gperf"
{"415", IrcCommand::ERR_MSGNEEDREGGEDNICK},
{""}, {""},
#line 105 "irc_commands.gperf"
{"351", IrcCommand::RPL_VERSION},
{""},
#line 108 "irc_commands.gperf"
{"315", IrcCommand::RPL_ENDOFWHO},
#line 52 "irc_commands.gperf"
{"253", IrcCommand::RPL_LUSERUNKNOWN},
{""}, {""}, {""}, {""}, {""}, {""},
#line 50 "irc_commands.gperf"
{"251", IrcCommand::RPL_LUSERCLIENT},
{""},
#line 30 "irc_commands.gperf"
{"215", IrcCommand::RPL_STATSILINE},
{""}, {""},
#line 220 "irc_commands.gperf"
{"710", IrcCommand::RPL_KNOCK},
#line 92 "irc_commands.gperf"
{"329", IrcCommand::RPL_CREATIONTIME},
#line 222 "irc_commands.gperf"
{"712", IrcCommand::ERR_TOOMANYKNOCK},
#line 242 "irc_commands.gperf"
{"733", IrcCommand::RPL_ENDOFMONLIS},
{""}, {""}, {""}, {""},
#line 227 "irc_commands.gperf"
{"717", IrcCommand::RPL_TARGNOTIFY},
{""},
#line 240 "irc_commands.gperf"
{"731", IrcCommand::RPL_MONOFFLINE},
{""}, {""}, {""}, {""},
#line 157 "irc_commands.gperf"
{"432", IrcCommand::ERR_ERRONEUSNICKNAME},
{""},
#line 13 "irc_commands.gperf"
{"015", IrcCommand::RPL_MAP},
#line 93 "irc_commands.gperf"
{"330", IrcCommand::RPL_WHOISLOGGEDIN},
{""},
#line 95 "irc_commands.gperf"
{"332", IrcCommand::RPL_TOPIC},
#line 161 "irc_commands.gperf"
{"437", IrcCommand::ERR_UNAVAILRESOURCE},
#line 201 "irc_commands.gperf"
{"494", IrcCommand::ERR_OWNMODE},
{""}, {""},
#line 206 "irc_commands.gperf"
{"513", IrcCommand::ERR_WRONGPONG},
#line 97 "irc_commands.gperf"
{"337", IrcCommand::RPL_WHOISTEXT},
#line 134 "irc_commands.gperf"
{"394", IrcCommand::RPL_ENDOFUSERS},
{""}, {""}, {""}, {""},
#line 195 "irc_commands.gperf"
{"484", IrcCommand::ERR_ISCHANSERVICE},
#line 84 "irc_commands.gperf"
{"319", IrcCommand::RPL_WHOISCHANNELS},
{""},
#line 234 "irc_commands.gperf"
{"725", IrcCommand::RPL_TESTLINE},
{""},
#line 128 "irc_commands.gperf"
{"384", IrcCommand::RPL_MYPORTIS},
{""}, {""}, {""}, {""}, {""},
#line 34 "irc_commands.gperf"
{"219", IrcCommand::RPL_ENDOFSTATS},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
#line 177 "irc_commands.gperf"
{"464", IrcCommand::ERR_PASSWDMISMATCH},
{""}, {""}, {""}, {""},
#line 115 "irc_commands.gperf"
{"364", IrcCommand::RPL_LINKS},
{""}, {""}, {""}, {""}, {""}, {""}, {""},
#line 250 "irc_commands.gperf"
{"751", IrcCommand::RPL_SCANUMODES},
{""},
#line 225 "irc_commands.gperf"
{"715", IrcCommand::ERR_KNOCKDISABLED},
#line 185 "irc_commands.gperf"
{"474", IrcCommand::ERR_BANNEDFROMCHAN},
{""}, {""}, {""},
#line 140 "irc_commands.gperf"
{"404", IrcCommand::ERR_CANNOTSENDTOCHAN},
#line 122 "irc_commands.gperf"
{"374", IrcCommand::RPL_ENDOFINFO},
{""},
#line 106 "irc_commands.gperf"
{"352", IrcCommand::RPL_WHOREPLY},
#line 172 "irc_commands.gperf"
{"457", IrcCommand::ERR_ACCEPTEXIST},
#line 72 "irc_commands.gperf"
{"304", IrcCommand::RPL_TEXT},
{""}, {""},
#line 159 "irc_commands.gperf"
{"435", IrcCommand::ERR_BANNICKCHANGE},
#line 238 "irc_commands.gperf"
{"729", IrcCommand::RPL_ENDOFQUIETLIS},
#line 255 "irc_commands.gperf"
{"904", IrcCommand::ERR_SASLFAIL},
#line 49 "irc_commands.gperf"
{"250", IrcCommand::RPL_STATSCONN},
{""},
#line 51 "irc_commands.gperf"
{"252", IrcCommand::RPL_LUSEROP},
{""},
#line 21 "irc_commands.gperf"
{"204", IrcCommand::RPL_TRACEOPERATOR},
{""}, {""}, {""},
#line 56 "irc_commands.gperf"
{"257", IrcCommand::RPL_ADMINLOC1},
#line 136 "irc_commands.gperf"
{"396", IrcCommand::RPL_HOSTHIDDEN},
{""}, {""},
#line 40 "irc_commands.gperf"
{"235", IrcCommand::RPL_SERVLISTEND},
{""},
#line 197 "irc_commands.gperf"
{"486", IrcCommand::ERR_NONONREG},
#line 239 "irc_commands.gperf"
{"730", IrcCommand::RPL_MONONLINE},
{""},
#line 241 "irc_commands.gperf"
{"732", IrcCommand::RPL_MONLIST},
{""},
#line 130 "irc_commands.gperf"
{"386", IrcCommand::RPL_RSACHALLENGE},
{""}, {""}, {""}, {""},
#line 9 "irc_commands.gperf"
{"004", IrcCommand::RPL_MYINFO},
{""}, {""},
#line 167 "irc_commands.gperf"
{"444", IrcCommand::ERR_NOLOGIN},
{""}, {""}, {""}, {""},
#line 118 "irc_commands.gperf"
{"368", IrcCommand::RPL_ENDOFBANLIST},
{""},
#line 179 "irc_commands.gperf"
{"466", IrcCommand::ERR_YOUWILLBEBANNED},
{""}, {""}, {""}, {""},
#line 111 "irc_commands.gperf"
{"366", IrcCommand::RPL_ENDOFNAMES},
{""}, {""},
#line 44 "irc_commands.gperf"
{"244", IrcCommand::RPL_STATSHLINE},
#line 189 "irc_commands.gperf"
{"478", IrcCommand::ERR_BANLISTFULL},
{""},
#line 207 "irc_commands.gperf"
{"517", IrcCommand::ERR_DISABLED},
{""}, {""},
#line 125 "irc_commands.gperf"
{"378", IrcCommand::RPL_WHOISHOST},
#line 63 "irc_commands.gperf"
{"266", IrcCommand::RPL_GLOBALUSERS},
#line 187 "irc_commands.gperf"
{"476", IrcCommand::ERR_BADCHANMASK},
{""}, {""}, {""},
#line 142 "irc_commands.gperf"
{"406", IrcCommand::ERR_WASNOSUCHNICK},
#line 124 "irc_commands.gperf"
{"376", IrcCommand::RPL_ENDOFMOTD},
{""},
#line 259 "irc_commands.gperf"
{"908", IrcCommand::RPL_SASLMECHS},
{""},
#line 74 "irc_commands.gperf"
{"306", IrcCommand::RPL_NOWAWAY},
{""}, {""},
#line 24 "irc_commands.gperf"
{"208", IrcCommand::RPL_TRACENEWTYPE},
{""},
#line 257 "irc_commands.gperf"
{"906", IrcCommand::ERR_SASLABORTED},
#line 65 "irc_commands.gperf"
{"276", IrcCommand::RPL_WHOISCERTFP},
{""},
#line 54 "irc_commands.gperf"
{"255", IrcCommand::RPL_LUSERME},
{""},
#line 23 "irc_commands.gperf"
{"206", IrcCommand::RPL_TRACESERVER},
#line 249 "irc_commands.gperf"
{"750", IrcCommand::RPL_SCANMATCHED},
{""}, {""}, {""},
#line 214 "irc_commands.gperf"
{"704", IrcCommand::RPL_HELPSTART},
{""}, {""}, {""}, {""}, {""}, {""}, {""},
#line 11 "irc_commands.gperf"
{"008", IrcCommand::RPL_SNOMASK},
{""}, {""}, {""}, {""}, {""}, {""}, {""},
#line 103 "irc_commands.gperf"
{"348", IrcCommand::RPL_EXCEPTLIST},
{""},
#line 169 "irc_commands.gperf"
{"446", IrcCommand::ERR_USERSDISABLED},
{""}, {""}, {""}, {""},
#line 101 "irc_commands.gperf"
{"346", IrcCommand::RPL_INVITELIST},
{""}, {""},
#line 47 "irc_commands.gperf"
{"248", IrcCommand::RPL_STATSULINE},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
{""}, {""},
#line 248 "irc_commands.gperf"
{"744", IrcCommand::ERR_TOPICLOCK},
#line 58 "irc_commands.gperf"
{"259", IrcCommand::RPL_ADMINEMAIL},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
#line 218 "irc_commands.gperf"
{"708", IrcCommand::RPL_ETRACEFULL},
{""}, {""}, {""}, {""},
#line 155 "irc_commands.gperf"
{"424", IrcCommand::ERR_FILEERROR},
{""},
#line 216 "irc_commands.gperf"
{"706", IrcCommand::RPL_ENDOFHELP},
{""}, {""},
#line 89 "irc_commands.gperf"
{"324", IrcCommand::RPL_CHANNELMODEIS},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
#line 37 "irc_commands.gperf"
{"224", IrcCommand::RPL_STATSFLINE},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
{""}, {""}, {""}, {""}, {""}, {""}, {""},
#line 149 "irc_commands.gperf"
{"414", IrcCommand::ERR_WILDTOPLEVEL},
{""}, {""}, {""}, {""},
#line 79 "irc_commands.gperf"
{"314", IrcCommand::RPL_WHOWASUSER},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
#line 29 "irc_commands.gperf"
{"214", IrcCommand::RPL_STATSNLINE},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
#line 205 "irc_commands.gperf"
{"504", IrcCommand::ERR_USERNOTONSERV},
{""}, {""}, {""}, {""}, {""},
#line 91 "irc_commands.gperf"
{"328", IrcCommand::RPL_CHANNELURL},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
{""}, {""}, {""},
#line 233 "irc_commands.gperf"
{"724", IrcCommand::RPL_TESTMASK},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
#line 83 "irc_commands.gperf"
{"318", IrcCommand::RPL_ENDOFWHOIS},
{""},
#line 151 "irc_commands.gperf"
{"416", IrcCommand::ERR_TOOMANYMATCHES},
{""}, {""}, {""}, {""},
#line 81 "irc_commands.gperf"
{"316", IrcCommand::RPL_WHOISCHANOP},
{""}, {""},
#line 33 "irc_commands.gperf"
{"218", IrcCommand::RPL_STATSYLINE},
{""}, {""}, {""}, {""}, {""}, {""},
#line 31 "irc_commands.gperf"
{"216", IrcCommand::RPL_STATSKLINE},
{""}, {""}, {""}, {""},
#line 224 "irc_commands.gperf"
{"714", IrcCommand::ERR_KNOCKONCHAN},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
{""}, {""}, {""}, {""}, {""},
#line 14 "irc_commands.gperf"
{"016", IrcCommand::RPL_MAPMORE},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
{""},
#line 237 "irc_commands.gperf"
{"728", IrcCommand::RPL_QUIETLIST},
{""},
#line 39 "irc_commands.gperf"
{"234", IrcCommand::RPL_SERVLIST},
{""}, {""}, {""}, {""},
#line 235 "irc_commands.gperf"
{"726", IrcCommand::RPL_NOTESTLINE},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
{""}, {""}, {""}, {""}, {""}, {""},
#line 228 "irc_commands.gperf"
{"718", IrcCommand::RPL_UMODEGMSG},
{""}, {""}, {""}, {""}, {""}, {""},
#line 226 "irc_commands.gperf"
{"716", IrcCommand::ERR_TARGUMODEG},
{""}, {""}, {""}, {""}, {""},
#line 162 "irc_commands.gperf"
{"438", IrcCommand::ERR_NICKTOOFAST},
{""},
#line 107 "irc_commands.gperf"
{"354", IrcCommand::RPL_WHOSPCRPL},
{""}, {""},
#line 98 "irc_commands.gperf"
{"338", IrcCommand::RPL_WHOISACTUALLY},
{""},
#line 160 "irc_commands.gperf"
{"436", IrcCommand::ERR_NICKCOLLISION},
{""}, {""}, {""}, {""},
#line 53 "irc_commands.gperf"
{"254", IrcCommand::RPL_LUSERCHANNELS},
{""}, {""}, {""}, {""},
#line 208 "irc_commands.gperf"
{"524", IrcCommand::ERR_HELPNOTFOUND},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
#line 243 "irc_commands.gperf"
{"734", IrcCommand::ERR_MONLISTFULL},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
#line 173 "irc_commands.gperf"
{"458", IrcCommand::ERR_ACCEPTNOT},
{""}, {""}, {""}, {""}, {""}, {""},
#line 171 "irc_commands.gperf"
{"456", IrcCommand::ERR_ACCEPTFULL},
{""}, {""}, {""}, {""}, {""}, {""}, {""},
#line 57 "irc_commands.gperf"
{"258", IrcCommand::RPL_ADMINLOC2},
{""}, {""}, {""}, {""}, {""}, {""},
#line 55 "irc_commands.gperf"
{"256", IrcCommand::RPL_ADMINME}
};
if (len <= MAX_WORD_LENGTH && len >= MIN_WORD_LENGTH)
{
unsigned int key = hash (str, len);
if (key <= MAX_HASH_VALUE)
{
const char *s = wordlist[key].text;
if (*str == *s && !strcmp (str + 1, s + 1))
return &wordlist[key];
}
}
return 0;
}

View File

@@ -1,31 +0,0 @@
#include "irc_parse_thread.hpp"
#include "connection.hpp"
#include <cstring>
namespace {
#include "irc_commands.inc"
} // namespace
auto IrcParseThread::start(Connection& connection) -> void
{
connection.add_listener<LineEvent>([&connection](LineEvent const& event)
{
auto const msg = parse_irc_message(event.line);
auto const recognized = IrcCommandHash::in_word_set(msg.command.data(), msg.command.size());
auto const command
= recognized
&& recognized->min_args <= msg.args.size()
&& recognized->max_args >= msg.args.size()
? recognized->command : IrcCommand::UNKNOWN;
if (IrcCommand::UNKNOWN == command)
{
std::cout << "Unrecognized command: " << msg.command << " " << msg.args.size() << std::endl;
}
connection.make_event<IrcMsgEvent>(command, msg);
});
}

View File

@@ -1,149 +0,0 @@
#include <cstring>
#include <optional>
#include "ircmsg.hpp"
namespace {
class Parser {
char* msg_;
inline static char empty[1];
inline void trim() {
while (*msg_ == ' ') msg_++;
}
public:
Parser(char* msg) : msg_(msg) {
if (msg_ == nullptr) {
msg_ = empty;
} else {
trim();
}
}
char* word() {
auto const start = msg_;
while (*msg_ != '\0' && *msg_ != ' ') msg_++;
if (*msg_ != '\0') { // prepare for next token
*msg_++ = '\0';
trim();
}
return start;
}
bool match(char c) {
if (c == *msg_) {
msg_++;
return true;
}
return false;
}
bool isempty() const {
return *msg_ == '\0';
}
char const* peek() {
return msg_;
}
};
std::string_view unescape_tag_value(char* const val)
{
// only start copying at the first escape character
// skip everything before that
auto cursor = strchr(val, '\\');
if (cursor == nullptr) { return {val}; }
auto write = cursor;
for (; *cursor; cursor++)
{
if (*cursor == '\\')
{
cursor++;
switch (*cursor)
{
default : *write++ = *cursor; break;
case ':' : *write++ = ';' ; break;
case 's' : *write++ = ' ' ; break;
case 'r' : *write++ = '\r' ; break;
case 'n' : *write++ = '\n' ; break;
case '\0': return {val, write};
}
}
else
{
*write++ = *cursor;
}
}
return {val, write};
}
} // namespace
auto parse_irc_tags(char* str) -> std::vector<irctag>
{
std::vector<irctag> tags;
do {
auto val = strsep(&str, ";");
auto key = strsep(&val, "=");
if ('\0' == *key) {
throw irc_parse_error(irc_error_code::MISSING_TAG);
}
if (nullptr == val) {
tags.emplace_back(key, "");
} else {
tags.emplace_back(std::string_view{key, val-1}, unescape_tag_value(val));
}
} while(nullptr != str);
return tags;
}
auto parse_irc_message(char* msg) -> IrcMsg
{
Parser p {msg};
IrcMsg out;
/* MESSAGE TAGS */
if (p.match('@')) {
out.tags = parse_irc_tags(p.word());
}
/* MESSAGE SOURCE */
if (p.match(':')) {
out.source = p.word();
}
/* MESSAGE COMMANDS */
out.command = p.word();
if (out.command.empty()) {
throw irc_parse_error{irc_error_code::MISSING_COMMAND};
}
/* MESSAGE ARGUMENTS */
while (!p.isempty()) {
if (p.match(':')) {
out.args.push_back(p.peek());
break;
}
out.args.push_back(p.word());
}
return out;
}
auto IrcMsg::hassource() const -> bool
{
return source.data() != nullptr;
}
auto operator<<(std::ostream& out, irc_error_code code) -> std::ostream&
{
switch(code) {
case irc_error_code::MISSING_COMMAND: out << "MISSING COMMAND"; return out;
case irc_error_code::MISSING_TAG: out << "MISSING TAG"; return out;
default: return out;
}
}

View File

@@ -1,62 +0,0 @@
#pragma once
#include <iostream>
#include <string_view>
#include <vector>
struct irctag
{
std::string_view key;
std::string_view val;
irctag(std::string_view key, std::string_view val) : key{key}, val{val} {}
friend auto operator==(irctag const&, irctag const&) -> bool = default;
};
struct IrcMsg
{
std::vector<irctag> tags;
std::vector<std::string_view> args;
std::string_view source;
std::string_view command;
IrcMsg() = default;
IrcMsg(
std::vector<irctag> && tags,
std::string_view source,
std::string_view command,
std::vector<std::string_view> && args)
: tags(std::move(tags)),
args(std::move(args)),
source{source},
command{command} {}
bool hassource() const;
friend bool operator==(IrcMsg const&, IrcMsg const&) = default;
};
enum class irc_error_code {
MISSING_TAG,
MISSING_COMMAND,
};
auto operator<<(std::ostream& out, irc_error_code) -> std::ostream&;
struct irc_parse_error : public std::exception {
irc_error_code code;
irc_parse_error(irc_error_code code) : code(code) {}
};
/**
* Parses the given IRC message into a structured format.
* The original message is mangled to store string fragments
* that are pointed to by the structured message type.
*
* Returns zero for success, non-zero for parse error.
*/
auto parse_irc_message(char* msg) -> IrcMsg;
auto parse_irc_tags(char* msg) -> std::vector<irctag>;

View File

@@ -1,97 +0,0 @@
#pragma once
/**
* @file linebuffer.hpp
* @author Eric Mertens <emertens@gmail.com>
* @brief A line buffering class
* @version 0.1
* @date 2023-08-22
*
* @copyright Copyright (c) 2023
*
*/
#include <boost/asio/buffer.hpp>
#include <algorithm>
#include <concepts>
#include <vector>
/**
* @brief Fixed-size buffer with line-oriented dispatch
*
*/
class LineBuffer
{
std::vector<char> buffer;
// [buffer.begin(), end_) contains buffered data
// [end_, buffer.end()) is available buffer space
std::vector<char>::iterator end_;
public:
/**
* @brief Construct a new Line Buffer object
*
* @param n Buffer size
*/
LineBuffer(std::size_t n) : buffer(n), end_{buffer.begin()} {}
/**
* @brief Get the available buffer space
*
* @return boost::asio::mutable_buffer
*/
auto get_buffer() -> boost::asio::mutable_buffer
{
return boost::asio::buffer(&*end_, std::distance(end_, buffer.end()));
}
/**
* @brief Commit new buffer bytes and dispatch line callback
*
* The first n bytes of the buffer will be considered to be
* populated. The line callback function will be called once
* per completed line. Those lines are removed from the buffer
* and the is ready for additional calls to get_buffer and
* add_bytes.
*
* @param n Bytes written to the last call of get_buffer
* @param line_cb Callback function to run on each completed line
*/
auto add_bytes(std::size_t n, std::invocable<char *> auto line_cb) -> void
{
auto const start = end_;
std::advance(end_, n);
// new data is now located in [start, end_)
// cursor marks the beginning of the current line
auto cursor = buffer.begin();
for (auto nl = std::find(start, end_, '\n');
nl != end_;
nl = std::find(cursor, end_, '\n'))
{
// Null-terminate the line. Support both \n and \r\n
if (cursor < nl && *std::prev(nl) == '\r')
{
*std::prev(nl) = '\0';
}
else
{
*nl = '\0';
}
line_cb(&*cursor);
cursor = std::next(nl);
}
// If any lines were processed, move all processed lines to
// the front of the buffer
if (cursor != buffer.begin())
{
end_ = std::move(cursor, end_, buffer.begin());
}
}
};

View File

@@ -1,97 +0,0 @@
#include <boost/asio.hpp>
#include "connection.hpp"
#include "event.hpp"
#include "ircmsg.hpp"
#include "linebuffer.hpp"
#include "settings.hpp"
#include "write_irc.hpp"
#include "command_thread.hpp"
#include "irc_parse_thread.hpp"
#include "ping_thread.hpp"
#include "registration_thread.hpp"
#include "self_thread.hpp"
#include "snote_thread.hpp"
#include "watchdog_thread.hpp"
#include <algorithm>
#include <chrono>
#include <fstream>
#include <coroutine>
#include <iostream>
#include <limits>
#include <list>
#include <memory>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <variant>
#include <vector>
#include <unordered_map>
#include <unordered_set>
using namespace std::chrono_literals;
auto echo_thread(Connection& connection) -> void
{
connection.add_listener<CommandEvent>([&connection](CommandEvent& event)
{
if ("raw" == event.command
and "glguy" == event.oper
and "glguy" == event.account)
{
connection.write_line(std::string{event.arg});
event.handled_ = true;
send_notice(connection, event.nick, "ack");
}
});
}
auto start(boost::asio::io_context & io, Settings const& settings) -> void
{
auto connection = std::make_shared<Connection>(io);
WatchdogThread::start(*connection);
IrcParseThread::start(*connection);
PingThread::start(*connection);
SelfThread::start(*connection);
RegistrationThread::start(*connection, settings.password, settings.username, settings.realname, settings.nickname);
SnoteThread::start(*connection);
CommandThread::start(*connection);
echo_thread(*connection);
boost::asio::co_spawn(
io,
connection->connect(io, settings.host, settings.service),
[&io, &settings](std::exception_ptr e)
{
auto timer = std::make_shared<boost::asio::steady_timer>(io);
timer->expires_from_now(5s);
timer->async_wait([&io, &settings, timer](auto) {
start(io, settings);
});
});
}
auto get_settings() -> Settings
{
if (auto config_stream = std::ifstream {"config.toml"})
{
return Settings::from_stream(config_stream);
}
else
{
std::cerr << "Unable to open config.toml\n";
std::exit(1);
}
}
auto main() -> int
{
auto const settings = get_settings();
auto io = boost::asio::io_context{};
start(io, settings);
io.run();
}

2
mybase64/CMakeLists.txt Normal file
View File

@@ -0,0 +1,2 @@
add_library(mybase64 STATIC mybase64.cpp)
target_include_directories(mybase64 PUBLIC include)

View File

@@ -0,0 +1,41 @@
/**
* @file mybase64.hpp
* @author Eric Mertens (emertens@gmail.com)
* @brief Base64 encoding and decoding
*
*/
#pragma once
#include <cstddef>
#include <string_view>
namespace mybase64 {
inline constexpr auto encoded_size(std::size_t len) -> std::size_t
{
return (len + 2) / 3 * 4;
}
inline constexpr auto decoded_size(std::size_t len) -> std::size_t
{
return (len + 3) / 4 * 3;
}
/**
* @brief Encode a string into base64
*
* @param input input text
* @param output Target buffer for encoded value
*/
auto encode(std::string_view input, char* output) -> void;
/**
* @brief Decode a base64 encoded string
*
* @param input Base64 input text
* @param output Target buffer for decoded value
* @return pointer to end of output on success
*/
auto decode(std::string_view input, char* output) -> char*;
} // namespace

102
mybase64/mybase64.cpp Normal file
View File

@@ -0,0 +1,102 @@
#include "mybase64.hpp"
#include <array>
#include <climits>
#include <cstdint>
#include <string_view>
namespace mybase64 {
namespace {
constexpr std::array<char, 64> alphabet{
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
};
constexpr std::array<std::int8_t, 256> alphabet_values = []() constexpr {
std::array<std::int8_t, 256> result;
result.fill(-1);
std::int8_t v = 0;
for (auto const k : alphabet)
{
result[k] = v++;
}
return result;
}();
}
static_assert(CHAR_BIT == 8);
auto encode(std::string_view const input, char* output) -> void
{
auto cursor = std::begin(input);
auto const end = std::end(input);
while (end - cursor >= 3)
{
std::uint32_t buffer = std::uint8_t(*cursor++);
buffer <<= 8;
buffer |= std::uint8_t(*cursor++);
buffer <<= 8;
buffer |= std::uint8_t(*cursor++);
*output++ = alphabet[(buffer >> 6 * 3) % 64];
*output++ = alphabet[(buffer >> 6 * 2) % 64];
*output++ = alphabet[(buffer >> 6 * 1) % 64];
*output++ = alphabet[(buffer >> 6 * 0) % 64];
}
if (cursor < end)
{
std::uint32_t buffer = std::uint8_t(*cursor++) << 10;
if (cursor < end)
buffer |= std::uint8_t(*cursor) << 2;
*output++ = alphabet[(buffer >> 12) % 64];
*output++ = alphabet[(buffer >> 6) % 64];
*output++ = cursor < end ? alphabet[(buffer % 64)] : '=';
*output++ = '=';
}
*output = '\0';
}
auto decode(std::string_view const input, char* output) -> char*
{
std::uint32_t buffer = 1;
for (auto const c : input)
{
if (auto const value = alphabet_values[uint8_t(c)]; -1 != value)
{
buffer = (buffer << 6) | value;
if (buffer & 1 << 6 * 4)
{
*output++ = buffer >> 16;
*output++ = buffer >> 8;
*output++ = buffer >> 0;
buffer = 1;
}
}
}
if (buffer & 1 << 6 * 3)
{
*output++ = buffer >> 10;
*output++ = buffer >> 2;
}
else if (buffer & 1 << 6 * 2)
{
*output++ = buffer >> 4;
}
else if (buffer & 1 << 6 * 1)
{
return nullptr;
}
return output;
}
} // namespace

33
myirc/CMakeLists.txt Normal file
View File

@@ -0,0 +1,33 @@
add_custom_command(
OUTPUT irc_commands.inc
COMMAND
gperf
-C -Z IrcCommandHash -K text -L C++ -t
--output-file irc_commands.inc
${CMAKE_CURRENT_SOURCE_DIR}/irc_commands.gperf
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/irc_commands.gperf
VERBATIM)
add_library(myirc STATIC
irc_commands.inc
bot.cpp
challenge.cpp
client.cpp
connection.cpp
ircmsg.cpp
openssl_utils.cpp
registration.cpp
ratelimit.cpp
sasl_mechanism.cpp
snote.cpp
linebuffer.cpp
)
target_include_directories(myirc PUBLIC include)
target_include_directories(myirc PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(myirc PRIVATE
OpenSSL::SSL
Boost::signals2 Boost::log Boost::asio
tomlplusplus_tomlplusplus
PkgConfig::LIBHS
mysocks5 mybase64)

83
myirc/bot.cpp Normal file
View File

@@ -0,0 +1,83 @@
#include "myirc/bot.hpp"
#include <boost/log/trivial.hpp>
namespace myirc {
auto Bot::start(std::shared_ptr<Client> self) -> std::shared_ptr<Bot>
{
const auto thread = std::make_shared<Bot>(std::move(self));
thread->self_->sig_chat.connect([thread](auto &chat, bool) {
thread->on_chat(chat);
});
return thread;
}
auto Bot::process_command(std::string_view message, const Chat &chat) -> void
{
const auto cmdstart = message.find_first_not_of(' ');
if (cmdstart == message.npos) return;
message = message.substr(cmdstart);
if (not message.starts_with(command_prefix_)) return;
message = message.substr(1); // discard the prefix
auto cmdend = message.find(' ');
std::string_view command =
cmdend == message.npos ? message : message.substr(0, cmdend);
std::string_view arguments =
cmdend == message.npos ? std::string_view{} : message.substr(cmdend + 1);
std::string_view oper;
std::string_view account;
for (auto [key, value] : chat.tags)
{
if (key == "account")
{
account = value;
}
else if (key == "solanum.chat/oper")
{
oper = value;
}
}
sig_command({
.source = chat.source,
.target = chat.target,
.oper = oper,
.account = account,
.command = command,
.arguments = arguments
});
}
auto Bot::on_chat(const Chat &chat) -> void
{
if (not chat.is_notice)
{
if (self_->is_my_nick(chat.target))
{
process_command(chat.message, chat);
}
else if (self_->is_channel(chat.target))
{
const auto colon = chat.message.find(':');
if (colon == chat.message.npos) return;
if (not self_->is_my_nick(chat.message.substr(0, colon))) return;
process_command(chat.message.substr(colon + 1), chat);
}
}
}
auto Bot::shutdown() -> void
{
sig_command.disconnect_all_slots();
}
} // namespace myirc

95
myirc/challenge.cpp Normal file
View File

@@ -0,0 +1,95 @@
#include "myirc/challenge.hpp"
#include "myirc/openssl_utils.hpp"
#include <mybase64.hpp>
#include <openssl/evp.h>
#include <openssl/rsa.h>
#include <boost/log/trivial.hpp>
#include <memory>
#include <string>
namespace myirc {
Challenge::Challenge(Ref<EVP_PKEY> key, std::shared_ptr<Client> client)
: key_{std::move(key)}
, client_{std::move(client)}
{}
auto Challenge::on_ircmsg(IrcCommand cmd, const IrcMsg &msg) -> void {
switch (cmd) {
default: break;
case IrcCommand::RPL_RSACHALLENGE2:
buffer_ += msg.args[1];
break;
case IrcCommand::ERR_NOOPERHOST:
slot_.disconnect();
BOOST_LOG_TRIVIAL(error) << "Challenge: No oper host";
break;
case IrcCommand::RPL_YOUREOPER:
slot_.disconnect();
break;
case IrcCommand::RPL_ENDOFRSACHALLENGE2:
finish_challenge();
break;
}
}
auto Challenge::finish_challenge() -> void
{
unsigned int digestlen = EVP_MAX_MD_SIZE;
unsigned char digest[EVP_MAX_MD_SIZE];
size_t len = mybase64::decoded_size(buffer_.size());
std::vector<unsigned char> ciphertext(len, 0);
auto decode_end = mybase64::decode(buffer_, reinterpret_cast<char*>(ciphertext.data()));
if (decode_end == nullptr )
return log_openssl_errors("Challenge base64::decode: ");
ciphertext.resize(decode_end - reinterpret_cast<char*>(ciphertext.data()));
// Setup decryption context
Ref<EVP_PKEY_CTX> ctx{EVP_PKEY_CTX_new(key_.get(), nullptr)};
if (not ctx)
return log_openssl_errors("Challenge EVP_PKEY_CTX_new: ");
if (1 != EVP_PKEY_decrypt_init(ctx.get()))
return log_openssl_errors("Challenge EVP_PKEY_decrypt_init: ");
if (0 >= EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_OAEP_PADDING))
return log_openssl_errors("Challenge EVP_PKEY_CTX_set_rsa_padding: ");
// Determine output size
if (1 != EVP_PKEY_decrypt(ctx.get(), nullptr, &len, ciphertext.data(), ciphertext.size()))
return log_openssl_errors("Challenge EVP_PKEY_decrypt (size): ");
buffer_.resize(len);
// Decrypt ciphertext
if (1 != EVP_PKEY_decrypt(ctx.get(), reinterpret_cast<unsigned char*>(buffer_.data()), &len, ciphertext.data(), ciphertext.size()))
return log_openssl_errors("Challenge EVP_PKEY_decrypt: ");
buffer_.resize(len);
// Hash the decrypted message
if (1 != EVP_Digest(buffer_.data(), buffer_.size(), digest, &digestlen, EVP_sha1(), nullptr))
return log_openssl_errors("Challenge EVP_Digest: ");
// Construct reply as '+' and base64 encoded digest
buffer_.resize(mybase64::encoded_size(digestlen) + 1);
buffer_[0] = '+';
mybase64::encode(std::string_view{(char*)digest, digestlen}, buffer_.data() + 1);
client_->send_challenge(buffer_);
buffer_.clear();
}
auto Challenge::start(std::shared_ptr<Client> client, const std::string_view user, Ref<EVP_PKEY> ref) -> std::shared_ptr<Challenge>
{
auto self = std::make_shared<Challenge>(std::move(ref), client);
self->slot_ = client->get_connection().sig_ircmsg.connect([self](auto cmd, auto &msg, bool) { self->on_ircmsg(cmd, msg); });
client->send_challenge(user);
return self;
}
} // namespace myirc

651
myirc/client.cpp Normal file
View File

@@ -0,0 +1,651 @@
#include "myirc/client.hpp"
#include "myirc/connection.hpp"
#include "myirc/sasl_mechanism.hpp"
#include "myirc/snote.hpp"
#include <mybase64.hpp>
#include <boost/container/flat_map.hpp>
#include <boost/log/trivial.hpp>
namespace myirc {
using namespace std::literals;
auto Client::on_welcome(const IrcMsg &irc) -> void
{
nickname_ = irc.args[0];
}
auto Client::on_registered() -> void
{
sig_registered();
sig_registered.disconnect_all_slots();
}
auto Client::on_nick(const IrcMsg &irc) -> void
{
if (is_my_mask(irc.source))
{
nickname_ = irc.args[0];
}
}
auto Client::on_umodeis(const IrcMsg &irc) -> void
{
mode_ = irc.args[1];
}
auto Client::on_join(const IrcMsg &irc) -> void
{
if (is_my_mask(irc.source))
{
channels_.insert(casemap(irc.args[0]));
}
}
auto Client::on_kick(const IrcMsg &irc) -> void
{
if (is_my_nick(irc.args[1]))
{
channels_.erase(casemap(irc.args[0]));
}
}
auto Client::on_part(const IrcMsg &irc) -> void
{
if (is_my_mask(irc.source))
{
channels_.erase(casemap(irc.args[0]));
}
}
auto Client::on_mode(const IrcMsg &irc) -> void
{
if (is_my_nick(irc.args[0]))
{
auto polarity = true;
for (const char c : irc.args[1])
{
switch (c)
{
case '+':
polarity = true;
break;
case '-':
polarity = false;
break;
default:
if (polarity)
{
mode_ += c;
}
else
{
const auto ix = mode_.find(c);
if (ix != std::string::npos)
{
mode_.erase(ix, 1);
}
}
break;
}
}
}
}
auto Client::on_isupport(const IrcMsg &msg) -> void
{
const auto hi = msg.args.size() - 1;
for (int i = 1; i < hi; ++i)
{
auto &entry = msg.args[i];
// Leading minus means to stop support
if (entry.starts_with("-"))
{
const auto key = std::string{entry.substr(1)};
if (auto cursor = isupport_.find(key); cursor != isupport_.end())
{
isupport_.erase(cursor);
}
}
else if (const auto cursor = entry.find('='); cursor != entry.npos)
{
isupport_.emplace(entry.substr(0, cursor), entry.substr(cursor + 1));
}
else
{
isupport_.emplace(entry, std::string{});
}
}
}
auto Client::on_chat(bool notice, const IrcMsg &irc, bool flush) -> void
{
char status_msg = '\0';
std::string_view target = irc.args[0];
if (not target.empty() && status_msg_.find(target[0]) != std::string::npos)
{
status_msg = target[0];
target = target.substr(1);
}
sig_chat({
.tags = irc.tags,
.is_notice = notice,
.status_msg = '\0',
.source = irc.source,
.target = irc.args[0],
.message = irc.args[1],
}, flush);
}
auto Client::start(std::shared_ptr<Connection> connection) -> std::shared_ptr<Client>
{
auto thread = std::make_shared<Client>(connection);
connection->sig_ircmsg.connect([thread](auto cmd, auto &msg, bool flush) {
switch (cmd)
{
case IrcCommand::PRIVMSG:
thread->on_chat(false, msg, flush);
break;
case IrcCommand::NOTICE:
if (auto match = snoteCore.match(msg))
{
thread->sig_snote(*match, flush);
} else {
thread->on_chat(true, msg, flush);
}
break;
case IrcCommand::JOIN:
thread->on_join(msg);
break;
case IrcCommand::KICK:
thread->on_kick(msg);
break;
case IrcCommand::MODE:
thread->on_mode(msg);
break;
case IrcCommand::NICK:
thread->on_nick(msg);
break;
case IrcCommand::PART:
thread->on_part(msg);
break;
case IrcCommand::RPL_ISUPPORT:
thread->on_isupport(msg);
break;
case IrcCommand::RPL_UMODEIS:
thread->on_umodeis(msg);
break;
case IrcCommand::RPL_WELCOME:
thread->on_welcome(msg);
break;
case IrcCommand::RPL_ENDOFMOTD:
case IrcCommand::ERR_NOMOTD:
thread->on_registered();
break;
case IrcCommand::CAP:
thread->on_cap(msg);
break;
case IrcCommand::AUTHENTICATE:
thread->on_authenticate_chunk(msg.args[0]);
break;
default:
break;
}
});
connection->sig_disconnect.connect([thread](auto) {
thread->sig_registered.disconnect_all_slots();
thread->sig_cap_ls.disconnect_all_slots();
thread->sig_chat.disconnect_all_slots();
thread->sig_snote.disconnect_all_slots();
});
return thread;
}
auto Client::get_my_nick() const -> const std::string &
{
return nickname_;
}
auto Client::get_my_mode() const -> const std::string &
{
return mode_;
}
auto Client::get_my_channels() const -> const std::unordered_set<std::string> &
{
return channels_;
}
auto Client::is_my_nick(std::string_view nick) const -> bool
{
return casemap_compare(nick, nickname_) == 0;
}
auto Client::is_my_mask(std::string_view mask) const -> bool
{
const auto bang = mask.find('!');
return bang != std::string_view::npos && is_my_nick(mask.substr(0, bang));
}
auto Client::is_channel(std::string_view name) const -> bool
{
return not name.empty() && channel_prefix_.find(name[0]) != channel_prefix_.npos;
}
auto Client::is_on_channel(std::string_view name) const -> bool
{
return channels_.contains(casemap(name));
}
namespace {
template <class... Ts>
struct overloaded : Ts...
{
using Ts::operator()...;
};
template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
}
auto Client::on_authenticate(const std::string_view body) -> void
{
if (not sasl_mechanism_)
{
BOOST_LOG_TRIVIAL(warning) << "Unexpected AUTHENTICATE from server"sv;
send_authenticate_abort();
return;
}
std::visit(
overloaded{
[this](const std::string &reply) {
send_authenticate_encoded(reply);
},
[this](SaslMechanism::NoReply) {
send_authenticate("*"sv);
},
[this](SaslMechanism::Failure) {
send_authenticate_abort();
},
},
sasl_mechanism_->step(body));
if (sasl_mechanism_->is_complete())
{
sasl_mechanism_.reset();
}
}
auto Client::start_sasl(std::unique_ptr<SaslMechanism> mechanism) -> void
{
if (sasl_mechanism_)
{
send_authenticate("*"sv); // abort SASL
}
sasl_mechanism_ = std::move(mechanism);
send_authenticate(sasl_mechanism_->mechanism_name());
}
static auto tolower_rfc1459(int c) -> int
{
return 97 <= c && c <= 126 ? c - 32 : c;
}
static auto tolower_rfc1459_strict(int c) -> int
{
return 97 <= c && c <= 125 ? c - 32 : c;
}
template <int lower(int)>
static auto casemap_impl(std::string_view str) -> std::string
{
std::string result;
result.reserve(str.size());
for (auto c : str) {
result.push_back(lower(c));
}
return result;
}
auto Client::casemap(std::string_view str) const -> std::string
{
switch (casemap_) {
case Casemap::Ascii: return casemap_impl<tolower>(str);
case Casemap::Rfc1459: return casemap_impl<tolower_rfc1459>(str);
case Casemap::Rfc1459_Strict: return casemap_impl<tolower_rfc1459_strict>(str);
}
}
template <int lower(int)>
static auto casemap_compare_impl(std::string_view lhs, std::string_view rhs) -> int
{
size_t n1 = lhs.size();
size_t n2 = rhs.size();
size_t n = std::min(n1, n2);
for (size_t i = 0; i < n; ++i) {
if (lower(lhs[i]) < lower(rhs[i])) return -1;
if (lower(lhs[i]) > lower(rhs[i])) return 1;
}
if (n1 < n2) return -1;
if (n1 > n2) return 1;
return 0;
}
auto Client::casemap_compare(std::string_view lhs, std::string_view rhs) const -> int
{
switch (casemap_) {
case Casemap::Ascii: return casemap_compare_impl<tolower>(lhs, rhs);
case Casemap::Rfc1459: return casemap_compare_impl<tolower_rfc1459>(lhs, rhs);
case Casemap::Rfc1459_Strict: return casemap_compare_impl<tolower_rfc1459_strict>(lhs, rhs);
}
}
auto Client::list_caps() -> void
{
caps_available_.clear();
send_cap_ls();
}
auto Client::on_cap(const IrcMsg &msg) -> void
{
if ("ACK" == msg.args[1] && msg.args.size() == 3) {
auto in = std::istringstream{std::string{msg.args[2]}};
std::for_each(
std::istream_iterator<std::string>{in},
std::istream_iterator<std::string>{},
[this](std::string x) {
caps_.insert(std::move(x));
}
);
} else if ("NAK" == msg.args[1] && msg.args.size() == 3) {
auto in = std::istringstream{std::string{msg.args[2]}};
std::for_each(
std::istream_iterator<std::string>{in},
std::istream_iterator<std::string>{},
[this](std::string x) {
caps_.erase(x);
}
);
} else if ("LS" == msg.args[1]) {
const std::string_view *kvs;
bool last;
if (3 == msg.args.size())
{
kvs = &msg.args[2];
last = true;
}
else if (4 == msg.args.size() && "*" == msg.args[2])
{
kvs = &msg.args[3];
last = false;
}
else
{
BOOST_LOG_TRIVIAL(warning) << "Malformed CAP LS";
return;
}
auto in = std::istringstream{std::string{*kvs}};
std::for_each(
std::istream_iterator<std::string>{in},
std::istream_iterator<std::string>{},
[this](std::string x) {
const auto eq = x.find('=');
if (eq == x.npos)
{
caps_available_.emplace(x, std::string{});
}
else
{
caps_available_.emplace(std::string{x, 0, eq}, std::string{x, eq + 1, x.npos});
}
}
);
if (last)
{
sig_cap_ls(caps_available_);
}
} else if ("NEW" == msg.args[1] && msg.args.size() == 3) {
auto in = std::istringstream{std::string{msg.args[2]}};
std::for_each(
std::istream_iterator<std::string>{in},
std::istream_iterator<std::string>{},
[this](std::string x) {
const auto eq = x.find('=');
if (eq == x.npos)
{
caps_available_.emplace(x, std::string{});
}
else
{
caps_available_.emplace(std::string{x, 0, eq}, std::string{x, eq + 1, x.npos});
}
}
);
} else if ("DEL" == msg.args[1] && msg.args.size() == 3) {
auto in = std::istringstream{std::string{msg.args[2]}};
std::for_each(
std::istream_iterator<std::string>{in},
std::istream_iterator<std::string>{},
[this](std::string x) {
caps_available_.erase(x);
caps_.erase(x);
}
);
}
}
auto Client::on_authenticate_chunk(const std::string_view chunk) -> void
{
if (chunk != "+"sv)
{
authenticate_buffer_ += chunk;
}
if (chunk.size() != 400)
{
std::string decoded;
decoded.resize(mybase64::decoded_size(authenticate_buffer_.size()));
std::size_t len;
if (auto decode_end = mybase64::decode(authenticate_buffer_, decoded.data()))
{
decoded.resize(decode_end - decoded.data());
on_authenticate(decoded);
}
else
{
BOOST_LOG_TRIVIAL(debug) << "Invalid AUTHENTICATE base64"sv;
send_authenticate("*"sv); // abort SASL
}
authenticate_buffer_.clear();
}
else if (authenticate_buffer_.size() > 1024)
{
BOOST_LOG_TRIVIAL(debug) << "AUTHENTICATE buffer overflow"sv;
authenticate_buffer_.clear();
send_authenticate("*"sv); // abort SASL
}
}
auto Client::send_ping(std::string_view txt) -> void
{
connection_->write_irc("PING", txt);
}
auto Client::send_pong(std::string_view txt) -> void
{
connection_->write_irc("PONG", txt);
}
auto Client::send_pass(std::string_view password) -> void
{
connection_->write_irc("PASS", password);
}
auto Client::send_user(std::string_view user, std::string_view real) -> void
{
connection_->write_irc("USER", user, "*", "*", real);
}
auto Client::send_nick(std::string_view nick) -> void
{
connection_->write_irc("NICK", nick);
}
auto Client::send_cap_ls() -> void
{
connection_->write_irc("CAP", "LS", "302");
}
auto Client::send_cap_end() -> void
{
connection_->write_irc("CAP", "END");
}
auto Client::send_cap_req(std::string_view caps) -> void
{
connection_->write_irc("CAP", "REQ", caps);
}
auto Client::send_privmsg(std::string_view target, std::string_view message) -> void
{
connection_->write_irc("PRIVMSG", target, message);
}
auto Client::send_notice(std::string_view target, std::string_view message) -> void
{
connection_->write_irc("NOTICE", target, message);
}
auto Client::send_wallops(std::string_view message) -> void
{
connection_->write_irc("WALLOPS", message);
}
auto Client::send_names(std::string_view channel) -> void
{
connection_->write_irc("NAMES", channel);
}
auto Client::send_map() -> void
{
connection_->write_irc("MAP");
}
auto Client::send_get_topic(std::string_view channel) -> void
{
connection_->write_irc("TOPIC", channel);
}
auto Client::send_set_topic(std::string_view channel, std::string_view message) -> void
{
connection_->write_irc("TOPIC", channel, message);
}
auto Client::send_testline(std::string_view target) -> void
{
connection_->write_irc("TESTLINE", target);
}
auto Client::send_masktrace_gecos(std::string_view target, std::string_view gecos) -> void
{
connection_->write_irc("MASKTRACE", target, gecos);
}
auto Client::send_masktrace(std::string_view target) -> void
{
connection_->write_irc("MASKTRACE", target);
}
auto Client::send_testmask_gecos(std::string_view target, std::string_view gecos) -> void
{
connection_->write_irc("TESTMASK", target, gecos);
}
auto Client::send_testmask(std::string_view target) -> void
{
connection_->write_irc("TESTMASK", target);
}
auto Client::send_authenticate(std::string_view message) -> void
{
connection_->write_irc("AUTHENTICATE", message);
}
auto Client::send_join(std::string_view channel) -> void
{
connection_->write_irc("JOIN", channel);
}
auto Client::send_challenge(std::string_view message) -> void
{
connection_->write_irc("CHALLENGE", message);
}
auto Client::send_oper(std::string_view user, std::string_view pass) -> void
{
connection_->write_irc("OPER", user, pass);
}
auto Client::send_kick(std::string_view channel, std::string_view nick, std::string_view reason) -> void
{
connection_->write_irc("KICK", channel, nick, reason);
}
auto Client::send_kill(std::string_view nick, std::string_view reason) -> void
{
connection_->write_irc("KILL", nick, reason);
}
auto Client::send_quit(std::string_view message) -> void
{
connection_->write_irc("QUIT", message);
}
auto Client::send_whois(std::string_view arg1) -> void
{
connection_->write_irc("WHOIS", arg1);
}
auto Client::send_whois_remote(std::string_view arg1, std::string_view arg2) -> void
{
connection_->write_irc("WHOIS", arg1, arg2);
}
auto Client::send_authenticate_abort() -> void
{
send_authenticate("*");
}
auto Client::send_authenticate_encoded(std::string_view body) -> void
{
std::string encoded(mybase64::encoded_size(body.size()), 0);
mybase64::encode(body, encoded.data());
for (size_t lo = 0; lo < encoded.size(); lo += 400)
{
const auto hi = std::min(lo + 400, encoded.size());
const std::string_view chunk{encoded.begin() + lo, encoded.begin() + hi};
send_authenticate(chunk);
}
if (encoded.size() % 400 == 0)
{
send_authenticate("+"sv);
}
}
} // namespace myirc

453
myirc/connection.cpp Normal file
View File

@@ -0,0 +1,453 @@
#include "myirc/connection.hpp"
#include "myirc/linebuffer.hpp"
#include <openssl/asn1.h>
#include <openssl/ssl.h>
#include <openssl/x509.h>
#include <socks5.hpp>
#include <mybase64.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/log/trivial.hpp>
#include <sstream>
#include <iomanip>
namespace myirc {
#include "irc_commands.inc"
using tcp_type = boost::asio::ip::tcp::socket;
using tls_type = boost::asio::ssl::stream<tcp_type>;
using namespace std::literals;
Connection::Connection(boost::asio::io_context &io)
: stream_{io}
, watchdog_timer_{io}
, write_posted_{false}
, stalled_{false}
{
}
auto Connection::write_buffers() -> void
{
const auto available = write_strings_.size();
const auto [delay, count]
= rate_limit
? rate_limit->query(available)
: std::pair{0ms, available};
if (delay > 0ms) {
auto timer = std::make_shared<boost::asio::steady_timer>(stream_.get_executor(), delay);
timer->async_wait([timer, count, self = weak_from_this()](auto) {
if (auto lock = self.lock()) {
lock->write_buffers(count);
}
});
} else {
write_buffers(count);
}
}
auto Connection::write_buffers(size_t n) -> void
{
std::list<std::string> strings;
std::vector<boost::asio::const_buffer> buffers;
if (n == write_strings_.size()) {
strings = std::move(write_strings_);
write_strings_.clear();
} else {
strings.splice(
strings.begin(), // insert at
write_strings_, // remove from
write_strings_.begin(), // start removing at
std::next(write_strings_.begin(), n) // stop removing at
);
}
buffers.reserve(n);
for (const auto &elt : strings)
{
buffers.push_back(boost::asio::buffer(elt));
}
boost::asio::async_write(
stream_,
buffers,
[this, strings = std::move(strings)](const boost::system::error_code &error, std::size_t) {
if (not error)
{
if (write_strings_.empty())
{
write_posted_ = false;
}
else
{
write_buffers();
}
}
}
);
}
auto Connection::watchdog() -> void
{
watchdog_timer_.expires_after(watchdog_duration);
watchdog_timer_.async_wait([this](const auto &error) {
if (not error)
{
if (stalled_)
{
BOOST_LOG_TRIVIAL(debug) << "Watchdog timer elapsed, closing stream";
close();
}
else
{
write_irc("PING", "watchdog");
stalled_ = true;
watchdog();
}
}
});
}
auto Connection::watchdog_activity() -> void
{
stalled_ = false;
watchdog_timer_.expires_after(watchdog_duration);
}
/// Parse IRC message line and dispatch it to the ircmsg slot.
auto Connection::dispatch_line(char *line, bool flush) -> void
{
const auto msg = parse_irc_message(line);
const auto recognized = IrcCommandHash::in_word_set(msg.command.data(), msg.command.size());
const auto command
= recognized
&& recognized->min_args <= msg.args.size()
&& recognized->max_args >= msg.args.size()
? recognized->command
: IrcCommand::UNKNOWN;
switch (command)
{
// Respond to pings immediate and discard
case IrcCommand::PING:
write_irc("PONG", msg.args[0]);
break;
// Unknown message generate warnings but do not dispatch
// Messages can be unknown due to bad command or bad argument count
case IrcCommand::UNKNOWN:
BOOST_LOG_TRIVIAL(warning) << "Unrecognized command: " << msg.command << " " << msg.args.size();
break;
// Normal IRC commands
default:
sig_ircmsg(command, msg, flush);
break;
}
}
auto Connection::close() -> void
{
stream_.close();
}
auto Connection::write_irc(std::string message) -> void
{
BOOST_LOG_TRIVIAL(debug) << "SEND: " << message;
message += "\r\n";
write_strings_.push_back(std::move(message));
if (not write_posted_)
{
write_posted_ = true;
boost::asio::post(stream_.get_executor(), [weak = weak_from_this()]() {
if (auto self = weak.lock())
{
self->write_buffers();
}
});
}
}
auto Connection::write_irc(std::string front, std::string_view last) -> void
{
bool colon = last.starts_with(":");
for (const auto c : last) {
switch (c) {
case '\r': case '\n': case '\0': throw std::runtime_error{"bad irc argument"};
case ' ': colon = true;
default: break;
}
}
front += colon ? " :" : " ";
front += last;
write_irc(std::move(front));
}
static
auto set_buffer_size(tls_type& stream, std::size_t const n) -> void
{
auto const ssl = stream.native_handle();
BIO_set_buffer_size(SSL_get_rbio(ssl), n);
BIO_set_buffer_size(SSL_get_wbio(ssl), n);
}
static
auto set_buffer_size(tcp_type& socket, std::size_t const n) -> void
{
socket.set_option(tcp_type::send_buffer_size{static_cast<int>(n)});
socket.set_option(tcp_type::receive_buffer_size{static_cast<int>(n)});
}
static
auto set_cloexec(int const fd) -> void
{
auto const flags = fcntl(fd, F_GETFD);
if (-1 == flags)
{
throw std::system_error{errno, std::generic_category(), "failed to get file descriptor flags"};
}
if (-1 == fcntl(fd, F_SETFD, flags | FD_CLOEXEC))
{
throw std::system_error{errno, std::generic_category(), "failed to set file descriptor flags"};
}
}
template <std::size_t... Ns>
static
auto constexpr sum() -> std::size_t { return (0 + ... + Ns); }
/**
* @brief Build's the string format required for the ALPN extension
*
* @tparam Ns sizes of each protocol name
* @param protocols array of the names of the supported protocols
* @return encoded protocol names
*/
template <std::size_t... Ns>
static
auto constexpr alpn_encode(char const (&... protocols)[Ns]) -> std::array<unsigned char, sum<Ns...>()>
{
auto result = std::array<unsigned char, sum<Ns...>()>{};
auto cursor = std::begin(result);
auto const encode = [&cursor]<std::size_t N>(char const(&protocol)[N]) {
static_assert(N > 0, "Protocol name must be null-terminated");
static_assert(N < 256, "Protocol name too long");
if (protocol[N - 1] != '\0')
throw "Protocol name not null-terminated";
// Prefixed length byte
*cursor++ = N - 1;
// Add string skipping null terminator
cursor = std::copy(std::begin(protocol), std::end(protocol) - 1, cursor);
};
(encode(protocols), ...);
return result;
}
/**
* @brief Configure the TLS stream to request the IRC protocol.
*
* @param stream TLS stream
*/
static
auto set_alpn(tls_type& stream) -> void
{
auto constexpr protos = alpn_encode("irc");
SSL_set_alpn_protos(stream.native_handle(), protos.data(), protos.size());
}
static
auto build_ssl_context(
X509* client_cert,
EVP_PKEY* client_key
) -> boost::asio::ssl::context
{
boost::asio::ssl::context ssl_context{boost::asio::ssl::context::method::tls_client};
ssl_context.set_default_verify_paths();
if (nullptr != client_cert)
{
if (1 != SSL_CTX_use_certificate(ssl_context.native_handle(), client_cert))
{
throw std::runtime_error{"certificate file"};
}
}
if (nullptr != client_key)
{
if (1 != SSL_CTX_use_PrivateKey(ssl_context.native_handle(), client_key))
{
throw std::runtime_error{"private key"};
}
}
return ssl_context;
}
static auto peer_fingerprint(X509 *cer) -> std::string
{
std::ostringstream os;
std::vector<std::uint8_t> result;
EVP_MD *md_used;
if (auto digest = X509_digest_sig(cer, &md_used, nullptr))
{
os << EVP_MD_name(md_used) << ":" << std::hex << std::setfill('0');
EVP_MD_free(md_used);
for (int i = 0; i < digest->length; ++i) {
os << std::setw(2) << static_cast<unsigned>(digest->data[i]);
}
ASN1_OCTET_STRING_free(digest);
}
return os.str();
}
auto Connection::connect(
Settings settings
) -> boost::asio::awaitable<void>
{
using namespace std::placeholders;
// keep connection alive while coroutine is active
const auto self = shared_from_this();
const size_t irc_buffer_size = 32'768;
boost::asio::ip::tcp::endpoint socket_endpoint;
std::optional<boost::asio::ip::tcp::endpoint> socks_endpoint;
std::string fingerprint;
{
// Name resolution
auto resolver = boost::asio::ip::tcp::resolver{stream_.get_executor()};
const auto endpoints = co_await resolver.async_resolve(settings.host, std::to_string(settings.port), boost::asio::use_awaitable);
for (auto e : endpoints) {
BOOST_LOG_TRIVIAL(debug) << "DNS: " << e.endpoint();
}
// Connect to the IRC server
auto& socket = stream_.reset();
// If we're going to use SOCKS then the TCP connection host is actually the socks
// server and then the IRC server gets passed over the SOCKS protocol
auto const use_socks = not settings.socks_host.empty() && settings.socks_port != 0;
if (use_socks)
{
std::swap(settings.host, settings.socks_host);
std::swap(settings.port, settings.socks_port);
}
socket_endpoint = co_await boost::asio::async_connect(socket, endpoints, boost::asio::use_awaitable);
BOOST_LOG_TRIVIAL(debug) << "CONNECTED: " << socket_endpoint;
// Set socket options
socket.set_option(boost::asio::ip::tcp::no_delay(true));
set_buffer_size(socket, irc_buffer_size);
set_cloexec(socket.native_handle());
// Optionally negotiate SOCKS connection
if (use_socks)
{
auto auth = not settings.socks_user.empty() || not settings.socks_pass.empty()
? socks5::Auth{socks5::UsernamePasswordCredential{settings.socks_user, settings.socks_pass}}
: socks5::Auth{socks5::NoCredential{}};
socks_endpoint = co_await socks5::async_connect(
socket,
settings.socks_host, settings.socks_port, std::move(auth),
boost::asio::use_awaitable
);
}
}
if (settings.tls)
{
auto cxt = build_ssl_context(settings.client_cert.get(), settings.client_key.get());
// Upgrade stream_ to use TLS and invalidate socket
auto& stream = stream_.upgrade(cxt);
set_buffer_size(stream, irc_buffer_size);
set_alpn(stream);
if (not settings.verify.empty())
{
stream.set_verify_mode(boost::asio::ssl::verify_peer);
stream.set_verify_callback(boost::asio::ssl::host_name_verification(settings.verify));
}
if (not settings.sni.empty())
{
SSL_set_tlsext_host_name(stream.native_handle(), settings.sni.c_str());
}
co_await stream.async_handshake(stream.client, boost::asio::use_awaitable);
const auto cer = SSL_get0_peer_certificate(stream.native_handle());
fingerprint = peer_fingerprint(cer);
}
sig_connect(socket_endpoint, socks_endpoint, std::move(fingerprint));
watchdog();
for (LineBuffer buffer{irc_buffer_size};;)
{
boost::system::error_code error;
auto const chunk = buffer.prepare();
if (chunk.size() == 0) break;
const auto n = co_await stream_.async_read_some(chunk, boost::asio::redirect_error(boost::asio::use_awaitable, error));
if (error)
{
break;
}
buffer.commit(n);
auto line = buffer.next_nonempty_line();
if (line)
{
watchdog_activity();
do
{
BOOST_LOG_TRIVIAL(debug) << "RECV: " << line;
const auto next_line = buffer.next_nonempty_line();
dispatch_line(line, next_line == nullptr);
line = next_line;
} while (line);
}
buffer.shift();
}
watchdog_timer_.cancel();
stream_.close();
}
auto Connection::start(Settings settings) -> void
{
boost::asio::co_spawn(
stream_.get_executor(), connect(std::move(settings)),
[self = shared_from_this()](std::exception_ptr e) {
try
{
if (e)
std::rethrow_exception(e);
BOOST_LOG_TRIVIAL(debug) << "DISCONNECTED";
}
catch (const std::exception &e)
{
BOOST_LOG_TRIVIAL(debug) << "TERMINATED: " << e.what();
}
// Disconnect all slots to avoid circular references
self->sig_connect.disconnect_all_slots();
self->sig_ircmsg.disconnect_all_slots();
self->sig_disconnect(e);
self->sig_disconnect.disconnect_all_slots();
});
}
} // namespace myirc

View File

@@ -0,0 +1,49 @@
#pragma once
#include "client.hpp"
#include <boost/signals2.hpp>
#include <memory>
namespace myirc {
struct Bot : std::enable_shared_from_this<Bot>
{
struct Command
{
std::string_view source;
std::string_view target;
std::string_view oper;
std::string_view account;
std::string_view command;
std::string_view arguments;
auto nick() const -> std::string_view {
auto bang = source.find('!');
if (bang == std::string::npos) {
return "";
} else {
return source.substr(0, bang);
}
}
};
std::shared_ptr<Client> self_;
char command_prefix_;
boost::signals2::signal<void(const Command &)> sig_command;
Bot(std::shared_ptr<Client> self)
: self_{std::move(self)}
, command_prefix_{'!'}
{}
auto on_chat(const Chat &) -> void;
auto process_command(std::string_view message, const Chat &msg) -> void;
static auto start(std::shared_ptr<Client>) -> std::shared_ptr<Bot>;
auto shutdown() -> void;
};
} // namespace myirc

View File

@@ -1,5 +1,7 @@
#pragma once
namespace myirc {
template <typename> struct CCallback_;
template <typename F, typename R, typename... Ts>
struct CCallback_<R (F::*) (Ts...) const>
@@ -10,5 +12,9 @@ struct CCallback_<R (F::*) (Ts...) const>
}
};
/// @brief Wrapper for passing closures through C-style callbacks.
/// @tparam F Type of the closure
template <typename F>
using CCallback = CCallback_<decltype(&F::operator())>;
} // namespace myirc

View File

@@ -0,0 +1,35 @@
#pragma once
#include "myirc/client.hpp"
#include "myirc/ref.hpp"
#include <boost/signals2/connection.hpp>
#include <memory>
#include <string>
namespace myirc {
/// @brief Implements the CHALLENGE command protocol to identify as an operator.
class Challenge : std::enable_shared_from_this<Challenge>
{
Ref<EVP_PKEY> key_;
std::shared_ptr<Client> client_;
boost::signals2::scoped_connection slot_;
std::string buffer_;
auto on_ircmsg(IrcCommand cmd, const IrcMsg &msg) -> void;
auto finish_challenge() -> void;
public:
Challenge(Ref<EVP_PKEY>, std::shared_ptr<Client>);
/// @brief Starts the CHALLENGE protocol.
/// @param connection Registered connection.
/// @param user Operator username
/// @param key Operator private RSA key
/// @return Handle to the challenge object.
static auto start(std::shared_ptr<Client>, std::string_view user, Ref<EVP_PKEY> key) -> std::shared_ptr<Challenge>;
};
} // namespace myirc

View File

@@ -0,0 +1,145 @@
#pragma once
#include "myirc/connection.hpp"
#include "myirc/sasl_mechanism.hpp"
#include <string>
#include <unordered_set>
#include <span>
namespace myirc {
struct IrcMsg;
enum class Casemap
{
Rfc1459,
Rfc1459_Strict,
Ascii,
};
struct Chat {
std::span<const irctag> tags;
bool is_notice;
char status_msg;
std::string_view source;
std::string_view target;
std::string_view message;
};
/**
* @brief Thread to track this connection's identity, and IRC state.
*
*/
class Client
{
std::shared_ptr<Connection> connection_;
std::string nickname_;
std::string mode_;
std::unordered_set<std::string> channels_;
// RPL_ISUPPORT state
std::unordered_map<std::string, std::string> isupport_;
std::unique_ptr<SaslMechanism> sasl_mechanism_;
Casemap casemap_;
std::string channel_prefix_;
std::string status_msg_;
std::unordered_map<std::string, std::string> caps_available_;
std::unordered_set<std::string> caps_;
// AUTHENTICATE support
std::string authenticate_buffer_;
auto on_authenticate(std::string_view) -> void;
auto on_authenticate_chunk(std::string_view) -> void;
auto on_welcome(const IrcMsg &irc) -> void;
auto on_isupport(const IrcMsg &irc) -> void;
auto on_nick(const IrcMsg &irc) -> void;
auto on_umodeis(const IrcMsg &irc) -> void;
auto on_join(const IrcMsg &irc) -> void;
auto on_kick(const IrcMsg &irc) -> void;
auto on_part(const IrcMsg &irc) -> void;
auto on_mode(const IrcMsg &irc) -> void;
auto on_cap(const IrcMsg &irc) -> void;
auto on_registered() -> void;
auto on_chat(bool, const IrcMsg &irc, bool flush) -> void;
public:
boost::signals2::signal<void()> sig_registered;
boost::signals2::signal<void(const std::unordered_map<std::string, std::string> &)> sig_cap_ls;
boost::signals2::signal<void(const Chat &, bool flush)> sig_chat;
boost::signals2::signal<void(SnoteMatch &, bool flush)> sig_snote;
Client(std::shared_ptr<Connection> connection)
: connection_{std::move(connection)}
, casemap_{Casemap::Rfc1459}
, channel_prefix_{"#&"}
, status_msg_{"+@"}
{
}
auto get_connection() -> Connection & { return *connection_; }
static auto start(std::shared_ptr<Connection>) -> std::shared_ptr<Client>;
auto start_sasl(std::unique_ptr<SaslMechanism> mechanism) -> void;
auto get_connection() const -> std::shared_ptr<Connection>
{
return connection_->shared_from_this();
}
auto get_my_nick() const -> const std::string &;
auto get_my_mode() const -> const std::string &;
auto get_my_channels() const -> const std::unordered_set<std::string> &;
auto list_caps() -> void;
auto is_my_nick(std::string_view nick) const -> bool;
auto is_my_mask(std::string_view mask) const -> bool;
auto is_channel(std::string_view name) const -> bool;
auto is_on_channel(std::string_view name) const -> bool;
auto casemap(std::string_view) const -> std::string;
auto casemap_compare(std::string_view, std::string_view) const -> int;
auto shutdown() -> void;
auto send_ping(std::string_view) -> void;
auto send_pong(std::string_view) -> void;
auto send_pass(std::string_view) -> void;
auto send_user(std::string_view, std::string_view) -> void;
auto send_nick(std::string_view) -> void;
auto send_join(std::string_view) -> void;
auto send_names(std::string_view channel) -> void;
auto send_kick(std::string_view, std::string_view, std::string_view) -> void;
auto send_kill(std::string_view, std::string_view) -> void;
auto send_quit(std::string_view) -> void;
auto send_cap_ls() -> void;
auto send_cap_end() -> void;
auto send_map() -> void;
auto send_testline(std::string_view) -> void;
auto send_testmask(std::string_view) -> void;
auto send_testmask_gecos(std::string_view, std::string_view) -> void;
auto send_masktrace(std::string_view) -> void;
auto send_masktrace_gecos(std::string_view, std::string_view) -> void;
auto send_get_topic(std::string_view) -> void;
auto send_set_topic(std::string_view, std::string_view) -> void;
auto send_cap_req(std::string_view) -> void;
auto send_privmsg(std::string_view, std::string_view) -> void;
auto send_wallops(std::string_view) -> void;
auto send_notice(std::string_view, std::string_view) -> void;
auto send_authenticate(std::string_view message) -> void;
auto send_authenticate_encoded(std::string_view message) -> void;
auto send_authenticate_abort() -> void;
auto send_whois(std::string_view) -> void;
auto send_whois_remote(std::string_view, std::string_view) -> void;
auto send_challenge(std::string_view) -> void;
auto send_oper(std::string_view, std::string_view) -> void;
};
} // namespace myirc

View File

@@ -0,0 +1,107 @@
#pragma once
#include "irc_command.hpp"
#include "ircmsg.hpp"
#include "ratelimit.hpp"
#include "ref.hpp"
#include "stream.hpp"
#include <boost/asio.hpp>
#include <boost/signals2.hpp>
#include <list>
#include <memory>
#include <string>
namespace myirc {
struct SnoteMatch;
class Connection : public std::enable_shared_from_this<Connection>
{
public:
struct Settings
{
bool tls;
std::string host;
std::uint16_t port;
Ref<X509> client_cert;
Ref<EVP_PKEY> client_key;
std::string verify;
std::string sni;
std::string socks_host;
std::uint16_t socks_port;
std::string socks_user;
std::string socks_pass;
};
private:
Stream stream_;
boost::asio::steady_timer watchdog_timer_;
std::list<std::string> write_strings_;
bool write_posted_;
// Set true when watchdog triggers.
// Set false when message received.
bool stalled_;
/// write buffers after consulting with rate limit
auto write_buffers() -> void;
/// write a specific number of messages now
auto write_buffers(size_t) -> void;
auto dispatch_line(char *line, bool) -> void;
static constexpr std::chrono::seconds watchdog_duration = std::chrono::seconds{30};
auto watchdog() -> void;
auto watchdog_activity() -> void;
auto connect(Settings settings) -> boost::asio::awaitable<void>;
public:
boost::signals2::signal<void(
boost::asio::ip::tcp::endpoint,
std::optional<boost::asio::ip::tcp::endpoint>,
std::string
)> sig_connect;
boost::signals2::signal<void(std::exception_ptr)> sig_disconnect;
boost::signals2::signal<void(IrcCommand, const IrcMsg &, bool flush)> sig_ircmsg;
std::unique_ptr<RateLimit> rate_limit;
Connection(boost::asio::io_context &io);
auto get_executor() -> boost::asio::any_io_executor
{
return stream_.get_executor();
}
auto start(Settings) -> void;
auto close() -> void;
/// Build and send well-formed IRC message from individual parameters
auto write_irc(std::string) -> void;
auto write_irc(std::string, std::string_view) -> void;
template <typename... Args>
auto write_irc(std::string front, std::string_view next, Args... rest) -> void;
};
template <typename... Args>
auto Connection::write_irc(std::string front, std::string_view next, Args... rest) -> void
{
using namespace std::literals;
if (next.empty() || next.front() == ':' || next.find_first_of("\r\n \0"sv) != next.npos)
{
throw std::runtime_error{"bad irc argument"};
}
front += " ";
front += next;
write_irc(std::move(front), rest...);
}
} // namespace myirc

View File

@@ -1,11 +1,6 @@
#pragma once
#include "event.hpp"
#include <string_view>
class Connection;
namespace myirc {
enum class IrcCommand
{
@@ -18,7 +13,6 @@ enum class IrcCommand
RPL_SNOMASK,
RPL_REDIR,
RPL_MAP,
RPL_MAPMORE,
RPL_MAPEND,
RPL_SAVENICK,
RPL_TRACELINK,
@@ -33,7 +27,6 @@ enum class IrcCommand
RPL_STATSLINKINFO,
RPL_STATSCOMMANDS,
RPL_STATSCLINE,
RPL_STATSNLINE,
RPL_STATSILINE,
RPL_STATSKLINE,
RPL_STATSQLINE,
@@ -41,15 +34,11 @@ enum class IrcCommand
RPL_ENDOFSTATS,
RPL_STATSPLINE,
RPL_UMODEIS,
RPL_STATSFLINE,
RPL_STATSDLINE,
RPL_SERVLIST,
RPL_SERVLISTEND,
RPL_STATSLLINE,
RPL_STATSUPTIME,
RPL_STATSOLINE,
RPL_STATSHLINE,
RPL_STATSSLINE,
RPL_STATSXLINE,
RPL_STATSULINE,
RPL_STATSDEBUG,
@@ -63,7 +52,6 @@ enum class IrcCommand
RPL_ADMINLOC1,
RPL_ADMINLOC2,
RPL_ADMINEMAIL,
RPL_TRACELOG,
RPL_ENDOFTRACE,
RPL_LOAD2HI,
RPL_LOCALUSERS,
@@ -72,11 +60,9 @@ enum class IrcCommand
RPL_WHOISCERTFP,
RPL_ACCEPTLIST,
RPL_ENDOFACCEPT,
RPL_NONE,
RPL_AWAY,
RPL_USERHOST,
RPL_ISON,
RPL_TEXT,
RPL_UNAWAY,
RPL_NOWAWAY,
RPL_WHOISHELPOP,
@@ -85,7 +71,6 @@ enum class IrcCommand
RPL_WHOISOPERATOR,
RPL_WHOWASUSER,
RPL_ENDOFWHOWAS,
RPL_WHOISCHANOP,
RPL_WHOISIDLE,
RPL_ENDOFWHOIS,
RPL_WHOISCHANNELS,
@@ -101,10 +86,8 @@ enum class IrcCommand
RPL_NOTOPIC,
RPL_TOPIC,
RPL_TOPICWHOTIME,
RPL_WHOISTEXT,
RPL_WHOISACTUALLY,
RPL_INVITING,
RPL_SUMMONING,
RPL_INVITELIST,
RPL_ENDOFINVITELIST,
RPL_EXCEPTLIST,
@@ -116,7 +99,6 @@ enum class IrcCommand
RPL_NAMREPLY,
RPL_WHOWASREAL,
RPL_ENDOFNAMES,
RPL_KILLDONE,
RPL_CLOSING,
RPL_CLOSEEND,
RPL_LINKS,
@@ -125,21 +107,14 @@ enum class IrcCommand
RPL_ENDOFBANLIST,
RPL_INFO,
RPL_MOTD,
RPL_INFOSTART,
RPL_ENDOFINFO,
RPL_MOTDSTART,
RPL_ENDOFMOTD,
RPL_WHOISHOST,
RPL_YOUREOPER,
RPL_REHASHING,
RPL_MYPORTIS,
RPL_NOTOPERANYMORE,
RPL_RSACHALLENGE,
RPL_TIME,
RPL_USERSSTART,
RPL_USERS,
RPL_ENDOFUSERS,
RPL_NOUSERS,
RPL_HOSTHIDDEN,
ERR_NOSUCHNICK,
ERR_NOSUCHSERVER,
@@ -158,8 +133,6 @@ enum class IrcCommand
ERR_TOOMANYMATCHES,
ERR_UNKNOWNCOMMAND,
ERR_NOMOTD,
ERR_NOADMININFO,
ERR_FILEERROR,
ERR_NONICKNAMEGIVEN,
ERR_ERRONEUSNICKNAME,
ERR_NICKNAMEINUSE,
@@ -171,27 +144,20 @@ enum class IrcCommand
ERR_USERNOTINCHANNEL,
ERR_NOTONCHANNEL,
ERR_USERONCHANNEL,
ERR_NOLOGIN,
ERR_SUMMONDISABLED,
ERR_USERSDISABLED,
ERR_NOTREGISTERED,
ERR_ACCEPTFULL,
ERR_ACCEPTEXIST,
ERR_ACCEPTNOT,
ERR_NEEDMOREPARAMS,
ERR_ALREADYREGISTRED,
ERR_NOPERMFORHOST,
ERR_PASSWDMISMATCH,
ERR_YOUREBANNEDCREEP,
ERR_YOUWILLBEBANNED,
ERR_KEYSET,
ERR_LINKCHANNEL,
ERR_CHANNELISFULL,
ERR_UNKNOWNMODE,
ERR_INVITEONLYCHAN,
ERR_BANNEDFROMCHAN,
ERR_BADCHANNELKEY,
ERR_BADCHANMASK,
ERR_NEEDREGGEDNICK,
ERR_BANLISTFULL,
ERR_BADCHANNAME,
@@ -200,7 +166,6 @@ enum class IrcCommand
ERR_CHANOPRIVSNEEDED,
ERR_CANTKILLSERVER,
ERR_ISCHANSERVICE,
ERR_BANNEDNICK,
ERR_NONONREG,
ERR_VOICENEEDED,
ERR_NOOPERHOST,
@@ -208,7 +173,6 @@ enum class IrcCommand
ERR_OWNMODE,
ERR_UMODEUNKNOWNFLAG,
ERR_USERSDONTMATCH,
ERR_GHOSTEDCLIENT,
ERR_USERNOTONSERV,
ERR_WRONGPONG,
ERR_DISABLED,
@@ -237,10 +201,9 @@ enum class IrcCommand
RPL_OMOTD,
RPL_ENDOFOMOTD,
ERR_NOPRIVS,
RPL_TESTMASK,
RPL_TESTLINE,
RPL_NOTESTLINE,
RPL_TESTMASKGECO,
RPL_TESTMASKGECOS,
RPL_QUIETLIST,
RPL_ENDOFQUIETLIS,
RPL_MONONLINE,
@@ -252,7 +215,6 @@ enum class IrcCommand
RPL_ENDOFRSACHALLENGE2,
ERR_MLOCKRESTRICTE,
ERR_INVALIDBAN,
ERR_TOPICLOCK,
RPL_SCANMATCHED,
RPL_SCANUMODES,
RPL_LOGGEDIN,
@@ -272,28 +234,22 @@ enum class IrcCommand
CAP,
CHGHOST,
ERROR,
INVITE,
JOIN,
KICK,
KILL,
MODE,
NICK,
NOTICE,
PART,
PING,
PONG,
PRIVMSG,
QUIT,
SETNAME,
TAGMSG,
TOPIC,
WALLOPS,
};
struct IrcMsgEvent : Event
{
IrcMsgEvent(IrcCommand command, IrcMsg const& irc)
: command{command}, irc{irc} {}
IrcCommand command;
IrcMsg const& irc;
};
struct IrcParseThread
{
static auto start(Connection& connection) -> void;
};
} // namespace myirc

View File

@@ -0,0 +1,282 @@
#pragma once
#include "myirc/connection.hpp"
#include "myirc/snote.hpp"
#include <chrono>
#include <coroutine>
#include <initializer_list>
#include <utility>
#include <vector>
namespace myirc {
struct irc_promise;
/// A coroutine that can co_await on various IRC events
struct irc_coroutine : std::coroutine_handle<irc_promise>
{
using promise_type = irc_promise;
/// Start the coroutine and associate it with a specific connection.
auto start(Connection &connection) -> void;
/// Returns true when this coroutine is still waiting on events
auto is_running() -> bool;
/// Returns the exception that terminated this coroutine, if there is one.
auto exception() -> std::exception_ptr;
};
struct irc_promise
{
// Pointer to the connection while running. Cleared on termination.
std::shared_ptr<Connection> connection_;
// Pointer to exception that terminated this coroutine if there is one.
std::exception_ptr exception_;
auto get_return_object() -> irc_coroutine
{
return {irc_coroutine::from_promise(*this)};
}
// Suspend waiting for start() to initialize connection_
auto initial_suspend() noexcept -> std::suspend_always { return {}; }
// Suspend so that is_running() and exception() work
auto final_suspend() noexcept -> std::suspend_always { return {}; }
// Normal termination
auto return_void() -> void
{
connection_.reset();
}
// Abnormal termination - remember the exception
auto unhandled_exception() -> void
{
connection_.reset();
exception_ = std::current_exception();
}
};
template <typename... Ts>
class Wait;
/// Argument to a Wait that expects one or more IRC messages
class wait_ircmsg
{
// Vector of commands this wait is expecting. Leave empty to accept all messages.
std::vector<IrcCommand> want_cmds_;
// Slot for the ircmsg event
boost::signals2::scoped_connection ircmsg_slot_;
public:
using result_type = std::pair<IrcCommand, const IrcMsg &>;
wait_ircmsg(std::initializer_list<IrcCommand> want_cmds)
: want_cmds_{want_cmds}
{
}
template <size_t I, typename... Ts>
auto start(Wait<Ts...> &command) -> void;
auto stop() -> void { ircmsg_slot_.disconnect(); }
};
class wait_snote
{
// Vector of tags this wait is expecting. Leave empty to accept all messages.
std::vector<SnoteTag> want_tags_;
// Slot for the snote event
boost::signals2::scoped_connection snote_slot_;
public:
using result_type = SnoteMatch;
wait_snote(std::initializer_list<SnoteTag> want_tags)
: want_tags_{want_tags}
{
}
template <size_t I, typename... Ts>
auto start(Wait<Ts...> &command) -> void;
auto stop() -> void { snote_slot_.disconnect(); }
};
class wait_timeout
{
std::optional<boost::asio::steady_timer> timer_;
std::chrono::milliseconds timeout_;
public:
struct result_type
{
};
wait_timeout(std::chrono::milliseconds timeout)
: timeout_{timeout}
{
}
template <size_t I, typename... Ts>
auto start(Wait<Ts...> &command) -> void;
auto stop() -> void { timer_->cancel(); }
};
template <typename... Ts>
class Wait
{
// State associated with each wait mode
std::tuple<Ts...> modes_;
// Result from any one of the wait modes
std::optional<std::variant<typename Ts::result_type...>> result_;
// Handle of the continuation to be resumed when one of the wait
// modes is ready.
std::coroutine_handle<irc_promise> handle_;
// Slot for tearing down the irc_coroutine in case the connection
// fails before any wait modes complete.
boost::signals2::scoped_connection disconnect_slot_;
template <size_t I>
auto start_mode() -> void
{
std::get<I>(modes_).template start<I, Ts...>(*this);
}
template <std::size_t... Indices>
auto start_modes(std::index_sequence<Indices...>) -> void
{
(start_mode<Indices>(), ...);
}
template <std::size_t... Indices>
auto stop_modes(std::index_sequence<Indices...>) -> void
{
(std::get<Indices>(modes_).stop(), ...);
}
public:
Wait(Ts &&...modes)
: modes_{std::forward<Ts>(modes)...}
{
}
// Get the connection that this coroutine was started with.
auto get_connection() const -> Connection &
{
return *handle_.promise().connection_;
}
// Store a successful result and resume the coroutine
template <size_t I, typename... Args>
auto complete(Args &&...args) -> void
{
result_.emplace(std::in_place_index<I>, std::forward<Args>(args)...);
handle_.resume();
}
// The coroutine always needs to wait for a message. It will never
// be ready immediately.
auto await_ready() noexcept -> bool { return false; }
/// Install event handles in the connection that will resume this coroutine.
auto await_suspend(std::coroutine_handle<irc_promise> handle) -> void;
auto await_resume() -> std::variant<typename Ts::result_type...>;
};
template <size_t I, typename... Ts>
auto wait_ircmsg::start(Wait<Ts...> &command) -> void
{
ircmsg_slot_ = command.get_connection().sig_ircmsg.connect([this, &command](auto cmd, auto &msg) {
const auto wanted = want_cmds_.empty() || std::find(want_cmds_.begin(), want_cmds_.end(), cmd) != want_cmds_.end();
if (wanted)
{
command.template complete<I>(cmd, msg);
}
});
}
template <size_t I, typename... Ts>
auto wait_snote::start(Wait<Ts...> &command) -> void
{
snote_slot_ = command.get_connection().sig_snote.connect([this, &command](auto &match) {
const auto tag = match.get_tag();
const auto wanted = want_tags_.empty() || std::find(want_tags_.begin(), want_tags_.end(), tag) != want_tags_.end();
if (wanted)
{
command.template complete<I>(match);
}
});
}
template <size_t I, typename... Ts>
auto wait_timeout::start(Wait<Ts...> &command) -> void
{
timer_.emplace(command.get_connection().get_executor());
timer_->expires_after(timeout_);
timer_->async_wait([this, &command](const auto &error) {
if (not error)
{
timer_.reset();
command.template complete<I>();
}
});
}
template <typename... Ts>
auto Wait<Ts...>::await_suspend(std::coroutine_handle<irc_promise> handle) -> void
{
handle_ = handle;
const auto tuple_size = std::tuple_size_v<decltype(modes_)>;
start_modes(std::make_index_sequence<tuple_size>{});
disconnect_slot_ = get_connection().sig_disconnect.connect([this](auto) {
handle_.resume();
});
}
template <typename... Ts>
auto Wait<Ts...>::await_resume() -> std::variant<typename Ts::result_type...>
{
const auto tuple_size = std::tuple_size_v<decltype(modes_)>;
stop_modes(std::make_index_sequence<tuple_size>{});
disconnect_slot_.disconnect();
if (result_)
{
return std::move(*result_);
}
else
{
throw std::runtime_error{"connection terminated"};
}
}
/// Start the coroutine and associate it with a specific connection.
inline auto irc_coroutine::start(Connection &connection) -> void
{
promise().connection_ = connection.shared_from_this();
resume();
}
/// Returns true when this coroutine is still waiting on events
inline auto irc_coroutine::is_running() -> bool
{
return promise().connection_ != nullptr;
}
inline auto irc_coroutine::exception() -> std::exception_ptr
{
return promise().exception_;
}
} // namespace myirc

View File

@@ -0,0 +1,78 @@
#pragma once
#include <iostream>
#include <string_view>
#include <vector>
namespace myirc {
struct irctag
{
std::string_view key;
std::string_view val;
irctag(std::string_view key, std::string_view val)
: key{key}
, val{val}
{
}
friend auto operator==(const irctag &, const irctag &) -> bool = default;
};
struct IrcMsg
{
std::vector<irctag> tags;
std::vector<std::string_view> args;
std::string_view source;
std::string_view command;
IrcMsg() = default;
IrcMsg(
std::vector<irctag> &&tags,
std::string_view source,
std::string_view command,
std::vector<std::string_view> &&args
)
: tags(std::move(tags))
, args(std::move(args))
, source{source}
, command{command}
{
}
bool hassource() const;
friend bool operator==(const IrcMsg &, const IrcMsg &) = default;
};
enum class irc_error_code
{
MISSING_TAG,
MISSING_COMMAND,
};
auto operator<<(std::ostream &out, irc_error_code) -> std::ostream &;
struct irc_parse_error : public std::exception
{
irc_error_code code;
irc_parse_error(irc_error_code code)
: code(code)
{
}
};
/**
* Parses the given IRC message into a structured format.
* The original message is mangled to store string fragments
* that are pointed to by the structured message type.
*
* Returns zero for success, non-zero for parse error.
*/
auto parse_irc_message(char *msg) -> IrcMsg;
auto parse_irc_tags(char *msg) -> std::vector<irctag>;
} // namespace myirc

View File

@@ -0,0 +1,104 @@
#pragma once
/**
* @file linebuffer.hpp
* @author Eric Mertens <emertens@gmail.com>
* @brief A line buffering class
* @version 0.1
* @date 2023-08-22
*
* @copyright Copyright (c) 2023
*
*/
#include <boost/asio/buffer.hpp>
#include <vector>
namespace myirc {
/**
* @brief Fixed-size buffer with line-oriented dispatch
*
*/
class LineBuffer
{
std::vector<char> buffer_;
// [std::begin(buffer), end_) contains buffered data
// [end_, std::end(buffer)) is available buffer space
decltype(buffer_)::iterator start_;
decltype(buffer_)::iterator search_;
decltype(buffer_)::iterator end_;
public:
/**
* @brief Construct a new Line Buffer object
*
* @param n Buffer size
*/
LineBuffer(std::size_t n)
: buffer_(n)
, start_{buffer_.begin()}
, search_{buffer_.begin()}
, end_{buffer_.begin()}
{
}
// can't copy the iterator member safely
LineBuffer(LineBuffer const&) = delete;
LineBuffer(LineBuffer&&) = delete;
auto operator=(LineBuffer const&) -> LineBuffer& = delete;
auto operator=(LineBuffer&&) -> LineBuffer& = delete;
/**
* @brief Get the available buffer space
*
* @return boost::asio::mutable_buffer
*/
auto prepare() -> boost::asio::mutable_buffer
{
return boost::asio::buffer(&*end_, std::distance(end_, buffer_.end()));
}
/**
* @brief Commit new buffer bytes and dispatch line callback
*
* The first n bytes of the buffer will be considered to be
* populated. The line callback function will be called once
* per completed line. Those lines are removed from the buffer
* and the is ready for additional calls to prepare and
* commit.
*
* @param n Bytes written to the last call of prepare
* @param line_cb Callback function to run on each completed line
*/
auto commit(std::size_t const n) -> void
{
std::advance(end_, n);
}
/**
* @brief Return the next null-terminated line in the buffer
*
* This function should be repeatedly called until it returns
* nullptr. After that shift can be used to reclaim the
* previously used buffer.
*
* @return null-terminated line or nullptr if no line is ready
*/
auto next_line() -> char*;
/**
* @brief Return the next non-empty line if there is one.
*/
auto next_nonempty_line() -> char*;
/**
* @brief Reclaim used buffer space invalidating all previous
* next_line() results;
*
*/
auto shift() -> void;
};
} // namespace

View File

@@ -0,0 +1,13 @@
#pragma once
#include "ref.hpp"
#include <string_view>
namespace myirc {
auto log_openssl_errors(const std::string_view prefix) -> void;
auto key_from_file(const std::string &filename, const std::string_view password) -> Ref<EVP_PKEY>;
auto cert_from_file(const std::string &filename) -> Ref<X509>;
} // namespace myirc

View File

@@ -0,0 +1,24 @@
#pragma once
#include <chrono>
#include <utility>
namespace myirc {
struct RateLimit {
virtual ~RateLimit();
auto virtual query(size_t want_to_send) -> std::pair<std::chrono::milliseconds, size_t> = 0;
};
struct Rfc1459RateLimit final : RateLimit
{
using clock = std::chrono::steady_clock;
std::chrono::milliseconds cost_ {2'000};
std::chrono::milliseconds allowance_ {10'000};
clock::time_point horizon_{};
auto query(size_t want_to_send) -> std::pair<std::chrono::milliseconds, size_t> override;
};
} // namespace myirc

View File

@@ -0,0 +1,68 @@
#pragma once
#include <openssl/evp.h>
#include <openssl/x509.h>
#include <memory>
namespace myirc {
// Specializations must Free to release a reference
// Specializations can implement UpRef to increase a reference count on copy
template <typename> struct RefTraits {};
template <> struct RefTraits<EVP_PKEY> {
static constexpr void (*Free)(EVP_PKEY*) = EVP_PKEY_free;
static constexpr int (*UpRef)(EVP_PKEY*) = EVP_PKEY_up_ref;
};
template <> struct RefTraits<X509> {
static constexpr void (*Free)(X509*) = X509_free;
static constexpr int (*UpRef)(X509*) = X509_up_ref;
};
template <> struct RefTraits<EVP_PKEY_CTX> {
static constexpr void (*Free)(EVP_PKEY_CTX*) = EVP_PKEY_CTX_free;
// this type does not implement UpRef
};
template <typename T>
struct RefDeleter {
auto operator()(T *ptr) const -> void { RefTraits<T>::Free(ptr); }
};
template <typename T>
struct Ref : std::unique_ptr<T, RefDeleter<T>>
{
using base = std::unique_ptr<T, RefDeleter<T>>;
/// Owns nothing
Ref() noexcept = default;
/// Takes ownership of the pointer
explicit Ref(T *x) noexcept : base{x} {}
/// Takes ownership of the pointer
static auto borrow(T *x) -> Ref {
RefTraits<T>::UpRef(x);
return Ref{x};
}
Ref(Ref &&ref) noexcept = default;
Ref(const Ref &ref) noexcept : base{ref.get()} {
if (*this) {
RefTraits<T>::UpRef(this->get());
}
}
Ref &operator=(Ref&&) noexcept = default;
Ref &operator=(const Ref &ref) noexcept {
if (ref) {
RefTraits<T>::UpRef(ref.get());
}
this->reset(ref.get());
return *this;
}
};
} // namespace myirc

View File

@@ -0,0 +1,50 @@
#pragma once
#include "connection.hpp"
#include "client.hpp"
#include <memory>
#include <string>
#include <unordered_map>
namespace myirc {
class Registration : public std::enable_shared_from_this<Registration>
{
public:
struct Settings {
std::string nickname;
std::string username;
std::string realname;
std::string password;
std::unique_ptr<SaslMechanism> sasl_mechanism;
};
private:
Settings settings_;
std::shared_ptr<Client> client_;
boost::signals2::scoped_connection slot_;
boost::signals2::scoped_connection caps_slot_;
auto on_connect() -> void;
auto on_cap_list(const std::unordered_map<std::string, std::string> &) -> void;
auto on_ircmsg(IrcCommand, const IrcMsg &msg) -> void;
auto send_req() -> void;
auto randomize_nick() -> void;
public:
Registration(
Settings,
std::shared_ptr<Client>
);
static auto start(
Settings,
std::shared_ptr<Client>
) -> std::shared_ptr<Registration>;
};
} // namespace myirc

View File

@@ -0,0 +1,114 @@
#pragma once
#include "ref.hpp"
#include <boost/signals2.hpp>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <variant>
namespace myirc {
class SaslMechanism
{
public:
struct NoReply {};
struct Failure {};
using StepResult = std::variant<std::string, NoReply, Failure>;
virtual ~SaslMechanism() {}
virtual auto mechanism_name() const -> std::string = 0;
virtual auto step(std::string_view msg) -> StepResult = 0;
virtual auto is_complete() const -> bool = 0;
};
class SaslPlain final : public SaslMechanism
{
std::string authcid_;
std::string authzid_;
std::string password_;
bool complete_;
public:
SaslPlain(std::string authcid, std::string authzid, std::string password)
: authcid_{std::move(authcid)}
, authzid_{std::move(authzid)}
, password_{std::move(password)}
, complete_{false}
{}
auto mechanism_name() const -> std::string override
{
return "PLAIN";
}
auto step(std::string_view msg) -> StepResult override;
auto is_complete() const -> bool override
{
return complete_;
}
};
class SaslExternal final : public SaslMechanism
{
std::string authzid_;
bool complete_;
public:
SaslExternal(std::string authzid)
: authzid_{std::move(authzid)}
, complete_{false}
{}
auto mechanism_name() const -> std::string override
{
return "EXTERNAL";
}
auto step(std::string_view msg) -> StepResult override;
auto is_complete() const -> bool override
{
return complete_;
}
};
class SaslEcdsa final : public SaslMechanism
{
std::string message1_;
Ref<EVP_PKEY> key_;
int stage_;
public:
SaslEcdsa(std::string authcid, std::string authzid, Ref<EVP_PKEY> key)
: message1_{std::move(authcid)}
, key_{std::move(key)}
, stage_{0}
{
if (not authzid.empty()) {
message1_.push_back(0);
message1_.append(authzid);
}
}
auto mechanism_name() const -> std::string override
{
return "ECDSA-NIST256P-CHALLENGE";
}
auto step(std::string_view msg) -> StepResult override;
auto is_complete() const -> bool override
{
return stage_ == 2;;
}
};
} // namespace myirc

View File

@@ -0,0 +1,101 @@
#pragma once
#include "ircmsg.hpp"
#include <memory>
#include <optional>
#include <regex>
#include <string_view>
#include <utility>
#include <variant>
struct hs_database;
struct hs_scratch;
namespace myirc {
enum class SnoteTag
{
ClientConnecting,
ClientExiting,
CreateChannel,
DisconnectingKlined,
DroppedChannel,
DroppedNick,
DroppedNickRename,
DroppedAccount,
FailedChallenge,
FailedChallengeFingerprintMismatch,
FailedChallengeHostMismatch,
FailedChallengeMissingSecure,
FailedChallengeNoBlock,
FailedChallengeTls,
Freeze,
IsNowOper,
IsNowOperGlobal,
JoinedJuped,
Killed,
KilledRemote,
KilledRemoteOper,
LoginAttempts,
NewPropagatedKline,
NewTemporaryKline,
NickChange,
NickCollision,
NickCollisionServices,
OperspyWhois,
OperspyWho,
PossibleFlooder,
PropagatedBanExpired,
RejectingKlined,
SaveMessage,
SetVhostOnMarkedAccount,
SighupReloadingConf,
Spambot,
TemporaryDline,
TemporaryKlineExpired,
TooManyGlobalConnections,
TooManyUserConnections,
};
class SnoteMatch
{
SnoteTag tag_;
std::variant<std::pair<const std::regex &, std::string_view>, std::match_results<std::string_view::const_iterator>> components_;
public:
SnoteMatch(SnoteTag tag, const std::regex &regex, std::string_view full)
: tag_{tag}
, components_{std::make_pair(std::ref(regex), full)}
{
}
auto get_tag() -> SnoteTag { return tag_; }
auto get_results() -> const std::match_results<std::string_view::const_iterator> &;
};
struct SnoteCore
{
struct DbDeleter
{
auto operator()(hs_database *db) const -> void;
};
struct ScratchDeleter
{
auto operator()(hs_scratch *scratch) const -> void;
};
/// @brief Database of server notice patterns
std::unique_ptr<hs_database, DbDeleter> db_;
/// @brief HyperScan scratch space
std::unique_ptr<hs_scratch, ScratchDeleter> scratch_;
SnoteCore();
auto match(const IrcMsg &msg) -> std::optional<SnoteMatch>;
};
extern SnoteCore snoteCore;
} // namespace myirc

View File

@@ -0,0 +1,118 @@
#pragma once
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <cstddef>
#include <variant>
namespace myirc {
/// @brief Abstraction over plain-text and TLS streams.
class Stream : private
std::variant<
boost::asio::ip::tcp::socket,
boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>
{
public:
using tcp_socket = boost::asio::ip::tcp::socket;
using tls_stream = boost::asio::ssl::stream<tcp_socket>;
/// @brief The type of the executor associated with the stream.
using executor_type = boost::asio::any_io_executor;
/// @brief Type of the lowest layer of this stream
using lowest_layer_type = tcp_socket::lowest_layer_type;
private:
using base_type = std::variant<tcp_socket, tls_stream>;
auto base() -> base_type& { return *this; }
auto base() const -> base_type const& { return *this; }
public:
/// @brief Initialize stream with a plain TCP socket
/// @param ioc IO context of stream
template <typename T>
Stream(T&& executor) : base_type{std::in_place_type<tcp_socket>, std::forward<T>(executor)} {}
/// @brief Reset stream to a plain TCP socket
/// @return Reference to internal socket object
auto reset() -> tcp_socket&
{
return base().emplace<tcp_socket>(get_executor());
}
/// @brief Upgrade a plain TCP socket into a TLS stream.
/// @param ctx TLS context used for handshake
/// @return Reference to internal stream object
auto upgrade(boost::asio::ssl::context& ctx) -> tls_stream&
{
auto socket = std::move(std::get<tcp_socket>(base()));
return base().emplace<tls_stream>(std::move(socket), ctx);
}
/// @brief Get underlying basic socket
/// @return Reference to underlying socket
auto lowest_layer() -> lowest_layer_type&
{
return std::visit([](auto&& x) -> decltype(auto) { return x.lowest_layer(); }, base());
}
/// @brief Get underlying basic socket
/// @return Reference to underlying socket
auto lowest_layer() const -> lowest_layer_type const&
{
return std::visit([](auto&& x) -> decltype(auto) { return x.lowest_layer(); }, base());
}
/// @brief Get the executor associated with this stream.
/// @return The executor associated with the stream.
auto get_executor() -> executor_type const&
{
return lowest_layer().get_executor();
}
/// @brief Initiates an asynchronous read operation.
/// @tparam MutableBufferSequence Type of the buffer sequence.
/// @tparam Token Type of the completion token.
/// @param buffers The buffer sequence into which data will be read.
/// @param token The completion token for the read operation.
/// @return The result determined by the completion token.
template <
typename MutableBufferSequence,
boost::asio::completion_token_for<void(boost::system::error_code, std::size_t)> Token>
auto async_read_some(MutableBufferSequence&& buffers, Token&& token) -> decltype(auto)
{
return std::visit([&buffers, &token](auto&& x) -> decltype(auto) {
return x.async_read_some(std::forward<MutableBufferSequence>(buffers), std::forward<Token>(token));
}, base());
}
/// @brief Initiates an asynchronous write operation.
/// @tparam ConstBufferSequence Type of the buffer sequence.
/// @tparam Token Type of the completion token.
/// @param buffers The buffer sequence from which data will be written.
/// @param token The completion token for the write operation.
/// @return The result determined by the completion token.
template <
typename ConstBufferSequence,
boost::asio::completion_token_for<void(boost::system::error_code, std::size_t)> Token>
auto async_write_some(ConstBufferSequence&& buffers, Token&& token) -> decltype(auto)
{
return std::visit([&buffers, &token](auto&& x) -> decltype(auto) {
return x.async_write_some(std::forward<ConstBufferSequence>(buffers), std::forward<Token>(token));
}, base());
}
/// @brief Tear down the network stream
auto close() -> void
{
boost::system::error_code err;
auto& socket = lowest_layer();
socket.shutdown(socket.shutdown_both, err);
socket.lowest_layer().close(err);
}
};
} // namespace myirc

252
myirc/irc_commands.gperf Normal file
View File

@@ -0,0 +1,252 @@
struct RecognizedCommand {
char const* text;
IrcCommand command;
std::size_t min_args;
std::size_t max_args;
};
%%
001, IrcCommand::RPL_WELCOME, 2, 2
002, IrcCommand::RPL_YOURHOST, 2, 2
003, IrcCommand::RPL_CREATED, 2, 2
004, IrcCommand::RPL_MYINFO, 5, 6
005, IrcCommand::RPL_ISUPPORT, 2, 15
008, IrcCommand::RPL_SNOMASK, 3, 3
010, IrcCommand::RPL_REDIR, 4, 4
015, IrcCommand::RPL_MAP, 2, 2
017, IrcCommand::RPL_MAPEND, 2, 2
043, IrcCommand::RPL_SAVENICK, 3, 3
200, IrcCommand::RPL_TRACELINK, 5, 5
201, IrcCommand::RPL_TRACECONNECTING, 4, 4
202, IrcCommand::RPL_TRACEHANDSHAKE, 4, 4
203, IrcCommand::RPL_TRACEUNKNOWN, 6, 6
204, IrcCommand::RPL_TRACEOPERATOR, 7, 7
205, IrcCommand::RPL_TRACEUSER, 7, 7
206, IrcCommand::RPL_TRACESERVER, 8, 8
208, IrcCommand::RPL_TRACENEWTYPE, 4, 4
209, IrcCommand::RPL_TRACECLASS, 4, 4
211, IrcCommand::RPL_STATSLINKINFO, 8, 8
212, IrcCommand::RPL_STATSCOMMANDS, 5, 5
213, IrcCommand::RPL_STATSCLINE, 8, 8
215, IrcCommand::RPL_STATSILINE, 8, 8
216, IrcCommand::RPL_STATSKLINE, 6, 6
217, IrcCommand::RPL_STATSQLINE, 5, 5
218, IrcCommand::RPL_STATSYLINE, 10, 10
219, IrcCommand::RPL_ENDOFSTATS, 3, 3
220, IrcCommand::RPL_STATSPLINE, 6, 6
221, IrcCommand::RPL_UMODEIS, 2, 2
225, IrcCommand::RPL_STATSDLINE, 4, 4
241, IrcCommand::RPL_STATSLLINE, 7, 7
242, IrcCommand::RPL_STATSUPTIME, 2, 2
243, IrcCommand::RPL_STATSOLINE, 7, 7
244, IrcCommand::RPL_STATSHLINE, 7, 7
247, IrcCommand::RPL_STATSXLINE, 5, 5
248, IrcCommand::RPL_STATSULINE, 5, 5
249, IrcCommand::RPL_STATSDEBUG, 3, 3
250, IrcCommand::RPL_STATSCONN, 2, 2
251, IrcCommand::RPL_LUSERCLIENT, 2, 2
252, IrcCommand::RPL_LUSEROP, 3, 3
253, IrcCommand::RPL_LUSERUNKNOWN, 3, 3
254, IrcCommand::RPL_LUSERCHANNELS, 3, 3
255, IrcCommand::RPL_LUSERME, 2, 2
256, IrcCommand::RPL_ADMINME, 3, 3
257, IrcCommand::RPL_ADMINLOC1, 2, 2
258, IrcCommand::RPL_ADMINLOC2, 2, 2
259, IrcCommand::RPL_ADMINEMAIL, 2, 2
262, IrcCommand::RPL_ENDOFTRACE, 3, 3
263, IrcCommand::RPL_LOAD2HI, 3, 3
265, IrcCommand::RPL_LOCALUSERS, 4, 4
266, IrcCommand::RPL_GLOBALUSERS, 4, 4
270, IrcCommand::RPL_PRIVS, 3, 3
276, IrcCommand::RPL_WHOISCERTFP, 3, 3
281, IrcCommand::RPL_ACCEPTLIST, 1, 15
282, IrcCommand::RPL_ENDOFACCEPT, 2, 2
301, IrcCommand::RPL_AWAY, 3, 3
302, IrcCommand::RPL_USERHOST, 2, 2
303, IrcCommand::RPL_ISON, 2, 2
305, IrcCommand::RPL_UNAWAY, 2, 2
306, IrcCommand::RPL_NOWAWAY, 2, 2
310, IrcCommand::RPL_WHOISHELPOP, 3, 3
311, IrcCommand::RPL_WHOISUSER, 6, 6
312, IrcCommand::RPL_WHOISSERVER, 4, 4
313, IrcCommand::RPL_WHOISOPERATOR, 3, 3
314, IrcCommand::RPL_WHOWASUSER, 6, 6
369, IrcCommand::RPL_ENDOFWHOWAS, 3, 3
317, IrcCommand::RPL_WHOISIDLE, 5, 5
318, IrcCommand::RPL_ENDOFWHOIS, 3, 3
319, IrcCommand::RPL_WHOISCHANNELS, 3, 3
320, IrcCommand::RPL_WHOISSPECIAL, 3, 3
321, IrcCommand::RPL_LISTSTART, 3, 3
322, IrcCommand::RPL_LIST, 4, 4
323, IrcCommand::RPL_LISTEND, 2, 2
324, IrcCommand::RPL_CHANNELMODEIS, 3, 3
325, IrcCommand::RPL_CHANNELMLOCK, 4, 4
328, IrcCommand::RPL_CHANNELURL, 2, 2
329, IrcCommand::RPL_CREATIONTIME, 3, 3
330, IrcCommand::RPL_WHOISLOGGEDIN, 4, 4
331, IrcCommand::RPL_NOTOPIC, 3, 3
332, IrcCommand::RPL_TOPIC, 3, 3
333, IrcCommand::RPL_TOPICWHOTIME, 4, 4
338, IrcCommand::RPL_WHOISACTUALLY, 4, 4
341, IrcCommand::RPL_INVITING, 3, 3
346, IrcCommand::RPL_INVITELIST, 5, 5
347, IrcCommand::RPL_ENDOFINVITELIST, 3, 3
348, IrcCommand::RPL_EXCEPTLIST, 5, 5
349, IrcCommand::RPL_ENDOFEXCEPTLIST, 3, 3
351, IrcCommand::RPL_VERSION, 4, 4
352, IrcCommand::RPL_WHOREPLY, 8, 8
354, IrcCommand::RPL_WHOSPCRPL, 1, 15
315, IrcCommand::RPL_ENDOFWHO, 3, 3
353, IrcCommand::RPL_NAMREPLY, 4, 4
360, IrcCommand::RPL_WHOWASREAL, 3, 3
366, IrcCommand::RPL_ENDOFNAMES, 3, 3
362, IrcCommand::RPL_CLOSING, 3, 3
363, IrcCommand::RPL_CLOSEEND, 3, 3
364, IrcCommand::RPL_LINKS, 4, 4
365, IrcCommand::RPL_ENDOFLINKS, 3, 3
367, IrcCommand::RPL_BANLIST, 5, 5
368, IrcCommand::RPL_ENDOFBANLIST, 3, 3
371, IrcCommand::RPL_INFO, 2, 2
372, IrcCommand::RPL_MOTD, 2, 2
374, IrcCommand::RPL_ENDOFINFO, 2, 2
375, IrcCommand::RPL_MOTDSTART, 2, 2
376, IrcCommand::RPL_ENDOFMOTD, 2, 2
378, IrcCommand::RPL_WHOISHOST, 3, 3
381, IrcCommand::RPL_YOUREOPER, 2, 2
382, IrcCommand::RPL_REHASHING, 3, 3
386, IrcCommand::RPL_RSACHALLENGE, 2, 2
391, IrcCommand::RPL_TIME, 3, 3
396, IrcCommand::RPL_HOSTHIDDEN, 2, 2
401, IrcCommand::ERR_NOSUCHNICK, 3, 3
402, IrcCommand::ERR_NOSUCHSERVER, 3, 3
403, IrcCommand::ERR_NOSUCHCHANNEL, 3, 3
404, IrcCommand::ERR_CANNOTSENDTOCHAN, 3, 3
405, IrcCommand::ERR_TOOMANYCHANNELS, 3, 3
406, IrcCommand::ERR_WASNOSUCHNICK, 3, 3
407, IrcCommand::ERR_TOOMANYTARGETS, 3, 3
409, IrcCommand::ERR_NOORIGIN, 2, 2
410, IrcCommand::ERR_INVALIDCAPCMD, 3, 3
411, IrcCommand::ERR_NORECIPIENT, 2, 2
412, IrcCommand::ERR_NOTEXTTOSEND, 2, 2
413, IrcCommand::ERR_NOTOPLEVEL, 3, 3
414, IrcCommand::ERR_WILDTOPLEVEL, 3, 3
415, IrcCommand::ERR_MSGNEEDREGGEDNICK, 3, 3
416, IrcCommand::ERR_TOOMANYMATCHES, 3, 3
421, IrcCommand::ERR_UNKNOWNCOMMAND, 3, 3
422, IrcCommand::ERR_NOMOTD, 2, 2
431, IrcCommand::ERR_NONICKNAMEGIVEN, 2, 2
432, IrcCommand::ERR_ERRONEUSNICKNAME, 3, 3
433, IrcCommand::ERR_NICKNAMEINUSE, 3, 3
435, IrcCommand::ERR_BANNICKCHANGE, 3, 3
436, IrcCommand::ERR_NICKCOLLISION, 3, 3
437, IrcCommand::ERR_UNAVAILRESOURCE, 3, 3
438, IrcCommand::ERR_NICKTOOFAST, 4, 4
440, IrcCommand::ERR_SERVICESDOWN, 3, 3
441, IrcCommand::ERR_USERNOTINCHANNEL, 4, 4
442, IrcCommand::ERR_NOTONCHANNEL, 3, 3
443, IrcCommand::ERR_USERONCHANNEL, 4, 4
451, IrcCommand::ERR_NOTREGISTERED, 2, 2
456, IrcCommand::ERR_ACCEPTFULL, 2, 2
457, IrcCommand::ERR_ACCEPTEXIST, 3, 3
458, IrcCommand::ERR_ACCEPTNOT, 3, 3
461, IrcCommand::ERR_NEEDMOREPARAMS, 3, 3
462, IrcCommand::ERR_ALREADYREGISTRED, 2, 2
464, IrcCommand::ERR_PASSWDMISMATCH, 2, 2
465, IrcCommand::ERR_YOUREBANNEDCREEP, 2, 2
470, IrcCommand::ERR_LINKCHANNEL, 4, 4
471, IrcCommand::ERR_CHANNELISFULL, 3, 3
472, IrcCommand::ERR_UNKNOWNMODE, 3, 3
473, IrcCommand::ERR_INVITEONLYCHAN, 3, 3
474, IrcCommand::ERR_BANNEDFROMCHAN, 3, 3
475, IrcCommand::ERR_BADCHANNELKEY, 3, 3
477, IrcCommand::ERR_NEEDREGGEDNICK, 3, 3
478, IrcCommand::ERR_BANLISTFULL, 4, 4
479, IrcCommand::ERR_BADCHANNAME, 3, 3
480, IrcCommand::ERR_THROTTLE, 3, 3
481, IrcCommand::ERR_NOPRIVILEGES, 2, 2
482, IrcCommand::ERR_CHANOPRIVSNEEDED, 3, 3
483, IrcCommand::ERR_CANTKILLSERVER, 2, 2
484, IrcCommand::ERR_ISCHANSERVICE, 4, 4
486, IrcCommand::ERR_NONONREG, 3, 3
489, IrcCommand::ERR_VOICENEEDED, 3, 3
491, IrcCommand::ERR_NOOPERHOST, 2, 2
492, IrcCommand::ERR_CANNOTSENDTOUSER, 2, 2
494, IrcCommand::ERR_OWNMODE, 3, 3
501, IrcCommand::ERR_UMODEUNKNOWNFLAG, 2, 2
502, IrcCommand::ERR_USERSDONTMATCH, 2, 2
504, IrcCommand::ERR_USERNOTONSERV, 3, 3
513, IrcCommand::ERR_WRONGPONG, 2, 2
517, IrcCommand::ERR_DISABLED, 3, 3
524, IrcCommand::ERR_HELPNOTFOUND, 3, 3
670, IrcCommand::RPL_STARTTLS, 2, 2
671, IrcCommand::RPL_WHOISSECURE, 3, 3
691, IrcCommand::ERR_STARTTLS, 2, 2
702, IrcCommand::RPL_MODLIST, 5, 5
703, IrcCommand::RPL_ENDOFMODLIST, 2, 2
704, IrcCommand::RPL_HELPSTART, 3, 3
705, IrcCommand::RPL_HELPTXT, 3, 3
706, IrcCommand::RPL_ENDOFHELP, 3, 3
707, IrcCommand::ERR_TARGCHANGE, 3, 3
708, IrcCommand::RPL_ETRACEFULL, 10, 10
709, IrcCommand::RPL_ETRACE, 8, 8
710, IrcCommand::RPL_KNOCK, 4, 4
711, IrcCommand::RPL_KNOCKDLVR, 3, 3
712, IrcCommand::ERR_TOOMANYKNOCK, 3, 3
713, IrcCommand::ERR_CHANOPEN, 3, 3
714, IrcCommand::ERR_KNOCKONCHAN, 3, 3
715, IrcCommand::ERR_KNOCKDISABLED, 2, 2
716, IrcCommand::ERR_TARGUMODEG, 3, 3
717, IrcCommand::RPL_TARGNOTIFY, 3, 3
718, IrcCommand::RPL_UMODEGMSG, 4, 4
720, IrcCommand::RPL_OMOTDSTART, 2, 2
721, IrcCommand::RPL_OMOTD, 2, 2
722, IrcCommand::RPL_ENDOFOMOTD, 2, 2
723, IrcCommand::ERR_NOPRIVS, 3, 3
725, IrcCommand::RPL_TESTLINE, 5, 5
726, IrcCommand::RPL_NOTESTLINE, 3, 3
727, IrcCommand::RPL_TESTMASKGECOS, 6, 6
728, IrcCommand::RPL_QUIETLIST, 6, 6
729, IrcCommand::RPL_ENDOFQUIETLIS, 4, 4
730, IrcCommand::RPL_MONONLINE, 2, 2
731, IrcCommand::RPL_MONOFFLINE, 2, 2
732, IrcCommand::RPL_MONLIST, 2, 2
733, IrcCommand::RPL_ENDOFMONLIS, 2, 2
734, IrcCommand::ERR_MONLISTFULL, 4, 4
740, IrcCommand::RPL_RSACHALLENGE2, 2, 2
741, IrcCommand::RPL_ENDOFRSACHALLENGE2, 2, 2
742, IrcCommand::ERR_MLOCKRESTRICTE, 5, 5
743, IrcCommand::ERR_INVALIDBAN, 5, 5
750, IrcCommand::RPL_SCANMATCHED, 3, 3
751, IrcCommand::RPL_SCANUMODES, 8, 8
900, IrcCommand::RPL_LOGGEDIN, 4, 4
901, IrcCommand::RPL_LOGGEDOUT, 3, 3
902, IrcCommand::ERR_NICKLOCKED, 2, 2
903, IrcCommand::RPL_SASLSUCCESS, 2, 2
904, IrcCommand::ERR_SASLFAIL, 2, 2
905, IrcCommand::ERR_SASLTOOLONG, 2, 2
906, IrcCommand::ERR_SASLABORTED, 2, 2
907, IrcCommand::ERR_SASLALREADY, 2, 2
908, IrcCommand::RPL_SASLMECHS, 3, 3
ACCOUNT, IrcCommand::ACCOUNT, 1, 1
AUTHENTICATE, IrcCommand::AUTHENTICATE, 1, 1
AWAY, IrcCommand::AWAY, 0, 1
BATCH, IrcCommand::BATCH, 1, 15
BOUNCER, IrcCommand::BOUNCER, 1, 15
CAP, IrcCommand::CAP, 1, 15
CHGHOST, IrcCommand::CHGHOST, 2, 2
ERROR, IrcCommand::ERROR, 1, 1
INVITE, IrcCommand::INVITE, 2, 2
JOIN, IrcCommand::JOIN, 1, 3
KICK, IrcCommand::KICK, 3, 3
KILL, IrcCommand::KILL, 2, 2
MODE, IrcCommand::MODE, 2, 15
NICK, IrcCommand::NICK, 1, 1
NOTICE, IrcCommand::NOTICE, 2, 2
PART, IrcCommand::PART, 1, 2
PING, IrcCommand::PING, 1, 1
PONG, IrcCommand::PONG, 2, 2
PRIVMSG, IrcCommand::PRIVMSG, 2, 2
QUIT, IrcCommand::QUIT, 1, 1
SETNAME, IrcCommand::SETNAME, 1, 1
TAGMSG, IrcCommand::TAGMSG, 1, 1
TOPIC, IrcCommand::TOPIC, 2, 2
WALLOPS, IrcCommand::WALLOPS, 1, 1

190
myirc/ircmsg.cpp Normal file
View File

@@ -0,0 +1,190 @@
#include "myirc/ircmsg.hpp"
#include <cstring>
#include <optional>
namespace {
class Parser
{
char *msg_;
inline static char empty[1];
inline void trim()
{
while (*msg_ == ' ')
msg_++;
}
public:
Parser(char *msg)
: msg_(msg)
{
if (msg_ == nullptr)
{
msg_ = empty;
}
else
{
trim();
}
}
char *word()
{
const auto start = msg_;
while (*msg_ != '\0' && *msg_ != ' ')
msg_++;
if (*msg_ != '\0')
{ // prepare for next token
*msg_++ = '\0';
trim();
}
return start;
}
bool match(char c)
{
if (c == *msg_)
{
msg_++;
return true;
}
return false;
}
bool isempty() const { return *msg_ == '\0'; }
const char *peek() { return msg_; }
};
std::string_view unescape_tag_value(char *const val)
{
// only start copying at the first escape character
// skip everything before that
auto cursor = strchr(val, '\\');
if (cursor == nullptr)
{
return {val};
}
auto write = cursor;
for (; *cursor; cursor++)
{
if (*cursor == '\\')
{
cursor++;
switch (*cursor)
{
default:
*write++ = *cursor;
break;
case ':':
*write++ = ';';
break;
case 's':
*write++ = ' ';
break;
case 'r':
*write++ = '\r';
break;
case 'n':
*write++ = '\n';
break;
case '\0':
return {val, write};
}
}
else
{
*write++ = *cursor;
}
}
return {val, write};
}
} // namespace
namespace myirc {
auto parse_irc_tags(char *str) -> std::vector<irctag>
{
std::vector<irctag> tags;
do
{
auto val = strsep(&str, ";");
auto key = strsep(&val, "=");
if ('\0' == *key)
{
throw irc_parse_error(irc_error_code::MISSING_TAG);
}
if (nullptr == val)
{
tags.emplace_back(key, "");
}
else
{
tags.emplace_back(std::string_view{key, val - 1}, unescape_tag_value(val));
}
}
while (nullptr != str);
return tags;
}
auto parse_irc_message(char *msg) -> IrcMsg
{
Parser p{msg};
IrcMsg out;
/* MESSAGE TAGS */
if (p.match('@'))
{
out.tags = parse_irc_tags(p.word());
}
/* MESSAGE SOURCE */
if (p.match(':'))
{
out.source = p.word();
}
/* MESSAGE COMMANDS */
out.command = p.word();
if (out.command.empty())
{
throw irc_parse_error{irc_error_code::MISSING_COMMAND};
}
/* MESSAGE ARGUMENTS */
while (!p.isempty())
{
if (p.match(':'))
{
out.args.push_back(p.peek());
break;
}
out.args.push_back(p.word());
}
return out;
}
auto IrcMsg::hassource() const -> bool { return source.data() != nullptr; }
auto operator<<(std::ostream &out, irc_error_code code) -> std::ostream &
{
switch (code)
{
case irc_error_code::MISSING_COMMAND:
out << "MISSING COMMAND";
return out;
case irc_error_code::MISSING_TAG:
out << "MISSING TAG";
return out;
default:
return out;
}
}
} // namespace myirc

54
myirc/linebuffer.cpp Normal file
View File

@@ -0,0 +1,54 @@
#include "myirc/linebuffer.hpp"
namespace myirc {
auto LineBuffer::next_line() -> char*
{
auto const nl = std::find(search_, end_, '\n');
if (nl == end_) // no newline found, line incomplete
{
search_ = end_;
return nullptr;
}
// Null-terminate the line. Support both \n and \r\n
*(start_ < nl && *std::prev(nl) == '\r' ? std::prev(nl) : nl) = '\0';
auto const result = start_;
start_ = search_ = std::next(nl);
return &*result;
}
// Get the next complete line skipping over empty lines
auto LineBuffer::next_nonempty_line() -> char*
{
char* line;
while ((line = next_line()))
{
while (*line == ' ')
{
line++;
}
if ('\0' != *line)
{
break;
}
}
return line;
}
auto LineBuffer::shift() -> void
{
auto const first = std::begin(buffer_);
auto const gap = std::distance(start_, first);
if (gap != 0) // relocate incomplete line to front of buffer
{
end_ = std::move(start_, end_, first);
start_ = first;
std::advance(search_, gap);
}
}
} // namespace myirc

71
myirc/openssl_utils.cpp Normal file
View File

@@ -0,0 +1,71 @@
#include "myirc/openssl_utils.hpp"
#include "myirc/c_callback.hpp"
#include <openssl/err.h>
#include <openssl/pem.h>
#include <boost/log/trivial.hpp>
#include <cstdio>
using namespace std::literals;
namespace myirc {
auto log_openssl_errors(const std::string_view prefix) -> void
{
auto err_cb = [prefix](const char *str, size_t len) -> int {
BOOST_LOG_TRIVIAL(error) << prefix << std::string_view{str, len};
return 0;
};
ERR_print_errors_cb(CCallback<decltype(err_cb)>::invoke, &err_cb);
}
auto cert_from_file(const std::string &filename) -> Ref<X509>
{
Ref<X509> cert;
if (const auto fp = fopen(filename.c_str(), "r"))
{
cert.reset(PEM_read_X509(fp, nullptr, nullptr, nullptr));
if (cert.get() == nullptr)
{
log_openssl_errors("Reading certificate: "sv);
}
fclose(fp);
}
else
{
const auto err = strerror(errno);
BOOST_LOG_TRIVIAL(error) << "Opening certificate: " << err;
}
return cert;
}
auto key_from_file(const std::string &filename, const std::string_view password) -> Ref<EVP_PKEY>
{
Ref<EVP_PKEY> key;
if (const auto fp = fopen(filename.c_str(), "r"))
{
auto cb = [password](char * const buf, int const size, int) -> int {
if (std::cmp_less(size, password.size())) { return -1; }
std::copy(password.begin(), password.end(), buf);
return static_cast<int>(password.size());
};
key.reset(PEM_read_PrivateKey(fp, nullptr, CCallback<decltype(cb)>::invoke, &cb));
if (key.get() == nullptr)
{
log_openssl_errors("Reading private key: "sv);
}
fclose(fp);
}
else
{
const auto err = strerror(errno);
BOOST_LOG_TRIVIAL(error) << "Opening private key: " << err;
}
return key;
}
} // namespace myirc

28
myirc/ratelimit.cpp Normal file
View File

@@ -0,0 +1,28 @@
#include "myirc/ratelimit.hpp"
#include <chrono>
namespace myirc {
using namespace std::literals;
using ms = std::chrono::milliseconds;
auto Rfc1459RateLimit::query(size_t want_to_send) -> std::pair<ms, size_t>
{
const auto now = clock::now();
if (horizon_ < now) horizon_ = now;
auto gap = std::chrono::floor<ms>(now + allowance_ - horizon_);
auto send = gap / cost_;
if (std::cmp_greater(send, want_to_send)) send = want_to_send;
if (send > 0) {
horizon_ += send * cost_;
return {0ms, send};
} else {
horizon_ += cost_;
return {cost_ - gap, 1};
}
}
} // namespace myirc

147
myirc/registration.cpp Normal file
View File

@@ -0,0 +1,147 @@
#include "myirc/registration.hpp"
#include "myirc/connection.hpp"
#include "myirc/ircmsg.hpp"
#include <memory>
#include <random>
#include <unordered_map>
namespace myirc {
Registration::Registration(
Settings settings,
std::shared_ptr<Client> client
)
: settings_{std::move(settings)}
, client_{std::move(client)}
{
}
auto Registration::on_connect() -> void
{
auto &connection = client_->get_connection();
client_->list_caps();
caps_slot_ = client_->sig_cap_ls.connect([self = shared_from_this()](auto &caps) {
self->caps_slot_.disconnect();
self->on_cap_list(caps);
});
slot_ = connection.sig_ircmsg.connect(
[self = shared_from_this()](const auto cmd, auto &msg, auto)
{
self->on_ircmsg(cmd, msg);
}
);
if (not settings_.password.empty())
{
client_->send_pass(settings_.password);
}
client_->send_user(settings_.username, settings_.realname);
client_->send_nick(settings_.nickname);
}
auto Registration::on_cap_list(const std::unordered_map<std::string, std::string> &caps) -> void
{
std::string request;
static const char * const want [] {
"account-notify",
"account-tag",
"batch",
"chghost",
"draft/chathistory",
"extended-join",
"invite-notify",
"server-time",
"setname",
"soju.im/no-implicit-names",
"solanum.chat/identify-msg",
"solanum.chat/oper",
"solanum.chat/realhost",
};
for (const auto cap : want)
{
if (caps.contains(cap))
{
request.append(cap);
request.push_back(' ');
}
}
bool do_sasl = settings_.sasl_mechanism && caps.contains("sasl");
if (do_sasl) {
request.append("sasl ");
}
if (not request.empty())
{
request.pop_back(); // trailing space
client_->send_cap_req(request);
}
if (do_sasl) {
client_->start_sasl(std::move(settings_.sasl_mechanism));
} else {
client_->send_cap_end();
}
}
auto Registration::start(
Settings settings,
std::shared_ptr<Client> client
) -> std::shared_ptr<Registration>
{
const auto thread = std::make_shared<Registration>(std::move(settings), std::move(client));
thread->slot_ = thread->client_->get_connection().sig_connect.connect([thread](auto, auto, auto) {
thread->slot_.disconnect();
thread->on_connect();
});
return thread;
}
auto Registration::randomize_nick() -> void
{
std::string new_nick = settings_.nickname.substr(0, 8);
std::random_device rd;
std::mt19937 gen{rd()};
std::uniform_int_distribution<> distrib(0, 35);
for (int i = 0; i < 8; ++i) {
const auto x = distrib(gen);
new_nick += x < 10 ? '0' + x : 'A' + (x-10);
}
client_->send_nick(new_nick);
}
auto Registration::on_ircmsg(const IrcCommand cmd, const IrcMsg &msg) -> void
{
switch (cmd)
{
default: break;
case IrcCommand::ERR_NICKNAMEINUSE:
case IrcCommand::ERR_ERRONEUSNICKNAME:
case IrcCommand::ERR_UNAVAILRESOURCE:
randomize_nick();
break;
case IrcCommand::RPL_WELCOME:
slot_.disconnect();
caps_slot_.disconnect();
break;
case IrcCommand::RPL_SASLSUCCESS:
case IrcCommand::ERR_SASLFAIL:
client_->send_cap_end();
break;
}
}
} // namespace myirc

79
myirc/sasl_mechanism.cpp Normal file
View File

@@ -0,0 +1,79 @@
#include "myirc/sasl_mechanism.hpp"
#include "myirc/openssl_utils.hpp"
#include <openssl/evp.h>
namespace myirc {
auto SaslPlain::step(std::string_view msg) -> StepResult {
if (complete_) {
return Failure{};
} else {
std::string reply;
reply += authzid_;
reply += '\0';
reply += authcid_;
reply += '\0';
reply += password_;
complete_ = true;
return std::move(reply);
}
}
auto SaslExternal::step(std::string_view msg) -> StepResult {
if (complete_) {
return Failure{};
} else {
complete_ = true;
return std::move(authzid_);
}
}
auto SaslEcdsa::step(std::string_view msg) -> StepResult {
switch (stage_) {
case 0:
stage_ = 1;
return std::move(message1_);
case 1:
{
stage_ = 2;
Ref<EVP_PKEY_CTX> ctx {EVP_PKEY_CTX_new(key_.get(), nullptr)};
if (not ctx) {
log_openssl_errors("ECDSA new context: ");
return Failure{};
}
if (0 >= EVP_PKEY_sign_init(ctx.get()))
{
log_openssl_errors("ECDSA init: ");
return Failure{};
}
const auto input = reinterpret_cast<const unsigned char *>(msg.data());
size_t siglen;
if (0 >= EVP_PKEY_sign(ctx.get(), nullptr, &siglen, input, msg.size()))
{
log_openssl_errors("ECDSA signature (presize): ");
return Failure{};
}
std::string result(siglen, '\0');
const auto output = reinterpret_cast<unsigned char *>(result.data());
if (0 >= EVP_PKEY_sign(ctx.get(), output, &siglen, input, msg.size()))
{
log_openssl_errors("ECDSA signature: ");
return Failure{};
}
result.resize(siglen);
return std::move(result);
}
default:
return Failure{};
}
}
} // namespace myirc

286
myirc/snote.cpp Normal file
View File

@@ -0,0 +1,286 @@
#include "myirc/snote.hpp"
#include "myirc/c_callback.hpp"
#include <hs.h>
#include <boost/log/trivial.hpp>
#include <cstdlib>
#include <cstring>
#include <optional>
#include <regex>
#include <stdexcept>
#include <utility>
namespace myirc {
namespace {
struct SnotePattern
{
SnotePattern(SnoteTag tag, const char *expression)
: tag{tag}
, expression{expression}
, regex{expression, std::regex_constants::ECMAScript | std::regex_constants::optimize}
{
}
SnoteTag tag;
const char *expression;
std::regex regex;
};
using namespace std::literals;
const SnotePattern static patterns[] = {
{SnoteTag::ClientConnecting,
R"(^Client connecting: ([^ ]+) \(([^@ ]+)@([^) ]+)\) \[(.*)\] \{([^ ]*)\} <([^ ]*)> \[(.*)\]$)"},
{SnoteTag::ClientExiting,
R"(^Client exiting: ([^ ]+) \(([^@ ]+)@([^) ]+)\) \[(.*)\] \[(.*)\]$)"},
{SnoteTag::RejectingKlined,
R"(^Rejecting K-Lined user ([^ ]+)\[([^@]+)@([^\]]+)\] \[([^\] ]+)\] \((.*)\)$)"},
{SnoteTag::NickChange,
R"(^Nick change: From ([^ ]+) to ([^ ]+) \[([^@]+)@([^ ]+)\]$)"},
{SnoteTag::CreateChannel,
R"(^([^ ]+) is creating new channel ([^ ]+)$)"},
{SnoteTag::TemporaryKlineExpired,
R"(^Temporary K-line for \[([^ ]+)\] expired$)"},
{SnoteTag::PropagatedBanExpired,
R"(^Propagated ban for \[([^ ]+)\] expired$)"},
{SnoteTag::DisconnectingKlined,
R"(^Disconnecting K-Lined user ([^ ]+)\[([^@]+)@([^ ]+)\] \((.*)\)$)"},
{SnoteTag::NewPropagatedKline,
R"(^([^ ]+)!([^ ]+)@([^ ]+)\{([^ ]+)\} added global ([^ ]+) min\. K-Line for \[([^ ]+)\] \[(.*)\]$)"},
{SnoteTag::NewTemporaryKline,
R"(^([^ ]+)!([^ ]+)@([^ ]+)\{([^ ]+)\} added temporary ([^ ]+) min\. K-Line for \[([^ ]+)\] \[(.*)\]$)"},
{SnoteTag::LoginAttempts,
"^Warning: \x02([^ ]+)\x02 failed login attempts to \x02([^ ]+)\x02\\. Last attempt received from \x02(.+)\x02.*$"},
{SnoteTag::PossibleFlooder,
R"(^Possible Flooder ([^ ]+)\[([^ ]+)@[^ ]+\] on ([^ ]+) target: ([^ ]+)$)"},
{SnoteTag::KilledRemote,
R"(^Received KILL message for ([^ ]+)!([^ ]+)@([^ ]+)\. From ([^ ]+) Path: ([^ ]+) \((.*)\)$)"},
{SnoteTag::KilledRemoteOper,
R"(^Received KILL message for ([^ ]+)!([^ ]+)@([^ ]+)\. From ([^ ]+) Path: ([^ ]+)!([^ ]+)!([^ ]+)!([^ ]+) (.*)$)"},
{SnoteTag::Killed,
R"(^Received KILL message for ([^ ]+)!([^ ]+)@([^ ]+)\. From ([^ ]+) (.*)$)"},
{SnoteTag::TooManyGlobalConnections,
R"(^Too many global connections for ([^ ]+)\[([^ ]+)@([^ ]+)\] \[(.*)\]$)"},
{SnoteTag::TooManyUserConnections,
R"(^Too many user connections for ([^ ]+)\[([^ ]+)@([^ ]+)\] \[(.*)\]$)"},
{SnoteTag::SetVhostOnMarkedAccount,
"^\x02([^ ]+)\x02 set vhost ([^ ]+) on the \x02MARKED\x02 account ([^ ]+).$"},
{SnoteTag::IsNowOper,
R"(^([^ ]+) \(([^ ]+)!([^ ]+)@([^ ]+)\) is now an operator$)"},
{SnoteTag::IsNowOperGlobal,
R"(^([^ ]+) \(([^ ]+)@([^ ]+)\) is now an operator$)"},
{SnoteTag::OperspyWhois,
R"(^OPERSPY ([^ ]+)!([^ ]+)@([^ ]+)\{([^ ]+)\} WHOIS ([^ ]+)!([^ ]+)@([^ ]+) ([^ ]+) $)"}, // trailing space intentional
{SnoteTag::OperspyWho,
R"(^OPERSPY ([^ ]+)!([^ ]+)@([^ ]+)\{([^ ]+)\} WHO ([^ ]+)$)"},
{SnoteTag::Freeze,
"^\x02([^ ]+)\x02 froze the account \x02([^ ]+)\x02 \\((.*)\\)\\.$"},
{SnoteTag::DroppedChannel,
"^\x02([^ ]+)\x02 dropped the channel \x02([^ ]+)\x02$"},
{SnoteTag::DroppedAccount,
"^\x02([^ ]+)\x02 dropped the account \x02([^ ]+)\x02$"},
{SnoteTag::DroppedNick,
"^\x02([^ ]+)\x02 dropped the nick \x02([^ ]+)\x02 from ([^ ]+)$"},
{SnoteTag::DroppedNickRename,
"^\x02([^ ]+)\x02 dropped the nick \x02([^ ]+)\x02 from ([^ ]+), changing account name to \x02([^ ]+)\x02$"},
{SnoteTag::Spambot,
R"(^User ([^ ]+) \(([^ ]+)@([^ ]+)\) trying to join ([^ ]+) is a possible spambot$)"},
{SnoteTag::SaveMessage,
R"(^Received SAVE message for ([^ ]+) from ([^ ]+)$)"},
{SnoteTag::NickCollisionServices,
R"(^Nick collision due to services forced nick change on ([^ ]+)$)"},
{SnoteTag::NickCollision,
R"(^Nick collision on ([^ ]+)\(([^ ]+) <- ([^ ]+)\)\(([^ ]+) ([^ ]+)\)$)"},
{SnoteTag::TemporaryDline,
R"(^([^ ]+) added temporary ([^ ]+) min\. D-Line for \[([^ ]+)\] \[(.*)\]$)"},
{SnoteTag::FailedChallengeMissingSecure,
R"(^Failed CHALLENGE attempt - missing secure connection by ([^ ]+) \(([^ ]+)@([^ ]+)\)$)"},
{SnoteTag::FailedChallenge,
R"(^Failed CHALLENGE attempt by ([^ ]+) \(([^ ]+)@([^ ]+)\)$)"},
{SnoteTag::FailedChallengeHostMismatch,
R"(^Failed CHALLENGE attempt - host mismatch by ([^ ]+) \(([^ ]+)@([^ ]+)\)$)"},
{SnoteTag::FailedChallengeNoBlock,
R"(^Failed CHALLENGE attempt - user@host mismatch or no operator block for ([^ ]+) by ([^ ]+) \(([^ ]+)@([^ ]+)\)$)"},
{SnoteTag::FailedChallengeTls,
R"(^Failed CHALLENGE attempt - missing SSL/TLS by ([^ ]+) \(([^ ]+)@([^ ]+)\)$)"},
{SnoteTag::FailedChallengeFingerprintMismatch,
R"(^Failed CHALLENGE attempt - client certificate fingerprint mismatch by ([^ ]+) \(([^ ]+)@([^ ]+)\)$)"},
{SnoteTag::SighupReloadingConf,
R"(^Got signal SIGHUP, reloading ircd conf\. file$)"},
{SnoteTag::JoinedJuped,
R"(^User ([^ ]+) \(([^ ]+)@([^ ]+)\) is attempting to join locally juped channel ([^ ]+) \((.*)\)$)"},
};
static auto setup_database() -> hs_database_t *
{
const auto n = std::size(patterns);
std::vector<const char *> expressions;
std::vector<unsigned> flags(n, HS_FLAG_SINGLEMATCH);
std::vector<unsigned> ids;
expressions.reserve(n);
ids.reserve(n);
for (unsigned i = 0; i < n; i++)
{
expressions.push_back(patterns[i].expression);
ids.push_back(i);
}
hs_database_t *db;
hs_compile_error *error;
hs_platform_info_t *platform = nullptr; // target current platform
switch (hs_compile_multi(expressions.data(), flags.data(), ids.data(), static_cast<unsigned>(expressions.size()), HS_MODE_BLOCK, platform, &db, &error))
{
case HS_COMPILER_ERROR: {
std::string msg = std::to_string(error->expression) + ": " + error->message;
hs_free_compile_error(error);
throw std::runtime_error{std::move(msg)};
}
case HS_SUCCESS:
break;
default:
abort();
}
return db;
}
} // namespace
SnoteCore::SnoteCore()
{
db_.reset(setup_database());
hs_scratch_t *scratch = nullptr;
if (HS_SUCCESS != hs_alloc_scratch(db_.get(), &scratch))
{
abort();
}
scratch_.reset(scratch);
}
auto SnoteCore::match(const IrcMsg &msg) -> std::optional<SnoteMatch>
{
static const char *const prefix = "*** Notice -- ";
auto &args = msg.args;
if ("*" != args[0] || !args[1].starts_with(prefix))
{
return std::nullopt;
}
const auto message = args[1].substr(strlen(prefix));
unsigned match_id;
auto cb = [&match_id](unsigned id, unsigned long long, unsigned long long, unsigned) -> int {
match_id = id;
return 1; // stop scanning
};
const auto scan_result = hs_scan(
db_.get(),
message.data(),
static_cast<unsigned>(message.size()),
0, // no flags
scratch_.get(),
CCallback<decltype(cb)>::invoke, &cb
);
switch (scan_result)
{
case HS_SUCCESS:
BOOST_LOG_TRIVIAL(warning) << "Unknown snote: " << message;
return std::nullopt;
case HS_SCAN_TERMINATED: {
auto &pattern = patterns[match_id];
return SnoteMatch{pattern.tag, pattern.regex, message};
}
default:
abort();
}
}
auto SnoteMatch::get_results() -> const std::match_results<std::string_view::const_iterator> &
{
if (auto results = std::get_if<1>(&components_))
{
return *results;
}
auto [regex, message] = std::get<0>(components_);
auto &results = components_.emplace<1>();
if (not std::regex_match(message.begin(), message.end(), results, regex))
{
// something went wrong - hyperscan disagrees with std::regex
abort();
}
return results;
}
auto SnoteCore::DbDeleter::operator()(hs_database_t *db) const -> void
{
if (HS_SUCCESS != hs_free_database(db))
{
abort();
}
}
auto SnoteCore::ScratchDeleter::operator()(hs_scratch_t *scratch) const -> void
{
if (HS_SUCCESS != hs_free_scratch(scratch))
{
abort();
}
}
SnoteCore snoteCore;
} // namespace myirc

3
mysocks5/CMakeLists.txt Normal file
View File

@@ -0,0 +1,3 @@
add_library(mysocks5 STATIC socks5.cpp)
target_include_directories(mysocks5 PUBLIC include)
target_link_libraries(mysocks5 PUBLIC Boost::asio Boost::endian)

404
mysocks5/include/socks5.hpp Normal file
View File

@@ -0,0 +1,404 @@
#pragma once
#include <boost/asio.hpp>
#include <boost/endian.hpp>
#include <cstdint>
#include <cstring>
#include <stdexcept>
#include <string>
#include <string_view>
#include <variant>
namespace socks5 {
struct SocksErrCategory : boost::system::error_category
{
char const* name() const noexcept override;
std::string message(int) const override;
};
extern SocksErrCategory const theSocksErrCategory;
enum class SocksErrc
{
// Errors from the server
Succeeded = 0,
GeneralFailure = 1,
NotAllowed = 2,
NetworkUnreachable = 3,
HostUnreachable = 4,
ConnectionRefused = 5,
TtlExpired = 6,
CommandNotSupported = 7,
AddressNotSupported = 8,
// Errors from the client
WrongVersion = 256,
NoAcceptableMethods,
AuthenticationFailed,
UnsupportedEndpointAddress,
DomainTooLong,
UsernameTooLong,
PasswordTooLong,
};
/// Either a hostname or an address. Hostnames are resolved locally on the proxy server
using Host = std::variant<std::string_view, boost::asio::ip::address>;
struct NoCredential
{
};
struct UsernamePasswordCredential
{
std::string_view username;
std::string_view password;
};
using Auth = std::variant<NoCredential, UsernamePasswordCredential>;
namespace detail {
template <class... Ts>
struct overloaded : Ts...
{
using Ts::operator()...;
};
template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
auto make_socks_error(SocksErrc const err) -> boost::system::error_code;
inline auto push_buffer(std::vector<std::uint8_t>& buffer, auto const& thing) -> void
{
buffer.push_back(thing.size());
buffer.insert(buffer.end(), thing.begin(), thing.end());
}
uint8_t const socks_version_tag = 5;
uint8_t const auth_version_tag = 1;
enum class AuthMethod
{
NoAuth = 0,
Gssapi = 1,
UsernamePassword = 2,
NoAcceptableMethods = 255,
};
enum class Command
{
Connect = 1,
Bind = 2,
UdpAssociate = 3,
};
enum class AddressType
{
IPv4 = 1,
DomainName = 3,
IPv6 = 4,
};
/// @brief Encode the given host into the end of the buffer
/// @param host host to encode
/// @param buffer target to push bytes onto
/// @return true for success and false for failure
auto push_host(Host const& host, std::vector<uint8_t>& buffer) -> void;
template <typename AsyncStream>
struct SocksImplementation
{
AsyncStream& socket_;
Host const host_;
boost::endian::big_uint16_t const port_;
Auth const auth_;
/// buffer used to back async read/write operations
std::vector<uint8_t> buffer_;
// Representations of states in the protocol
struct Start
{
};
struct HelloRecvd
{
static const std::size_t READ = 2; // version, method
};
struct AuthRecvd
{
static const std::size_t READ = 2; // subversion, status
};
struct ReplyRecvd
{
static const std::size_t READ = 4; // version, reply, reserved, address-tag
};
struct FinishIpv4
{
static const std::size_t READ = 6; // ipv4 + port = 6 bytes
};
struct FinishIpv6
{
static const std::size_t READ = 18; // ipv6 + port = 18 bytes
};
/// @brief State when the application needs to receive some bytes
/// @tparam Next State to transistion to after read is successful
template <typename Next>
struct Sent
{
};
/// @brief intermediate completion callback
/// @tparam Self type of enclosing intermediate completion handler
/// @tparam State protocol state tag type
/// @param self enclosing intermediate completion handler
/// @param state protocol state tag value
/// @param error error code of read/write operation
/// @param size bytes read or written
/// @param
template <typename Self, typename State = Start>
auto operator()(
Self& self,
State state = {},
boost::system::error_code const error = {},
std::size_t = 0
) -> void
{
if (error)
{
self.complete(error, {});
}
else
{
step(self, state);
}
}
/// @brief Write the buffer to the socket and then read N bytes back into the buffer
/// @tparam Next state to resume after read
/// @tparam N number of bytes to read
/// @tparam Self type of enclosing intermediate completion handler
/// @param self enclosing intermediate completion handler
template <typename Next, typename Self>
auto transact(Self& self) -> void
{
boost::asio::async_write(
socket_,
boost::asio::buffer(buffer_),
[self = std::move(self)](boost::system::error_code const err, std::size_t const n) mutable {
self(Sent<Next>{}, err, n);
}
);
}
/// @brief Notify the caller of a failure and terminate the protocol
/// @param self intermediate completion handler
/// @param err error code to return to the caller
auto failure(auto& self, SocksErrc const err) -> void
{
self.complete(make_socks_error(err), {});
}
/// @brief Read bytes needed by Next state from the socket and then proceed to Next state
/// @tparam Self type of enclosing intermediate completion handler
/// @tparam Next state to transition to after read
/// @param self enclosing intermediate completion handler
/// @param state protocol state tag
template <typename Self, typename Next>
auto step(Self& self, Sent<Next>) -> void
{
buffer_.resize(Next::READ);
boost::asio::async_read(
socket_,
boost::asio::buffer(buffer_),
[self = std::move(self)](boost::system::error_code const err, std::size_t n) mutable {
self(Next{}, err, n);
}
);
}
// Send hello and offer authentication methods
template <typename Self>
auto step(Self& self, Start) -> void
{
if (auto const* const host = std::get_if<std::string_view>(&host_))
{
if (host->size() >= 256)
{
return failure(self, SocksErrc::DomainTooLong);
}
}
if (auto const* const plain = std::get_if<UsernamePasswordCredential>(&auth_))
{
if (plain->username.size() >= 256)
{
return failure(self, SocksErrc::UsernameTooLong);
}
if (plain->password.size() >= 256)
{
return failure(self, SocksErrc::PasswordTooLong);
}
}
buffer_ = {socks_version_tag, 1 /* number of methods */, static_cast<uint8_t>(method_wanted())};
transact<HelloRecvd>(self);
}
// Send TCP connection request for the domain name and port
template <typename Self>
auto step(Self& self, HelloRecvd) -> void
{
if (socks_version_tag != buffer_[0])
{
return failure(self, SocksErrc::WrongVersion);
}
auto const wanted = method_wanted();
auto const selected = static_cast<AuthMethod>(buffer_[1]);
if (AuthMethod::NoAuth == wanted && wanted == selected)
{
send_connect(self);
}
else if (AuthMethod::UsernamePassword == wanted && wanted == selected)
{
send_usernamepassword(self);
}
else
{
failure(self, SocksErrc::NoAcceptableMethods);
}
}
/// @brief Transmit the username and password to the server
/// @tparam Self type of enclosing intermediate completion handler
/// @param self enclosing intermediate completion handler
template <typename Self>
auto send_usernamepassword(Self& self) -> void
{
buffer_ = {
auth_version_tag,
};
auto const [username, password] = std::get<1>(auth_);
push_buffer(buffer_, username);
push_buffer(buffer_, password);
transact<AuthRecvd>(self);
}
template <typename Self>
auto step(Self& self, AuthRecvd) -> void
{
if (auth_version_tag != buffer_[0])
{
return failure(self, SocksErrc::WrongVersion);
}
// STATUS zero is success, non-zero is failure
if (0 != buffer_[1])
{
return failure(self, SocksErrc::AuthenticationFailed);
}
send_connect(self);
}
template <typename Self>
auto send_connect(Self& self) -> void
{
buffer_ = {
socks_version_tag,
static_cast<uint8_t>(Command::Connect),
0 /* reserved */,
};
push_host(host_, buffer_);
buffer_.insert(buffer_.end(), port_.data(), port_.data() + 2);
transact<ReplyRecvd>(self);
}
// Waiting on the remaining variable-sized address portion of the response
template <typename Self>
auto step(Self& self, ReplyRecvd) -> void
{
if (socks_version_tag != buffer_[0])
{
return failure(self, SocksErrc::WrongVersion);
}
auto const reply = static_cast<SocksErrc>(buffer_[1]);
if (SocksErrc::Succeeded != reply)
{
return failure(self, reply);
}
switch (static_cast<AddressType>(buffer_[3]))
{
case AddressType::IPv4:
return step(self, Sent<FinishIpv4>{});
case AddressType::IPv6:
return step(self, Sent<FinishIpv6>{});
default:
return failure(self, SocksErrc::UnsupportedEndpointAddress);
}
}
// Protocol complete! Return the client's remote endpoint
template <typename Self>
void step(Self& self, FinishIpv4)
{
boost::asio::ip::address_v4::bytes_type bytes;
boost::endian::big_uint16_t port;
std::memcpy(bytes.data(), &buffer_[0], 4);
std::memcpy(port.data(), &buffer_[4], 2);
self.complete({}, {boost::asio::ip::make_address_v4(bytes), port});
}
// Protocol complete! Return the client's remote endpoint
template <typename Self>
void step(Self& self, FinishIpv6)
{
boost::asio::ip::address_v6::bytes_type bytes;
boost::endian::big_uint16_t port;
std::memcpy(bytes.data(), &buffer_[0], 16);
std::memcpy(port.data(), &buffer_[16], 2);
self.complete({}, {boost::asio::ip::make_address_v6(bytes), port});
}
auto method_wanted() const -> AuthMethod
{
return std::visit(
overloaded{
[](NoCredential) { return AuthMethod::NoAuth; },
[](UsernamePasswordCredential) { return AuthMethod::UsernamePassword; },
},
auth_
);
}
};
} // namespace detail
using Signature = void(boost::system::error_code, boost::asio::ip::tcp::endpoint);
/// @brief Asynchronous SOCKS5 connection request
/// @tparam AsyncStream Type of socket
/// @tparam CompletionToken Token accepting: error_code, address, port
/// @param socket Established connection to SOCKS5 server
/// @param host Connection target host
/// @param port Connection target port
/// @param token Completion token
/// @return Behavior determined by completion token type
template <
typename AsyncStream,
boost::asio::completion_token_for<Signature> CompletionToken>
auto async_connect(
AsyncStream& socket,
Host const host,
uint16_t const port,
Auth const auth,
CompletionToken&& token
)
{
return boost::asio::async_compose<CompletionToken, Signature>(detail::SocksImplementation<AsyncStream>{socket, host, port, auth, {}}, token, socket);
}
} // namespace socks5

93
mysocks5/socks5.cpp Normal file
View File

@@ -0,0 +1,93 @@
#include "socks5.hpp"
#include <boost/asio.hpp>
#include <iterator>
#include <stdexcept>
#include <vector>
namespace socks5 {
SocksErrCategory const theSocksErrCategory;
char const* SocksErrCategory::name() const noexcept
{
return "socks5";
}
std::string SocksErrCategory::message(int ev) const
{
switch (static_cast<SocksErrc>(ev))
{
case SocksErrc::Succeeded:
return "succeeded";
case SocksErrc::GeneralFailure:
return "general SOCKS server failure";
case SocksErrc::NotAllowed:
return "connection not allowed by ruleset";
case SocksErrc::NetworkUnreachable:
return "network unreachable";
case SocksErrc::HostUnreachable:
return "host unreachable";
case SocksErrc::ConnectionRefused:
return "connection refused";
case SocksErrc::TtlExpired:
return "TTL expired";
case SocksErrc::CommandNotSupported:
return "command not supported";
case SocksErrc::AddressNotSupported:
return "address type not supported";
case SocksErrc::WrongVersion:
return "bad server protocol version";
case SocksErrc::NoAcceptableMethods:
return "server rejected authentication methods";
case SocksErrc::AuthenticationFailed:
return "server rejected authentication";
case SocksErrc::UnsupportedEndpointAddress:
return "server sent unknown endpoint address";
case SocksErrc::DomainTooLong:
return "domain name too long";
case SocksErrc::UsernameTooLong:
return "username too long";
case SocksErrc::PasswordTooLong:
return "password too long";
default:
return "(unrecognized error)";
}
}
namespace detail {
auto make_socks_error(SocksErrc const err) -> boost::system::error_code
{
return boost::system::error_code{int(err), theSocksErrCategory};
}
auto push_host(Host const& host, std::vector<uint8_t>& buffer) -> void
{
std::visit(overloaded{[&buffer](std::string_view const hostname) {
buffer.push_back(uint8_t(AddressType::DomainName));
push_buffer(buffer, hostname);
},
[&buffer](boost::asio::ip::address const& address) {
if (address.is_v4())
{
buffer.push_back(uint8_t(AddressType::IPv4));
push_buffer(buffer, address.to_v4().to_bytes());
}
else if (address.is_v6())
{
buffer.push_back(uint8_t(AddressType::IPv6));
push_buffer(buffer, address.to_v6().to_bytes());
}
else
{
throw std::logic_error{"unexpected address type"};
}
}},
host);
}
} // namespace detail
} // namespace socks5

View File

@@ -1,17 +0,0 @@
#include "ping_thread.hpp"
#include "irc_parse_thread.hpp"
#include "write_irc.hpp"
auto PingThread::start(Connection& connection) -> void
{
connection.add_listener<IrcMsgEvent>([&connection](IrcMsgEvent& event)
{
auto& irc = event.irc;
if (IrcCommand::PING == event.command)
{
send_pong(connection, irc.args[0]);
event.handled_ = true;
}
});
}

View File

@@ -1,9 +0,0 @@
#pragma once
#include "connection.hpp"
#include "event.hpp"
struct PingThread
{
static auto start(Connection& connection) -> void;
};

View File

@@ -1,178 +0,0 @@
#include "registration_thread.hpp"
#include <memory>
#include <unordered_set>
#include <unordered_map>
RegistrationThread::RegistrationThread(
Connection& connection,
std::string password,
std::string username,
std::string realname,
std::string nickname
)
: connection_{connection}
, password_{password}
, username_{username}
, realname_{realname}
, nickname_{nickname}
{
}
auto RegistrationThread::on_connect() -> void
{
send_cap_ls(connection_);
send_pass(connection_, password_);
send_user(connection_, username_, realname_);
send_nick(connection_, nickname_);
connection_.remove_listener(connect_handle_);
}
auto RegistrationThread::send_req() -> void
{
std::string request;
char const* const want[] = {
"extended-join",
"account-notify",
"draft/chathistory",
"batch",
"soju.im/no-implicit-names",
"chghost",
"setname",
"account-tag",
"solanum.chat/oper",
"solanum.chat/identify-msg",
"solanum.chat/realhost",
"server-time",
"invite-notify",
"extended-join"
};
for (auto cap : want)
{
if (caps.contains(cap))
{
request.append(cap);
request.push_back(' ');
outstanding.insert(cap);
}
}
connection_.remove_listener(message_handle_);
if (not outstanding.empty())
{
request.pop_back();
send_cap_req(connection_, request);
listen_for_cap_ack();
}
else
{
send_cap_end(connection_);
}
}
auto RegistrationThread::on_msg_cap_ack(IrcMsg const& msg) -> void
{
auto in = std::istringstream{std::string{msg.args[2]}};
std::for_each(
std::istream_iterator<std::string>{in},
std::istream_iterator<std::string>{},
[this](std::string x) {
outstanding.erase(x);
}
);
if (outstanding.empty())
{
send_cap_end(connection_);
connection_.remove_listener(message_handle_);
}
}
auto RegistrationThread::on_msg_cap_ls(IrcMsg const& msg) -> void
{
std::string_view const* kvs;
bool last;
if (3 == msg.args.size())
{
kvs = &msg.args[2];
last = true;
}
else if (4 == msg.args.size() && "*" == msg.args[2])
{
kvs = &msg.args[3];
last = false;
}
else
{
return;
}
auto in = std::istringstream{std::string{*kvs}};
std::for_each(
std::istream_iterator<std::string>{in},
std::istream_iterator<std::string>{},
[this](std::string x) {
auto const eq = x.find('=');
if (eq == x.npos)
{
caps.emplace(x, std::string{});
}
else
{
caps.emplace(std::string{x, 0, eq}, std::string{x, eq+1, x.npos});
}
}
);
if (last)
{
send_req();
}
}
auto RegistrationThread::start(
Connection& connection,
std::string password,
std::string username,
std::string realname,
std::string nickname
) -> std::shared_ptr<RegistrationThread>
{
auto const thread = std::make_shared<RegistrationThread>(connection, password, username, realname, nickname);
thread->listen_for_cap_ls();
thread->connect_handle_ = connection.add_listener<ConnectEvent>([thread](ConnectEvent const&)
{
thread->on_connect();
});
return thread;
}
auto RegistrationThread::listen_for_cap_ack() -> void
{
message_handle_ = connection_.add_listener<IrcMsgEvent>([thread = shared_from_this()](IrcMsgEvent const& event)
{
if (IrcCommand::CAP == event.command && event.irc.args.size() >= 2 && "*" == event.irc.args[0] && "ACK" == event.irc.args[1])
{
thread->on_msg_cap_ack(event.irc);
}
});
}
auto RegistrationThread::listen_for_cap_ls() -> void
{
message_handle_ = connection_.add_listener<IrcMsgEvent>([thread = shared_from_this()](IrcMsgEvent const& event)
{
if (IrcCommand::CAP == event.command && event.irc.args.size() >= 2 && "*" == event.irc.args[0] && "LS" == event.irc.args[1])
{
thread->on_msg_cap_ls(event.irc);
}
});
}

View File

@@ -1,53 +0,0 @@
#pragma once
#include "connection.hpp"
#include "event.hpp"
#include "irc_parse_thread.hpp"
#include "write_irc.hpp"
#include <eventpp/eventdispatcher.h>
#include <memory>
#include <string>
#include <unordered_map>
#include <unordered_set>
class RegistrationThread : public std::enable_shared_from_this<RegistrationThread>
{
Connection& connection_;
std::string password_;
std::string username_;
std::string realname_;
std::string nickname_;
std::unordered_map<std::string, std::string> caps;
std::unordered_set<std::string> outstanding;
Connection::Handle<ConnectEvent> connect_handle_;
Connection::Handle<IrcMsgEvent> message_handle_;
auto on_connect() -> void;
auto send_req() -> void;
auto on_msg_cap_ls(IrcMsg const& msg) -> void;
auto on_msg_cap_ack(IrcMsg const& msg) -> void;
auto listen_for_cap_ack() -> void;
auto listen_for_cap_ls() -> void;
public:
RegistrationThread(
Connection& connection_,
std::string password,
std::string username,
std::string realname,
std::string nickname
);
static auto start(
Connection& connection,
std::string password,
std::string username,
std::string realname,
std::string nickname
) -> std::shared_ptr<RegistrationThread>;
};

View File

@@ -1,76 +0,0 @@
#include "self_thread.hpp"
#include "connection.hpp"
#include "irc_parse_thread.hpp"
auto SelfThread::start(Connection& connection) -> std::shared_ptr<SelfThread>
{
auto thread = std::make_shared<SelfThread>(connection);
connection.add_listener<IrcMsgEvent>([thread](IrcMsgEvent& event)
{
switch (event.command)
{
// Learn nickname from 001
case IrcCommand::RPL_WELCOME:
thread->nickname_ = event.irc.args[0];
break;
// Track changes to our nickname
case IrcCommand::NICK:
{
auto const bang = event.irc.source.find('!');
if (bang != std::string::npos
&& thread->nickname_ == event.irc.source.substr(0, bang)
)
{
thread->nickname_ = event.irc.args[0];
}
break;
}
// Re-establish user modes
case IrcCommand::RPL_UMODEIS:
thread->mode_ = event.irc.args[1];
break;
// Interpret self mode changes
case IrcCommand::MODE:
if (2 == event.irc.args.size()
&& thread->nickname_ == event.irc.args[0]
)
{
auto polarity = true;
for (char const c : event.irc.args[1])
{
switch (c)
{
case '+':
polarity = true;
break;
case '-':
polarity = false;
break;
default:
if (polarity)
{
thread->mode_ += c;
}
else
{
auto const ix = thread->mode_.find(c);
if (ix != std::string::npos)
{
thread->mode_.erase(ix, 1);
}
}
break;
}
}
}
default: break;
}
});
return thread;
}

View File

@@ -1,17 +0,0 @@
#pragma once
#include <string>
#include <memory>
struct Connection;
class SelfThread
{
Connection& connection_;
std::string nickname_;
std::string mode_;
public:
SelfThread(Connection& connection) : connection_{connection} {}
static auto start(Connection&) -> std::shared_ptr<SelfThread>;
};

View File

@@ -1,17 +0,0 @@
#include "settings.hpp"
#define TOML_ENABLE_FORMATTERS 0
#include <toml++/toml.hpp>
auto Settings::from_stream(std::istream & in) -> Settings
{
auto const config = toml::parse(in);
return Settings{
.host = config["host"].value_or(std::string{"*"}),
.service = config["service"].value_or(std::string{"*"}),
.password = config["password"].value_or(std::string{"*"}),
.username = config["username"].value_or(std::string{"*"}),
.realname = config["realname"].value_or(std::string{"*"}),
.nickname = config["nickname"].value_or(std::string{"*"})
};
}

View File

@@ -1,17 +0,0 @@
#pragma once
#include <string>
#include <iostream>
struct Settings
{
std::string host;
std::string service;
std::string password;
std::string username;
std::string realname;
std::string nickname;
static auto from_stream(std::istream & in) -> Settings;
};

View File

@@ -1,178 +0,0 @@
#include "snote_thread.hpp"
#include "irc_parse_thread.hpp"
#include "connection.hpp"
#include "c_callback.hpp"
#include <cstring>
#include <cstdlib>
#include <stdexcept>
#include <utility>
#include <regex>
namespace {
struct SnotePattern
{
SnotePattern(SnoteTag tag, char const* expression, unsigned flags = 0)
: tag{tag}
, expression{expression}
, regex{expression, std::regex_constants::ECMAScript | std::regex_constants::optimize}
{
}
SnoteTag tag;
char const* expression;
std::regex regex;
};
SnotePattern const patterns[] =
{
{SnoteTag::ClientConnecting,
R"(^Client connecting: ([^ ]+) \(([^@ ]+)@([^) ]+)\) \[(.*)\] \{([^ ]*)\} <([^ ]*)> \[(.*)\]$)"},
{SnoteTag::ClientExiting,
R"(^Client exiting: ([^ ]+) \(([^@ ]+)@([^) ]+)\) \[(.*)\] \[(.*)\]$)"},
{SnoteTag::RejectingKlined,
R"(^Rejecting K-Lined user ([^ ]+)\[([^@]+)@([^\]]+)\] \[([^\] ]+)\] \((.*)\)$)"},
{SnoteTag::NickChange,
R"(^Nick change: From ([^ ]+) to ([^ ]+) \[([^@]+)@([^ ]+)\]$)"},
{SnoteTag::CreateChannel,
R"(^([^ ]+) is creating new channel ([^ ]+)$)"},
{SnoteTag::TemporaryKlineExpired,
R"(^Temporary K-line for \[([^ ]+)\] expired$)"},
{SnoteTag::PropagatedBanExpired,
R"(^Propagated ban for \[([^ ]+)\] expired$)"},
{SnoteTag::DisconnectingKlined,
R"(^Disconnecting K-Lined user ([^ ]+)\[([^@]+)@([^ ]+)\] \((.*)\)$)"},
{SnoteTag::NewPropagatedKline,
R"(^([^ ]+)!([^ ]+)@([^ ]+)\{([^ ]+)\} added global ([^ ]+) min\. K-Line for \[([^ ]+)\] \[(.*)\]$)"},
{SnoteTag::NewTemporaryKline,
R"(^([^ ]+)!([^ ]+)@([^ ]+)\{([^ ]+)\} added temporary ([^ ]+) min\. K-Line for \[([^ ]+)\] \[(.*)\]$)"},
{SnoteTag::LoginAttempts,
"^Warning: \x02([^ ]+)\x02 failed login attempts to \x02([^ ]+)\x02\\. Last attempt received from \x02(.+)\x02.*$"},
{SnoteTag::PossibleFlooder,
R"(^Possible Flooder ([^ ]+)\[([^ ]+)@[^ ]+\] on ([^ ]+) target: ([^ ]+)$)"},
{SnoteTag::Killed,
R"(^Received KILL message for ([^ ]+)!([^ ]+)@([^ ]+)\. From ([^ ]+) Path: ([^ ]+) \((.*)\)$)"},
};
auto setup_database() -> hs_database_t*
{
auto const n = std::size(patterns);
std::vector<char const*> expressions;
std::vector<unsigned> flags(n, HS_FLAG_SINGLEMATCH);
std::vector<unsigned> ids;
expressions.reserve(n);
ids.reserve(n);
for (std::size_t i = 0; i < n; i++)
{
expressions.push_back(patterns[i].expression);
ids.push_back(i);
}
hs_database_t* db;
hs_compile_error *error;
hs_platform_info_t *platform = nullptr; // target current platform
switch (hs_compile_multi(expressions.data(), flags.data(), ids.data(), expressions.size(), HS_MODE_BLOCK, platform, &db, &error))
{
case HS_COMPILER_ERROR:
{
std::string msg = error->message;
hs_free_compile_error(error);
throw std::runtime_error{std::move(msg)};
}
case HS_SUCCESS:
break;
default:
abort();
}
return db;
}
} // namespace
auto SnoteThread::start(Connection& connection) -> std::shared_ptr<SnoteThread>
{
auto thread = std::make_shared<SnoteThread>();
thread->db_.reset(setup_database());
hs_scratch_t* scratch = nullptr;
if (HS_SUCCESS != hs_alloc_scratch(thread->db_.get(), &scratch))
{
abort();
}
thread->scratch_.reset(scratch);
static char const* const prefix = "*** Notice -- ";
connection.add_listener<IrcMsgEvent>([&connection, thread](IrcMsgEvent& event)
{
auto& args = event.irc.args;
if (IrcCommand::NOTICE == event.command && "*" == args[0] && args[1].starts_with(prefix))
{
event.handled_ = true;
auto const message = args[1].substr(strlen(prefix));
unsigned match_id;
auto cb = [&match_id](unsigned id, unsigned long long, unsigned long long, unsigned) -> int
{
match_id = id;
return 1; // stop scanning
};
auto const scan_result =
hs_scan(
thread->db_.get(),
message.data(), message.size(),
0, // no flags
thread->scratch_.get(),
CCallback<decltype(cb)>::invoke, &cb
);
switch (scan_result)
{
case HS_SUCCESS:
std::cout << "Unknown snote: " << message << std::endl;
break;
case HS_SCAN_TERMINATED:
{
auto& pattern = patterns[match_id];
std::match_results<std::string_view::const_iterator> results;
if (not std::regex_match(message.begin(), message.end(), results, pattern.regex))
{
// something went wrong - hyperscan disagrees with std::regex
abort();
}
std::vector<std::string_view> parts;
for (auto const sub : results)
{
parts.push_back(std::string_view{sub.first, sub.second});
}
connection.make_event<SnoteEvent>(pattern.tag, std::move(parts));
break;
}
default:
abort();
}
}
});
return thread;
}

View File

@@ -1,67 +0,0 @@
#pragma once
#include "event.hpp"
#include <hs.h>
#include <memory>
class Connection;
enum class SnoteTag
{
ClientConnecting,
ClientExiting,
RejectingKlined,
NickChange,
CreateChannel,
TemporaryKlineExpired,
PropagatedBanExpired,
DisconnectingKlined,
NewPropagatedKline,
NewTemporaryKline,
LoginAttempts,
PossibleFlooder,
Killed,
};
struct SnoteEvent : Event
{
SnoteEvent(SnoteTag tag, std::vector<std::string_view> parts)
: tag{tag}
, parts{std::move(parts)}
{}
SnoteTag tag;
std::vector<std::string_view> parts;
};
struct SnoteThread
{
struct DbDeleter
{
auto operator()(hs_database_t * db) const -> void
{
if (HS_SUCCESS != hs_free_database(db))
{
abort();
}
}
};
struct ScratchDeleter
{
auto operator()(hs_scratch_t * scratch) const -> void
{
if (HS_SUCCESS != hs_free_scratch(scratch))
{
abort();
}
}
};
std::unique_ptr<hs_database_t, DbDeleter> db_;
std::unique_ptr<hs_scratch_t, ScratchDeleter> scratch_;
static auto start(Connection& connection) -> std::shared_ptr<SnoteThread>;
};

View File

@@ -1,82 +0,0 @@
#include "watchdog_thread.hpp"
#include "connection.hpp"
#include "irc_parse_thread.hpp"
#include "write_irc.hpp"
#include <boost/asio/steady_timer.hpp>
#include <chrono>
#include <memory>
using namespace std::chrono_literals;
WatchdogThread::WatchdogThread(Connection& connection)
: connection_{connection}
, timer_{connection.get_executor()}
, tried_ping{false}
{
}
auto WatchdogThread::on_activity() -> void
{
tried_ping = false;
timer_.expires_from_now(30s);
}
auto WatchdogThread::timeout_token()
{
return [weak = weak_from_this()](auto const& error)
{
if (not error)
{
if (auto self = weak.lock())
{
self->on_timeout();
}
}
};
}
auto WatchdogThread::on_timeout() -> void
{
if (tried_ping)
{
connection_.close();
}
else
{
send_ping(connection_, "watchdog");
tried_ping = true;
timer_.expires_from_now(30s);
timer_.async_wait(timeout_token());
}
}
auto WatchdogThread::on_connect() -> void
{
on_activity();
timer_.async_wait(timeout_token());
}
auto WatchdogThread::on_disconnect() -> void
{
timer_.cancel();
}
auto WatchdogThread::start(Connection& connection) -> void
{
auto const thread = std::make_shared<WatchdogThread>(connection);
connection.add_listener<ConnectEvent>([thread](auto&)
{
thread->on_connect();
});
connection.add_listener<DisconnectEvent>([thread](auto&)
{
thread->on_disconnect();
});
connection.add_listener<IrcMsgEvent>([thread](auto&)
{
thread->on_activity();
});
}

View File

@@ -1,24 +0,0 @@
#pragma once
#include <boost/asio/steady_timer.hpp>
#include <memory>
class Connection;
class WatchdogThread : std::enable_shared_from_this<WatchdogThread>
{
Connection& connection_;
boost::asio::steady_timer timer_;
bool tried_ping;
auto on_activity() -> void;
auto timeout_token();
auto on_timeout() -> void;
auto on_connect() -> void;
auto on_disconnect() -> void;
public:
WatchdogThread(Connection& connection);
static auto start(Connection& connection) -> void;
};

View File

@@ -1,97 +0,0 @@
#include "write_irc.hpp"
namespace {
auto write_irc(Connection& connection, std::string message) -> void
{
connection.write_line(std::move(message));
}
auto is_invalid_last(char x) -> bool
{
return x == '\0' || x == '\r' || x == '\n';
}
auto is_invalid(char x) -> bool
{
return x == '\0' || x == '\r' || x == '\n' || x == ' ';
}
auto write_irc(Connection& connection, std::string front, std::string_view last) -> void
{
if (last.end() != std::find_if(last.begin(), last.end(), is_invalid_last))
{
throw std::runtime_error{"bad irc argument"};
}
front += " :";
front += last;
write_irc(connection, std::move(front));
}
template <typename... Args>
auto write_irc(Connection& connection, std::string front, std::string_view next, Args ...rest) -> void
{
if (next.empty()
|| next.front() == ':'
|| next.end() != std::find_if(next.begin(), next.end(), is_invalid))
{
throw std::runtime_error{"bad irc argument"};
}
front += " ";
front += next;
write_irc(connection, std::move(front), rest...);
}
} // namespace
auto send_ping(Connection& connection, std::string_view txt) -> void
{
write_irc(connection, "PING", txt);
}
auto send_pong(Connection& connection, std::string_view txt) -> void
{
write_irc(connection, "PONG", txt);
}
auto send_pass(Connection& connection, std::string_view password) -> void
{
write_irc(connection, "PASS", password);
}
auto send_user(Connection& connection, std::string_view user, std::string_view real) -> void
{
write_irc(connection, "USER", user, "*", "*", real);
}
auto send_nick(Connection& connection, std::string_view nick) -> void
{
write_irc(connection, "NICK", nick);
}
auto send_cap_ls(Connection& connection) -> void
{
write_irc(connection, "CAP", "LS", "302");
}
auto send_cap_end(Connection& connection) -> void
{
write_irc(connection, "CAP", "END");
}
auto send_cap_req(Connection& connection, std::string_view caps) -> void
{
write_irc(connection, "CAP", "REQ", caps);
}
auto send_privmsg(Connection& connection, std::string_view target, std::string_view message) -> void
{
write_irc(connection, "PRIVMSG", target, message);
}
auto send_notice(Connection& connection, std::string_view target, std::string_view message) -> void
{
write_irc(connection, "NOTICE", target, message);
}

View File

@@ -1,14 +0,0 @@
#pragma once
#include "connection.hpp"
auto send_ping(Connection&, std::string_view) -> void;
auto send_pong(Connection&, std::string_view) -> void;
auto send_pass(Connection&, std::string_view) -> void;
auto send_user(Connection&, std::string_view, std::string_view) -> void;
auto send_nick(Connection&, std::string_view) -> void;
auto send_cap_ls(Connection&) -> void;
auto send_cap_end(Connection&) -> void;
auto send_cap_req(Connection&, std::string_view) -> void;
auto send_privmsg(Connection&, std::string_view, std::string_view) -> void;
auto send_notice(Connection&, std::string_view, std::string_view) -> void;