commit 0ea7ec3437ea0bcd0e73a212017cd75a3af39e0e Author: Nils Date: Fri Jun 18 15:25:14 2021 +0200 Initial commit diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..0353d6c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.5) + +project(asbots LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_executable(asbots + main.cpp + instance.cpp + config.cpp +) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(libasync-uv REQUIRED IMPORTED_TARGET libasync-uv) +pkg_check_modules(fmt REQUIRED IMPORTED_TARGET fmt) + +target_link_libraries(asbots PRIVATE PkgConfig::libasync-uv PkgConfig::fmt) diff --git a/CMakeLists.txt.user b/CMakeLists.txt.user new file mode 100644 index 0000000..6120418 --- /dev/null +++ b/CMakeLists.txt.user @@ -0,0 +1,372 @@ + + + + + + EnvironmentId + {5d908aae-49cb-4b8b-8f9a-9f24b78d3565} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + false + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 80 + true + true + 1 + false + true + false + 0 + true + true + 0 + 8 + true + false + 1 + true + true + true + *.md, *.MD, Makefile + false + true + + + + ProjectExplorer.Project.PluginSettings + + + true + false + true + true + true + true + + + 0 + true + + true + Builtin.BuildSystem + + true + true + Builtin.DefaultTidyAndClazy + 2 + + + + true + + + + + ProjectExplorer.Project.Target.0 + + Desktop + Desktop + Desktop + {fbd9cd40-b514-4726-85e4-90c4e3e2ff63} + 0 + 0 + 0 + + Debug + -GUnix Makefiles +-DCMAKE_BUILD_TYPE:STRING=Debug +-DCMAKE_PROJECT_INCLUDE_BEFORE:PATH=%{IDE:ResourcePath}/package-manager/auto-setup.cmake +-DQT_QMAKE_EXECUTABLE:STRING=%{Qt:qmakeExecutable} +-DCMAKE_PREFIX_PATH:STRING=%{Qt:QT_INSTALL_PREFIX} +-DCMAKE_C_COMPILER:STRING=%{Compiler:Executable:C} +-DCMAKE_CXX_COMPILER:STRING=%{Compiler:Executable:Cxx} + /mnt/ghdd/Programme/OSS/asbots/../build-asbots-Desktop-Debug + + + + all + + true + Build + CMakeProjectManager.MakeStep + + 1 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + + clean + + true + Build + CMakeProjectManager.MakeStep + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Debug + CMakeProjectManager.CMakeBuildConfiguration + + + Release + -GUnix Makefiles +-DCMAKE_BUILD_TYPE:STRING=Release +-DCMAKE_PROJECT_INCLUDE_BEFORE:PATH=%{IDE:ResourcePath}/package-manager/auto-setup.cmake +-DQT_QMAKE_EXECUTABLE:STRING=%{Qt:qmakeExecutable} +-DCMAKE_PREFIX_PATH:STRING=%{Qt:QT_INSTALL_PREFIX} +-DCMAKE_C_COMPILER:STRING=%{Compiler:Executable:C} +-DCMAKE_CXX_COMPILER:STRING=%{Compiler:Executable:Cxx} + /mnt/ghdd/Programme/OSS/asbots/../build-asbots-Desktop-Release + + + + all + + true + CMakeProjectManager.MakeStep + + 1 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + + clean + + true + CMakeProjectManager.MakeStep + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Release + CMakeProjectManager.CMakeBuildConfiguration + + + RelWithDebInfo + -GUnix Makefiles +-DCMAKE_BUILD_TYPE:STRING=RelWithDebInfo +-DCMAKE_PROJECT_INCLUDE_BEFORE:PATH=%{IDE:ResourcePath}/package-manager/auto-setup.cmake +-DQT_QMAKE_EXECUTABLE:STRING=%{Qt:qmakeExecutable} +-DCMAKE_PREFIX_PATH:STRING=%{Qt:QT_INSTALL_PREFIX} +-DCMAKE_C_COMPILER:STRING=%{Compiler:Executable:C} +-DCMAKE_CXX_COMPILER:STRING=%{Compiler:Executable:Cxx} + /mnt/ghdd/Programme/OSS/asbots/../build-asbots-Desktop-RelWithDebInfo + + + + all + + true + CMakeProjectManager.MakeStep + + 1 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + + clean + + true + CMakeProjectManager.MakeStep + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Release with Debug Information + CMakeProjectManager.CMakeBuildConfiguration + + + MinSizeRel + -GUnix Makefiles +-DCMAKE_BUILD_TYPE:STRING=MinSizeRel +-DCMAKE_PROJECT_INCLUDE_BEFORE:PATH=%{IDE:ResourcePath}/package-manager/auto-setup.cmake +-DQT_QMAKE_EXECUTABLE:STRING=%{Qt:qmakeExecutable} +-DCMAKE_PREFIX_PATH:STRING=%{Qt:QT_INSTALL_PREFIX} +-DCMAKE_C_COMPILER:STRING=%{Compiler:Executable:C} +-DCMAKE_CXX_COMPILER:STRING=%{Compiler:Executable:Cxx} + /mnt/ghdd/Programme/OSS/asbots/../build-asbots-Desktop-MinSizeRel + + + + all + + true + CMakeProjectManager.MakeStep + + 1 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + + clean + + true + CMakeProjectManager.MakeStep + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Minimum Size Release + CMakeProjectManager.CMakeBuildConfiguration + + 4 + + + 0 + Deploy + Deploy + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ProjectExplorer.DefaultDeployConfiguration + + 1 + + dwarf + + cpu-cycles + + + 250 + + -e + cpu-cycles + --call-graph + dwarf,4096 + -F + 250 + + -F + true + 4096 + false + false + 1000 + + true + + + false + false + false + false + true + 0.01 + 10 + true + kcachegrind + 1 + + 25 + + 1 + true + false + true + + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + + 2 + + ProjectExplorer.CustomExecutableRunConfiguration + + false + true + false + true + + 1 + + + + ProjectExplorer.Project.TargetCount + 1 + + + ProjectExplorer.Project.Updater.FileVersion + 22 + + + Version + 22 + + diff --git a/config.cpp b/config.cpp new file mode 100644 index 0000000..b474387 --- /dev/null +++ b/config.cpp @@ -0,0 +1,19 @@ +#include "config.hpp" + + + +Config config = { + .connection = { + .addr = "127.0.0.1", + .port = 6667, + }, + .auth = { + .send_password = "849372523412", + .accept_password = "843574835765" + }, + .server = { + .name = "services.", + .description = "A test service!!", + .uid = "23X" + } +}; diff --git a/config.hpp b/config.hpp new file mode 100644 index 0000000..5cf810c --- /dev/null +++ b/config.hpp @@ -0,0 +1,23 @@ +#ifndef _CONFIG_HPP +#define _CONFIG_HPP +#include "uid.hpp" + +#include + + + +struct Config { + struct Connection { + std::string addr; + int port; + } connection; + struct Auth { + std::string send_password, accept_password; + } auth; + struct Server { + std::string name, description; + SUID uid; // Fourth one reserved for null-terminator + bool hidden = false; + } server; +} extern config; +#endif diff --git a/instance.cpp b/instance.cpp new file mode 100644 index 0000000..ecbc634 --- /dev/null +++ b/instance.cpp @@ -0,0 +1,207 @@ +#include "instance.hpp" +#include "config.hpp" +#include "uid.hpp" + +#include +#include +#include +#include + +using fmt::operator""_format; + + + +inline std::string_view operator ""_sv(const char *str, unsigned long len) { + return {str, static_cast(len)}; +} + +static std::vector strsplit(std::string_view s, char delimiter, std::vector::size_type times = 0) { + std::vector to_return; + decltype(s.size()) start = 0, finish = 0; + while ((finish = s.find_first_of(delimiter, start)) != std::string_view::npos) { + to_return.emplace_back(s.substr(start, finish - start)); + start = finish + 1; + if (to_return.size() == times) { break; } + } + to_return.emplace_back(s.substr(start)); + return to_return; +} + + +void Event::parse(std::string_view str) { + auto split = strsplit(str, ' ', 2); + // Check split size + if (split.size() < 2) { + throw ParseError("In event parser: Events are expected to have both a sender and name"); + } + // Move values + split[0].erase(0, 1); // Erase leading ':' + auto id_len = split[0].size(); + if (id_len == SUID_len) { + sender = SUID(std::move(split[0])); + } else if (id_len == UUID_len) { + sender = UUID(std::move(split[0])); + } else { + sender = std::move(split[0]); + } + name = std::move(split[1]); + if (split.size() == 3) { + // Get args and text + split = strsplit(std::move(split[2]), ':', 1); + if (split.size() == 2) { + text = std::move(split[1]); + } + if (!split.empty()) { + args = std::move(split[0]); + } + } +} + + +void Command::parse(std::string_view str) { + auto split = strsplit(str, ' ', 1); + name = std::move(split[0]); + // Get text from args + split = strsplit(std::move(split[1]), ':', 1); + if (split.size() == 2) { + text = std::move(split[1]); + } + if (!split.empty()) { + args = std::move(split[0]); + } +} + + +void User::parse(Event event) { + this->server = std::get(event.sender.id); + auto split = strsplit(event.args, ' ', 8); + // Check size + if (split.size() != 9) { + throw ParseError("In euid parser: This euid event does not have enough arguments"); + } + // Move values + nick = std::move(split[0]); + hops = std::stoull(std::move(split[1])); + ts = std::stoull(std::move(split[2])); + umode = std::move(split[3]); + ident = std::move(split[4]); + host = std::move(split[5]); + realhost = std::move(split[6]); + id = UUID(std::move(split[7])); + // Get realname + realname = std::move(event.text); +} + + +async::result Instance::run() { + // Create connection + socket = new uvpp::tcp{s}; + co_await socket->connect(addr); + socket->recv_start(); + + // Login + co_await login(); + + // Mainloop + while (true) { + // Read + auto data = co_await socket->recv(); + // Check for general error + if (data->error()) { + continue; + } + // Check for broken connection + if (data->broken()) { + break; + } + // Make string + auto dataStr = std::string_view{ + data->data.get(), + static_cast(data->nread) + }; + // Split by newlines + for (auto& line : strsplit(dataStr, '\n')) { + if (line.size() < 2) { + continue; // Empty line + } + // Remove \r + if (line.back() == '\r') { + line.pop_back(); + } + // Check if server sent an event or command, then parse and process it + if (line[0] == ':') { + Event event; + event.parse(std::move(line)); + std::clog << event.dump() << std::flush; + async::detach(process(std::move(event))); + } else { + Command command; + command.parse(std::move(line)); + std::clog << command.dump() << std::flush; + async::detach(process(std::move(command))); + } + } + } +} + +async::result Instance::login() { + co_await socket->send("PASS {} TS 6 :{}\n"_format(config.auth.send_password, config.server.uid.str())); + co_await socket->send("CAPAB :QS EX IE KLN UNKLN ENCAP TB SERVICES EUID EOPMOD MLOCK\n"_sv); + co_await socket->send("SERVER {} 1 :{}{}\n"_format(config.server.name, config.server.hidden ? "(H) " : "", config.server.description)); + co_await socket->send("SVINFO 6 3 0 :{}\n"_format(time(nullptr))); +} + +async::result Instance::burst() { + // End burst + co_await send_ping(); + client_bursting = false; + std::cout << "I'm done bursting too. Ready." << std::endl; +} + +async::result Instance::process(const Command command) { + if (command.name == "PASS") { + // Check password + { + auto given_password = strsplit(command.args, ' ', 1)[0]; + if (given_password != config.auth.accept_password) { + throw ConnectionError("Server supplied wrong password during authentication"); + } + authed = true; + } + // Get server ID + connected_server.uid = SUID(command.text); + } if (!authed) { + throw ConnectionError("Server tried to execute a command before authenticating"); + } else if (command.name == "SERVER") { + // Get name and description of connected server + connected_server.name = strsplit(command.args, ' ', 1)[0]; + connected_server.description = command.text; + } else if (command.name == "PING") { + // End of burst + if (server_bursting) { + server_bursting = false; + std::cout << "Server burst is over. It's my turn." << std::endl; + co_await burst(); + } + // Reply + co_await socket->send(":{} PONG {} {} :{}\n"_format(config.server.uid.str(), config.server.name, command.args, command.text)); + } +} + +async::result Instance::process(const Event event) { + if (event.name == "NOTICE") { + // Don't do anything special + } else if (!authed) { + throw ConnectionError("Server tried to send an event before authenticating"); + } else if (event.name == "EUID") { + User user; + user.parse(event); + std::cout << "User registered: " << user.dump().dump() << std::flush; + users[user.id.str()] = std::move(user); + } + co_return; +} + +async::result Instance::send_ping() { + co_await socket->send("PING :{}\n"_format(connected_server.uid.str())); +} diff --git a/instance.hpp b/instance.hpp new file mode 100644 index 0000000..a8f32ec --- /dev/null +++ b/instance.hpp @@ -0,0 +1,96 @@ +#ifndef _INSTANCE_HPP +#define _INSTANCE_HPP +#include "config.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + + + +struct ParseError : public std::runtime_error { + using std::runtime_error::runtime_error; +}; +struct ConnectionError : public std::runtime_error { + using std::runtime_error::runtime_error; +}; + +struct Event { + AnyUID sender; + std::string name, args, text; + + std::string dump() const { + return fmt::format(":{} {} {}{}\n", sender.str(), name, args, (text.empty()?"":":"+text)); + } + void parse(std::string_view str); +}; + +struct Command { + std::string name, args, text; + + std::string dump() const { + return fmt::format("{} {}{}\n", name, args, (text.empty()?"":":"+text)); + } + void parse(std::string_view str); +}; + +struct User { + SUID server; + // ... == "EUID" + std::string nick; + size_t hops; + time_t ts; + std::string umode; + std::string ident; + std::string host; + std::string realhost; + UUID id; + std::string realname; + + User() { + ts = time(nullptr); + } + + Event dump() const { + return Event{ + .sender = SUID(server), + .name = "EUID", + .args = fmt::format("{} 1 {} {} {} {} 0 {} * * :{}", nick, ts, umode, ident, host, id.str(), realname) + }; + } + void parse(Event event); +}; + +class Instance { + uvpp::loop_service &s; + const Config& config; + uvpp::Addr addr; + uvpp::tcp *socket = nullptr; + Config::Server connected_server; + + bool server_bursting = true, client_bursting = true; + bool authed = false; + std::unordered_map users; + +public: + Instance(uvpp::loop_service &s, const Config& config) : s(s), config(config) { + addr = uvpp::make_ipv4(config.connection.addr, config.connection.port); + } + ~Instance() { + if (socket) delete socket; + } + + async::result run(); + async::result login(); + async::result burst(); + async::result process(const Command); + async::result process(const Event); + + async::result send_ping(); +}; +#endif diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..c0940fb --- /dev/null +++ b/main.cpp @@ -0,0 +1,23 @@ +#include "config.hpp" +#include "instance.hpp" + +#include +#include +#include +#include +#include +using namespace std; + + + +int main() { + using namespace uvpp; + async::run_queue rq; + async::queue_scope qs{&rq}; + loop_service service; + + Instance instance(service, config); + + async::detach(instance.run()); + async::run_forever(rq.run_token(), loop_service_wrapper{service}); +} diff --git a/uid.hpp b/uid.hpp new file mode 100644 index 0000000..6ddb287 --- /dev/null +++ b/uid.hpp @@ -0,0 +1,88 @@ +#ifndef _UID_HPP +#define _UID_HPP +#include +#include +#include +#include + + + +template +class UID { +public: + std::array array; + + constexpr UID() : array({'\0'}) {} + constexpr UID(std::string_view initializer) { + for (typeof(len) it = 0; it != len; it++) { + array[it] = *(initializer.begin() + it); + } + //array[len] = '\0'; + } + constexpr UID(const char *initializer) { + *this = UID(std::string_view{initializer, len}); + } + + constexpr bool has_value() const { + return array[0] != '\0'; + } + constexpr std::string_view str() const { + if (has_value()) { + return {array.begin(), static_cast(len)}; + } else { + return "NUL_V"; + } + } +}; + +constexpr int SUID_len = 3; +constexpr int UUID_len = 9; +using SUID = UID; +using UUID = UID; + + +struct AnyUID { + std::variant id; + enum Type { + USER, + SERVER, + OTHER, + NUL + } type = NUL; + + std::string_view str() const { + if (type == SERVER) { + return std::get(id).str(); + } else if (type == USER) { + return std::get(id).str(); + } else if (type == OTHER) { + return std::get(id); + } else { + return "NUL_T"; + } + } + + auto operator =(const SUID& val) { + type = SERVER; + id = val; + } + auto operator =(const UUID& val) { + type = USER; + id = val; + } + auto operator =(const std::string& val) { + type = OTHER; + id = std::string(val); + } + auto operator =(std::nullptr_t) { + type = NUL; + id = nullptr; + } + + AnyUID() {} + template + AnyUID(const T& val) { + *this = val; + } +}; +#endif