Compare commits
77 Commits
7c6ef18a6c
...
main
Author | SHA1 | Date | |
---|---|---|---|
7793e8b02c | |||
04a092b9a3 | |||
4fc8d4d49c | |||
44ef4c0689 | |||
847a149e12 | |||
2b4bb1f071 | |||
5f2439e5af | |||
5aec8397bb | |||
39a4d84a54 | |||
5cfb47ce92 | |||
53771396ca | |||
178d7dfcfe | |||
8c9678708b | |||
4c119c6138 | |||
1a6ec835ed | |||
8324a496b6 | |||
68429bc1e4 | |||
7728bc6aee | |||
eb01b304e3 | |||
15c48ab1dc | |||
206b4c9d89 | |||
0e708e72f8 | |||
c3650ba38d | |||
281937e2c5 | |||
5218ea0892 | |||
0e88f3bd7a | |||
8d544e31de | |||
|
5f32505b93 | ||
|
a9efb96837 | ||
|
bdf7202e7d | ||
|
ef223f9cc1 | ||
|
5801a5404a | ||
f5b49ebf66 | |||
b0f254eb13 | |||
9f49baa6ad | |||
1aa56453cc | |||
|
72b2756f34 | ||
|
40bd9186da | ||
|
de19233dd7 | ||
|
21090f05ab | ||
763bcffe23 | |||
e7aba11d05 | |||
41b1148005 | |||
|
7cd92ececb | ||
|
a9d6eb3811 | ||
|
b9d88bbd0c | ||
eb7b27ebe3 | |||
cc06429a69 | |||
a49389d508 | |||
135f5aa47d | |||
bb7f09f2e9 | |||
ebe884e9d5 | |||
8be5332692 | |||
093515c3ec | |||
fd4612d385 | |||
4d298b7eec | |||
|
1a1deb03b7 | ||
|
3b48ff7c7e | ||
37721092db | |||
|
553d261d73 | ||
d92c6fee21 | |||
7665f4c0f5 | |||
d11412e73f | |||
efb49b8708 | |||
0aab173d94 | |||
|
f59049187e | ||
|
7999a0672b | ||
|
7e4346a50e | ||
|
5f0eb57e83 | ||
|
5b19afa0a4 | ||
7eb725fd5b | |||
7ea1d8c322 | |||
|
7458c8278c | ||
1c3b9eb50f | |||
f33bc5cc87 | |||
|
61bd4b558e | ||
|
e23fb33d89 |
232
.clang-format
Normal file
232
.clang-format
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
---
|
||||||
|
Language: Cpp
|
||||||
|
# BasedOnStyle: WebKit
|
||||||
|
AccessModifierOffset: -4
|
||||||
|
AlignAfterOpenBracket: BlockIndent
|
||||||
|
AlignArrayOfStructures: None
|
||||||
|
AlignConsecutiveAssignments:
|
||||||
|
Enabled: false
|
||||||
|
AcrossEmptyLines: false
|
||||||
|
AcrossComments: false
|
||||||
|
AlignCompound: false
|
||||||
|
AlignFunctionPointers: false
|
||||||
|
PadOperators: true
|
||||||
|
AlignConsecutiveBitFields:
|
||||||
|
Enabled: false
|
||||||
|
AcrossEmptyLines: false
|
||||||
|
AcrossComments: false
|
||||||
|
AlignCompound: false
|
||||||
|
AlignFunctionPointers: false
|
||||||
|
PadOperators: false
|
||||||
|
AlignConsecutiveDeclarations:
|
||||||
|
Enabled: false
|
||||||
|
AcrossEmptyLines: false
|
||||||
|
AcrossComments: false
|
||||||
|
AlignCompound: false
|
||||||
|
AlignFunctionPointers: false
|
||||||
|
PadOperators: false
|
||||||
|
AlignConsecutiveMacros:
|
||||||
|
Enabled: false
|
||||||
|
AcrossEmptyLines: false
|
||||||
|
AcrossComments: false
|
||||||
|
AlignCompound: false
|
||||||
|
AlignFunctionPointers: false
|
||||||
|
PadOperators: false
|
||||||
|
AlignConsecutiveShortCaseStatements:
|
||||||
|
Enabled: false
|
||||||
|
AcrossEmptyLines: false
|
||||||
|
AcrossComments: false
|
||||||
|
AlignCaseColons: false
|
||||||
|
AlignEscapedNewlines: Right
|
||||||
|
AlignOperands: DontAlign
|
||||||
|
AlignTrailingComments:
|
||||||
|
Kind: Never
|
||||||
|
OverEmptyLines: 0
|
||||||
|
AllowAllArgumentsOnNextLine: true
|
||||||
|
AllowAllParametersOfDeclarationOnNextLine: true
|
||||||
|
AllowBreakBeforeNoexceptSpecifier: Never
|
||||||
|
AllowShortBlocksOnASingleLine: Empty
|
||||||
|
AllowShortCaseLabelsOnASingleLine: false
|
||||||
|
AllowShortCompoundRequirementOnASingleLine: true
|
||||||
|
AllowShortEnumsOnASingleLine: true
|
||||||
|
AllowShortFunctionsOnASingleLine: All
|
||||||
|
AllowShortIfStatementsOnASingleLine: Never
|
||||||
|
AllowShortLambdasOnASingleLine: All
|
||||||
|
AllowShortLoopsOnASingleLine: false
|
||||||
|
AlwaysBreakAfterDefinitionReturnType: None
|
||||||
|
AlwaysBreakAfterReturnType: None
|
||||||
|
AlwaysBreakBeforeMultilineStrings: false
|
||||||
|
AlwaysBreakTemplateDeclarations: MultiLine
|
||||||
|
AttributeMacros:
|
||||||
|
- __capability
|
||||||
|
BinPackArguments: true
|
||||||
|
BinPackParameters: true
|
||||||
|
BitFieldColonSpacing: Both
|
||||||
|
BraceWrapping:
|
||||||
|
AfterCaseLabel: false
|
||||||
|
AfterClass: true
|
||||||
|
AfterControlStatement: Always
|
||||||
|
AfterEnum: true
|
||||||
|
AfterExternBlock: false
|
||||||
|
AfterFunction: true
|
||||||
|
AfterNamespace: false
|
||||||
|
AfterObjCDeclaration: false
|
||||||
|
AfterStruct: true
|
||||||
|
AfterUnion: true
|
||||||
|
BeforeCatch: true
|
||||||
|
BeforeElse: true
|
||||||
|
BeforeLambdaBody: false
|
||||||
|
BeforeWhile: true
|
||||||
|
IndentBraces: false
|
||||||
|
SplitEmptyFunction: true
|
||||||
|
SplitEmptyRecord: true
|
||||||
|
SplitEmptyNamespace: true
|
||||||
|
BreakAdjacentStringLiterals: true
|
||||||
|
BreakAfterAttributes: Leave
|
||||||
|
BreakAfterJavaFieldAnnotations: false
|
||||||
|
BreakArrays: true
|
||||||
|
BreakBeforeBinaryOperators: All
|
||||||
|
BreakBeforeConceptDeclarations: Always
|
||||||
|
BreakBeforeBraces: Custom
|
||||||
|
BreakBeforeInlineASMColon: OnlyMultiline
|
||||||
|
BreakBeforeTernaryOperators: true
|
||||||
|
BreakConstructorInitializers: BeforeComma
|
||||||
|
BreakInheritanceList: BeforeColon
|
||||||
|
BreakStringLiterals: true
|
||||||
|
ColumnLimit: 0
|
||||||
|
CommentPragmas: "^ IWYU pragma:"
|
||||||
|
CompactNamespaces: false
|
||||||
|
ConstructorInitializerIndentWidth: 4
|
||||||
|
ContinuationIndentWidth: 4
|
||||||
|
Cpp11BracedListStyle: true
|
||||||
|
DerivePointerAlignment: false
|
||||||
|
DisableFormat: false
|
||||||
|
EmptyLineAfterAccessModifier: Never
|
||||||
|
EmptyLineBeforeAccessModifier: LogicalBlock
|
||||||
|
ExperimentalAutoDetectBinPacking: false
|
||||||
|
FixNamespaceComments: false
|
||||||
|
ForEachMacros: []
|
||||||
|
IfMacros: []
|
||||||
|
IncludeBlocks: Preserve
|
||||||
|
IncludeCategories:
|
||||||
|
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
|
||||||
|
Priority: 2
|
||||||
|
SortPriority: 0
|
||||||
|
CaseSensitive: false
|
||||||
|
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
|
||||||
|
Priority: 3
|
||||||
|
SortPriority: 0
|
||||||
|
CaseSensitive: false
|
||||||
|
- Regex: ".*"
|
||||||
|
Priority: 1
|
||||||
|
SortPriority: 0
|
||||||
|
CaseSensitive: false
|
||||||
|
IncludeIsMainRegex: "(Test)?$"
|
||||||
|
IncludeIsMainSourceRegex: ""
|
||||||
|
IndentAccessModifiers: false
|
||||||
|
IndentCaseBlocks: false
|
||||||
|
IndentCaseLabels: false
|
||||||
|
IndentExternBlock: AfterExternBlock
|
||||||
|
IndentGotoLabels: true
|
||||||
|
IndentPPDirectives: None
|
||||||
|
IndentRequiresClause: true
|
||||||
|
IndentWidth: 4
|
||||||
|
IndentWrappedFunctionNames: false
|
||||||
|
InsertBraces: false
|
||||||
|
InsertNewlineAtEOF: false
|
||||||
|
InsertTrailingCommas: None
|
||||||
|
IntegerLiteralSeparator:
|
||||||
|
Binary: 0
|
||||||
|
BinaryMinDigits: 0
|
||||||
|
Decimal: 0
|
||||||
|
DecimalMinDigits: 0
|
||||||
|
Hex: 0
|
||||||
|
HexMinDigits: 0
|
||||||
|
JavaScriptQuotes: Leave
|
||||||
|
JavaScriptWrapImports: true
|
||||||
|
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||||
|
KeepEmptyLinesAtEOF: false
|
||||||
|
LambdaBodyIndentation: Signature
|
||||||
|
LineEnding: LF
|
||||||
|
MacroBlockBegin: ""
|
||||||
|
MacroBlockEnd: ""
|
||||||
|
MaxEmptyLinesToKeep: 1
|
||||||
|
NamespaceIndentation: Inner
|
||||||
|
ObjCBinPackProtocolList: Auto
|
||||||
|
ObjCBlockIndentWidth: 4
|
||||||
|
ObjCBreakBeforeNestedBlockParam: true
|
||||||
|
ObjCSpaceAfterProperty: true
|
||||||
|
ObjCSpaceBeforeProtocolList: true
|
||||||
|
PackConstructorInitializers: BinPack
|
||||||
|
PenaltyBreakAssignment: 2
|
||||||
|
PenaltyBreakBeforeFirstCallParameter: 19
|
||||||
|
PenaltyBreakComment: 300
|
||||||
|
PenaltyBreakFirstLessLess: 120
|
||||||
|
PenaltyBreakOpenParenthesis: 0
|
||||||
|
PenaltyBreakScopeResolution: 500
|
||||||
|
PenaltyBreakString: 1000
|
||||||
|
PenaltyBreakTemplateDeclaration: 10
|
||||||
|
PenaltyExcessCharacter: 1000000
|
||||||
|
PenaltyIndentedWhitespace: 0
|
||||||
|
PenaltyReturnTypeOnItsOwnLine: 60
|
||||||
|
PointerAlignment: Right
|
||||||
|
PPIndentWidth: -1
|
||||||
|
QualifierAlignment: Left
|
||||||
|
ReferenceAlignment: Pointer
|
||||||
|
ReflowComments: true
|
||||||
|
RemoveBracesLLVM: false
|
||||||
|
RemoveParentheses: Leave
|
||||||
|
RemoveSemicolon: false
|
||||||
|
RequiresClausePosition: OwnLine
|
||||||
|
RequiresExpressionIndentation: OuterScope
|
||||||
|
SeparateDefinitionBlocks: Leave
|
||||||
|
ShortNamespaceLines: 1
|
||||||
|
SkipMacroDefinitionBody: false
|
||||||
|
SortIncludes: CaseSensitive
|
||||||
|
SortJavaStaticImport: Before
|
||||||
|
SortUsingDeclarations: LexicographicNumeric
|
||||||
|
SpaceAfterCStyleCast: false
|
||||||
|
SpaceAfterLogicalNot: false
|
||||||
|
SpaceAfterTemplateKeyword: true
|
||||||
|
SpaceAroundPointerQualifiers: Default
|
||||||
|
SpaceBeforeAssignmentOperators: true
|
||||||
|
SpaceBeforeCaseColon: false
|
||||||
|
SpaceBeforeCpp11BracedList: false
|
||||||
|
SpaceBeforeCtorInitializerColon: true
|
||||||
|
SpaceBeforeInheritanceColon: true
|
||||||
|
SpaceBeforeJsonColon: false
|
||||||
|
SpaceBeforeParens: ControlStatements
|
||||||
|
SpaceBeforeParensOptions:
|
||||||
|
AfterControlStatements: true
|
||||||
|
AfterForeachMacros: true
|
||||||
|
AfterFunctionDefinitionName: false
|
||||||
|
AfterFunctionDeclarationName: false
|
||||||
|
AfterIfMacros: true
|
||||||
|
AfterOverloadedOperator: false
|
||||||
|
AfterPlacementOperator: true
|
||||||
|
AfterRequiresInClause: false
|
||||||
|
AfterRequiresInExpression: false
|
||||||
|
BeforeNonEmptyParentheses: false
|
||||||
|
SpaceBeforeRangeBasedForLoopColon: true
|
||||||
|
SpaceBeforeSquareBrackets: false
|
||||||
|
SpaceInEmptyBlock: true
|
||||||
|
SpacesBeforeTrailingComments: 1
|
||||||
|
SpacesInAngles: Never
|
||||||
|
SpacesInContainerLiterals: true
|
||||||
|
SpacesInLineCommentPrefix:
|
||||||
|
Minimum: 1
|
||||||
|
Maximum: -1
|
||||||
|
SpacesInParens: Never
|
||||||
|
SpacesInParensOptions:
|
||||||
|
InCStyleCasts: false
|
||||||
|
InConditionalStatements: false
|
||||||
|
InEmptyParentheses: false
|
||||||
|
Other: false
|
||||||
|
SpacesInSquareBrackets: false
|
||||||
|
Standard: Latest
|
||||||
|
StatementAttributeLikeMacros: []
|
||||||
|
StatementMacros: []
|
||||||
|
TabWidth: 8
|
||||||
|
UseTab: Never
|
||||||
|
VerilogBreakBetweenInstancePorts: true
|
||||||
|
WhitespaceSensitiveMacros: []
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
/out
|
/out
|
||||||
/config.toml
|
/config.toml
|
||||||
|
/privs.toml
|
||||||
/.ccls
|
/.ccls
|
||||||
/archive
|
/archive
|
||||||
/.vscode
|
/.vscode
|
||||||
|
@@ -1,46 +1,32 @@
|
|||||||
cmake_minimum_required(VERSION 3.13)
|
cmake_minimum_required(VERSION 3.25)
|
||||||
set(CMAKE_C_STANDARD 11)
|
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
project(xbot
|
project(xbot
|
||||||
VERSION 1
|
VERSION 1
|
||||||
LANGUAGES C CXX
|
LANGUAGES CXX
|
||||||
)
|
)
|
||||||
|
|
||||||
find_package(Boost REQUIRED)
|
|
||||||
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 endian beast json)
|
||||||
|
set(BOOST_ENABLE_CMAKE ON)
|
||||||
include(FetchContent)
|
include(FetchContent)
|
||||||
|
|
||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
tomlplusplus
|
Boost
|
||||||
GIT_REPOSITORY https://github.com/marzer/tomlplusplus.git
|
URL https://github.com/boostorg/boost/releases/download/boost-1.87.0/boost-1.87.0-cmake.tar.xz
|
||||||
GIT_TAG v3.4.0
|
URL_HASH SHA1=4ec86f884ffb57ce7f6a6c6eb05b1af247aba0ac
|
||||||
)
|
)
|
||||||
|
FetchContent_Declare(
|
||||||
|
tomlplusplus
|
||||||
|
GIT_REPOSITORY https://github.com/marzer/tomlplusplus.git
|
||||||
|
GIT_TAG v3.4.0
|
||||||
|
)
|
||||||
FetchContent_MakeAvailable(tomlplusplus)
|
FetchContent_MakeAvailable(tomlplusplus)
|
||||||
|
FetchContent_MakeAvailable(Boost)
|
||||||
|
|
||||||
FetchContent_Declare(
|
add_subdirectory(mybase64)
|
||||||
eventpp
|
add_subdirectory(mysocks5)
|
||||||
GIT_REPOSITORY https://github.com/wqking/eventpp.git
|
add_subdirectory(myirc)
|
||||||
GIT_TAG v0.1.3
|
add_subdirectory(driver)
|
||||||
)
|
|
||||||
FetchContent_MakeAvailable(eventpp)
|
|
||||||
|
|
||||||
add_custom_command(
|
|
||||||
OUTPUT irc_commands.inc
|
|
||||||
COMMAND
|
|
||||||
gperf
|
|
||||||
-C -Z IrcCommandHash -K text -L C++ -t
|
|
||||||
--output-file irc_commands.inc
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/irc_commands.gperf
|
|
||||||
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/irc_commands.gperf
|
|
||||||
VERBATIM)
|
|
||||||
|
|
||||||
add_executable(xbot
|
|
||||||
main.cpp irc_commands.inc ircmsg.cpp settings.cpp connection.cpp
|
|
||||||
thread.cpp snote_thread.cpp watchdog_thread.cpp write_irc.cpp
|
|
||||||
ping_thread.cpp irc_parse_thread.cpp registration_thread.cpp
|
|
||||||
self_thread.cpp command_thread.cpp)
|
|
||||||
target_include_directories(xbot PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
|
||||||
target_link_libraries(xbot PRIVATE Boost::headers tomlplusplus_tomlplusplus eventpp PkgConfig::LIBHS)
|
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 6,
|
||||||
"configurePresets": [
|
"configurePresets": [
|
||||||
{
|
{
|
||||||
"name": "arm-mac",
|
"name": "default",
|
||||||
"displayName": "Configure preset using toolchain file",
|
"displayName": "Configure preset using toolchain file",
|
||||||
"description": "Sets Ninja generator, build and install directory",
|
"description": "Sets Ninja generator, build and install directory",
|
||||||
"generator": "Ninja",
|
"generator": "Ninja",
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
],
|
],
|
||||||
"buildPresets": [
|
"buildPresets": [
|
||||||
{
|
{
|
||||||
"name": "arm-mac",
|
"name": "default",
|
||||||
"configurePreset": "arm-mac"
|
"configurePreset": "default"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -1,52 +0,0 @@
|
|||||||
#include "command_thread.hpp"
|
|
||||||
|
|
||||||
#include "connection.hpp"
|
|
||||||
#include "irc_parse_thread.hpp"
|
|
||||||
#include <iostream>
|
|
||||||
auto CommandThread::start(Connection& connection) -> void
|
|
||||||
{
|
|
||||||
connection.add_listener<IrcMsgEvent>([&connection](IrcMsgEvent& event)
|
|
||||||
{
|
|
||||||
if (IrcCommand::PRIVMSG != event.command) return;
|
|
||||||
auto const message = event.irc.args[1];
|
|
||||||
if (message.empty()) return;
|
|
||||||
if ('!' != message.front()) return;
|
|
||||||
|
|
||||||
CommandEvent command_event;
|
|
||||||
|
|
||||||
{
|
|
||||||
auto const cmdend = message.find(' ');
|
|
||||||
if (std::string_view::npos == cmdend)
|
|
||||||
{
|
|
||||||
command_event.command = message.substr(1);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
command_event.command = message.substr(1, cmdend - 1);
|
|
||||||
command_event.arg = message.substr(cmdend + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto const nickend = event.irc.source.find('!');
|
|
||||||
if (std::string_view::npos != nickend)
|
|
||||||
{
|
|
||||||
command_event.nick = event.irc.source.substr(0, nickend);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto const& [k,v] : event.irc.tags)
|
|
||||||
{
|
|
||||||
if ("solanum.chat/oper" == k)
|
|
||||||
{
|
|
||||||
command_event.oper = v;
|
|
||||||
}
|
|
||||||
else if ("account" == k)
|
|
||||||
{
|
|
||||||
command_event.account = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.dispatch(command_event);
|
|
||||||
});
|
|
||||||
}
|
|
@@ -1,21 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "thread.hpp"
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
class Connection;
|
|
||||||
|
|
||||||
struct CommandEvent : Event
|
|
||||||
{
|
|
||||||
std::string_view oper;
|
|
||||||
std::string_view account;
|
|
||||||
std::string_view nick;
|
|
||||||
std::string_view command;
|
|
||||||
std::string_view arg;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct CommandThread
|
|
||||||
{
|
|
||||||
static auto start(Connection&) -> void;
|
|
||||||
};
|
|
105
connection.cpp
105
connection.cpp
@@ -1,105 +0,0 @@
|
|||||||
#include "connection.hpp"
|
|
||||||
|
|
||||||
Connection::Connection(boost::asio::io_context & io)
|
|
||||||
: stream_{io}
|
|
||||||
, write_timer_{io, std::chrono::steady_clock::time_point::max()}
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
auto Connection::writer_() -> void
|
|
||||||
{
|
|
||||||
std::vector<boost::asio::const_buffer> buffers;
|
|
||||||
buffers.reserve(write_strings_.size());
|
|
||||||
for (auto const& elt : write_strings_)
|
|
||||||
{
|
|
||||||
buffers.push_back(boost::asio::buffer(elt));
|
|
||||||
}
|
|
||||||
boost::asio::async_write(
|
|
||||||
stream_,
|
|
||||||
buffers,
|
|
||||||
[weak = weak_from_this()
|
|
||||||
,strings = std::move(write_strings_)
|
|
||||||
](boost::system::error_code const& error, std::size_t)
|
|
||||||
{
|
|
||||||
if (not error)
|
|
||||||
{
|
|
||||||
if (auto self = weak.lock())
|
|
||||||
{
|
|
||||||
self->writer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
write_strings_.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto Connection::writer() -> void
|
|
||||||
{
|
|
||||||
if (write_strings_.empty())
|
|
||||||
{
|
|
||||||
write_timer_.async_wait([weak = weak_from_this()](auto){
|
|
||||||
if (auto self = weak.lock())
|
|
||||||
{
|
|
||||||
if (not self->write_strings_.empty())
|
|
||||||
{
|
|
||||||
self->writer_();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
writer_();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto Connection::connect(
|
|
||||||
boost::asio::io_context & io,
|
|
||||||
std::string host,
|
|
||||||
std::string port
|
|
||||||
) -> boost::asio::awaitable<void>
|
|
||||||
{
|
|
||||||
using namespace std::placeholders;
|
|
||||||
|
|
||||||
// keep connection alive while coroutine is active
|
|
||||||
auto const self = shared_from_this();
|
|
||||||
|
|
||||||
{
|
|
||||||
auto resolver = boost::asio::ip::tcp::resolver{io};
|
|
||||||
auto const endpoints = co_await resolver.async_resolve(host, port, boost::asio::use_awaitable);
|
|
||||||
auto const endpoint = co_await boost::asio::async_connect(stream_, endpoints, boost::asio::use_awaitable);
|
|
||||||
|
|
||||||
make_event<ConnectEvent>();
|
|
||||||
}
|
|
||||||
|
|
||||||
self->writer();
|
|
||||||
for(LineBuffer buffer{32'768};;)
|
|
||||||
{
|
|
||||||
boost::system::error_code error;
|
|
||||||
auto const n = co_await stream_.async_read_some(buffer.get_buffer(), boost::asio::redirect_error(boost::asio::use_awaitable, error));
|
|
||||||
if (error)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
buffer.add_bytes(n, [this](char * line) {
|
|
||||||
make_event<LineEvent>(line);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
make_event<DisconnectEvent>();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto Connection::write_line(std::string message) -> void
|
|
||||||
{
|
|
||||||
message += "\r\n";
|
|
||||||
auto const need_cancel = write_strings_.empty();
|
|
||||||
write_strings_.push_back(std::move(message));
|
|
||||||
if (need_cancel)
|
|
||||||
{
|
|
||||||
write_timer_.cancel_one();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto Connection::close() -> void
|
|
||||||
{
|
|
||||||
stream_.close();
|
|
||||||
}
|
|
110
connection.hpp
110
connection.hpp
@@ -1,110 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "linebuffer.hpp"
|
|
||||||
#include "settings.hpp"
|
|
||||||
#include "thread.hpp"
|
|
||||||
|
|
||||||
#include <eventpp/eventdispatcher.h>
|
|
||||||
#include <eventpp/utilities/argumentadapter.h>
|
|
||||||
|
|
||||||
#include <boost/asio.hpp>
|
|
||||||
|
|
||||||
#include <chrono>
|
|
||||||
#include <functional>
|
|
||||||
#include <concepts>
|
|
||||||
#include <iostream>
|
|
||||||
#include <list>
|
|
||||||
#include <memory>
|
|
||||||
#include <string>
|
|
||||||
#include <string_view>
|
|
||||||
#include <tuple>
|
|
||||||
#include <utility>
|
|
||||||
#include <variant>
|
|
||||||
#include <vector>
|
|
||||||
#include <typeinfo>
|
|
||||||
#include <typeindex>
|
|
||||||
|
|
||||||
struct ConnectEvent : Event
|
|
||||||
{
|
|
||||||
};
|
|
||||||
|
|
||||||
struct DisconnectEvent : Event
|
|
||||||
{
|
|
||||||
};
|
|
||||||
|
|
||||||
struct LineEvent : Event
|
|
||||||
{
|
|
||||||
explicit LineEvent(char * line) : line{line} {}
|
|
||||||
char * line;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Connection : public std::enable_shared_from_this<Connection>
|
|
||||||
{
|
|
||||||
using EventDispatcher = eventpp::EventDispatcher<std::type_index, void(Event&)>;
|
|
||||||
|
|
||||||
public:
|
|
||||||
template <typename T>
|
|
||||||
class Handle
|
|
||||||
{
|
|
||||||
EventDispatcher::Handle handle;
|
|
||||||
Handle(EventDispatcher::Handle handle) : handle{handle} {}
|
|
||||||
public:
|
|
||||||
Handle() : handle{} {}
|
|
||||||
friend Connection;
|
|
||||||
};
|
|
||||||
|
|
||||||
private:
|
|
||||||
boost::asio::ip::tcp::socket stream_;
|
|
||||||
boost::asio::steady_timer write_timer_;
|
|
||||||
std::list<std::string> write_strings_;
|
|
||||||
EventDispatcher dispatcher_;
|
|
||||||
|
|
||||||
auto writer() -> void;
|
|
||||||
auto writer_() -> void;
|
|
||||||
|
|
||||||
public:
|
|
||||||
Connection(boost::asio::io_context & io);
|
|
||||||
|
|
||||||
template <typename T, typename F>
|
|
||||||
auto add_listener(F f) -> Handle<T>
|
|
||||||
{
|
|
||||||
return Handle<T>{dispatcher_.appendListener(
|
|
||||||
typeid(T),
|
|
||||||
eventpp::argumentAdapter<void(T&)>(f)
|
|
||||||
)};
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
auto remove_listener(Handle<T> handle) -> void
|
|
||||||
{
|
|
||||||
dispatcher_.removeListener(typeid(T), handle.handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
auto dispatch(T& event) -> void
|
|
||||||
{
|
|
||||||
dispatcher_.dispatch(typeid(T), event);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto get_executor() -> boost::asio::any_io_executor {
|
|
||||||
return stream_.get_executor();
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T, typename... Args>
|
|
||||||
auto make_event(Args&& ... args) {
|
|
||||||
auto event = T{std::forward<Args>(args)...};
|
|
||||||
dispatch<T>(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write bytes into the socket. Messages should be properly newline terminated.
|
|
||||||
auto write_line(std::string message) -> void;
|
|
||||||
|
|
||||||
auto connect(
|
|
||||||
boost::asio::io_context & io,
|
|
||||||
std::string host,
|
|
||||||
std::string port
|
|
||||||
) -> boost::asio::awaitable<void>;
|
|
||||||
|
|
||||||
auto close() -> void;
|
|
||||||
};
|
|
||||||
|
|
13
driver/CMakeLists.txt
Normal file
13
driver/CMakeLists.txt
Normal 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
133
driver/main.cpp
Normal 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
61
driver/settings.cpp
Normal 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
38
driver/settings.hpp
Normal 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
624
driver/web.cpp
Normal 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
76
driver/web.hpp
Normal 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;
|
@@ -1,278 +0,0 @@
|
|||||||
struct RecognizedCommand {
|
|
||||||
char const* text;
|
|
||||||
IrcCommand command;
|
|
||||||
std::size_t min_args;
|
|
||||||
std::size_t max_args;
|
|
||||||
};
|
|
||||||
%%
|
|
||||||
001, IrcCommand::RPL_WELCOME, 2, 2
|
|
||||||
002, IrcCommand::RPL_YOURHOST, 2, 2
|
|
||||||
003, IrcCommand::RPL_CREATED, 2, 2
|
|
||||||
004, IrcCommand::RPL_MYINFO, 5, 5
|
|
||||||
005, IrcCommand::RPL_ISUPPORT, 2, 15
|
|
||||||
008, IrcCommand::RPL_SNOMASK, 3, 3
|
|
||||||
010, IrcCommand::RPL_REDIR, 4, 4
|
|
||||||
015, IrcCommand::RPL_MAP, 2, 2
|
|
||||||
017, IrcCommand::RPL_MAPEND, 2, 2
|
|
||||||
043, IrcCommand::RPL_SAVENICK, 3, 3
|
|
||||||
200, IrcCommand::RPL_TRACELINK, 5, 5
|
|
||||||
201, IrcCommand::RPL_TRACECONNECTING, 4, 4
|
|
||||||
202, IrcCommand::RPL_TRACEHANDSHAKE, 4, 4
|
|
||||||
203, IrcCommand::RPL_TRACEUNKNOWN, 6, 6
|
|
||||||
204, IrcCommand::RPL_TRACEOPERATOR
|
|
||||||
205, IrcCommand::RPL_TRACEUSER
|
|
||||||
206, IrcCommand::RPL_TRACESERVER
|
|
||||||
208, IrcCommand::RPL_TRACENEWTYPE
|
|
||||||
209, IrcCommand::RPL_TRACECLASS
|
|
||||||
211, IrcCommand::RPL_STATSLINKINFO
|
|
||||||
212, IrcCommand::RPL_STATSCOMMANDS
|
|
||||||
213, IrcCommand::RPL_STATSCLINE
|
|
||||||
214, IrcCommand::RPL_STATSNLINE
|
|
||||||
215, IrcCommand::RPL_STATSILINE, 8, 8
|
|
||||||
216, IrcCommand::RPL_STATSKLINE
|
|
||||||
217, IrcCommand::RPL_STATSQLINE
|
|
||||||
218, IrcCommand::RPL_STATSYLINE
|
|
||||||
219, IrcCommand::RPL_ENDOFSTATS
|
|
||||||
220, IrcCommand::RPL_STATSPLINE
|
|
||||||
221, IrcCommand::RPL_UMODEIS, 2, 2
|
|
||||||
224, IrcCommand::RPL_STATSFLINE
|
|
||||||
225, IrcCommand::RPL_STATSDLINE
|
|
||||||
234, IrcCommand::RPL_SERVLIST
|
|
||||||
235, IrcCommand::RPL_SERVLISTEND
|
|
||||||
241, IrcCommand::RPL_STATSLLINE
|
|
||||||
242, IrcCommand::RPL_STATSUPTIME
|
|
||||||
243, IrcCommand::RPL_STATSOLINE
|
|
||||||
244, IrcCommand::RPL_STATSHLINE
|
|
||||||
245, IrcCommand::RPL_STATSSLINE
|
|
||||||
247, IrcCommand::RPL_STATSXLINE
|
|
||||||
248, IrcCommand::RPL_STATSULINE
|
|
||||||
249, IrcCommand::RPL_STATSDEBUG
|
|
||||||
250, IrcCommand::RPL_STATSCONN
|
|
||||||
251, IrcCommand::RPL_LUSERCLIENT
|
|
||||||
252, IrcCommand::RPL_LUSEROP
|
|
||||||
253, IrcCommand::RPL_LUSERUNKNOWN
|
|
||||||
254, IrcCommand::RPL_LUSERCHANNELS
|
|
||||||
255, IrcCommand::RPL_LUSERME
|
|
||||||
256, IrcCommand::RPL_ADMINME, 3, 3
|
|
||||||
257, IrcCommand::RPL_ADMINLOC1, 2, 2
|
|
||||||
258, IrcCommand::RPL_ADMINLOC2, 2, 2
|
|
||||||
259, IrcCommand::RPL_ADMINEMAIL, 2, 2
|
|
||||||
261, IrcCommand::RPL_TRACELOG
|
|
||||||
262, IrcCommand::RPL_ENDOFTRACE, 3, 3
|
|
||||||
263, IrcCommand::RPL_LOAD2HI
|
|
||||||
265, IrcCommand::RPL_LOCALUSERS, 4, 4
|
|
||||||
266, IrcCommand::RPL_GLOBALUSERS, 4, 4
|
|
||||||
270, IrcCommand::RPL_PRIVS, 3, 3
|
|
||||||
276, IrcCommand::RPL_WHOISCERTFP, 3, 3
|
|
||||||
281, IrcCommand::RPL_ACCEPTLIST, 1, 15
|
|
||||||
282, IrcCommand::RPL_ENDOFACCEPT, 2, 2
|
|
||||||
300, IrcCommand::RPL_NONE
|
|
||||||
301, IrcCommand::RPL_AWAY
|
|
||||||
302, IrcCommand::RPL_USERHOST
|
|
||||||
303, IrcCommand::RPL_ISON, 2, 2
|
|
||||||
304, IrcCommand::RPL_TEXT
|
|
||||||
305, IrcCommand::RPL_UNAWAY
|
|
||||||
306, IrcCommand::RPL_NOWAWAY
|
|
||||||
310, IrcCommand::RPL_WHOISHELPOP
|
|
||||||
311, IrcCommand::RPL_WHOISUSER, 6, 6
|
|
||||||
312, IrcCommand::RPL_WHOISSERVER, 4, 4
|
|
||||||
313, IrcCommand::RPL_WHOISOPERATOR, 3, 3
|
|
||||||
314, IrcCommand::RPL_WHOWASUSER
|
|
||||||
369, IrcCommand::RPL_ENDOFWHOWAS
|
|
||||||
316, IrcCommand::RPL_WHOISCHANOP
|
|
||||||
317, IrcCommand::RPL_WHOISIDLE, 5, 5
|
|
||||||
318, IrcCommand::RPL_ENDOFWHOIS, 3, 3
|
|
||||||
319, IrcCommand::RPL_WHOISCHANNELS, 3, 3
|
|
||||||
320, IrcCommand::RPL_WHOISSPECIAL, 3, 3
|
|
||||||
321, IrcCommand::RPL_LISTSTART, 3, 3
|
|
||||||
322, IrcCommand::RPL_LIST, 4, 4
|
|
||||||
323, IrcCommand::RPL_LISTEND, 2, 2
|
|
||||||
324, IrcCommand::RPL_CHANNELMODEIS, 3, 3
|
|
||||||
325, IrcCommand::RPL_CHANNELMLOCK
|
|
||||||
328, IrcCommand::RPL_CHANNELURL
|
|
||||||
329, IrcCommand::RPL_CREATIONTIME, 3, 3
|
|
||||||
330, IrcCommand::RPL_WHOISLOGGEDIN, 4, 4
|
|
||||||
331, IrcCommand::RPL_NOTOPIC, 3, 3
|
|
||||||
332, IrcCommand::RPL_TOPIC, 3, 3
|
|
||||||
333, IrcCommand::RPL_TOPICWHOTIME, 4, 4
|
|
||||||
338, IrcCommand::RPL_WHOISACTUALLY
|
|
||||||
341, IrcCommand::RPL_INVITING
|
|
||||||
342, IrcCommand::RPL_SUMMONING
|
|
||||||
346, IrcCommand::RPL_INVITELIST
|
|
||||||
347, IrcCommand::RPL_ENDOFINVITELIST
|
|
||||||
348, IrcCommand::RPL_EXCEPTLIST
|
|
||||||
349, IrcCommand::RPL_ENDOFEXCEPTLIST, 3, 3
|
|
||||||
351, IrcCommand::RPL_VERSION, 4, 4
|
|
||||||
352, IrcCommand::RPL_WHOREPLY
|
|
||||||
354, IrcCommand::RPL_WHOSPCRPL
|
|
||||||
315, IrcCommand::RPL_ENDOFWHO
|
|
||||||
353, IrcCommand::RPL_NAMREPLY, 4, 4
|
|
||||||
360, IrcCommand::RPL_WHOWASREAL
|
|
||||||
366, IrcCommand::RPL_ENDOFNAMES, 3, 3
|
|
||||||
361, IrcCommand::RPL_KILLDONE
|
|
||||||
362, IrcCommand::RPL_CLOSING
|
|
||||||
363, IrcCommand::RPL_CLOSEEND
|
|
||||||
364, IrcCommand::RPL_LINKS, 4, 4
|
|
||||||
365, IrcCommand::RPL_ENDOFLINKS, 3, 3
|
|
||||||
367, IrcCommand::RPL_BANLIST, 5, 5
|
|
||||||
368, IrcCommand::RPL_ENDOFBANLIST, 3, 3
|
|
||||||
371, IrcCommand::RPL_INFO, 2, 2
|
|
||||||
372, IrcCommand::RPL_MOTD, 2, 2
|
|
||||||
373, IrcCommand::RPL_INFOSTART
|
|
||||||
374, IrcCommand::RPL_ENDOFINFO, 2, 2
|
|
||||||
375, IrcCommand::RPL_MOTDSTART, 2, 2
|
|
||||||
376, IrcCommand::RPL_ENDOFMOTD, 2, 2
|
|
||||||
378, IrcCommand::RPL_WHOISHOST
|
|
||||||
381, IrcCommand::RPL_YOUREOPER, 2, 2
|
|
||||||
382, IrcCommand::RPL_REHASHING
|
|
||||||
384, IrcCommand::RPL_MYPORTIS
|
|
||||||
385, IrcCommand::RPL_NOTOPERANYMORE
|
|
||||||
386, IrcCommand::RPL_RSACHALLENGE
|
|
||||||
391, IrcCommand::RPL_TIME, 3, 3
|
|
||||||
392, IrcCommand::RPL_USERSSTART
|
|
||||||
393, IrcCommand::RPL_USERS
|
|
||||||
394, IrcCommand::RPL_ENDOFUSERS
|
|
||||||
395, IrcCommand::RPL_NOUSERS
|
|
||||||
396, IrcCommand::RPL_HOSTHIDDEN
|
|
||||||
401, IrcCommand::ERR_NOSUCHNICK, 3, 3
|
|
||||||
402, IrcCommand::ERR_NOSUCHSERVER, 3, 3
|
|
||||||
403, IrcCommand::ERR_NOSUCHCHANNEL
|
|
||||||
404, IrcCommand::ERR_CANNOTSENDTOCHAN
|
|
||||||
405, IrcCommand::ERR_TOOMANYCHANNELS
|
|
||||||
406, IrcCommand::ERR_WASNOSUCHNICK
|
|
||||||
407, IrcCommand::ERR_TOOMANYTARGETS
|
|
||||||
409, IrcCommand::ERR_NOORIGIN
|
|
||||||
410, IrcCommand::ERR_INVALIDCAPCMD
|
|
||||||
411, IrcCommand::ERR_NORECIPIENT
|
|
||||||
412, IrcCommand::ERR_NOTEXTTOSEND
|
|
||||||
413, IrcCommand::ERR_NOTOPLEVEL
|
|
||||||
414, IrcCommand::ERR_WILDTOPLEVEL
|
|
||||||
415, IrcCommand::ERR_MSGNEEDREGGEDNICK
|
|
||||||
416, IrcCommand::ERR_TOOMANYMATCHES
|
|
||||||
421, IrcCommand::ERR_UNKNOWNCOMMAND
|
|
||||||
422, IrcCommand::ERR_NOMOTD, 2, 2
|
|
||||||
423, IrcCommand::ERR_NOADMININFO
|
|
||||||
424, IrcCommand::ERR_FILEERROR
|
|
||||||
431, IrcCommand::ERR_NONICKNAMEGIVEN
|
|
||||||
432, IrcCommand::ERR_ERRONEUSNICKNAME
|
|
||||||
433, IrcCommand::ERR_NICKNAMEINUSE, 3, 3
|
|
||||||
435, IrcCommand::ERR_BANNICKCHANGE
|
|
||||||
436, IrcCommand::ERR_NICKCOLLISION
|
|
||||||
437, IrcCommand::ERR_UNAVAILRESOURCE
|
|
||||||
438, IrcCommand::ERR_NICKTOOFAST
|
|
||||||
440, IrcCommand::ERR_SERVICESDOWN
|
|
||||||
441, IrcCommand::ERR_USERNOTINCHANNEL
|
|
||||||
442, IrcCommand::ERR_NOTONCHANNEL
|
|
||||||
443, IrcCommand::ERR_USERONCHANNEL
|
|
||||||
444, IrcCommand::ERR_NOLOGIN
|
|
||||||
445, IrcCommand::ERR_SUMMONDISABLED
|
|
||||||
446, IrcCommand::ERR_USERSDISABLED
|
|
||||||
451, IrcCommand::ERR_NOTREGISTERED
|
|
||||||
456, IrcCommand::ERR_ACCEPTFULL
|
|
||||||
457, IrcCommand::ERR_ACCEPTEXIST
|
|
||||||
458, IrcCommand::ERR_ACCEPTNOT
|
|
||||||
461, IrcCommand::ERR_NEEDMOREPARAMS
|
|
||||||
462, IrcCommand::ERR_ALREADYREGISTRED
|
|
||||||
463, IrcCommand::ERR_NOPERMFORHOST
|
|
||||||
464, IrcCommand::ERR_PASSWDMISMATCH
|
|
||||||
465, IrcCommand::ERR_YOUREBANNEDCREEP
|
|
||||||
466, IrcCommand::ERR_YOUWILLBEBANNED
|
|
||||||
467, IrcCommand::ERR_KEYSET
|
|
||||||
470, IrcCommand::ERR_LINKCHANNEL
|
|
||||||
471, IrcCommand::ERR_CHANNELISFULL
|
|
||||||
472, IrcCommand::ERR_UNKNOWNMODE
|
|
||||||
473, IrcCommand::ERR_INVITEONLYCHAN
|
|
||||||
474, IrcCommand::ERR_BANNEDFROMCHAN
|
|
||||||
475, IrcCommand::ERR_BADCHANNELKEY
|
|
||||||
476, IrcCommand::ERR_BADCHANMASK
|
|
||||||
477, IrcCommand::ERR_NEEDREGGEDNICK
|
|
||||||
478, IrcCommand::ERR_BANLISTFULL
|
|
||||||
479, IrcCommand::ERR_BADCHANNAME
|
|
||||||
480, IrcCommand::ERR_THROTTLE
|
|
||||||
481, IrcCommand::ERR_NOPRIVILEGES
|
|
||||||
482, IrcCommand::ERR_CHANOPRIVSNEEDED
|
|
||||||
483, IrcCommand::ERR_CANTKILLSERVER
|
|
||||||
484, IrcCommand::ERR_ISCHANSERVICE
|
|
||||||
485, IrcCommand::ERR_BANNEDNICK
|
|
||||||
486, IrcCommand::ERR_NONONREG
|
|
||||||
489, IrcCommand::ERR_VOICENEEDED
|
|
||||||
491, IrcCommand::ERR_NOOPERHOST
|
|
||||||
492, IrcCommand::ERR_CANNOTSENDTOUSER
|
|
||||||
494, IrcCommand::ERR_OWNMODE
|
|
||||||
501, IrcCommand::ERR_UMODEUNKNOWNFLAG
|
|
||||||
502, IrcCommand::ERR_USERSDONTMATCH, 2, 2
|
|
||||||
503, IrcCommand::ERR_GHOSTEDCLIENT
|
|
||||||
504, IrcCommand::ERR_USERNOTONSERV
|
|
||||||
513, IrcCommand::ERR_WRONGPONG
|
|
||||||
517, IrcCommand::ERR_DISABLED
|
|
||||||
524, IrcCommand::ERR_HELPNOTFOUND
|
|
||||||
670, IrcCommand::RPL_STARTTLS
|
|
||||||
671, IrcCommand::RPL_WHOISSECURE, 3, 3
|
|
||||||
691, IrcCommand::ERR_STARTTLS
|
|
||||||
702, IrcCommand::RPL_MODLIST
|
|
||||||
703, IrcCommand::RPL_ENDOFMODLIST
|
|
||||||
704, IrcCommand::RPL_HELPSTART
|
|
||||||
705, IrcCommand::RPL_HELPTXT
|
|
||||||
706, IrcCommand::RPL_ENDOFHELP
|
|
||||||
707, IrcCommand::ERR_TARGCHANGE
|
|
||||||
708, IrcCommand::RPL_ETRACEFULL, 10, 10
|
|
||||||
709, IrcCommand::RPL_ETRACE
|
|
||||||
710, IrcCommand::RPL_KNOCK
|
|
||||||
711, IrcCommand::RPL_KNOCKDLVR
|
|
||||||
712, IrcCommand::ERR_TOOMANYKNOCK
|
|
||||||
713, IrcCommand::ERR_CHANOPEN
|
|
||||||
714, IrcCommand::ERR_KNOCKONCHAN
|
|
||||||
715, IrcCommand::ERR_KNOCKDISABLED
|
|
||||||
716, IrcCommand::ERR_TARGUMODEG
|
|
||||||
717, IrcCommand::RPL_TARGNOTIFY
|
|
||||||
718, IrcCommand::RPL_UMODEGMSG
|
|
||||||
720, IrcCommand::RPL_OMOTDSTART
|
|
||||||
721, IrcCommand::RPL_OMOTD
|
|
||||||
722, IrcCommand::RPL_ENDOFOMOTD
|
|
||||||
723, IrcCommand::ERR_NOPRIVS
|
|
||||||
724, IrcCommand::RPL_TESTMASK
|
|
||||||
725, IrcCommand::RPL_TESTLINE
|
|
||||||
726, IrcCommand::RPL_NOTESTLINE
|
|
||||||
727, IrcCommand::RPL_TESTMASKGECO, 6, 6
|
|
||||||
728, IrcCommand::RPL_QUIETLIST, 6, 6
|
|
||||||
729, IrcCommand::RPL_ENDOFQUIETLIS, 4, 4
|
|
||||||
730, IrcCommand::RPL_MONONLINE
|
|
||||||
731, IrcCommand::RPL_MONOFFLINE
|
|
||||||
732, IrcCommand::RPL_MONLIST
|
|
||||||
733, IrcCommand::RPL_ENDOFMONLIS, 2, 2
|
|
||||||
734, IrcCommand::ERR_MONLISTFULL
|
|
||||||
740, IrcCommand::RPL_RSACHALLENGE2, 2, 2
|
|
||||||
741, IrcCommand::RPL_ENDOFRSACHALLENGE2, 2, 2
|
|
||||||
742, IrcCommand::ERR_MLOCKRESTRICTE
|
|
||||||
743, IrcCommand::ERR_INVALIDBAN
|
|
||||||
744, IrcCommand::ERR_TOPICLOCK
|
|
||||||
750, IrcCommand::RPL_SCANMATCHED
|
|
||||||
751, IrcCommand::RPL_SCANUMODES
|
|
||||||
900, IrcCommand::RPL_LOGGEDIN
|
|
||||||
901, IrcCommand::RPL_LOGGEDOUT
|
|
||||||
902, IrcCommand::ERR_NICKLOCKED
|
|
||||||
903, IrcCommand::RPL_SASLSUCCESS
|
|
||||||
904, IrcCommand::ERR_SASLFAIL
|
|
||||||
905, IrcCommand::ERR_SASLTOOLONG
|
|
||||||
906, IrcCommand::ERR_SASLABORTED
|
|
||||||
907, IrcCommand::ERR_SASLALREADY
|
|
||||||
908, IrcCommand::RPL_SASLMECHS
|
|
||||||
ACCOUNT, IrcCommand::ACCOUNT, 1, 1
|
|
||||||
AUTHENTICATE, IrcCommand::AUTHENTICATE, 1, 1
|
|
||||||
AWAY, IrcCommand::AWAY, 0, 1
|
|
||||||
BATCH, IrcCommand::BATCH
|
|
||||||
BOUNCER, IrcCommand::BOUNCER
|
|
||||||
CAP, IrcCommand::CAP, 1, 15
|
|
||||||
CHGHOST, IrcCommand::CHGHOST, 2, 2
|
|
||||||
ERROR, IrcCommand::ERROR, 1, 1
|
|
||||||
JOIN, IrcCommand::JOIN, 1, 3
|
|
||||||
KICK, IrcCommand::KICK, 3, 3
|
|
||||||
MODE, IrcCommand::MODE, 2, 15
|
|
||||||
NICK, IrcCommand::NICK, 1, 1
|
|
||||||
NOTICE, IrcCommand::NOTICE, 2, 2
|
|
||||||
PART, IrcCommand::PART, 1, 2
|
|
||||||
PING, IrcCommand::PING, 1, 1
|
|
||||||
PRIVMSG, IrcCommand::PRIVMSG, 2, 2
|
|
||||||
QUIT, IrcCommand::QUIT, 1, 1
|
|
||||||
SETNAME, IrcCommand::SETNAME, 1, 1
|
|
||||||
TOPIC, IrcCommand::TOPIC, 2, 2
|
|
857
irc_commands.hpp
857
irc_commands.hpp
@@ -1,857 +0,0 @@
|
|||||||
/* C++ code produced by gperf version 3.1 */
|
|
||||||
/* Command-line: /usr/local/bin/gperf -IC -Z IrcCommandHash -K text -L C++ --output-file irc_commands.hpp -t irc_commands.gperf */
|
|
||||||
/* Computed positions: -k'1-3' */
|
|
||||||
|
|
||||||
#if !((' ' == 32) && ('!' == 33) && ('"' == 34) && ('#' == 35) \
|
|
||||||
&& ('%' == 37) && ('&' == 38) && ('\'' == 39) && ('(' == 40) \
|
|
||||||
&& (')' == 41) && ('*' == 42) && ('+' == 43) && (',' == 44) \
|
|
||||||
&& ('-' == 45) && ('.' == 46) && ('/' == 47) && ('0' == 48) \
|
|
||||||
&& ('1' == 49) && ('2' == 50) && ('3' == 51) && ('4' == 52) \
|
|
||||||
&& ('5' == 53) && ('6' == 54) && ('7' == 55) && ('8' == 56) \
|
|
||||||
&& ('9' == 57) && (':' == 58) && (';' == 59) && ('<' == 60) \
|
|
||||||
&& ('=' == 61) && ('>' == 62) && ('?' == 63) && ('A' == 65) \
|
|
||||||
&& ('B' == 66) && ('C' == 67) && ('D' == 68) && ('E' == 69) \
|
|
||||||
&& ('F' == 70) && ('G' == 71) && ('H' == 72) && ('I' == 73) \
|
|
||||||
&& ('J' == 74) && ('K' == 75) && ('L' == 76) && ('M' == 77) \
|
|
||||||
&& ('N' == 78) && ('O' == 79) && ('P' == 80) && ('Q' == 81) \
|
|
||||||
&& ('R' == 82) && ('S' == 83) && ('T' == 84) && ('U' == 85) \
|
|
||||||
&& ('V' == 86) && ('W' == 87) && ('X' == 88) && ('Y' == 89) \
|
|
||||||
&& ('Z' == 90) && ('[' == 91) && ('\\' == 92) && (']' == 93) \
|
|
||||||
&& ('^' == 94) && ('_' == 95) && ('a' == 97) && ('b' == 98) \
|
|
||||||
&& ('c' == 99) && ('d' == 100) && ('e' == 101) && ('f' == 102) \
|
|
||||||
&& ('g' == 103) && ('h' == 104) && ('i' == 105) && ('j' == 106) \
|
|
||||||
&& ('k' == 107) && ('l' == 108) && ('m' == 109) && ('n' == 110) \
|
|
||||||
&& ('o' == 111) && ('p' == 112) && ('q' == 113) && ('r' == 114) \
|
|
||||||
&& ('s' == 115) && ('t' == 116) && ('u' == 117) && ('v' == 118) \
|
|
||||||
&& ('w' == 119) && ('x' == 120) && ('y' == 121) && ('z' == 122) \
|
|
||||||
&& ('{' == 123) && ('|' == 124) && ('}' == 125) && ('~' == 126))
|
|
||||||
/* The character set is not based on ISO-646. */
|
|
||||||
#error "gperf generated tables don't work with this execution character set. Please report a bug to <bug-gperf@gnu.org>."
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#line 1 "irc_commands.gperf"
|
|
||||||
struct RecognizedCommand {
|
|
||||||
char * text;
|
|
||||||
IrcCommand command;
|
|
||||||
};
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#define TOTAL_KEYWORDS 270
|
|
||||||
#define MIN_WORD_LENGTH 3
|
|
||||||
#define MAX_WORD_LENGTH 12
|
|
||||||
#define MIN_HASH_VALUE 4
|
|
||||||
#define MAX_HASH_VALUE 906
|
|
||||||
/* maximum key range = 903, duplicates = 0 */
|
|
||||||
|
|
||||||
class IrcCommandHash
|
|
||||||
{
|
|
||||||
private:
|
|
||||||
static inline unsigned int hash (const char *str, size_t len);
|
|
||||||
public:
|
|
||||||
static const struct RecognizedCommand *in_word_set (const char *str, size_t len);
|
|
||||||
};
|
|
||||||
|
|
||||||
inline unsigned int
|
|
||||||
IrcCommandHash::hash (const char *str, size_t len)
|
|
||||||
{
|
|
||||||
static const unsigned short asso_values[] =
|
|
||||||
{
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 55, 235,
|
|
||||||
203, 333, 93, 388, 35, 51, 15, 5, 0, 205,
|
|
||||||
20, 85, 98, 10, 100, 3, 435, 165, 500, 106,
|
|
||||||
493, 211, 30, 0, 907, 0, 907, 907, 907, 0,
|
|
||||||
20, 5, 5, 10, 0, 25, 0, 0, 907, 907,
|
|
||||||
0, 907, 0, 907, 0, 907, 0, 907, 20, 0,
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 907, 907,
|
|
||||||
907, 907, 907, 907, 907, 907, 907, 907, 907, 907
|
|
||||||
};
|
|
||||||
return len + asso_values[static_cast<unsigned char>(str[2]+14)] + asso_values[static_cast<unsigned char>(str[1])] + asso_values[static_cast<unsigned char>(str[0]+6)];
|
|
||||||
}
|
|
||||||
|
|
||||||
const struct RecognizedCommand *
|
|
||||||
IrcCommandHash::in_word_set (const char *str, size_t len)
|
|
||||||
{
|
|
||||||
static const struct RecognizedCommand wordlist[] =
|
|
||||||
{
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 272 "irc_commands.gperf"
|
|
||||||
{"PING", IrcCommand::PING},
|
|
||||||
#line 275 "irc_commands.gperf"
|
|
||||||
{"TOPIC", IrcCommand::TOPIC},
|
|
||||||
#line 264 "irc_commands.gperf"
|
|
||||||
{"CAP", IrcCommand::CAP},
|
|
||||||
#line 271 "irc_commands.gperf"
|
|
||||||
{"PART", IrcCommand::PART},
|
|
||||||
{""},
|
|
||||||
#line 269 "irc_commands.gperf"
|
|
||||||
{"NICK", IrcCommand::NICK},
|
|
||||||
#line 265 "irc_commands.gperf"
|
|
||||||
{"ERROR", IrcCommand::ERROR},
|
|
||||||
{""},
|
|
||||||
#line 273 "irc_commands.gperf"
|
|
||||||
{"PRIVMSG", IrcCommand::PRIVMSG},
|
|
||||||
{""},
|
|
||||||
#line 267 "irc_commands.gperf"
|
|
||||||
{"KICK", IrcCommand::KICK},
|
|
||||||
{""},
|
|
||||||
#line 133 "irc_commands.gperf"
|
|
||||||
{"393", IrcCommand::RPL_USERS},
|
|
||||||
{""},
|
|
||||||
#line 199 "irc_commands.gperf"
|
|
||||||
{"491", IrcCommand::ERR_NOOPERHOST},
|
|
||||||
#line 268 "irc_commands.gperf"
|
|
||||||
{"MODE", IrcCommand::MODE},
|
|
||||||
{""},
|
|
||||||
#line 194 "irc_commands.gperf"
|
|
||||||
{"483", IrcCommand::ERR_CANTKILLSERVER},
|
|
||||||
{""},
|
|
||||||
#line 131 "irc_commands.gperf"
|
|
||||||
{"391", IrcCommand::RPL_TIME},
|
|
||||||
#line 266 "irc_commands.gperf"
|
|
||||||
{"JOIN", IrcCommand::JOIN},
|
|
||||||
{""},
|
|
||||||
#line 270 "irc_commands.gperf"
|
|
||||||
{"NOTICE", IrcCommand::NOTICE},
|
|
||||||
{""},
|
|
||||||
#line 192 "irc_commands.gperf"
|
|
||||||
{"481", IrcCommand::ERR_NOPRIVILEGES},
|
|
||||||
#line 274 "irc_commands.gperf"
|
|
||||||
{"QUIT", IrcCommand::QUIT},
|
|
||||||
{""}, {""}, {""},
|
|
||||||
#line 126 "irc_commands.gperf"
|
|
||||||
{"381", IrcCommand::RPL_YOUREOPER},
|
|
||||||
{""}, {""}, {""},
|
|
||||||
#line 263 "irc_commands.gperf"
|
|
||||||
{"BOUNCER", IrcCommand::BOUNCER},
|
|
||||||
#line 211 "irc_commands.gperf"
|
|
||||||
{"691", IrcCommand::ERR_STARTTLS},
|
|
||||||
{""}, {""},
|
|
||||||
#line 176 "irc_commands.gperf"
|
|
||||||
{"463", IrcCommand::ERR_NOPERMFORHOST},
|
|
||||||
{""},
|
|
||||||
#line 66 "irc_commands.gperf"
|
|
||||||
{"281", IrcCommand::RPL_ACCEPTLIST},
|
|
||||||
{""}, {""},
|
|
||||||
#line 114 "irc_commands.gperf"
|
|
||||||
{"363", IrcCommand::RPL_CLOSEEND},
|
|
||||||
{""},
|
|
||||||
#line 174 "irc_commands.gperf"
|
|
||||||
{"461", IrcCommand::ERR_NEEDMOREPARAMS},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 112 "irc_commands.gperf"
|
|
||||||
{"361", IrcCommand::RPL_KILLDONE},
|
|
||||||
{""}, {""},
|
|
||||||
#line 61 "irc_commands.gperf"
|
|
||||||
{"263", IrcCommand::RPL_LOAD2HI},
|
|
||||||
#line 184 "irc_commands.gperf"
|
|
||||||
{"473", IrcCommand::ERR_INVITEONLYCHAN},
|
|
||||||
#line 262 "irc_commands.gperf"
|
|
||||||
{"BATCH", IrcCommand::BATCH},
|
|
||||||
{""}, {""},
|
|
||||||
#line 139 "irc_commands.gperf"
|
|
||||||
{"403", IrcCommand::ERR_NOSUCHCHANNEL},
|
|
||||||
#line 121 "irc_commands.gperf"
|
|
||||||
{"373", IrcCommand::RPL_INFOSTART},
|
|
||||||
#line 59 "irc_commands.gperf"
|
|
||||||
{"261", IrcCommand::RPL_TRACELOG},
|
|
||||||
#line 182 "irc_commands.gperf"
|
|
||||||
{"471", IrcCommand::ERR_CHANNELISFULL},
|
|
||||||
{""},
|
|
||||||
#line 71 "irc_commands.gperf"
|
|
||||||
{"303", IrcCommand::RPL_ISON},
|
|
||||||
{""},
|
|
||||||
#line 137 "irc_commands.gperf"
|
|
||||||
{"401", IrcCommand::ERR_NOSUCHNICK},
|
|
||||||
#line 119 "irc_commands.gperf"
|
|
||||||
{"371", IrcCommand::RPL_INFO},
|
|
||||||
{""},
|
|
||||||
#line 254 "irc_commands.gperf"
|
|
||||||
{"903", IrcCommand::RPL_SASLSUCCESS},
|
|
||||||
{""},
|
|
||||||
#line 69 "irc_commands.gperf"
|
|
||||||
{"301", IrcCommand::RPL_AWAY},
|
|
||||||
{""}, {""},
|
|
||||||
#line 20 "irc_commands.gperf"
|
|
||||||
{"203", IrcCommand::RPL_TRACEUNKNOWN},
|
|
||||||
{""},
|
|
||||||
#line 252 "irc_commands.gperf"
|
|
||||||
{"901", IrcCommand::RPL_LOGGEDOUT},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 18 "irc_commands.gperf"
|
|
||||||
{"201", IrcCommand::RPL_TRACECONNECTING},
|
|
||||||
#line 210 "irc_commands.gperf"
|
|
||||||
{"671", IrcCommand::RPL_WHOISSECURE},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
{""}, {""},
|
|
||||||
#line 8 "irc_commands.gperf"
|
|
||||||
{"003", IrcCommand::RPL_CREATED},
|
|
||||||
{""}, {""},
|
|
||||||
#line 166 "irc_commands.gperf"
|
|
||||||
{"443", IrcCommand::ERR_USERONCHANNEL},
|
|
||||||
{""}, {""}, {""},
|
|
||||||
#line 6 "irc_commands.gperf"
|
|
||||||
{"001", IrcCommand::RPL_WELCOME},
|
|
||||||
{""}, {""},
|
|
||||||
#line 164 "irc_commands.gperf"
|
|
||||||
{"441", IrcCommand::ERR_USERNOTINCHANNEL},
|
|
||||||
{""},
|
|
||||||
#line 200 "irc_commands.gperf"
|
|
||||||
{"492", IrcCommand::ERR_CANNOTSENDTOUSER},
|
|
||||||
{""}, {""},
|
|
||||||
#line 99 "irc_commands.gperf"
|
|
||||||
{"341", IrcCommand::RPL_INVITING},
|
|
||||||
{""},
|
|
||||||
#line 132 "irc_commands.gperf"
|
|
||||||
{"392", IrcCommand::RPL_USERSSTART},
|
|
||||||
#line 43 "irc_commands.gperf"
|
|
||||||
{"243", IrcCommand::RPL_STATSOLINE},
|
|
||||||
{""},
|
|
||||||
#line 191 "irc_commands.gperf"
|
|
||||||
{"480", IrcCommand::ERR_THROTTLE},
|
|
||||||
{""},
|
|
||||||
#line 193 "irc_commands.gperf"
|
|
||||||
{"482", IrcCommand::ERR_CHANOPRIVSNEEDED},
|
|
||||||
{""}, {""},
|
|
||||||
#line 41 "irc_commands.gperf"
|
|
||||||
{"241", IrcCommand::RPL_STATSLLINE},
|
|
||||||
{""},
|
|
||||||
#line 127 "irc_commands.gperf"
|
|
||||||
{"382", IrcCommand::RPL_REHASHING},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 67 "irc_commands.gperf"
|
|
||||||
{"282", IrcCommand::RPL_ENDOFACCEPT},
|
|
||||||
#line 16 "irc_commands.gperf"
|
|
||||||
{"043", IrcCommand::RPL_SAVENICK},
|
|
||||||
{""}, {""}, {""},
|
|
||||||
#line 175 "irc_commands.gperf"
|
|
||||||
{"462", IrcCommand::ERR_ALREADYREGISTRED},
|
|
||||||
{""}, {""},
|
|
||||||
#line 110 "irc_commands.gperf"
|
|
||||||
{"360", IrcCommand::RPL_WHOWASREAL},
|
|
||||||
{""},
|
|
||||||
#line 113 "irc_commands.gperf"
|
|
||||||
{"362", IrcCommand::RPL_CLOSING},
|
|
||||||
#line 180 "irc_commands.gperf"
|
|
||||||
{"467", IrcCommand::ERR_KEYSET},
|
|
||||||
{""},
|
|
||||||
#line 213 "irc_commands.gperf"
|
|
||||||
{"703", IrcCommand::RPL_ENDOFMODLIST},
|
|
||||||
{""}, {""},
|
|
||||||
#line 117 "irc_commands.gperf"
|
|
||||||
{"367", IrcCommand::RPL_BANLIST},
|
|
||||||
{""}, {""},
|
|
||||||
#line 181 "irc_commands.gperf"
|
|
||||||
{"470", IrcCommand::ERR_LINKCHANNEL},
|
|
||||||
#line 60 "irc_commands.gperf"
|
|
||||||
{"262", IrcCommand::RPL_ENDOFTRACE},
|
|
||||||
#line 183 "irc_commands.gperf"
|
|
||||||
{"472", IrcCommand::ERR_UNKNOWNMODE},
|
|
||||||
{""}, {""}, {""},
|
|
||||||
#line 138 "irc_commands.gperf"
|
|
||||||
{"402", IrcCommand::ERR_NOSUCHSERVER},
|
|
||||||
#line 120 "irc_commands.gperf"
|
|
||||||
{"372", IrcCommand::RPL_MOTD},
|
|
||||||
#line 188 "irc_commands.gperf"
|
|
||||||
{"477", IrcCommand::ERR_NEEDREGGEDNICK},
|
|
||||||
#line 68 "irc_commands.gperf"
|
|
||||||
{"300", IrcCommand::RPL_NONE},
|
|
||||||
{""},
|
|
||||||
#line 70 "irc_commands.gperf"
|
|
||||||
{"302", IrcCommand::RPL_USERHOST},
|
|
||||||
#line 143 "irc_commands.gperf"
|
|
||||||
{"407", IrcCommand::ERR_TOOMANYTARGETS},
|
|
||||||
{""},
|
|
||||||
#line 251 "irc_commands.gperf"
|
|
||||||
{"900", IrcCommand::RPL_LOGGEDIN},
|
|
||||||
#line 64 "irc_commands.gperf"
|
|
||||||
{"270", IrcCommand::RPL_PRIVS},
|
|
||||||
#line 253 "irc_commands.gperf"
|
|
||||||
{"902", IrcCommand::ERR_NICKLOCKED},
|
|
||||||
{""}, {""},
|
|
||||||
#line 17 "irc_commands.gperf"
|
|
||||||
{"200", IrcCommand::RPL_TRACELINK},
|
|
||||||
#line 209 "irc_commands.gperf"
|
|
||||||
{"670", IrcCommand::RPL_STARTTLS},
|
|
||||||
#line 19 "irc_commands.gperf"
|
|
||||||
{"202", IrcCommand::RPL_TRACEHANDSHAKE},
|
|
||||||
#line 258 "irc_commands.gperf"
|
|
||||||
{"907", IrcCommand::ERR_SASLALREADY},
|
|
||||||
{""}, {""}, {""},
|
|
||||||
#line 135 "irc_commands.gperf"
|
|
||||||
{"395", IrcCommand::RPL_NOUSERS},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 196 "irc_commands.gperf"
|
|
||||||
{"485", IrcCommand::ERR_BANNEDNICK},
|
|
||||||
#line 247 "irc_commands.gperf"
|
|
||||||
{"743", IrcCommand::ERR_INVALIDBAN},
|
|
||||||
{""}, {""}, {""},
|
|
||||||
#line 129 "irc_commands.gperf"
|
|
||||||
{"385", IrcCommand::RPL_NOTOPERANYMORE},
|
|
||||||
{""}, {""},
|
|
||||||
#line 245 "irc_commands.gperf"
|
|
||||||
{"741", IrcCommand::RPL_ENDOFRSACHALLENGE2},
|
|
||||||
{""},
|
|
||||||
#line 7 "irc_commands.gperf"
|
|
||||||
{"002", IrcCommand::RPL_YOURHOST},
|
|
||||||
#line 163 "irc_commands.gperf"
|
|
||||||
{"440", IrcCommand::ERR_SERVICESDOWN},
|
|
||||||
{""},
|
|
||||||
#line 165 "irc_commands.gperf"
|
|
||||||
{"442", IrcCommand::ERR_NOTONCHANNEL},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 100 "irc_commands.gperf"
|
|
||||||
{"342", IrcCommand::RPL_SUMMONING},
|
|
||||||
{""},
|
|
||||||
#line 178 "irc_commands.gperf"
|
|
||||||
{"465", IrcCommand::ERR_YOUREBANNEDCREEP},
|
|
||||||
{""}, {""}, {""},
|
|
||||||
#line 102 "irc_commands.gperf"
|
|
||||||
{"347", IrcCommand::RPL_ENDOFINVITELIST},
|
|
||||||
#line 116 "irc_commands.gperf"
|
|
||||||
{"365", IrcCommand::RPL_ENDOFLINKS},
|
|
||||||
#line 154 "irc_commands.gperf"
|
|
||||||
{"423", IrcCommand::ERR_NOADMININFO},
|
|
||||||
{""},
|
|
||||||
#line 42 "irc_commands.gperf"
|
|
||||||
{"242", IrcCommand::RPL_STATSUPTIME},
|
|
||||||
{""}, {""},
|
|
||||||
#line 88 "irc_commands.gperf"
|
|
||||||
{"323", IrcCommand::RPL_LISTEND},
|
|
||||||
#line 261 "irc_commands.gperf"
|
|
||||||
{"AWAY", IrcCommand::AWAY},
|
|
||||||
#line 152 "irc_commands.gperf"
|
|
||||||
{"421", IrcCommand::ERR_UNKNOWNCOMMAND},
|
|
||||||
#line 46 "irc_commands.gperf"
|
|
||||||
{"247", IrcCommand::RPL_STATSXLINE},
|
|
||||||
#line 62 "irc_commands.gperf"
|
|
||||||
{"265", IrcCommand::RPL_LOCALUSERS},
|
|
||||||
#line 186 "irc_commands.gperf"
|
|
||||||
{"475", IrcCommand::ERR_BADCHANNELKEY},
|
|
||||||
{""},
|
|
||||||
#line 86 "irc_commands.gperf"
|
|
||||||
{"321", IrcCommand::RPL_LISTSTART},
|
|
||||||
{""},
|
|
||||||
#line 141 "irc_commands.gperf"
|
|
||||||
{"405", IrcCommand::ERR_TOOMANYCHANNELS},
|
|
||||||
#line 123 "irc_commands.gperf"
|
|
||||||
{"375", IrcCommand::RPL_MOTDSTART},
|
|
||||||
{""}, {""}, {""},
|
|
||||||
#line 73 "irc_commands.gperf"
|
|
||||||
{"305", IrcCommand::RPL_UNAWAY},
|
|
||||||
#line 198 "irc_commands.gperf"
|
|
||||||
{"489", IrcCommand::ERR_VOICENEEDED},
|
|
||||||
{""},
|
|
||||||
#line 36 "irc_commands.gperf"
|
|
||||||
{"221", IrcCommand::RPL_UMODEIS},
|
|
||||||
{""},
|
|
||||||
#line 256 "irc_commands.gperf"
|
|
||||||
{"905", IrcCommand::ERR_SASLTOOLONG},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 22 "irc_commands.gperf"
|
|
||||||
{"205", IrcCommand::RPL_TRACEUSER},
|
|
||||||
{""}, {""},
|
|
||||||
#line 148 "irc_commands.gperf"
|
|
||||||
{"413", IrcCommand::ERR_NOTOPLEVEL},
|
|
||||||
{""},
|
|
||||||
#line 212 "irc_commands.gperf"
|
|
||||||
{"702", IrcCommand::RPL_MODLIST},
|
|
||||||
{""}, {""},
|
|
||||||
#line 78 "irc_commands.gperf"
|
|
||||||
{"313", IrcCommand::RPL_WHOISOPERATOR},
|
|
||||||
{""},
|
|
||||||
#line 146 "irc_commands.gperf"
|
|
||||||
{"411", IrcCommand::ERR_NORECIPIENT},
|
|
||||||
#line 217 "irc_commands.gperf"
|
|
||||||
{"707", IrcCommand::ERR_TARGCHANGE},
|
|
||||||
{""}, {""}, {""},
|
|
||||||
#line 76 "irc_commands.gperf"
|
|
||||||
{"311", IrcCommand::RPL_WHOISUSER},
|
|
||||||
#line 80 "irc_commands.gperf"
|
|
||||||
{"369", IrcCommand::RPL_ENDOFWHOWAS},
|
|
||||||
{""},
|
|
||||||
#line 28 "irc_commands.gperf"
|
|
||||||
{"213", IrcCommand::RPL_STATSCLINE},
|
|
||||||
{""},
|
|
||||||
#line 10 "irc_commands.gperf"
|
|
||||||
{"005", IrcCommand::RPL_ISUPPORT},
|
|
||||||
{""}, {""},
|
|
||||||
#line 168 "irc_commands.gperf"
|
|
||||||
{"445", IrcCommand::ERR_SUMMONDISABLED},
|
|
||||||
{""},
|
|
||||||
#line 26 "irc_commands.gperf"
|
|
||||||
{"211", IrcCommand::RPL_STATSLINKINFO},
|
|
||||||
{""},
|
|
||||||
#line 190 "irc_commands.gperf"
|
|
||||||
{"479", IrcCommand::ERR_BADCHANNAME},
|
|
||||||
#line 204 "irc_commands.gperf"
|
|
||||||
{"503", IrcCommand::ERR_GHOSTEDCLIENT},
|
|
||||||
{""},
|
|
||||||
#line 260 "irc_commands.gperf"
|
|
||||||
{"AUTHENTICATE", IrcCommand::AUTHENTICATE},
|
|
||||||
#line 144 "irc_commands.gperf"
|
|
||||||
{"409", IrcCommand::ERR_NOORIGIN},
|
|
||||||
{""}, {""}, {""},
|
|
||||||
#line 202 "irc_commands.gperf"
|
|
||||||
{"501", IrcCommand::ERR_UMODEUNKNOWNFLAG},
|
|
||||||
{""}, {""},
|
|
||||||
#line 45 "irc_commands.gperf"
|
|
||||||
{"245", IrcCommand::RPL_STATSSLINE},
|
|
||||||
{""}, {""},
|
|
||||||
#line 244 "irc_commands.gperf"
|
|
||||||
{"740", IrcCommand::RPL_RSACHALLENGE2},
|
|
||||||
{""},
|
|
||||||
#line 246 "irc_commands.gperf"
|
|
||||||
{"742", IrcCommand::ERR_MLOCKRESTRICTE},
|
|
||||||
{""}, {""},
|
|
||||||
#line 25 "irc_commands.gperf"
|
|
||||||
{"209", IrcCommand::RPL_TRACECLASS},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 232 "irc_commands.gperf"
|
|
||||||
{"723", IrcCommand::ERR_NOPRIVS},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 230 "irc_commands.gperf"
|
|
||||||
{"721", IrcCommand::RPL_OMOTD},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 153 "irc_commands.gperf"
|
|
||||||
{"422", IrcCommand::ERR_NOMOTD},
|
|
||||||
{""},
|
|
||||||
#line 215 "irc_commands.gperf"
|
|
||||||
{"705", IrcCommand::RPL_HELPTXT},
|
|
||||||
#line 85 "irc_commands.gperf"
|
|
||||||
{"320", IrcCommand::RPL_WHOISSPECIAL},
|
|
||||||
{""},
|
|
||||||
#line 87 "irc_commands.gperf"
|
|
||||||
{"322", IrcCommand::RPL_LIST},
|
|
||||||
#line 104 "irc_commands.gperf"
|
|
||||||
{"349", IrcCommand::RPL_ENDOFEXCEPTLIST},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 35 "irc_commands.gperf"
|
|
||||||
{"220", IrcCommand::RPL_STATSPLINE},
|
|
||||||
{""}, {""},
|
|
||||||
#line 48 "irc_commands.gperf"
|
|
||||||
{"249", IrcCommand::RPL_STATSDEBUG},
|
|
||||||
{""}, {""}, {""},
|
|
||||||
#line 223 "irc_commands.gperf"
|
|
||||||
{"713", IrcCommand::ERR_CHANOPEN},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 221 "irc_commands.gperf"
|
|
||||||
{"711", IrcCommand::RPL_KNOCKDLVR},
|
|
||||||
{""}, {""},
|
|
||||||
#line 145 "irc_commands.gperf"
|
|
||||||
{"410", IrcCommand::ERR_INVALIDCAPCMD},
|
|
||||||
{""},
|
|
||||||
#line 147 "irc_commands.gperf"
|
|
||||||
{"412", IrcCommand::ERR_NOTEXTTOSEND},
|
|
||||||
#line 158 "irc_commands.gperf"
|
|
||||||
{"433", IrcCommand::ERR_NICKNAMEINUSE},
|
|
||||||
{""},
|
|
||||||
#line 75 "irc_commands.gperf"
|
|
||||||
{"310", IrcCommand::RPL_WHOISHELPOP},
|
|
||||||
{""},
|
|
||||||
#line 77 "irc_commands.gperf"
|
|
||||||
{"312", IrcCommand::RPL_WHOISSERVER},
|
|
||||||
#line 96 "irc_commands.gperf"
|
|
||||||
{"333", IrcCommand::RPL_TOPICWHOTIME},
|
|
||||||
{""},
|
|
||||||
#line 156 "irc_commands.gperf"
|
|
||||||
{"431", IrcCommand::ERR_NONICKNAMEGIVEN},
|
|
||||||
{""}, {""},
|
|
||||||
#line 82 "irc_commands.gperf"
|
|
||||||
{"317", IrcCommand::RPL_WHOISIDLE},
|
|
||||||
{""},
|
|
||||||
#line 94 "irc_commands.gperf"
|
|
||||||
{"331", IrcCommand::RPL_NOTOPIC},
|
|
||||||
{""},
|
|
||||||
#line 27 "irc_commands.gperf"
|
|
||||||
{"212", IrcCommand::RPL_STATSCOMMANDS},
|
|
||||||
#line 219 "irc_commands.gperf"
|
|
||||||
{"709", IrcCommand::RPL_ETRACE},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 32 "irc_commands.gperf"
|
|
||||||
{"217", IrcCommand::RPL_STATSQLINE},
|
|
||||||
{""}, {""}, {""},
|
|
||||||
#line 203 "irc_commands.gperf"
|
|
||||||
{"502", IrcCommand::ERR_USERSDONTMATCH},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 12 "irc_commands.gperf"
|
|
||||||
{"010", IrcCommand::RPL_REDIR},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 90 "irc_commands.gperf"
|
|
||||||
{"325", IrcCommand::RPL_CHANNELMLOCK},
|
|
||||||
{""}, {""},
|
|
||||||
#line 15 "irc_commands.gperf"
|
|
||||||
{"017", IrcCommand::RPL_MAPEND},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 38 "irc_commands.gperf"
|
|
||||||
{"225", IrcCommand::RPL_STATSDLINE},
|
|
||||||
{""}, {""},
|
|
||||||
#line 229 "irc_commands.gperf"
|
|
||||||
{"720", IrcCommand::RPL_OMOTDSTART},
|
|
||||||
{""},
|
|
||||||
#line 231 "irc_commands.gperf"
|
|
||||||
{"722", IrcCommand::RPL_ENDOFOMOTD},
|
|
||||||
{""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 236 "irc_commands.gperf"
|
|
||||||
{"727", IrcCommand::RPL_TESTMASKGECO},
|
|
||||||
{""},
|
|
||||||
#line 109 "irc_commands.gperf"
|
|
||||||
{"353", IrcCommand::RPL_NAMREPLY},
|
|
||||||
{""},
|
|
||||||
#line 170 "irc_commands.gperf"
|
|
||||||
{"451", IrcCommand::ERR_NOTREGISTERED},
|
|
||||||
{""},
|
|
||||||
#line 150 "irc_commands.gperf"
|
|
||||||
{"415", IrcCommand::ERR_MSGNEEDREGGEDNICK},
|
|
||||||
{""}, {""},
|
|
||||||
#line 105 "irc_commands.gperf"
|
|
||||||
{"351", IrcCommand::RPL_VERSION},
|
|
||||||
{""},
|
|
||||||
#line 108 "irc_commands.gperf"
|
|
||||||
{"315", IrcCommand::RPL_ENDOFWHO},
|
|
||||||
#line 52 "irc_commands.gperf"
|
|
||||||
{"253", IrcCommand::RPL_LUSERUNKNOWN},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 50 "irc_commands.gperf"
|
|
||||||
{"251", IrcCommand::RPL_LUSERCLIENT},
|
|
||||||
{""},
|
|
||||||
#line 30 "irc_commands.gperf"
|
|
||||||
{"215", IrcCommand::RPL_STATSILINE},
|
|
||||||
{""}, {""},
|
|
||||||
#line 220 "irc_commands.gperf"
|
|
||||||
{"710", IrcCommand::RPL_KNOCK},
|
|
||||||
#line 92 "irc_commands.gperf"
|
|
||||||
{"329", IrcCommand::RPL_CREATIONTIME},
|
|
||||||
#line 222 "irc_commands.gperf"
|
|
||||||
{"712", IrcCommand::ERR_TOOMANYKNOCK},
|
|
||||||
#line 242 "irc_commands.gperf"
|
|
||||||
{"733", IrcCommand::RPL_ENDOFMONLIS},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 227 "irc_commands.gperf"
|
|
||||||
{"717", IrcCommand::RPL_TARGNOTIFY},
|
|
||||||
{""},
|
|
||||||
#line 240 "irc_commands.gperf"
|
|
||||||
{"731", IrcCommand::RPL_MONOFFLINE},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 157 "irc_commands.gperf"
|
|
||||||
{"432", IrcCommand::ERR_ERRONEUSNICKNAME},
|
|
||||||
{""},
|
|
||||||
#line 13 "irc_commands.gperf"
|
|
||||||
{"015", IrcCommand::RPL_MAP},
|
|
||||||
#line 93 "irc_commands.gperf"
|
|
||||||
{"330", IrcCommand::RPL_WHOISLOGGEDIN},
|
|
||||||
{""},
|
|
||||||
#line 95 "irc_commands.gperf"
|
|
||||||
{"332", IrcCommand::RPL_TOPIC},
|
|
||||||
#line 161 "irc_commands.gperf"
|
|
||||||
{"437", IrcCommand::ERR_UNAVAILRESOURCE},
|
|
||||||
#line 201 "irc_commands.gperf"
|
|
||||||
{"494", IrcCommand::ERR_OWNMODE},
|
|
||||||
{""}, {""},
|
|
||||||
#line 206 "irc_commands.gperf"
|
|
||||||
{"513", IrcCommand::ERR_WRONGPONG},
|
|
||||||
#line 97 "irc_commands.gperf"
|
|
||||||
{"337", IrcCommand::RPL_WHOISTEXT},
|
|
||||||
#line 134 "irc_commands.gperf"
|
|
||||||
{"394", IrcCommand::RPL_ENDOFUSERS},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 195 "irc_commands.gperf"
|
|
||||||
{"484", IrcCommand::ERR_ISCHANSERVICE},
|
|
||||||
#line 84 "irc_commands.gperf"
|
|
||||||
{"319", IrcCommand::RPL_WHOISCHANNELS},
|
|
||||||
{""},
|
|
||||||
#line 234 "irc_commands.gperf"
|
|
||||||
{"725", IrcCommand::RPL_TESTLINE},
|
|
||||||
{""},
|
|
||||||
#line 128 "irc_commands.gperf"
|
|
||||||
{"384", IrcCommand::RPL_MYPORTIS},
|
|
||||||
{""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 34 "irc_commands.gperf"
|
|
||||||
{"219", IrcCommand::RPL_ENDOFSTATS},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 177 "irc_commands.gperf"
|
|
||||||
{"464", IrcCommand::ERR_PASSWDMISMATCH},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 115 "irc_commands.gperf"
|
|
||||||
{"364", IrcCommand::RPL_LINKS},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 250 "irc_commands.gperf"
|
|
||||||
{"751", IrcCommand::RPL_SCANUMODES},
|
|
||||||
{""},
|
|
||||||
#line 225 "irc_commands.gperf"
|
|
||||||
{"715", IrcCommand::ERR_KNOCKDISABLED},
|
|
||||||
#line 185 "irc_commands.gperf"
|
|
||||||
{"474", IrcCommand::ERR_BANNEDFROMCHAN},
|
|
||||||
{""}, {""}, {""},
|
|
||||||
#line 140 "irc_commands.gperf"
|
|
||||||
{"404", IrcCommand::ERR_CANNOTSENDTOCHAN},
|
|
||||||
#line 122 "irc_commands.gperf"
|
|
||||||
{"374", IrcCommand::RPL_ENDOFINFO},
|
|
||||||
{""},
|
|
||||||
#line 106 "irc_commands.gperf"
|
|
||||||
{"352", IrcCommand::RPL_WHOREPLY},
|
|
||||||
#line 172 "irc_commands.gperf"
|
|
||||||
{"457", IrcCommand::ERR_ACCEPTEXIST},
|
|
||||||
#line 72 "irc_commands.gperf"
|
|
||||||
{"304", IrcCommand::RPL_TEXT},
|
|
||||||
{""}, {""},
|
|
||||||
#line 159 "irc_commands.gperf"
|
|
||||||
{"435", IrcCommand::ERR_BANNICKCHANGE},
|
|
||||||
#line 238 "irc_commands.gperf"
|
|
||||||
{"729", IrcCommand::RPL_ENDOFQUIETLIS},
|
|
||||||
#line 255 "irc_commands.gperf"
|
|
||||||
{"904", IrcCommand::ERR_SASLFAIL},
|
|
||||||
#line 49 "irc_commands.gperf"
|
|
||||||
{"250", IrcCommand::RPL_STATSCONN},
|
|
||||||
{""},
|
|
||||||
#line 51 "irc_commands.gperf"
|
|
||||||
{"252", IrcCommand::RPL_LUSEROP},
|
|
||||||
{""},
|
|
||||||
#line 21 "irc_commands.gperf"
|
|
||||||
{"204", IrcCommand::RPL_TRACEOPERATOR},
|
|
||||||
{""}, {""}, {""},
|
|
||||||
#line 56 "irc_commands.gperf"
|
|
||||||
{"257", IrcCommand::RPL_ADMINLOC1},
|
|
||||||
#line 136 "irc_commands.gperf"
|
|
||||||
{"396", IrcCommand::RPL_HOSTHIDDEN},
|
|
||||||
{""}, {""},
|
|
||||||
#line 40 "irc_commands.gperf"
|
|
||||||
{"235", IrcCommand::RPL_SERVLISTEND},
|
|
||||||
{""},
|
|
||||||
#line 197 "irc_commands.gperf"
|
|
||||||
{"486", IrcCommand::ERR_NONONREG},
|
|
||||||
#line 239 "irc_commands.gperf"
|
|
||||||
{"730", IrcCommand::RPL_MONONLINE},
|
|
||||||
{""},
|
|
||||||
#line 241 "irc_commands.gperf"
|
|
||||||
{"732", IrcCommand::RPL_MONLIST},
|
|
||||||
{""},
|
|
||||||
#line 130 "irc_commands.gperf"
|
|
||||||
{"386", IrcCommand::RPL_RSACHALLENGE},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 9 "irc_commands.gperf"
|
|
||||||
{"004", IrcCommand::RPL_MYINFO},
|
|
||||||
{""}, {""},
|
|
||||||
#line 167 "irc_commands.gperf"
|
|
||||||
{"444", IrcCommand::ERR_NOLOGIN},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 118 "irc_commands.gperf"
|
|
||||||
{"368", IrcCommand::RPL_ENDOFBANLIST},
|
|
||||||
{""},
|
|
||||||
#line 179 "irc_commands.gperf"
|
|
||||||
{"466", IrcCommand::ERR_YOUWILLBEBANNED},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 111 "irc_commands.gperf"
|
|
||||||
{"366", IrcCommand::RPL_ENDOFNAMES},
|
|
||||||
{""}, {""},
|
|
||||||
#line 44 "irc_commands.gperf"
|
|
||||||
{"244", IrcCommand::RPL_STATSHLINE},
|
|
||||||
#line 189 "irc_commands.gperf"
|
|
||||||
{"478", IrcCommand::ERR_BANLISTFULL},
|
|
||||||
{""},
|
|
||||||
#line 207 "irc_commands.gperf"
|
|
||||||
{"517", IrcCommand::ERR_DISABLED},
|
|
||||||
{""}, {""},
|
|
||||||
#line 125 "irc_commands.gperf"
|
|
||||||
{"378", IrcCommand::RPL_WHOISHOST},
|
|
||||||
#line 63 "irc_commands.gperf"
|
|
||||||
{"266", IrcCommand::RPL_GLOBALUSERS},
|
|
||||||
#line 187 "irc_commands.gperf"
|
|
||||||
{"476", IrcCommand::ERR_BADCHANMASK},
|
|
||||||
{""}, {""}, {""},
|
|
||||||
#line 142 "irc_commands.gperf"
|
|
||||||
{"406", IrcCommand::ERR_WASNOSUCHNICK},
|
|
||||||
#line 124 "irc_commands.gperf"
|
|
||||||
{"376", IrcCommand::RPL_ENDOFMOTD},
|
|
||||||
{""},
|
|
||||||
#line 259 "irc_commands.gperf"
|
|
||||||
{"908", IrcCommand::RPL_SASLMECHS},
|
|
||||||
{""},
|
|
||||||
#line 74 "irc_commands.gperf"
|
|
||||||
{"306", IrcCommand::RPL_NOWAWAY},
|
|
||||||
{""}, {""},
|
|
||||||
#line 24 "irc_commands.gperf"
|
|
||||||
{"208", IrcCommand::RPL_TRACENEWTYPE},
|
|
||||||
{""},
|
|
||||||
#line 257 "irc_commands.gperf"
|
|
||||||
{"906", IrcCommand::ERR_SASLABORTED},
|
|
||||||
#line 65 "irc_commands.gperf"
|
|
||||||
{"276", IrcCommand::RPL_WHOISCERTFP},
|
|
||||||
{""},
|
|
||||||
#line 54 "irc_commands.gperf"
|
|
||||||
{"255", IrcCommand::RPL_LUSERME},
|
|
||||||
{""},
|
|
||||||
#line 23 "irc_commands.gperf"
|
|
||||||
{"206", IrcCommand::RPL_TRACESERVER},
|
|
||||||
#line 249 "irc_commands.gperf"
|
|
||||||
{"750", IrcCommand::RPL_SCANMATCHED},
|
|
||||||
{""}, {""}, {""},
|
|
||||||
#line 214 "irc_commands.gperf"
|
|
||||||
{"704", IrcCommand::RPL_HELPSTART},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 11 "irc_commands.gperf"
|
|
||||||
{"008", IrcCommand::RPL_SNOMASK},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 103 "irc_commands.gperf"
|
|
||||||
{"348", IrcCommand::RPL_EXCEPTLIST},
|
|
||||||
{""},
|
|
||||||
#line 169 "irc_commands.gperf"
|
|
||||||
{"446", IrcCommand::ERR_USERSDISABLED},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 101 "irc_commands.gperf"
|
|
||||||
{"346", IrcCommand::RPL_INVITELIST},
|
|
||||||
{""}, {""},
|
|
||||||
#line 47 "irc_commands.gperf"
|
|
||||||
{"248", IrcCommand::RPL_STATSULINE},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
{""}, {""},
|
|
||||||
#line 248 "irc_commands.gperf"
|
|
||||||
{"744", IrcCommand::ERR_TOPICLOCK},
|
|
||||||
#line 58 "irc_commands.gperf"
|
|
||||||
{"259", IrcCommand::RPL_ADMINEMAIL},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 218 "irc_commands.gperf"
|
|
||||||
{"708", IrcCommand::RPL_ETRACEFULL},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 155 "irc_commands.gperf"
|
|
||||||
{"424", IrcCommand::ERR_FILEERROR},
|
|
||||||
{""},
|
|
||||||
#line 216 "irc_commands.gperf"
|
|
||||||
{"706", IrcCommand::RPL_ENDOFHELP},
|
|
||||||
{""}, {""},
|
|
||||||
#line 89 "irc_commands.gperf"
|
|
||||||
{"324", IrcCommand::RPL_CHANNELMODEIS},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 37 "irc_commands.gperf"
|
|
||||||
{"224", IrcCommand::RPL_STATSFLINE},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 149 "irc_commands.gperf"
|
|
||||||
{"414", IrcCommand::ERR_WILDTOPLEVEL},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 79 "irc_commands.gperf"
|
|
||||||
{"314", IrcCommand::RPL_WHOWASUSER},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 29 "irc_commands.gperf"
|
|
||||||
{"214", IrcCommand::RPL_STATSNLINE},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 205 "irc_commands.gperf"
|
|
||||||
{"504", IrcCommand::ERR_USERNOTONSERV},
|
|
||||||
{""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 91 "irc_commands.gperf"
|
|
||||||
{"328", IrcCommand::RPL_CHANNELURL},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
{""}, {""}, {""},
|
|
||||||
#line 233 "irc_commands.gperf"
|
|
||||||
{"724", IrcCommand::RPL_TESTMASK},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 83 "irc_commands.gperf"
|
|
||||||
{"318", IrcCommand::RPL_ENDOFWHOIS},
|
|
||||||
{""},
|
|
||||||
#line 151 "irc_commands.gperf"
|
|
||||||
{"416", IrcCommand::ERR_TOOMANYMATCHES},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 81 "irc_commands.gperf"
|
|
||||||
{"316", IrcCommand::RPL_WHOISCHANOP},
|
|
||||||
{""}, {""},
|
|
||||||
#line 33 "irc_commands.gperf"
|
|
||||||
{"218", IrcCommand::RPL_STATSYLINE},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 31 "irc_commands.gperf"
|
|
||||||
{"216", IrcCommand::RPL_STATSKLINE},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 224 "irc_commands.gperf"
|
|
||||||
{"714", IrcCommand::ERR_KNOCKONCHAN},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
{""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 14 "irc_commands.gperf"
|
|
||||||
{"016", IrcCommand::RPL_MAPMORE},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
{""},
|
|
||||||
#line 237 "irc_commands.gperf"
|
|
||||||
{"728", IrcCommand::RPL_QUIETLIST},
|
|
||||||
{""},
|
|
||||||
#line 39 "irc_commands.gperf"
|
|
||||||
{"234", IrcCommand::RPL_SERVLIST},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 235 "irc_commands.gperf"
|
|
||||||
{"726", IrcCommand::RPL_NOTESTLINE},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 228 "irc_commands.gperf"
|
|
||||||
{"718", IrcCommand::RPL_UMODEGMSG},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 226 "irc_commands.gperf"
|
|
||||||
{"716", IrcCommand::ERR_TARGUMODEG},
|
|
||||||
{""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 162 "irc_commands.gperf"
|
|
||||||
{"438", IrcCommand::ERR_NICKTOOFAST},
|
|
||||||
{""},
|
|
||||||
#line 107 "irc_commands.gperf"
|
|
||||||
{"354", IrcCommand::RPL_WHOSPCRPL},
|
|
||||||
{""}, {""},
|
|
||||||
#line 98 "irc_commands.gperf"
|
|
||||||
{"338", IrcCommand::RPL_WHOISACTUALLY},
|
|
||||||
{""},
|
|
||||||
#line 160 "irc_commands.gperf"
|
|
||||||
{"436", IrcCommand::ERR_NICKCOLLISION},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 53 "irc_commands.gperf"
|
|
||||||
{"254", IrcCommand::RPL_LUSERCHANNELS},
|
|
||||||
{""}, {""}, {""}, {""},
|
|
||||||
#line 208 "irc_commands.gperf"
|
|
||||||
{"524", IrcCommand::ERR_HELPNOTFOUND},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 243 "irc_commands.gperf"
|
|
||||||
{"734", IrcCommand::ERR_MONLISTFULL},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 173 "irc_commands.gperf"
|
|
||||||
{"458", IrcCommand::ERR_ACCEPTNOT},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 171 "irc_commands.gperf"
|
|
||||||
{"456", IrcCommand::ERR_ACCEPTFULL},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 57 "irc_commands.gperf"
|
|
||||||
{"258", IrcCommand::RPL_ADMINLOC2},
|
|
||||||
{""}, {""}, {""}, {""}, {""}, {""},
|
|
||||||
#line 55 "irc_commands.gperf"
|
|
||||||
{"256", IrcCommand::RPL_ADMINME}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (len <= MAX_WORD_LENGTH && len >= MIN_WORD_LENGTH)
|
|
||||||
{
|
|
||||||
unsigned int key = hash (str, len);
|
|
||||||
|
|
||||||
if (key <= MAX_HASH_VALUE)
|
|
||||||
{
|
|
||||||
const char *s = wordlist[key].text;
|
|
||||||
|
|
||||||
if (*str == *s && !strcmp (str + 1, s + 1))
|
|
||||||
return &wordlist[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
@@ -1,26 +0,0 @@
|
|||||||
#include "irc_parse_thread.hpp"
|
|
||||||
|
|
||||||
#include "connection.hpp"
|
|
||||||
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
#include "irc_commands.inc"
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
auto irc_parse_thread(Connection& connection) -> void
|
|
||||||
{
|
|
||||||
connection.add_listener<LineEvent>([&connection](LineEvent const& event)
|
|
||||||
{
|
|
||||||
auto const msg = parse_irc_message(event.line);
|
|
||||||
auto const recognized = IrcCommandHash::in_word_set(msg.command.data(), msg.command.size());
|
|
||||||
auto const command
|
|
||||||
= recognized
|
|
||||||
&& recognized->min_args <= msg.args.size()
|
|
||||||
&& recognized->max_args >= msg.args.size()
|
|
||||||
? recognized->command : IrcCommand::UNKNOWN;
|
|
||||||
connection.make_event<IrcMsgEvent>(command, msg);
|
|
||||||
});
|
|
||||||
}
|
|
149
ircmsg.cpp
149
ircmsg.cpp
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
62
ircmsg.hpp
62
ircmsg.hpp
@@ -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>;
|
|
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
122
main.cpp
122
main.cpp
@@ -1,122 +0,0 @@
|
|||||||
#include <boost/asio.hpp>
|
|
||||||
|
|
||||||
#include "connection.hpp"
|
|
||||||
#include "ircmsg.hpp"
|
|
||||||
#include "linebuffer.hpp"
|
|
||||||
#include "settings.hpp"
|
|
||||||
#include "thread.hpp"
|
|
||||||
#include "write_irc.hpp"
|
|
||||||
|
|
||||||
#include "irc_parse_thread.hpp"
|
|
||||||
#include "registration_thread.hpp"
|
|
||||||
#include "ping_thread.hpp"
|
|
||||||
#include "watchdog_thread.hpp"
|
|
||||||
#include "snote_thread.hpp"
|
|
||||||
#include "self_thread.hpp"
|
|
||||||
#include "command_thread.hpp"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <chrono>
|
|
||||||
#include <fstream>
|
|
||||||
#include <coroutine>
|
|
||||||
#include <iostream>
|
|
||||||
#include <limits>
|
|
||||||
#include <list>
|
|
||||||
#include <memory>
|
|
||||||
#include <string>
|
|
||||||
#include <string_view>
|
|
||||||
#include <tuple>
|
|
||||||
#include <utility>
|
|
||||||
#include <variant>
|
|
||||||
#include <vector>
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <unordered_set>
|
|
||||||
|
|
||||||
using namespace std::chrono_literals;
|
|
||||||
|
|
||||||
auto unhandled_message_thread(Connection& connection) -> void
|
|
||||||
{
|
|
||||||
connection.add_listener<IrcMsgEvent>([](IrcMsgEvent const& event)
|
|
||||||
{
|
|
||||||
if (IrcCommand::UNKNOWN == event.command)
|
|
||||||
{
|
|
||||||
auto& irc = event.irc;
|
|
||||||
std::cout << "Unknown message " << irc.command;
|
|
||||||
for (auto const arg : irc.args)
|
|
||||||
{
|
|
||||||
std::cout << " " << arg;
|
|
||||||
}
|
|
||||||
std::cout << "\n";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
auto echo_thread(Connection& connection) -> void
|
|
||||||
{
|
|
||||||
connection.add_listener<CommandEvent>([&connection](CommandEvent& event)
|
|
||||||
{
|
|
||||||
if ("raw" == event.command
|
|
||||||
and "glguy" == event.oper
|
|
||||||
and "glguy" == event.account)
|
|
||||||
{
|
|
||||||
auto txt = std::string{event.arg};
|
|
||||||
txt += "\r\n";
|
|
||||||
connection.write_line(std::move(txt));
|
|
||||||
event.handled_ = true;
|
|
||||||
send_notice(connection, event.nick, "ack");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
auto start(boost::asio::io_context & io, Settings const& settings) -> void
|
|
||||||
{
|
|
||||||
auto connection = std::make_shared<Connection>(io);
|
|
||||||
|
|
||||||
watchdog_thread(*connection);
|
|
||||||
irc_parse_thread(*connection);
|
|
||||||
ping_thread(*connection);
|
|
||||||
auto const self_thread = SelfThread::start(*connection);
|
|
||||||
registration_thread(*connection, settings.password, settings.username, settings.realname, settings.nickname);
|
|
||||||
SnoteThread::start(*connection);
|
|
||||||
CommandThread::start(*connection);
|
|
||||||
echo_thread(*connection);
|
|
||||||
unhandled_message_thread(*connection);
|
|
||||||
|
|
||||||
connection->add_listener<SnoteEvent>([](SnoteEvent& event)
|
|
||||||
{
|
|
||||||
std::cout << "Snote match " << static_cast<int>(event.tag) << 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_from_now(5s);
|
|
||||||
timer->async_wait([&io, &settings, timer](auto) {
|
|
||||||
start(io, settings);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
auto get_settings() -> Settings
|
|
||||||
{
|
|
||||||
if (auto config_stream = std::ifstream {"config.toml"})
|
|
||||||
{
|
|
||||||
return Settings::from_stream(config_stream);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
std::cerr << "Unable to open config.toml\n";
|
|
||||||
std::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto main() -> int
|
|
||||||
{
|
|
||||||
auto const settings = get_settings();
|
|
||||||
auto io = boost::asio::io_context{};
|
|
||||||
start(io, settings);
|
|
||||||
io.run();
|
|
||||||
}
|
|
2
mybase64/CMakeLists.txt
Normal file
2
mybase64/CMakeLists.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
add_library(mybase64 STATIC mybase64.cpp)
|
||||||
|
target_include_directories(mybase64 PUBLIC include)
|
41
mybase64/include/mybase64.hpp
Normal file
41
mybase64/include/mybase64.hpp
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* @file mybase64.hpp
|
||||||
|
* @author Eric Mertens (emertens@gmail.com)
|
||||||
|
* @brief Base64 encoding and decoding
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace mybase64 {
|
||||||
|
|
||||||
|
inline constexpr auto encoded_size(std::size_t len) -> std::size_t
|
||||||
|
{
|
||||||
|
return (len + 2) / 3 * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline constexpr auto decoded_size(std::size_t len) -> std::size_t
|
||||||
|
{
|
||||||
|
return (len + 3) / 4 * 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Encode a string into base64
|
||||||
|
*
|
||||||
|
* @param input input text
|
||||||
|
* @param output Target buffer for encoded value
|
||||||
|
*/
|
||||||
|
auto encode(std::string_view input, char* output) -> void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Decode a base64 encoded string
|
||||||
|
*
|
||||||
|
* @param input Base64 input text
|
||||||
|
* @param output Target buffer for decoded value
|
||||||
|
* @return pointer to end of output on success
|
||||||
|
*/
|
||||||
|
auto decode(std::string_view input, char* output) -> char*;
|
||||||
|
|
||||||
|
} // namespace
|
102
mybase64/mybase64.cpp
Normal file
102
mybase64/mybase64.cpp
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
#include "mybase64.hpp"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <climits>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace mybase64 {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr std::array<char, 64> alphabet{
|
||||||
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
|
||||||
|
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
|
||||||
|
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
|
||||||
|
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr std::array<std::int8_t, 256> alphabet_values = []() constexpr {
|
||||||
|
std::array<std::int8_t, 256> result;
|
||||||
|
result.fill(-1);
|
||||||
|
std::int8_t v = 0;
|
||||||
|
for (auto const k : alphabet)
|
||||||
|
{
|
||||||
|
result[k] = v++;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static_assert(CHAR_BIT == 8);
|
||||||
|
|
||||||
|
auto encode(std::string_view const input, char* output) -> void
|
||||||
|
{
|
||||||
|
auto cursor = std::begin(input);
|
||||||
|
auto const end = std::end(input);
|
||||||
|
|
||||||
|
while (end - cursor >= 3)
|
||||||
|
{
|
||||||
|
std::uint32_t buffer = std::uint8_t(*cursor++);
|
||||||
|
buffer <<= 8;
|
||||||
|
buffer |= std::uint8_t(*cursor++);
|
||||||
|
buffer <<= 8;
|
||||||
|
buffer |= std::uint8_t(*cursor++);
|
||||||
|
|
||||||
|
*output++ = alphabet[(buffer >> 6 * 3) % 64];
|
||||||
|
*output++ = alphabet[(buffer >> 6 * 2) % 64];
|
||||||
|
*output++ = alphabet[(buffer >> 6 * 1) % 64];
|
||||||
|
*output++ = alphabet[(buffer >> 6 * 0) % 64];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursor < end)
|
||||||
|
{
|
||||||
|
std::uint32_t buffer = std::uint8_t(*cursor++) << 10;
|
||||||
|
if (cursor < end)
|
||||||
|
buffer |= std::uint8_t(*cursor) << 2;
|
||||||
|
|
||||||
|
*output++ = alphabet[(buffer >> 12) % 64];
|
||||||
|
*output++ = alphabet[(buffer >> 6) % 64];
|
||||||
|
*output++ = cursor < end ? alphabet[(buffer % 64)] : '=';
|
||||||
|
*output++ = '=';
|
||||||
|
}
|
||||||
|
*output = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
auto decode(std::string_view const input, char* output) -> char*
|
||||||
|
{
|
||||||
|
std::uint32_t buffer = 1;
|
||||||
|
|
||||||
|
for (auto const c : input)
|
||||||
|
{
|
||||||
|
if (auto const value = alphabet_values[uint8_t(c)]; -1 != value)
|
||||||
|
{
|
||||||
|
buffer = (buffer << 6) | value;
|
||||||
|
if (buffer & 1 << 6 * 4)
|
||||||
|
{
|
||||||
|
*output++ = buffer >> 16;
|
||||||
|
*output++ = buffer >> 8;
|
||||||
|
*output++ = buffer >> 0;
|
||||||
|
buffer = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffer & 1 << 6 * 3)
|
||||||
|
{
|
||||||
|
*output++ = buffer >> 10;
|
||||||
|
*output++ = buffer >> 2;
|
||||||
|
}
|
||||||
|
else if (buffer & 1 << 6 * 2)
|
||||||
|
{
|
||||||
|
*output++ = buffer >> 4;
|
||||||
|
}
|
||||||
|
else if (buffer & 1 << 6 * 1)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
33
myirc/CMakeLists.txt
Normal file
33
myirc/CMakeLists.txt
Normal 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
83
myirc/bot.cpp
Normal 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
95
myirc/challenge.cpp
Normal 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
651
myirc/client.cpp
Normal 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
453
myirc/connection.cpp
Normal 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
|
49
myirc/include/myirc/bot.hpp
Normal file
49
myirc/include/myirc/bot.hpp
Normal 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
|
20
myirc/include/myirc/c_callback.hpp
Normal file
20
myirc/include/myirc/c_callback.hpp
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace myirc {
|
||||||
|
|
||||||
|
template <typename> struct CCallback_;
|
||||||
|
template <typename F, typename R, typename... Ts>
|
||||||
|
struct CCallback_<R (F::*) (Ts...) const>
|
||||||
|
{
|
||||||
|
static R invoke(Ts... args, void* u)
|
||||||
|
{
|
||||||
|
return (*reinterpret_cast<F*>(u))(args...);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// @brief Wrapper for passing closures through C-style callbacks.
|
||||||
|
/// @tparam F Type of the closure
|
||||||
|
template <typename F>
|
||||||
|
using CCallback = CCallback_<decltype(&F::operator())>;
|
||||||
|
|
||||||
|
} // namespace myirc
|
35
myirc/include/myirc/challenge.hpp
Normal file
35
myirc/include/myirc/challenge.hpp
Normal 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
|
145
myirc/include/myirc/client.hpp
Normal file
145
myirc/include/myirc/client.hpp
Normal 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
|
107
myirc/include/myirc/connection.hpp
Normal file
107
myirc/include/myirc/connection.hpp
Normal 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
|
@@ -1,11 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "thread.hpp"
|
namespace myirc {
|
||||||
|
|
||||||
#include <string_view>
|
|
||||||
|
|
||||||
class Connection;
|
|
||||||
|
|
||||||
|
|
||||||
enum class IrcCommand
|
enum class IrcCommand
|
||||||
{
|
{
|
||||||
@@ -18,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,
|
||||||
@@ -33,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,
|
||||||
@@ -41,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,
|
||||||
@@ -63,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,
|
||||||
@@ -72,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,
|
||||||
@@ -85,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,
|
||||||
@@ -101,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,
|
||||||
@@ -116,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,
|
||||||
@@ -125,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,
|
||||||
@@ -158,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,
|
||||||
@@ -171,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,
|
||||||
@@ -200,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,
|
||||||
@@ -208,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,
|
||||||
@@ -237,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,
|
||||||
@@ -252,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,
|
||||||
@@ -272,25 +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,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct IrcMsgEvent : Event
|
} // namespace myirc
|
||||||
{
|
|
||||||
IrcMsgEvent(IrcCommand command, IrcMsg const& irc)
|
|
||||||
: command{command}, irc{irc} {}
|
|
||||||
IrcCommand command;
|
|
||||||
IrcMsg const& irc;
|
|
||||||
};
|
|
||||||
|
|
||||||
auto irc_parse_thread(Connection& connection) -> void;
|
|
282
myirc/include/myirc/irc_coroutine.hpp
Normal file
282
myirc/include/myirc/irc_coroutine.hpp
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "myirc/connection.hpp"
|
||||||
|
#include "myirc/snote.hpp"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <coroutine>
|
||||||
|
#include <initializer_list>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace myirc {
|
||||||
|
|
||||||
|
struct irc_promise;
|
||||||
|
|
||||||
|
/// A coroutine that can co_await on various IRC events
|
||||||
|
struct irc_coroutine : std::coroutine_handle<irc_promise>
|
||||||
|
{
|
||||||
|
using promise_type = irc_promise;
|
||||||
|
|
||||||
|
/// Start the coroutine and associate it with a specific connection.
|
||||||
|
auto start(Connection &connection) -> void;
|
||||||
|
|
||||||
|
/// Returns true when this coroutine is still waiting on events
|
||||||
|
auto is_running() -> bool;
|
||||||
|
|
||||||
|
/// Returns the exception that terminated this coroutine, if there is one.
|
||||||
|
auto exception() -> std::exception_ptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct irc_promise
|
||||||
|
{
|
||||||
|
// Pointer to the connection while running. Cleared on termination.
|
||||||
|
std::shared_ptr<Connection> connection_;
|
||||||
|
|
||||||
|
// Pointer to exception that terminated this coroutine if there is one.
|
||||||
|
std::exception_ptr exception_;
|
||||||
|
|
||||||
|
auto get_return_object() -> irc_coroutine
|
||||||
|
{
|
||||||
|
return {irc_coroutine::from_promise(*this)};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suspend waiting for start() to initialize connection_
|
||||||
|
auto initial_suspend() noexcept -> std::suspend_always { return {}; }
|
||||||
|
|
||||||
|
// Suspend so that is_running() and exception() work
|
||||||
|
auto final_suspend() noexcept -> std::suspend_always { return {}; }
|
||||||
|
|
||||||
|
// Normal termination
|
||||||
|
auto return_void() -> void
|
||||||
|
{
|
||||||
|
connection_.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abnormal termination - remember the exception
|
||||||
|
auto unhandled_exception() -> void
|
||||||
|
{
|
||||||
|
connection_.reset();
|
||||||
|
exception_ = std::current_exception();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename... Ts>
|
||||||
|
class Wait;
|
||||||
|
|
||||||
|
/// Argument to a Wait that expects one or more IRC messages
|
||||||
|
class wait_ircmsg
|
||||||
|
{
|
||||||
|
// Vector of commands this wait is expecting. Leave empty to accept all messages.
|
||||||
|
std::vector<IrcCommand> want_cmds_;
|
||||||
|
|
||||||
|
// Slot for the ircmsg event
|
||||||
|
boost::signals2::scoped_connection ircmsg_slot_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
using result_type = std::pair<IrcCommand, const IrcMsg &>;
|
||||||
|
|
||||||
|
wait_ircmsg(std::initializer_list<IrcCommand> want_cmds)
|
||||||
|
: want_cmds_{want_cmds}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
template <size_t I, typename... Ts>
|
||||||
|
auto start(Wait<Ts...> &command) -> void;
|
||||||
|
auto stop() -> void { ircmsg_slot_.disconnect(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
class wait_snote
|
||||||
|
{
|
||||||
|
// Vector of tags this wait is expecting. Leave empty to accept all messages.
|
||||||
|
std::vector<SnoteTag> want_tags_;
|
||||||
|
|
||||||
|
// Slot for the snote event
|
||||||
|
boost::signals2::scoped_connection snote_slot_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
using result_type = SnoteMatch;
|
||||||
|
|
||||||
|
wait_snote(std::initializer_list<SnoteTag> want_tags)
|
||||||
|
: want_tags_{want_tags}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
template <size_t I, typename... Ts>
|
||||||
|
auto start(Wait<Ts...> &command) -> void;
|
||||||
|
auto stop() -> void { snote_slot_.disconnect(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
class wait_timeout
|
||||||
|
{
|
||||||
|
std::optional<boost::asio::steady_timer> timer_;
|
||||||
|
std::chrono::milliseconds timeout_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
struct result_type
|
||||||
|
{
|
||||||
|
};
|
||||||
|
wait_timeout(std::chrono::milliseconds timeout)
|
||||||
|
: timeout_{timeout}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
template <size_t I, typename... Ts>
|
||||||
|
auto start(Wait<Ts...> &command) -> void;
|
||||||
|
auto stop() -> void { timer_->cancel(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename... Ts>
|
||||||
|
class Wait
|
||||||
|
{
|
||||||
|
// State associated with each wait mode
|
||||||
|
std::tuple<Ts...> modes_;
|
||||||
|
|
||||||
|
// Result from any one of the wait modes
|
||||||
|
std::optional<std::variant<typename Ts::result_type...>> result_;
|
||||||
|
|
||||||
|
// Handle of the continuation to be resumed when one of the wait
|
||||||
|
// modes is ready.
|
||||||
|
std::coroutine_handle<irc_promise> handle_;
|
||||||
|
|
||||||
|
// Slot for tearing down the irc_coroutine in case the connection
|
||||||
|
// fails before any wait modes complete.
|
||||||
|
boost::signals2::scoped_connection disconnect_slot_;
|
||||||
|
|
||||||
|
template <size_t I>
|
||||||
|
auto start_mode() -> void
|
||||||
|
{
|
||||||
|
std::get<I>(modes_).template start<I, Ts...>(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <std::size_t... Indices>
|
||||||
|
auto start_modes(std::index_sequence<Indices...>) -> void
|
||||||
|
{
|
||||||
|
(start_mode<Indices>(), ...);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <std::size_t... Indices>
|
||||||
|
auto stop_modes(std::index_sequence<Indices...>) -> void
|
||||||
|
{
|
||||||
|
(std::get<Indices>(modes_).stop(), ...);
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
Wait(Ts &&...modes)
|
||||||
|
: modes_{std::forward<Ts>(modes)...}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the connection that this coroutine was started with.
|
||||||
|
auto get_connection() const -> Connection &
|
||||||
|
{
|
||||||
|
return *handle_.promise().connection_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store a successful result and resume the coroutine
|
||||||
|
template <size_t I, typename... Args>
|
||||||
|
auto complete(Args &&...args) -> void
|
||||||
|
{
|
||||||
|
result_.emplace(std::in_place_index<I>, std::forward<Args>(args)...);
|
||||||
|
handle_.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The coroutine always needs to wait for a message. It will never
|
||||||
|
// be ready immediately.
|
||||||
|
auto await_ready() noexcept -> bool { return false; }
|
||||||
|
|
||||||
|
/// Install event handles in the connection that will resume this coroutine.
|
||||||
|
auto await_suspend(std::coroutine_handle<irc_promise> handle) -> void;
|
||||||
|
|
||||||
|
auto await_resume() -> std::variant<typename Ts::result_type...>;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <size_t I, typename... Ts>
|
||||||
|
auto wait_ircmsg::start(Wait<Ts...> &command) -> void
|
||||||
|
{
|
||||||
|
ircmsg_slot_ = command.get_connection().sig_ircmsg.connect([this, &command](auto cmd, auto &msg) {
|
||||||
|
const auto wanted = want_cmds_.empty() || std::find(want_cmds_.begin(), want_cmds_.end(), cmd) != want_cmds_.end();
|
||||||
|
if (wanted)
|
||||||
|
{
|
||||||
|
command.template complete<I>(cmd, msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
template <size_t I, typename... Ts>
|
||||||
|
auto wait_snote::start(Wait<Ts...> &command) -> void
|
||||||
|
{
|
||||||
|
snote_slot_ = command.get_connection().sig_snote.connect([this, &command](auto &match) {
|
||||||
|
const auto tag = match.get_tag();
|
||||||
|
const auto wanted = want_tags_.empty() || std::find(want_tags_.begin(), want_tags_.end(), tag) != want_tags_.end();
|
||||||
|
if (wanted)
|
||||||
|
{
|
||||||
|
command.template complete<I>(match);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
template <size_t I, typename... Ts>
|
||||||
|
auto wait_timeout::start(Wait<Ts...> &command) -> void
|
||||||
|
{
|
||||||
|
timer_.emplace(command.get_connection().get_executor());
|
||||||
|
timer_->expires_after(timeout_);
|
||||||
|
timer_->async_wait([this, &command](const auto &error) {
|
||||||
|
if (not error)
|
||||||
|
{
|
||||||
|
timer_.reset();
|
||||||
|
command.template complete<I>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Ts>
|
||||||
|
auto Wait<Ts...>::await_suspend(std::coroutine_handle<irc_promise> handle) -> void
|
||||||
|
{
|
||||||
|
handle_ = handle;
|
||||||
|
|
||||||
|
const auto tuple_size = std::tuple_size_v<decltype(modes_)>;
|
||||||
|
start_modes(std::make_index_sequence<tuple_size>{});
|
||||||
|
|
||||||
|
disconnect_slot_ = get_connection().sig_disconnect.connect([this](auto) {
|
||||||
|
handle_.resume();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Ts>
|
||||||
|
auto Wait<Ts...>::await_resume() -> std::variant<typename Ts::result_type...>
|
||||||
|
{
|
||||||
|
const auto tuple_size = std::tuple_size_v<decltype(modes_)>;
|
||||||
|
stop_modes(std::make_index_sequence<tuple_size>{});
|
||||||
|
|
||||||
|
disconnect_slot_.disconnect();
|
||||||
|
|
||||||
|
if (result_)
|
||||||
|
{
|
||||||
|
return std::move(*result_);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw std::runtime_error{"connection terminated"};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the coroutine and associate it with a specific connection.
|
||||||
|
inline auto irc_coroutine::start(Connection &connection) -> void
|
||||||
|
{
|
||||||
|
promise().connection_ = connection.shared_from_this();
|
||||||
|
resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true when this coroutine is still waiting on events
|
||||||
|
inline auto irc_coroutine::is_running() -> bool
|
||||||
|
{
|
||||||
|
return promise().connection_ != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline auto irc_coroutine::exception() -> std::exception_ptr
|
||||||
|
{
|
||||||
|
return promise().exception_;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace myirc
|
78
myirc/include/myirc/ircmsg.hpp
Normal file
78
myirc/include/myirc/ircmsg.hpp
Normal 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
|
104
myirc/include/myirc/linebuffer.hpp
Normal file
104
myirc/include/myirc/linebuffer.hpp
Normal 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
|
13
myirc/include/myirc/openssl_utils.hpp
Normal file
13
myirc/include/myirc/openssl_utils.hpp
Normal 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
|
24
myirc/include/myirc/ratelimit.hpp
Normal file
24
myirc/include/myirc/ratelimit.hpp
Normal 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
|
68
myirc/include/myirc/ref.hpp
Normal file
68
myirc/include/myirc/ref.hpp
Normal 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
|
50
myirc/include/myirc/registration.hpp
Normal file
50
myirc/include/myirc/registration.hpp
Normal 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
|
114
myirc/include/myirc/sasl_mechanism.hpp
Normal file
114
myirc/include/myirc/sasl_mechanism.hpp
Normal 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
|
101
myirc/include/myirc/snote.hpp
Normal file
101
myirc/include/myirc/snote.hpp
Normal 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 ®ex, 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
|
118
myirc/include/myirc/stream.hpp
Normal file
118
myirc/include/myirc/stream.hpp
Normal 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
252
myirc/irc_commands.gperf
Normal 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
190
myirc/ircmsg.cpp
Normal 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
54
myirc/linebuffer.cpp
Normal 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
71
myirc/openssl_utils.cpp
Normal 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
28
myirc/ratelimit.cpp
Normal 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
147
myirc/registration.cpp
Normal 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
79
myirc/sasl_mechanism.cpp
Normal 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
286
myirc/snote.cpp
Normal 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
3
mysocks5/CMakeLists.txt
Normal 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
404
mysocks5/include/socks5.hpp
Normal 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
93
mysocks5/socks5.cpp
Normal 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
|
@@ -1,17 +0,0 @@
|
|||||||
#include "ping_thread.hpp"
|
|
||||||
|
|
||||||
#include "irc_parse_thread.hpp"
|
|
||||||
#include "write_irc.hpp"
|
|
||||||
|
|
||||||
auto ping_thread(Connection& connection) -> void
|
|
||||||
{
|
|
||||||
connection.add_listener<IrcMsgEvent>([&connection](IrcMsgEvent& event)
|
|
||||||
{
|
|
||||||
auto& irc = event.irc;
|
|
||||||
if (IrcCommand::PING == event.command)
|
|
||||||
{
|
|
||||||
send_pong(connection, irc.args[0]);
|
|
||||||
event.handled_ = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
@@ -1,6 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "connection.hpp"
|
|
||||||
#include "thread.hpp"
|
|
||||||
|
|
||||||
auto ping_thread(Connection& connection) -> void;
|
|
@@ -1,216 +0,0 @@
|
|||||||
#include "registration_thread.hpp"
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
#include <unordered_set>
|
|
||||||
#include <unordered_map>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
struct RegistrationThread : std::enable_shared_from_this<RegistrationThread>
|
|
||||||
{
|
|
||||||
Connection& connection_;
|
|
||||||
std::string password_;
|
|
||||||
std::string username_;
|
|
||||||
std::string realname_;
|
|
||||||
std::string nickname_;
|
|
||||||
|
|
||||||
std::unordered_map<std::string, std::string> caps;
|
|
||||||
std::unordered_set<std::string> outstanding;
|
|
||||||
|
|
||||||
Connection::Handle<ConnectEvent> connect_handle_;
|
|
||||||
Connection::Handle<IrcMsgEvent> message_handle_;
|
|
||||||
|
|
||||||
enum class Stage
|
|
||||||
{
|
|
||||||
LsReply,
|
|
||||||
AckReply,
|
|
||||||
};
|
|
||||||
|
|
||||||
Stage stage_;
|
|
||||||
|
|
||||||
RegistrationThread(
|
|
||||||
Connection& connection_,
|
|
||||||
std::string password,
|
|
||||||
std::string username,
|
|
||||||
std::string realname,
|
|
||||||
std::string nickname
|
|
||||||
);
|
|
||||||
|
|
||||||
auto on_connect() -> void;
|
|
||||||
|
|
||||||
auto send_req() -> void;
|
|
||||||
|
|
||||||
auto capack(IrcMsg const& msg) -> void;
|
|
||||||
|
|
||||||
auto capls(IrcMsg const& msg) -> void;
|
|
||||||
|
|
||||||
auto on_msg(IrcMsg const& msg) -> void;
|
|
||||||
};
|
|
||||||
|
|
||||||
RegistrationThread::RegistrationThread(
|
|
||||||
Connection& connection,
|
|
||||||
std::string password,
|
|
||||||
std::string username,
|
|
||||||
std::string realname,
|
|
||||||
std::string nickname
|
|
||||||
)
|
|
||||||
: connection_{connection}
|
|
||||||
, password_{password}
|
|
||||||
, username_{username}
|
|
||||||
, realname_{realname}
|
|
||||||
, nickname_{nickname}
|
|
||||||
, stage_{Stage::LsReply}
|
|
||||||
{}
|
|
||||||
|
|
||||||
auto RegistrationThread::on_connect() -> void
|
|
||||||
{
|
|
||||||
send_cap_ls(connection_);
|
|
||||||
send_pass(connection_, password_);
|
|
||||||
send_user(connection_, username_, realname_);
|
|
||||||
send_nick(connection_, nickname_);
|
|
||||||
|
|
||||||
connection_.remove_listener(connect_handle_);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto RegistrationThread::send_req() -> void
|
|
||||||
{
|
|
||||||
std::string request;
|
|
||||||
char const* const want[] = {
|
|
||||||
"extended-join",
|
|
||||||
"account-notify",
|
|
||||||
"draft/chathistory",
|
|
||||||
"batch",
|
|
||||||
"soju.im/no-implicit-names",
|
|
||||||
"chghost",
|
|
||||||
"setname",
|
|
||||||
"account-tag",
|
|
||||||
"solanum.chat/oper",
|
|
||||||
"solanum.chat/identify-msg",
|
|
||||||
"solanum.chat/realhost",
|
|
||||||
"server-time",
|
|
||||||
"invite-notify",
|
|
||||||
"extended-join"
|
|
||||||
};
|
|
||||||
|
|
||||||
for (auto cap : want)
|
|
||||||
{
|
|
||||||
if (caps.contains(cap))
|
|
||||||
{
|
|
||||||
request.append(cap);
|
|
||||||
request.push_back(' ');
|
|
||||||
outstanding.insert(cap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (not outstanding.empty())
|
|
||||||
{
|
|
||||||
request.pop_back();
|
|
||||||
send_cap_req(connection_, request);
|
|
||||||
stage_ = Stage::AckReply;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
send_cap_end(connection_);
|
|
||||||
connection_.remove_listener(message_handle_);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto RegistrationThread::capack(IrcMsg const& msg) -> void
|
|
||||||
{
|
|
||||||
auto const n = msg.args.size();
|
|
||||||
if (n >= 2 && "*" == msg.args[0] && "ACK" == msg.args[1])
|
|
||||||
{
|
|
||||||
auto in = std::istringstream{std::string{msg.args[2]}};
|
|
||||||
std::for_each(
|
|
||||||
std::istream_iterator<std::string>{in},
|
|
||||||
std::istream_iterator<std::string>{},
|
|
||||||
[this](std::string x) {
|
|
||||||
outstanding.erase(x);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (outstanding.empty())
|
|
||||||
{
|
|
||||||
send_cap_end(connection_);
|
|
||||||
connection_.remove_listener(message_handle_);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto RegistrationThread::capls(IrcMsg const& msg) -> void
|
|
||||||
{
|
|
||||||
auto const n = msg.args.size();
|
|
||||||
if (n >= 2 && "*" == msg.args[0] && "LS" == msg.args[1])
|
|
||||||
{
|
|
||||||
std::string_view const* kvs;
|
|
||||||
bool last;
|
|
||||||
|
|
||||||
if (3 == n)
|
|
||||||
{
|
|
||||||
kvs = &msg.args[2];
|
|
||||||
last = true;
|
|
||||||
}
|
|
||||||
else if (4 == n && "*" == msg.args[2])
|
|
||||||
{
|
|
||||||
kvs = &msg.args[3];
|
|
||||||
last = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto in = std::istringstream{std::string{*kvs}};
|
|
||||||
|
|
||||||
std::for_each(
|
|
||||||
std::istream_iterator<std::string>{in},
|
|
||||||
std::istream_iterator<std::string>{},
|
|
||||||
[this](std::string x) {
|
|
||||||
auto const eq = x.find('=');
|
|
||||||
if (eq == x.npos)
|
|
||||||
{
|
|
||||||
caps.emplace(x, std::string{});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
caps.emplace(std::string{x, 0, eq}, std::string{x, eq+1, x.npos});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (last)
|
|
||||||
{
|
|
||||||
send_req();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto RegistrationThread::on_msg(IrcMsg const& msg) -> void
|
|
||||||
{
|
|
||||||
switch (stage_)
|
|
||||||
{
|
|
||||||
case Stage::LsReply: capls(msg); return;
|
|
||||||
case Stage::AckReply: capack(msg); return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
auto registration_thread(
|
|
||||||
Connection& connection,
|
|
||||||
std::string password,
|
|
||||||
std::string username,
|
|
||||||
std::string realname,
|
|
||||||
std::string nickname
|
|
||||||
) -> void
|
|
||||||
{
|
|
||||||
auto const thread = std::make_shared<RegistrationThread>(connection, password, username, realname, nickname);
|
|
||||||
thread->message_handle_ = connection.add_listener<IrcMsgEvent>([thread](IrcMsgEvent const& event)
|
|
||||||
{
|
|
||||||
if (IrcCommand::CAP == event.command)
|
|
||||||
{
|
|
||||||
thread->on_msg(event.irc);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
thread->connect_handle_ = connection.add_listener<ConnectEvent>([thread](ConnectEvent const&)
|
|
||||||
{
|
|
||||||
thread->on_connect();
|
|
||||||
});
|
|
||||||
}
|
|
@@ -1,18 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "thread.hpp"
|
|
||||||
#include "connection.hpp"
|
|
||||||
#include "irc_parse_thread.hpp"
|
|
||||||
#include "write_irc.hpp"
|
|
||||||
|
|
||||||
#include <eventpp/eventdispatcher.h>
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
auto registration_thread(
|
|
||||||
Connection& connection,
|
|
||||||
std::string password,
|
|
||||||
std::string username,
|
|
||||||
std::string realname,
|
|
||||||
std::string nickname
|
|
||||||
) -> void;
|
|
@@ -1,76 +0,0 @@
|
|||||||
#include "self_thread.hpp"
|
|
||||||
|
|
||||||
#include "connection.hpp"
|
|
||||||
#include "irc_parse_thread.hpp"
|
|
||||||
|
|
||||||
auto SelfThread::start(Connection& connection) -> std::shared_ptr<SelfThread>
|
|
||||||
{
|
|
||||||
auto thread = std::make_shared<SelfThread>(connection);
|
|
||||||
|
|
||||||
connection.add_listener<IrcMsgEvent>([thread](IrcMsgEvent& event)
|
|
||||||
{
|
|
||||||
switch (event.command)
|
|
||||||
{
|
|
||||||
// Learn nickname from 001
|
|
||||||
case IrcCommand::RPL_WELCOME:
|
|
||||||
thread->nickname_ = event.irc.args[0];
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Track changes to our nickname
|
|
||||||
case IrcCommand::NICK:
|
|
||||||
{
|
|
||||||
auto const bang = event.irc.source.find('!');
|
|
||||||
if (bang != std::string::npos
|
|
||||||
&& thread->nickname_ == event.irc.source.substr(0, bang)
|
|
||||||
)
|
|
||||||
{
|
|
||||||
thread->nickname_ = event.irc.args[0];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-establish user modes
|
|
||||||
case IrcCommand::RPL_UMODEIS:
|
|
||||||
thread->mode_ = event.irc.args[1];
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Interpret self mode changes
|
|
||||||
case IrcCommand::MODE:
|
|
||||||
if (2 == event.irc.args.size()
|
|
||||||
&& thread->nickname_ == event.irc.args[0]
|
|
||||||
)
|
|
||||||
{
|
|
||||||
auto polarity = true;
|
|
||||||
for (char const c : event.irc.args[1])
|
|
||||||
{
|
|
||||||
switch (c)
|
|
||||||
{
|
|
||||||
case '+':
|
|
||||||
polarity = true;
|
|
||||||
break;
|
|
||||||
case '-':
|
|
||||||
polarity = false;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (polarity)
|
|
||||||
{
|
|
||||||
thread->mode_ += c;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
auto const ix = thread->mode_.find(c);
|
|
||||||
if (ix != std::string::npos)
|
|
||||||
{
|
|
||||||
thread->mode_.erase(ix, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default: break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return thread;
|
|
||||||
}
|
|
@@ -1,18 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
struct Connection;
|
|
||||||
|
|
||||||
class SelfThread
|
|
||||||
{
|
|
||||||
Connection& connection_;
|
|
||||||
std::string nickname_;
|
|
||||||
std::string mode_;
|
|
||||||
|
|
||||||
public:
|
|
||||||
SelfThread(Connection& connection) : connection_{connection} {}
|
|
||||||
static auto start(Connection&) -> std::shared_ptr<SelfThread>;
|
|
||||||
};
|
|
||||||
|
|
17
settings.cpp
17
settings.cpp
@@ -1,17 +0,0 @@
|
|||||||
#include "settings.hpp"
|
|
||||||
|
|
||||||
#define TOML_ENABLE_FORMATTERS 0
|
|
||||||
#include <toml++/toml.hpp>
|
|
||||||
|
|
||||||
auto Settings::from_stream(std::istream & in) -> Settings
|
|
||||||
{
|
|
||||||
auto const config = toml::parse(in);
|
|
||||||
return Settings{
|
|
||||||
.host = config["host"].value_or(std::string{"*"}),
|
|
||||||
.service = config["service"].value_or(std::string{"*"}),
|
|
||||||
.password = config["password"].value_or(std::string{"*"}),
|
|
||||||
.username = config["username"].value_or(std::string{"*"}),
|
|
||||||
.realname = config["realname"].value_or(std::string{"*"}),
|
|
||||||
.nickname = config["nickname"].value_or(std::string{"*"})
|
|
||||||
};
|
|
||||||
}
|
|
17
settings.hpp
17
settings.hpp
@@ -1,17 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
struct Settings
|
|
||||||
{
|
|
||||||
std::string host;
|
|
||||||
std::string service;
|
|
||||||
std::string password;
|
|
||||||
std::string username;
|
|
||||||
std::string realname;
|
|
||||||
std::string nickname;
|
|
||||||
|
|
||||||
static auto from_stream(std::istream & in) -> Settings;
|
|
||||||
};
|
|
||||||
|
|
138
snote_thread.cpp
138
snote_thread.cpp
@@ -1,138 +0,0 @@
|
|||||||
#include "snote_thread.hpp"
|
|
||||||
|
|
||||||
#include "irc_parse_thread.hpp"
|
|
||||||
#include "connection.hpp"
|
|
||||||
|
|
||||||
#include <cstring>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <stdexcept>
|
|
||||||
#include <utility>
|
|
||||||
#include <regex>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
struct SnotePattern
|
|
||||||
{
|
|
||||||
SnotePattern(SnoteTag tag, char const* expression, unsigned flags = 0)
|
|
||||||
: tag{tag}
|
|
||||||
, expression{expression}
|
|
||||||
, flags{flags}
|
|
||||||
, regex{expression, std::regex_constants::ECMAScript | std::regex_constants::optimize}
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
SnoteTag tag;
|
|
||||||
char const* expression;
|
|
||||||
unsigned flags;
|
|
||||||
std::regex regex;
|
|
||||||
};
|
|
||||||
|
|
||||||
SnotePattern const patterns[] =
|
|
||||||
{
|
|
||||||
{SnoteTag::ClientConnecting,
|
|
||||||
R"(^Client connecting: ([^ ]+) \(([^@ ]+)@([^) ]+)\) \[(.*)\] \{([^ ]*)\} <([^ ]*)> \[(.*)\]$)"},
|
|
||||||
|
|
||||||
{SnoteTag::ClientExiting,
|
|
||||||
R"(^Client exiting: ([^ ]+) \(([^@ ]+)@([^) ]+)\) \[(.*)\] \[(.*)\]$)"},
|
|
||||||
};
|
|
||||||
|
|
||||||
auto setup_database() -> std::unique_ptr<hs_database_t, SnoteThread::DbDeleter>
|
|
||||||
{
|
|
||||||
std::vector<char const*> expressions;
|
|
||||||
std::vector<unsigned> flags;
|
|
||||||
std::vector<unsigned> ids;
|
|
||||||
|
|
||||||
expressions.reserve(std::size(patterns));
|
|
||||||
flags.reserve(std::size(patterns));
|
|
||||||
ids.reserve(std::size(patterns));
|
|
||||||
|
|
||||||
unsigned id = 0;
|
|
||||||
|
|
||||||
for (auto const& pattern : patterns)
|
|
||||||
{
|
|
||||||
expressions.push_back(pattern.expression);
|
|
||||||
flags.push_back(pattern.flags);
|
|
||||||
ids.push_back(id++);
|
|
||||||
}
|
|
||||||
|
|
||||||
hs_database_t* db;
|
|
||||||
hs_compile_error *error;
|
|
||||||
|
|
||||||
switch (hs_compile_multi(expressions.data(), flags.data(), ids.data(), expressions.size(), HS_MODE_BLOCK, nullptr, &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 std::unique_ptr<hs_database_t, SnoteThread::DbDeleter>{db};
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
auto SnoteThread::start(Connection& connection) -> std::shared_ptr<SnoteThread>
|
|
||||||
{
|
|
||||||
auto thread = std::make_shared<SnoteThread>();
|
|
||||||
|
|
||||||
thread->db_ = setup_database();
|
|
||||||
|
|
||||||
hs_scratch_t* scratch = nullptr;
|
|
||||||
if (HS_SUCCESS != hs_alloc_scratch(thread->db_.get(), &scratch))
|
|
||||||
{
|
|
||||||
abort();
|
|
||||||
}
|
|
||||||
thread->scratch_ = std::unique_ptr<hs_scratch_t, ScratchDeleter>{scratch};
|
|
||||||
|
|
||||||
static char const* const prefix = "*** Notice -- ";
|
|
||||||
connection.add_listener<IrcMsgEvent>([&connection, thread](IrcMsgEvent& event)
|
|
||||||
{
|
|
||||||
auto& args = event.irc.args;
|
|
||||||
if (IrcCommand::NOTICE == event.command
|
|
||||||
&& "*" == args[0]
|
|
||||||
&& args[1].starts_with(prefix))
|
|
||||||
{
|
|
||||||
event.handled_ = true;
|
|
||||||
auto message = args[1].substr(strlen(prefix));
|
|
||||||
unsigned int match_id = -1;
|
|
||||||
auto const scan_result = hs_scan(thread->db_.get(), message.data(), message.size(), 0, thread->scratch_.get(),
|
|
||||||
[](unsigned int id, unsigned long long from, unsigned long long to, unsigned int flags, void *context) -> int
|
|
||||||
{
|
|
||||||
int* const match_id = static_cast<int*>(context);
|
|
||||||
*match_id = id;
|
|
||||||
return 1; // stop scanning
|
|
||||||
}
|
|
||||||
, &match_id);
|
|
||||||
|
|
||||||
if (scan_result != HS_SUCCESS && scan_result != HS_SCAN_TERMINATED)
|
|
||||||
{
|
|
||||||
abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match_id != -1)
|
|
||||||
{
|
|
||||||
auto& pattern = patterns[match_id];
|
|
||||||
std::match_results<std::string_view::const_iterator> results;
|
|
||||||
if (not std::regex_match(message.begin(), message.end(), results, pattern.regex))
|
|
||||||
{
|
|
||||||
// something went wrong - hyperscan disagrees with std::regex
|
|
||||||
abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string_view> parts;
|
|
||||||
for (auto const sub : results)
|
|
||||||
{
|
|
||||||
parts.push_back(std::string_view{sub.first, sub.second});
|
|
||||||
}
|
|
||||||
connection.make_event<SnoteEvent>(pattern.tag, std::move(parts));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return thread;
|
|
||||||
}
|
|
@@ -1,56 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "thread.hpp"
|
|
||||||
|
|
||||||
#include <hs.h>
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
class Connection;
|
|
||||||
|
|
||||||
enum class SnoteTag
|
|
||||||
{
|
|
||||||
ClientConnecting,
|
|
||||||
ClientExiting,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SnoteEvent : Event
|
|
||||||
{
|
|
||||||
SnoteEvent(SnoteTag tag, std::vector<std::string_view> parts)
|
|
||||||
: tag{tag}
|
|
||||||
, parts{std::move(parts)}
|
|
||||||
{}
|
|
||||||
|
|
||||||
SnoteTag tag;
|
|
||||||
std::vector<std::string_view> parts;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SnoteThread
|
|
||||||
{
|
|
||||||
struct DbDeleter
|
|
||||||
{
|
|
||||||
auto operator()(hs_database_t * db) const -> void
|
|
||||||
{
|
|
||||||
if (HS_SUCCESS != hs_free_database(db))
|
|
||||||
{
|
|
||||||
abort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ScratchDeleter
|
|
||||||
{
|
|
||||||
auto operator()(hs_scratch_t * scratch) const -> void
|
|
||||||
{
|
|
||||||
if (HS_SUCCESS != hs_free_scratch(scratch))
|
|
||||||
{
|
|
||||||
abort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
std::unique_ptr<hs_database_t, DbDeleter> db_;
|
|
||||||
std::unique_ptr<hs_scratch_t, ScratchDeleter> scratch_;
|
|
||||||
|
|
||||||
static auto start(Connection& connection) -> std::shared_ptr<SnoteThread>;
|
|
||||||
};
|
|
@@ -1 +0,0 @@
|
|||||||
#include "thread.hpp"
|
|
11
thread.hpp
11
thread.hpp
@@ -1,11 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "ircmsg.hpp"
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
struct Event {
|
|
||||||
virtual ~Event() {}
|
|
||||||
bool handled_ = false;
|
|
||||||
};
|
|
@@ -1,93 +0,0 @@
|
|||||||
#include "watchdog_thread.hpp"
|
|
||||||
|
|
||||||
#include "connection.hpp"
|
|
||||||
#include "irc_parse_thread.hpp"
|
|
||||||
#include "write_irc.hpp"
|
|
||||||
|
|
||||||
#include <boost/asio/steady_timer.hpp>
|
|
||||||
|
|
||||||
#include <chrono>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
using namespace std::chrono_literals;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
struct WatchdogThread : std::enable_shared_from_this<WatchdogThread>
|
|
||||||
{
|
|
||||||
WatchdogThread(Connection& connection)
|
|
||||||
: connection_{connection}
|
|
||||||
, timer_{connection.get_executor()}
|
|
||||||
, tried_ping{false}
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
Connection& connection_;
|
|
||||||
boost::asio::steady_timer timer_;
|
|
||||||
bool tried_ping;
|
|
||||||
|
|
||||||
auto on_activity() -> void
|
|
||||||
{
|
|
||||||
tried_ping = false;
|
|
||||||
timer_.expires_from_now(30s);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto timeout_token()
|
|
||||||
{
|
|
||||||
return [weak = weak_from_this()](auto const& error)
|
|
||||||
{
|
|
||||||
if (not error)
|
|
||||||
{
|
|
||||||
if (auto self = weak.lock())
|
|
||||||
{
|
|
||||||
self->on_timeout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
auto on_timeout() -> void
|
|
||||||
{
|
|
||||||
if (tried_ping)
|
|
||||||
{
|
|
||||||
connection_.close();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
send_ping(connection_, "watchdog");
|
|
||||||
tried_ping = true;
|
|
||||||
timer_.expires_from_now(30s);
|
|
||||||
timer_.async_wait(timeout_token());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto on_connect() -> void
|
|
||||||
{
|
|
||||||
on_activity();
|
|
||||||
timer_.async_wait(timeout_token());
|
|
||||||
}
|
|
||||||
|
|
||||||
auto on_disconnect() -> void
|
|
||||||
{
|
|
||||||
timer_.cancel();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
auto watchdog_thread(Connection& connection) -> void
|
|
||||||
{
|
|
||||||
auto const thread = std::make_shared<WatchdogThread>(connection);
|
|
||||||
connection.add_listener<ConnectEvent>([thread](auto&)
|
|
||||||
{
|
|
||||||
thread->on_connect();
|
|
||||||
});
|
|
||||||
connection.add_listener<DisconnectEvent>([thread](auto&)
|
|
||||||
{
|
|
||||||
thread->on_disconnect();
|
|
||||||
});
|
|
||||||
connection.add_listener<IrcMsgEvent>([thread](auto&)
|
|
||||||
{
|
|
||||||
thread->on_activity();
|
|
||||||
});
|
|
||||||
}
|
|
@@ -1,4 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
class Connection;
|
|
||||||
auto watchdog_thread(Connection& connection) -> void;
|
|
@@ -1,97 +0,0 @@
|
|||||||
#include "write_irc.hpp"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
auto write_irc(Connection& connection, std::string message) -> void
|
|
||||||
{
|
|
||||||
connection.write_line(std::move(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
auto is_invalid_last(char x) -> bool
|
|
||||||
{
|
|
||||||
return x == '\0' || x == '\r' || x == '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
auto is_invalid(char x) -> bool
|
|
||||||
{
|
|
||||||
return x == '\0' || x == '\r' || x == '\n' || x == ' ';
|
|
||||||
}
|
|
||||||
|
|
||||||
auto write_irc(Connection& connection, std::string front, std::string_view last) -> void
|
|
||||||
{
|
|
||||||
if (last.end() != std::find_if(last.begin(), last.end(), is_invalid_last))
|
|
||||||
{
|
|
||||||
throw std::runtime_error{"bad irc argument"};
|
|
||||||
}
|
|
||||||
|
|
||||||
front += " :";
|
|
||||||
front += last;
|
|
||||||
write_irc(connection, std::move(front));
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename... Args>
|
|
||||||
auto write_irc(Connection& connection, std::string front, std::string_view next, Args ...rest) -> void
|
|
||||||
{
|
|
||||||
if (next.empty()
|
|
||||||
|| next.front() == ':'
|
|
||||||
|| next.end() != std::find_if(next.begin(), next.end(), is_invalid))
|
|
||||||
{
|
|
||||||
throw std::runtime_error{"bad irc argument"};
|
|
||||||
}
|
|
||||||
|
|
||||||
front += " ";
|
|
||||||
front += next;
|
|
||||||
write_irc(connection, std::move(front), rest...);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
auto send_ping(Connection& connection, std::string_view txt) -> void
|
|
||||||
{
|
|
||||||
write_irc(connection, "PING", txt);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto send_pong(Connection& connection, std::string_view txt) -> void
|
|
||||||
{
|
|
||||||
write_irc(connection, "PONG", txt);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto send_pass(Connection& connection, std::string_view password) -> void
|
|
||||||
{
|
|
||||||
write_irc(connection, "PASS", password);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto send_user(Connection& connection, std::string_view user, std::string_view real) -> void
|
|
||||||
{
|
|
||||||
write_irc(connection, "USER", user, "*", "*", real);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto send_nick(Connection& connection, std::string_view nick) -> void
|
|
||||||
{
|
|
||||||
write_irc(connection, "NICK", nick);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto send_cap_ls(Connection& connection) -> void
|
|
||||||
{
|
|
||||||
write_irc(connection, "CAP", "LS", "302");
|
|
||||||
}
|
|
||||||
|
|
||||||
auto send_cap_end(Connection& connection) -> void
|
|
||||||
{
|
|
||||||
write_irc(connection, "CAP", "END");
|
|
||||||
}
|
|
||||||
|
|
||||||
auto send_cap_req(Connection& connection, std::string_view caps) -> void
|
|
||||||
{
|
|
||||||
write_irc(connection, "CAP", "REQ", caps);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto send_privmsg(Connection& connection, std::string_view target, std::string_view message) -> void
|
|
||||||
{
|
|
||||||
write_irc(connection, "PRIVMSG", target, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto send_notice(Connection& connection, std::string_view target, std::string_view message) -> void
|
|
||||||
{
|
|
||||||
write_irc(connection, "NOTICE", target, message);
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "connection.hpp"
|
|
||||||
|
|
||||||
auto send_ping(Connection&, std::string_view) -> void;
|
|
||||||
auto send_pong(Connection&, std::string_view) -> void;
|
|
||||||
auto send_pass(Connection&, std::string_view) -> void;
|
|
||||||
auto send_user(Connection&, std::string_view, std::string_view) -> void;
|
|
||||||
auto send_nick(Connection&, std::string_view) -> void;
|
|
||||||
auto send_cap_ls(Connection&) -> void;
|
|
||||||
auto send_cap_end(Connection&) -> void;
|
|
||||||
auto send_cap_req(Connection&, std::string_view) -> void;
|
|
||||||
auto send_privmsg(Connection&, std::string_view, std::string_view) -> void;
|
|
||||||
auto send_notice(Connection&, std::string_view, std::string_view) -> void;
|
|
Reference in New Issue
Block a user