Compare commits

..

54 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
60 changed files with 5328 additions and 2140 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: []

View File

@@ -6,10 +6,11 @@ project(xbot
) )
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
find_package(OpenSSL REQUIRED)
pkg_check_modules(LIBHS libhs REQUIRED IMPORTED_TARGET) pkg_check_modules(LIBHS libhs REQUIRED IMPORTED_TARGET)
set(BOOST_INCLUDE_LIBRARIES asio log signals2) set(BOOST_INCLUDE_LIBRARIES asio log signals2 endian beast json)
set(BOOST_ENABLE_CMAKE ON) set(BOOST_ENABLE_CMAKE ON)
include(FetchContent) include(FetchContent)
FetchContent_Declare( FetchContent_Declare(
@@ -25,26 +26,7 @@ FetchContent_Declare(
FetchContent_MakeAvailable(tomlplusplus) FetchContent_MakeAvailable(tomlplusplus)
FetchContent_MakeAvailable(Boost) FetchContent_MakeAvailable(Boost)
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_subdirectory(mybase64) add_subdirectory(mybase64)
add_subdirectory(mysocks5)
add_executable(xbot add_subdirectory(myirc)
main.cpp irc_commands.inc ircmsg.cpp settings.cpp connection.cpp add_subdirectory(driver)
registration_thread.cpp
snote.cpp
self_thread.cpp
sasl_mechanism.cpp
irc_coroutine.cpp
)
target_include_directories(xbot PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(xbot PRIVATE Boost::signals2 Boost::log Boost::asio tomlplusplus_tomlplusplus PkgConfig::LIBHS mybase64)

View File

@@ -1,313 +0,0 @@
#include "connection.hpp"
#include "linebuffer.hpp"
#include <mybase64.hpp>
#include <boost/log/trivial.hpp>
namespace {
#include "irc_commands.inc"
} // namespace
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
{
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,
[this, strings = std::move(write_strings_)](boost::system::error_code const& error, std::size_t)
{
if (not error) {
if (write_strings_.empty()) {
write_posted_ = false;
} else {
write_buffers();
}
}
});
write_strings_.clear();
}
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);
BOOST_LOG_TRIVIAL(debug) << "CONNECTED: " << endpoint;
sig_connect();
}
// Start the queue writer after connection
watchdog();
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) {
BOOST_LOG_TRIVIAL(debug) << "RECV: " << line;
watchdog_activity();
dispatch_line(line);
});
}
watchdog_timer_.cancel();
stream_.close();
sig_disconnect();
}
auto Connection::watchdog() -> void
{
watchdog_timer_.expires_after(watchdog_duration);
watchdog_timer_.async_wait([this](auto const& error)
{
if (not error)
{
if (stalled_)
{
BOOST_LOG_TRIVIAL(debug) << "Watchdog timer elapsed, closing stream";
close();
}
else
{
send_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) -> void
{
auto const msg = parse_irc_message(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;
switch (command) {
// Respond to pings immediate and discard
case IrcCommand::PING:
send_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;
case IrcCommand::AUTHENTICATE:
on_authenticate(msg.args[0]);
break;
// Server notice generate snote events but not IRC command events
case IrcCommand::NOTICE:
if (auto match = snoteCore.match(msg)) {
sig_snote(*match);
break;
}
/* FALLTHROUGH */
// Normal IRC commands
default:
sig_ircmsg(command, msg);
break;
}
}
auto Connection::write_line(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::close() -> void
{
stream_.close();
}
static
auto is_invalid_last(char x) -> bool
{
return x == '\0' || x == '\r' || x == '\n';
}
auto Connection::write_irc(std::string message) -> void
{
write_line(std::move(message));
}
auto Connection::write_irc(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(std::move(front));
}
auto Connection::send_ping(std::string_view txt) -> void
{
write_irc("PING", txt);
}
auto Connection::send_pong(std::string_view txt) -> void
{
write_irc("PONG", txt);
}
auto Connection::send_pass(std::string_view password) -> void
{
write_irc("PASS", password);
}
auto Connection::send_user(std::string_view user, std::string_view real) -> void
{
write_irc("USER", user, "*", "*", real);
}
auto Connection::send_nick(std::string_view nick) -> void
{
write_irc("NICK", nick);
}
auto Connection::send_cap_ls() -> void
{
write_irc("CAP", "LS", "302");
}
auto Connection::send_cap_end() -> void
{
write_irc("CAP", "END");
}
auto Connection::send_cap_req(std::string_view caps) -> void
{
write_irc("CAP", "REQ", caps);
}
auto Connection::send_privmsg(std::string_view target, std::string_view message) -> void
{
write_irc("PRIVMSG", target, message);
}
auto Connection::send_notice(std::string_view target, std::string_view message) -> void
{
write_irc("NOTICE", target, message);
}
auto Connection::send_authenticate(std::string_view message) -> void
{
write_irc("AUTHENTICATE", message);
}
auto Connection::on_authenticate(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 (mybase64::decode(authenticate_buffer_, decoded.data(), &len))
{
decoded.resize(len);
sig_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 Connection::send_authenticate_abort() -> void
{
send_authenticate("*");
}
auto Connection::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);
}
}

View File

@@ -1,101 +0,0 @@
#pragma once
#include "ircmsg.hpp"
#include "irc_command.hpp"
#include "snote.hpp"
#include <boost/asio.hpp>
#include <boost/signals2.hpp>
#include <list>
#include <memory>
#include <string>
class Connection : public std::enable_shared_from_this<Connection>
{
private:
boost::asio::ip::tcp::socket 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_;
// AUTHENTICATE support
std::string authenticate_buffer_;
auto write_buffers() -> void;
auto dispatch_line(char * line) -> void;
static constexpr std::chrono::seconds watchdog_duration = std::chrono::seconds{30};
auto watchdog() -> void;
auto watchdog_activity() -> void;
/// Write bytes into the socket. Messages should be properly newline terminated.
auto write_line(std::string message) -> 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;
public:
Connection(boost::asio::io_context & io);
boost::signals2::signal<void()> sig_connect;
boost::signals2::signal<void()> sig_disconnect;
boost::signals2::signal<void(IrcCommand, const IrcMsg &)> sig_ircmsg;
boost::signals2::signal<void(SnoteMatch &)> sig_snote;
boost::signals2::signal<void(std::string_view)> sig_authenticate;
auto get_executor() -> boost::asio::any_io_executor {
return stream_.get_executor();
}
auto connect(
boost::asio::io_context & io,
std::string host,
std::string port
) -> boost::asio::awaitable<void>;
auto close() -> void;
auto on_authenticate(std::string_view) -> 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_cap_ls() -> void;
auto send_cap_end() -> void;
auto send_cap_req(std::string_view) -> void;
auto send_privmsg(std::string_view, 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;
};
template <typename... Args>
auto Connection::write_irc(std::string front, std::string_view next, Args ...rest) -> void
{
auto const is_invalid = [](char const x) -> bool
{
return x == '\0' || x == '\r' || x == '\n' || x == ' ';
};
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(std::move(front), rest...);
}

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,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, 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
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, 2, 2
251, IrcCommand::RPL_LUSERCLIENT, 2, 2
252, IrcCommand::RPL_LUSEROP
253, IrcCommand::RPL_LUSERUNKNOWN
254, IrcCommand::RPL_LUSERCHANNELS
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
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, 6, 6
369, IrcCommand::RPL_ENDOFWHOWAS, 3, 3
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, 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
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,21 +0,0 @@
#include "irc_coroutine.hpp"
auto irc_coroutine::is_running() -> bool {
return promise().connection_ != nullptr;
}
auto irc_coroutine::exception() -> std::exception_ptr {
return promise().exception_;
}
auto irc_coroutine::start(Connection& connection) -> void {
promise().connection_ = connection.shared_from_this();
resume();
}
void wait_ircmsg::stop() {
ircmsg_slot_.disconnect();
}
void wait_timeout::stop() {
timer_.reset();
}

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,64 +0,0 @@
#include "connection.hpp"
#include "settings.hpp"
#include <boost/asio.hpp>
#include <fstream>
#include <iostream>
#include <memory>
#include <string>
#include "registration_thread.hpp"
#include "self_thread.hpp"
using namespace std::chrono_literals;
auto start(boost::asio::io_context & io, Settings const& settings) -> void
{
auto const connection = std::make_shared<Connection>(io);
auto const selfThread = SelfThread::start(*connection);
RegistrationThread::start(*connection, settings, selfThread);
connection->sig_snote.connect([](auto &match) {
std::cout << "SNOTE " << static_cast<int>(match.get_tag()) << std::endl;
for (auto c : match.get_results())
{
std::cout << " " << std::string_view{c.first, c.second} << std::endl;
}
});
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_after(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();
}

View File

@@ -2,15 +2,14 @@
* @file mybase64.hpp * @file mybase64.hpp
* @author Eric Mertens (emertens@gmail.com) * @author Eric Mertens (emertens@gmail.com)
* @brief Base64 encoding and decoding * @brief Base64 encoding and decoding
* *
*/ */
#pragma once #pragma once
#include <cstddef> #include <cstddef>
#include <string_view> #include <string_view>
namespace mybase64 namespace mybase64 {
{
inline constexpr auto encoded_size(std::size_t len) -> std::size_t inline constexpr auto encoded_size(std::size_t len) -> std::size_t
{ {
@@ -24,7 +23,7 @@ inline constexpr auto decoded_size(std::size_t len) -> std::size_t
/** /**
* @brief Encode a string into base64 * @brief Encode a string into base64
* *
* @param input input text * @param input input text
* @param output Target buffer for encoded value * @param output Target buffer for encoded value
*/ */
@@ -32,13 +31,11 @@ auto encode(std::string_view input, char* output) -> void;
/** /**
* @brief Decode a base64 encoded string * @brief Decode a base64 encoded string
* *
* @param input Base64 input text * @param input Base64 input text
* @param output Target buffer for decoded value * @param output Target buffer for decoded value
* @param outlen Output parameter for decoded length * @return pointer to end of output on success
* @return true success
* @return false failure
*/ */
auto decode(std::string_view input, char* output, std::size_t* outlen) -> bool; auto decode(std::string_view input, char* output) -> char*;
} // namespace } // namespace

View File

@@ -1,113 +1,102 @@
#include "mybase64.hpp" #include "mybase64.hpp"
#include <array>
#include <climits> #include <climits>
#include <cstdint> #include <cstdint>
#include <string_view> #include <string_view>
namespace mybase64 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); static_assert(CHAR_BIT == 8);
auto encode(std::string_view const input, char* output) -> void auto encode(std::string_view const input, char* output) -> void
{ {
static char const* const alphabet = auto cursor = std::begin(input);
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" auto const end = std::end(input);
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
auto cursor = std::begin(input); while (end - cursor >= 3)
auto const end = std::end(input); {
std::uint32_t buffer = std::uint8_t(*cursor++);
buffer <<= 8;
buffer |= std::uint8_t(*cursor++);
buffer <<= 8;
buffer |= std::uint8_t(*cursor++);
while (end - cursor >= 3) *output++ = alphabet[(buffer >> 6 * 3) % 64];
{ *output++ = alphabet[(buffer >> 6 * 2) % 64];
uint32_t buffer = uint8_t(*cursor++); *output++ = alphabet[(buffer >> 6 * 1) % 64];
buffer <<= 8; buffer |= uint8_t(*cursor++); *output++ = alphabet[(buffer >> 6 * 0) % 64];
buffer <<= 8; buffer |= uint8_t(*cursor++); }
*output++ = alphabet[(buffer >> 6 * 3) % 64]; if (cursor < end)
*output++ = alphabet[(buffer >> 6 * 2) % 64]; {
*output++ = alphabet[(buffer >> 6 * 1) % 64]; std::uint32_t buffer = std::uint8_t(*cursor++) << 10;
*output++ = alphabet[(buffer >> 6 * 0) % 64]; if (cursor < end)
} buffer |= std::uint8_t(*cursor) << 2;
if (cursor < end) *output++ = alphabet[(buffer >> 12) % 64];
{ *output++ = alphabet[(buffer >> 6) % 64];
uint32_t buffer = uint8_t(*cursor++) << 10; *output++ = cursor < end ? alphabet[(buffer % 64)] : '=';
if (cursor < end) buffer |= uint8_t(*cursor) << 2; *output++ = '=';
}
*output++ = alphabet[(buffer >> 12) % 64]; *output = '\0';
*output++ = alphabet[(buffer >> 6) % 64];
*output++ = cursor < end ? alphabet[(buffer % 64)] : '=';
*output++ = '=';
}
*output = '\0';
} }
auto decode(std::string_view const input, char* const output, std::size_t* const outlen) -> bool auto decode(std::string_view const input, char* output) -> char*
{ {
static int8_t const alphabet_values[] = { std::uint32_t buffer = 1;
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, 0x3e, -1, -1, -1, 0x3f,
0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b,
0x3c, 0x3d, -1, -1, -1, -1, -1, -1,
-1, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
0x17, 0x18, 0x19, -1, -1, -1, -1, -1,
-1, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30,
0x31, 0x32, 0x33, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
};
uint32_t buffer = 1; for (auto const c : input)
char* cursor = output; {
if (auto const value = alphabet_values[uint8_t(c)]; -1 != value)
for (char c : input) { {
int8_t const value = alphabet_values[uint8_t(c)]; buffer = (buffer << 6) | value;
if (-1 == value) continue; if (buffer & 1 << 6 * 4)
{
buffer = (buffer << 6) | value; *output++ = buffer >> 16;
*output++ = buffer >> 8;
if (buffer & 1<<6*4) { *output++ = buffer >> 0;
*cursor++ = buffer >> 8*2; buffer = 1;
*cursor++ = buffer >> 8*1; }
*cursor++ = buffer >> 8*0;
buffer = 1;
} }
} }
if (buffer & 1<<6*3) { if (buffer & 1 << 6 * 3)
*cursor++ = buffer >> 10; {
*cursor++ = buffer >> 2; *output++ = buffer >> 10;
} else if (buffer & 1<<6*2) { *output++ = buffer >> 2;
*cursor++ = buffer >> 4;
} else if (buffer & 1<<6*1) {
return false;
} }
*outlen = cursor - output; else if (buffer & 1 << 6 * 2)
return true; {
*output++ = buffer >> 4;
}
else if (buffer & 1 << 6 * 1)
{
return nullptr;
}
return output;
} }
} // namespace } // 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 #pragma once
namespace myirc {
template <typename> struct CCallback_; template <typename> struct CCallback_;
template <typename F, typename R, typename... Ts> template <typename F, typename R, typename... Ts>
struct CCallback_<R (F::*) (Ts...) const> 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> template <typename F>
using CCallback = CCallback_<decltype(&F::operator())>; 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,3 +1,7 @@
#pragma once
namespace myirc {
enum class IrcCommand enum class IrcCommand
{ {
UNKNOWN, UNKNOWN,
@@ -9,7 +13,6 @@ enum class IrcCommand
RPL_SNOMASK, RPL_SNOMASK,
RPL_REDIR, RPL_REDIR,
RPL_MAP, RPL_MAP,
RPL_MAPMORE,
RPL_MAPEND, RPL_MAPEND,
RPL_SAVENICK, RPL_SAVENICK,
RPL_TRACELINK, RPL_TRACELINK,
@@ -24,7 +27,6 @@ enum class IrcCommand
RPL_STATSLINKINFO, RPL_STATSLINKINFO,
RPL_STATSCOMMANDS, RPL_STATSCOMMANDS,
RPL_STATSCLINE, RPL_STATSCLINE,
RPL_STATSNLINE,
RPL_STATSILINE, RPL_STATSILINE,
RPL_STATSKLINE, RPL_STATSKLINE,
RPL_STATSQLINE, RPL_STATSQLINE,
@@ -32,15 +34,11 @@ enum class IrcCommand
RPL_ENDOFSTATS, RPL_ENDOFSTATS,
RPL_STATSPLINE, RPL_STATSPLINE,
RPL_UMODEIS, RPL_UMODEIS,
RPL_STATSFLINE,
RPL_STATSDLINE, RPL_STATSDLINE,
RPL_SERVLIST,
RPL_SERVLISTEND,
RPL_STATSLLINE, RPL_STATSLLINE,
RPL_STATSUPTIME, RPL_STATSUPTIME,
RPL_STATSOLINE, RPL_STATSOLINE,
RPL_STATSHLINE, RPL_STATSHLINE,
RPL_STATSSLINE,
RPL_STATSXLINE, RPL_STATSXLINE,
RPL_STATSULINE, RPL_STATSULINE,
RPL_STATSDEBUG, RPL_STATSDEBUG,
@@ -54,7 +52,6 @@ enum class IrcCommand
RPL_ADMINLOC1, RPL_ADMINLOC1,
RPL_ADMINLOC2, RPL_ADMINLOC2,
RPL_ADMINEMAIL, RPL_ADMINEMAIL,
RPL_TRACELOG,
RPL_ENDOFTRACE, RPL_ENDOFTRACE,
RPL_LOAD2HI, RPL_LOAD2HI,
RPL_LOCALUSERS, RPL_LOCALUSERS,
@@ -63,11 +60,9 @@ enum class IrcCommand
RPL_WHOISCERTFP, RPL_WHOISCERTFP,
RPL_ACCEPTLIST, RPL_ACCEPTLIST,
RPL_ENDOFACCEPT, RPL_ENDOFACCEPT,
RPL_NONE,
RPL_AWAY, RPL_AWAY,
RPL_USERHOST, RPL_USERHOST,
RPL_ISON, RPL_ISON,
RPL_TEXT,
RPL_UNAWAY, RPL_UNAWAY,
RPL_NOWAWAY, RPL_NOWAWAY,
RPL_WHOISHELPOP, RPL_WHOISHELPOP,
@@ -76,7 +71,6 @@ enum class IrcCommand
RPL_WHOISOPERATOR, RPL_WHOISOPERATOR,
RPL_WHOWASUSER, RPL_WHOWASUSER,
RPL_ENDOFWHOWAS, RPL_ENDOFWHOWAS,
RPL_WHOISCHANOP,
RPL_WHOISIDLE, RPL_WHOISIDLE,
RPL_ENDOFWHOIS, RPL_ENDOFWHOIS,
RPL_WHOISCHANNELS, RPL_WHOISCHANNELS,
@@ -92,10 +86,8 @@ enum class IrcCommand
RPL_NOTOPIC, RPL_NOTOPIC,
RPL_TOPIC, RPL_TOPIC,
RPL_TOPICWHOTIME, RPL_TOPICWHOTIME,
RPL_WHOISTEXT,
RPL_WHOISACTUALLY, RPL_WHOISACTUALLY,
RPL_INVITING, RPL_INVITING,
RPL_SUMMONING,
RPL_INVITELIST, RPL_INVITELIST,
RPL_ENDOFINVITELIST, RPL_ENDOFINVITELIST,
RPL_EXCEPTLIST, RPL_EXCEPTLIST,
@@ -107,7 +99,6 @@ enum class IrcCommand
RPL_NAMREPLY, RPL_NAMREPLY,
RPL_WHOWASREAL, RPL_WHOWASREAL,
RPL_ENDOFNAMES, RPL_ENDOFNAMES,
RPL_KILLDONE,
RPL_CLOSING, RPL_CLOSING,
RPL_CLOSEEND, RPL_CLOSEEND,
RPL_LINKS, RPL_LINKS,
@@ -116,21 +107,14 @@ enum class IrcCommand
RPL_ENDOFBANLIST, RPL_ENDOFBANLIST,
RPL_INFO, RPL_INFO,
RPL_MOTD, RPL_MOTD,
RPL_INFOSTART,
RPL_ENDOFINFO, RPL_ENDOFINFO,
RPL_MOTDSTART, RPL_MOTDSTART,
RPL_ENDOFMOTD, RPL_ENDOFMOTD,
RPL_WHOISHOST, RPL_WHOISHOST,
RPL_YOUREOPER, RPL_YOUREOPER,
RPL_REHASHING, RPL_REHASHING,
RPL_MYPORTIS,
RPL_NOTOPERANYMORE,
RPL_RSACHALLENGE, RPL_RSACHALLENGE,
RPL_TIME, RPL_TIME,
RPL_USERSSTART,
RPL_USERS,
RPL_ENDOFUSERS,
RPL_NOUSERS,
RPL_HOSTHIDDEN, RPL_HOSTHIDDEN,
ERR_NOSUCHNICK, ERR_NOSUCHNICK,
ERR_NOSUCHSERVER, ERR_NOSUCHSERVER,
@@ -149,8 +133,6 @@ enum class IrcCommand
ERR_TOOMANYMATCHES, ERR_TOOMANYMATCHES,
ERR_UNKNOWNCOMMAND, ERR_UNKNOWNCOMMAND,
ERR_NOMOTD, ERR_NOMOTD,
ERR_NOADMININFO,
ERR_FILEERROR,
ERR_NONICKNAMEGIVEN, ERR_NONICKNAMEGIVEN,
ERR_ERRONEUSNICKNAME, ERR_ERRONEUSNICKNAME,
ERR_NICKNAMEINUSE, ERR_NICKNAMEINUSE,
@@ -162,27 +144,20 @@ enum class IrcCommand
ERR_USERNOTINCHANNEL, ERR_USERNOTINCHANNEL,
ERR_NOTONCHANNEL, ERR_NOTONCHANNEL,
ERR_USERONCHANNEL, ERR_USERONCHANNEL,
ERR_NOLOGIN,
ERR_SUMMONDISABLED,
ERR_USERSDISABLED,
ERR_NOTREGISTERED, ERR_NOTREGISTERED,
ERR_ACCEPTFULL, ERR_ACCEPTFULL,
ERR_ACCEPTEXIST, ERR_ACCEPTEXIST,
ERR_ACCEPTNOT, ERR_ACCEPTNOT,
ERR_NEEDMOREPARAMS, ERR_NEEDMOREPARAMS,
ERR_ALREADYREGISTRED, ERR_ALREADYREGISTRED,
ERR_NOPERMFORHOST,
ERR_PASSWDMISMATCH, ERR_PASSWDMISMATCH,
ERR_YOUREBANNEDCREEP, ERR_YOUREBANNEDCREEP,
ERR_YOUWILLBEBANNED,
ERR_KEYSET,
ERR_LINKCHANNEL, ERR_LINKCHANNEL,
ERR_CHANNELISFULL, ERR_CHANNELISFULL,
ERR_UNKNOWNMODE, ERR_UNKNOWNMODE,
ERR_INVITEONLYCHAN, ERR_INVITEONLYCHAN,
ERR_BANNEDFROMCHAN, ERR_BANNEDFROMCHAN,
ERR_BADCHANNELKEY, ERR_BADCHANNELKEY,
ERR_BADCHANMASK,
ERR_NEEDREGGEDNICK, ERR_NEEDREGGEDNICK,
ERR_BANLISTFULL, ERR_BANLISTFULL,
ERR_BADCHANNAME, ERR_BADCHANNAME,
@@ -191,7 +166,6 @@ enum class IrcCommand
ERR_CHANOPRIVSNEEDED, ERR_CHANOPRIVSNEEDED,
ERR_CANTKILLSERVER, ERR_CANTKILLSERVER,
ERR_ISCHANSERVICE, ERR_ISCHANSERVICE,
ERR_BANNEDNICK,
ERR_NONONREG, ERR_NONONREG,
ERR_VOICENEEDED, ERR_VOICENEEDED,
ERR_NOOPERHOST, ERR_NOOPERHOST,
@@ -199,7 +173,6 @@ enum class IrcCommand
ERR_OWNMODE, ERR_OWNMODE,
ERR_UMODEUNKNOWNFLAG, ERR_UMODEUNKNOWNFLAG,
ERR_USERSDONTMATCH, ERR_USERSDONTMATCH,
ERR_GHOSTEDCLIENT,
ERR_USERNOTONSERV, ERR_USERNOTONSERV,
ERR_WRONGPONG, ERR_WRONGPONG,
ERR_DISABLED, ERR_DISABLED,
@@ -228,10 +201,9 @@ enum class IrcCommand
RPL_OMOTD, RPL_OMOTD,
RPL_ENDOFOMOTD, RPL_ENDOFOMOTD,
ERR_NOPRIVS, ERR_NOPRIVS,
RPL_TESTMASK,
RPL_TESTLINE, RPL_TESTLINE,
RPL_NOTESTLINE, RPL_NOTESTLINE,
RPL_TESTMASKGECO, RPL_TESTMASKGECOS,
RPL_QUIETLIST, RPL_QUIETLIST,
RPL_ENDOFQUIETLIS, RPL_ENDOFQUIETLIS,
RPL_MONONLINE, RPL_MONONLINE,
@@ -243,7 +215,6 @@ enum class IrcCommand
RPL_ENDOFRSACHALLENGE2, RPL_ENDOFRSACHALLENGE2,
ERR_MLOCKRESTRICTE, ERR_MLOCKRESTRICTE,
ERR_INVALIDBAN, ERR_INVALIDBAN,
ERR_TOPICLOCK,
RPL_SCANMATCHED, RPL_SCANMATCHED,
RPL_SCANUMODES, RPL_SCANUMODES,
RPL_LOGGEDIN, RPL_LOGGEDIN,
@@ -263,15 +234,22 @@ enum class IrcCommand
CAP, CAP,
CHGHOST, CHGHOST,
ERROR, ERROR,
INVITE,
JOIN, JOIN,
KICK, KICK,
KILL,
MODE, MODE,
NICK, NICK,
NOTICE, NOTICE,
PART, PART,
PING, PING,
PONG,
PRIVMSG, PRIVMSG,
QUIT, QUIT,
SETNAME, SETNAME,
TAGMSG,
TOPIC, TOPIC,
WALLOPS,
}; };
} // namespace myirc

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "connection.hpp" #include "myirc/connection.hpp"
#include "myirc/snote.hpp"
#include <chrono> #include <chrono>
#include <coroutine> #include <coroutine>
@@ -8,10 +9,13 @@
#include <utility> #include <utility>
#include <vector> #include <vector>
namespace myirc {
struct irc_promise; struct irc_promise;
/// A coroutine that can co_await on various IRC events /// A coroutine that can co_await on various IRC events
struct irc_coroutine : std::coroutine_handle<irc_promise> { struct irc_coroutine : std::coroutine_handle<irc_promise>
{
using promise_type = irc_promise; using promise_type = irc_promise;
/// Start the coroutine and associate it with a specific connection. /// Start the coroutine and associate it with a specific connection.
@@ -44,22 +48,25 @@ struct irc_promise
auto final_suspend() noexcept -> std::suspend_always { return {}; } auto final_suspend() noexcept -> std::suspend_always { return {}; }
// Normal termination // Normal termination
auto return_void() -> void { auto return_void() -> void
{
connection_.reset(); connection_.reset();
} }
// Abnormal termination - remember the exception // Abnormal termination - remember the exception
auto unhandled_exception() -> void { auto unhandled_exception() -> void
{
connection_.reset(); connection_.reset();
exception_ = std::current_exception(); exception_ = std::current_exception();
} }
}; };
template<typename ... Ts> template <typename... Ts>
class Wait; class Wait;
/// Argument to a Wait that expects one or more IRC messages /// Argument to a Wait that expects one or more IRC messages
class wait_ircmsg { class wait_ircmsg
{
// Vector of commands this wait is expecting. Leave empty to accept all messages. // Vector of commands this wait is expecting. Leave empty to accept all messages.
std::vector<IrcCommand> want_cmds_; std::vector<IrcCommand> want_cmds_;
@@ -69,27 +76,59 @@ class wait_ircmsg {
public: public:
using result_type = std::pair<IrcCommand, const IrcMsg &>; using result_type = std::pair<IrcCommand, const IrcMsg &>;
wait_ircmsg(std::initializer_list<IrcCommand> want_cmds) : want_cmds_{want_cmds} {} wait_ircmsg(std::initializer_list<IrcCommand> want_cmds)
: want_cmds_{want_cmds}
{
}
template <size_t I, typename... Ts> auto start(Wait<Ts...>& command) -> void; template <size_t I, typename... Ts>
auto stop() -> void; auto start(Wait<Ts...> &command) -> void;
auto stop() -> void { ircmsg_slot_.disconnect(); }
}; };
class wait_timeout { 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::optional<boost::asio::steady_timer> timer_;
std::chrono::milliseconds timeout_; std::chrono::milliseconds timeout_;
public: public:
struct result_type {}; struct result_type
wait_timeout(std::chrono::milliseconds timeout) : timeout_{timeout} {} {
};
wait_timeout(std::chrono::milliseconds timeout)
: timeout_{timeout}
{
}
template <size_t I, typename... Ts> auto start(Wait<Ts...>& command) -> void; template <size_t I, typename... Ts>
auto stop() -> void; auto start(Wait<Ts...> &command) -> void;
auto stop() -> void { timer_->cancel(); }
}; };
template<typename ... Ts> template <typename... Ts>
class Wait { class Wait
{
// State associated with each wait mode // State associated with each wait mode
std::tuple<Ts...> modes_; std::tuple<Ts...> modes_;
@@ -105,31 +144,39 @@ class Wait {
boost::signals2::scoped_connection disconnect_slot_; boost::signals2::scoped_connection disconnect_slot_;
template <size_t I> template <size_t I>
auto start_mode() -> void { auto start_mode() -> void
{
std::get<I>(modes_).template start<I, Ts...>(*this); std::get<I>(modes_).template start<I, Ts...>(*this);
} }
template <std::size_t... Indices> template <std::size_t... Indices>
auto start_modes(std::index_sequence<Indices...>) -> void { auto start_modes(std::index_sequence<Indices...>) -> void
{
(start_mode<Indices>(), ...); (start_mode<Indices>(), ...);
} }
template <std::size_t... Indices> template <std::size_t... Indices>
auto stop_modes(std::index_sequence<Indices...>) -> void { auto stop_modes(std::index_sequence<Indices...>) -> void
{
(std::get<Indices>(modes_).stop(), ...); (std::get<Indices>(modes_).stop(), ...);
} }
public: public:
Wait(Ts &&...modes) : modes_{std::forward<Ts>(modes)...} {} Wait(Ts &&...modes)
: modes_{std::forward<Ts>(modes)...}
{
}
// Get the connection that this coroutine was started with. // Get the connection that this coroutine was started with.
auto get_connection() const -> Connection & { auto get_connection() const -> Connection &
{
return *handle_.promise().connection_; return *handle_.promise().connection_;
} }
// Store a successful result and resume the coroutine // Store a successful result and resume the coroutine
template <size_t I, typename... Args> template <size_t I, typename... Args>
auto complete(Args &&...args) -> void { auto complete(Args &&...args) -> void
{
result_.emplace(std::in_place_index<I>, std::forward<Args>(args)...); result_.emplace(std::in_place_index<I>, std::forward<Args>(args)...);
handle_.resume(); handle_.resume();
} }
@@ -148,23 +195,35 @@ template <size_t I, typename... Ts>
auto wait_ircmsg::start(Wait<Ts...> &command) -> void auto wait_ircmsg::start(Wait<Ts...> &command) -> void
{ {
ircmsg_slot_ = command.get_connection().sig_ircmsg.connect([this, &command](auto cmd, auto &msg) { ircmsg_slot_ = command.get_connection().sig_ircmsg.connect([this, &command](auto cmd, auto &msg) {
auto const wanted = const auto wanted = want_cmds_.empty() || std::find(want_cmds_.begin(), want_cmds_.end(), cmd) != want_cmds_.end();
want_cmds_.empty() || if (wanted)
std::find(want_cmds_.begin(), want_cmds_.end(), cmd) != want_cmds_.end(); {
if (wanted) {
command.template complete<I>(cmd, msg); command.template complete<I>(cmd, msg);
} }
}); });
} }
template <size_t I, typename... Ts> template <size_t I, typename... Ts>
auto wait_timeout::start(Wait<Ts...>& command) -> void 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_.emplace(command.get_connection().get_executor());
timer_->expires_after(timeout_); timer_->expires_after(timeout_);
timer_->async_wait([this, &command](auto const& error) timer_->async_wait([this, &command](const auto &error) {
{ if (not error)
if (not error) { {
timer_.reset(); timer_.reset();
command.template complete<I>(); command.template complete<I>();
} }
@@ -176,10 +235,10 @@ auto Wait<Ts...>::await_suspend(std::coroutine_handle<irc_promise> handle) -> vo
{ {
handle_ = handle; handle_ = handle;
auto const tuple_size = std::tuple_size_v<decltype(modes_)>; const auto tuple_size = std::tuple_size_v<decltype(modes_)>;
start_modes(std::make_index_sequence<tuple_size>{}); start_modes(std::make_index_sequence<tuple_size>{});
disconnect_slot_ = get_connection().sig_disconnect.connect([this]() { disconnect_slot_ = get_connection().sig_disconnect.connect([this](auto) {
handle_.resume(); handle_.resume();
}); });
} }
@@ -187,14 +246,37 @@ auto Wait<Ts...>::await_suspend(std::coroutine_handle<irc_promise> handle) -> vo
template <typename... Ts> template <typename... Ts>
auto Wait<Ts...>::await_resume() -> std::variant<typename Ts::result_type...> auto Wait<Ts...>::await_resume() -> std::variant<typename Ts::result_type...>
{ {
auto const tuple_size = std::tuple_size_v<decltype(modes_)>; const auto tuple_size = std::tuple_size_v<decltype(modes_)>;
stop_modes(std::make_index_sequence<tuple_size>{}); stop_modes(std::make_index_sequence<tuple_size>{});
disconnect_slot_.disconnect(); disconnect_slot_.disconnect();
if (result_) { if (result_)
{
return std::move(*result_); return std::move(*result_);
} else { }
else
{
throw std::runtime_error{"connection terminated"}; 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,191 +0,0 @@
#include "registration_thread.hpp"
#include "connection.hpp"
#include "ircmsg.hpp"
#include "sasl_mechanism.hpp"
#include <memory>
#include <unordered_map>
#include <unordered_set>
RegistrationThread::RegistrationThread(
Connection& connection,
const Settings &settings,
std::shared_ptr<SelfThread> self
)
: connection_{connection}
, settings_{settings}
, self_{std::move(self)}
{
}
auto RegistrationThread::on_connect() -> void
{
connection_.send_cap_ls();
connection_.send_pass(settings_.password);
connection_.send_user(settings_.username, settings_.realname);
connection_.send_nick(settings_.nickname);
}
auto RegistrationThread::send_req() -> void
{
std::string request;
std::vector<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",
};
if (settings_.sasl_mechanism == "PLAIN") {
want.push_back("sasl");
}
for (auto const cap : want)
{
if (caps.contains(cap))
{
request.append(cap);
request.push_back(' ');
outstanding.insert(cap);
}
}
if (not outstanding.empty())
{
request.pop_back();
connection_.send_cap_req(request);
listen_for_cap_ack();
}
else
{
connection_.send_cap_end();
}
}
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())
{
message_handle_.disconnect();
if (settings_.sasl_mechanism.empty()) {
connection_.send_cap_end();
} else {
self_->start_sasl(std::make_unique<SaslPlain>(settings_.sasl_authcid, settings_.sasl_authzid, settings_.sasl_password));
connection_.sig_ircmsg.connect_extended([thread = shared_from_this()](auto &slot, auto cmd, auto &msg) {
switch (cmd) {
default: break;
case IrcCommand::RPL_SASLSUCCESS:
case IrcCommand::ERR_SASLFAIL:
thread->connection_.send_cap_end();
slot.disconnect();
}
});
}
}
}
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)
{
message_handle_.disconnect();
send_req();
}
}
auto RegistrationThread::start(
Connection& connection,
const Settings &settings,
std::shared_ptr<SelfThread> self
) -> std::shared_ptr<RegistrationThread>
{
auto const thread = std::make_shared<RegistrationThread>(connection, std::move(settings), std::move(self));
thread->listen_for_cap_ls();
thread->connect_handle_ = connection.sig_connect.connect([thread]()
{
thread->connect_handle_.disconnect();
thread->on_connect();
});
return thread;
}
auto RegistrationThread::listen_for_cap_ack() -> void
{
message_handle_ = connection_.sig_ircmsg.connect([thread = shared_from_this()](IrcCommand cmd, IrcMsg const& msg)
{
if (IrcCommand::CAP == cmd && msg.args.size() >= 2 && "ACK" == msg.args[1])
{
thread->on_msg_cap_ack(msg);
}
});
}
auto RegistrationThread::listen_for_cap_ls() -> void
{
message_handle_ = connection_.sig_ircmsg.connect([thread = shared_from_this()](IrcCommand cmd, IrcMsg const& msg)
{
if (IrcCommand::CAP == cmd && msg.args.size() >= 2 && "LS" == msg.args[1])
{
thread->on_msg_cap_ls(msg);
}
});
}

View File

@@ -1,44 +0,0 @@
#pragma once
#include "connection.hpp"
#include "settings.hpp"
#include "self_thread.hpp"
#include <memory>
#include <string>
#include <unordered_map>
#include <unordered_set>
class RegistrationThread : public std::enable_shared_from_this<RegistrationThread>
{
Connection& connection_;
const Settings &settings_;
std::shared_ptr<SelfThread> self_;
std::unordered_map<std::string, std::string> caps;
std::unordered_set<std::string> outstanding;
boost::signals2::scoped_connection connect_handle_;
boost::signals2::scoped_connection 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_,
const Settings &,
std::shared_ptr<SelfThread> self
);
static auto start(
Connection& connection,
const Settings &,
std::shared_ptr<SelfThread> self
) -> std::shared_ptr<RegistrationThread>;
};

View File

@@ -1,19 +0,0 @@
#include "sasl_mechanism.hpp"
auto SaslPlain::step(std::string_view msg) -> std::optional<std::string> {
if (complete_) {
return std::nullopt;
} else {
std::string reply;
reply += authzid_;
reply += '\0';
reply += authcid_;
reply += '\0';
reply += password_;
complete_ = true;
return {std::move(reply)};
}
}

View File

@@ -1,50 +0,0 @@
#pragma once
#include <boost/signals2.hpp>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include "event.hpp"
struct Connection;
class SaslMechanism
{
public:
virtual ~SaslMechanism() {}
virtual auto mechanism_name() const -> std::string = 0;
virtual auto step(std::string_view msg) -> std::optional<std::string> = 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) -> std::optional<std::string> override;
auto is_complete() const -> bool override
{
return complete_;
}
};

View File

@@ -1,191 +0,0 @@
#include "self_thread.hpp"
#include "connection.hpp"
#include <mybase64.hpp>
#include <boost/container/flat_map.hpp>
#include <boost/log/trivial.hpp>
using namespace std::literals;
auto SelfThread::on_welcome(IrcMsg const& irc) -> void
{
nickname_ = irc.args[0];
}
auto SelfThread::on_nick(IrcMsg const& irc) -> void
{
if (is_my_mask(irc.source))
{
nickname_ = irc.args[0];
}
}
auto SelfThread::on_umodeis(IrcMsg const& irc) -> void
{
mode_ = irc.args[1];
}
auto SelfThread::on_join(IrcMsg const& irc) -> void
{
if (is_my_mask(irc.source))
{
channels_.insert(std::string{irc.args[0]});
}
}
auto SelfThread::on_kick(IrcMsg const& irc) -> void
{
if (is_my_nick(irc.args[1]))
{
channels_.erase(std::string{irc.args[0]});
}
}
auto SelfThread::on_part(IrcMsg const& irc) -> void
{
if (is_my_mask(irc.source))
{
channels_.erase(std::string{irc.args[0]});
}
}
auto SelfThread::on_mode(IrcMsg const& irc) -> void
{
if (is_my_nick(irc.args[0]))
{
auto polarity = true;
for (char const c : irc.args[1])
{
switch (c)
{
case '+':
polarity = true;
break;
case '-':
polarity = false;
break;
default:
if (polarity)
{
mode_ += c;
}
else
{
auto const ix = mode_.find(c);
if (ix != std::string::npos)
{
mode_.erase(ix, 1);
}
}
break;
}
}
}
}
auto SelfThread::on_isupport(const IrcMsg &msg) -> void
{
auto const 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("-")) {
auto const key = std::string{entry.substr(1)};
if (auto cursor = isupport_.find(key); cursor != isupport_.end()) {
isupport_.erase(cursor);
}
} else if (auto const cursor = entry.find('='); cursor != entry.npos) {
isupport_.emplace(entry.substr(0, cursor), entry.substr(cursor+1));
} else {
isupport_.emplace(entry, std::string{});
}
}
}
auto SelfThread::start(Connection& connection) -> std::shared_ptr<SelfThread>
{
auto thread = std::make_shared<SelfThread>(connection);
connection.sig_ircmsg.connect([thread](auto cmd, auto& msg)
{
switch (cmd)
{
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;
default: break;
}
});
connection.sig_authenticate.connect([thread](auto msg) {
thread->on_authenticate(msg);
});
return thread;
}
auto SelfThread::get_my_nickname() const -> std::string const&
{
return nickname_;
}
auto SelfThread::get_my_mode() const -> std::string const&
{
return mode_;
}
auto SelfThread::get_my_channels() const -> std::unordered_set<std::string> const&
{
return channels_;
}
auto SelfThread::is_my_nick(std::string_view nick) const -> bool
{
return nick == nickname_;
}
auto SelfThread::is_my_mask(std::string_view mask) const -> bool
{
auto const bang = mask.find('!');
return bang != std::string_view::npos && nickname_ == mask.substr(0, bang);
}
auto SelfThread::on_authenticate(const std::string_view body) -> void
{
if (not sasl_mechanism_)
{
BOOST_LOG_TRIVIAL(warning) << "Unexpected AUTHENTICATE from server"sv;
connection_.send_authenticate_abort();
return;
}
if (auto reply = sasl_mechanism_->step(body)) {
connection_.send_authenticate_encoded(*reply);
// Clean up completed SASL transactions
if (sasl_mechanism_->is_complete())
{
sasl_mechanism_.reset();
}
}
}
auto SelfThread::start_sasl(std::unique_ptr<SaslMechanism> mechanism) -> void
{
if (sasl_mechanism_) {
connection_.send_authenticate("*"sv); // abort SASL
}
sasl_mechanism_ = std::move(mechanism);
connection_.send_authenticate(sasl_mechanism_->mechanism_name());
}

View File

@@ -1,52 +0,0 @@
#pragma once
#include "connection.hpp"
#include "sasl_mechanism.hpp"
#include <string>
#include <unordered_set>
struct Connection;
struct IrcMsg;
/**
* @brief Thread to track this connection's identity, and IRC state.
*
*/
class SelfThread
{
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_;
auto on_welcome(IrcMsg const& irc) -> void;
auto on_isupport(IrcMsg const& irc) -> void;
auto on_nick(IrcMsg const& irc) -> void;
auto on_umodeis(IrcMsg const& irc) -> void;
auto on_join(IrcMsg const& irc) -> void;
auto on_kick(IrcMsg const& irc) -> void;
auto on_part(IrcMsg const& irc) -> void;
auto on_mode(IrcMsg const& irc) -> void;
auto on_authenticate(std::string_view) -> void;
public:
SelfThread(Connection& connection) : connection_{connection} {}
static auto start(Connection&) -> std::shared_ptr<SelfThread>;
auto start_sasl(std::unique_ptr<SaslMechanism> mechanism) -> void;
auto get_my_nickname() const -> std::string const&;
auto get_my_mode() const -> std::string const&;
auto get_my_channels() const -> std::unordered_set<std::string> const&;
auto is_my_nick(std::string_view nick) const -> bool;
auto is_my_mask(std::string_view nick) const -> bool;
};

View File

@@ -1,21 +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{}),
.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{})
};
}

View File

@@ -1,22 +0,0 @@
#pragma once
#include <string>
#include <istream>
struct Settings
{
std::string host;
std::string 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;
static auto from_stream(std::istream & in) -> Settings;
};

205
snote.cpp
View File

@@ -1,205 +0,0 @@
#include "snote.hpp"
#include "c_callback.hpp"
#include <hs.h>
#include <boost/log/trivial.hpp>
#include <cstdlib>
#include <cstring>
#include <optional>
#include <regex>
#include <stdexcept>
#include <utility>
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 static 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: ([^ ]+) \((.*)\)$)"},
{SnoteTag::TooManyGlobalConnections,
R"(^Too many global connections for ([^ ]+)\[([^ ]+)@([^ ]+)\] \[(.*)\]$)"},
{SnoteTag::SetVhostOnMarkedAccount,
"^\x02([^ ]+)\x02 set vhost ([^ ]+) on the \x02MARKED\x02 account ([^ ]+).$"},
};
static 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
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 char const* const prefix = "*** Notice -- ";
auto& args = msg.args;
if ("*" != args[0] || !args[1].starts_with(prefix)) {
return std::nullopt;
}
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(
db_.get(),
message.data(), 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() -> std::match_results<std::string_view::const_iterator> const&
{
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;

View File

@@ -1,71 +0,0 @@
#pragma once
#include "ircmsg.hpp"
#include <memory>
#include <optional>
#include <regex>
#include <string_view>
#include <utility>
#include <variant>
struct hs_database;
struct hs_scratch;
enum class SnoteTag
{
ClientConnecting,
ClientExiting,
RejectingKlined,
NickChange,
CreateChannel,
TemporaryKlineExpired,
PropagatedBanExpired,
DisconnectingKlined,
NewPropagatedKline,
NewTemporaryKline,
LoginAttempts,
PossibleFlooder,
Killed,
TooManyGlobalConnections,
SetVhostOnMarkedAccount,
};
class SnoteMatch
{
SnoteTag tag_;
std::variant<std::pair<std::regex const&, std::string_view>, std::match_results<std::string_view::const_iterator>> components_;
public:
SnoteMatch(SnoteTag tag, std::regex const& regex, std::string_view full)
: tag_{tag}
, components_{std::make_pair(std::ref(regex), full)}
{}
auto get_tag() -> SnoteTag { return tag_; }
auto get_results() -> std::match_results<std::string_view::const_iterator> const&;
};
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;