From 0ea7ec3437ea0bcd0e73a212017cd75a3af39e0e Mon Sep 17 00:00:00 2001 From: Nils Date: Fri, 18 Jun 2021 15:25:14 +0200 Subject: [PATCH] Initial commit --- CMakeLists.txt | 18 +++ CMakeLists.txt.user | 372 ++++++++++++++++++++++++++++++++++++++++++++ config.cpp | 19 +++ config.hpp | 23 +++ instance.cpp | 207 ++++++++++++++++++++++++ instance.hpp | 96 ++++++++++++ main.cpp | 23 +++ uid.hpp | 88 +++++++++++ 8 files changed, 846 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 CMakeLists.txt.user create mode 100644 config.cpp create mode 100644 config.hpp create mode 100644 instance.cpp create mode 100644 instance.hpp create mode 100644 main.cpp create mode 100644 uid.hpp 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