From d251a641c00568a21862c436875d553bcb2de75b Mon Sep 17 00:00:00 2001 From: Adrian 'nex' Schollmeyer Date: Sun, 17 Oct 2021 16:15:11 +0200 Subject: [PATCH] Initial commit, version 1.0 Signed-off-by: Adrian 'nex' Schollmeyer --- .clang-format | 72 +++++++++++++++++ .gitignore | 3 + CMakeLists.txt | 6 ++ README.md | 15 ++++ src/CMakeLists.txt | 14 ++++ src/ClientHandler.cpp | 166 ++++++++++++++++++++++++++++++++++++++ src/ClientHandler.hpp | 41 ++++++++++ src/ConnectionHandler.cpp | 135 +++++++++++++++++++++++++++++++ src/ConnectionHandler.hpp | 31 +++++++ src/main.cpp | 152 ++++++++++++++++++++++++++++++++++ src/vprint.cpp | 3 + src/vprint.hpp | 16 ++++ 12 files changed, 654 insertions(+) create mode 100644 .clang-format create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 src/CMakeLists.txt create mode 100644 src/ClientHandler.cpp create mode 100644 src/ClientHandler.hpp create mode 100644 src/ConnectionHandler.cpp create mode 100644 src/ConnectionHandler.hpp create mode 100644 src/main.cpp create mode 100644 src/vprint.cpp create mode 100644 src/vprint.hpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..8f9bab2 --- /dev/null +++ b/.clang-format @@ -0,0 +1,72 @@ +BasedOnStyle: LLVM # LLVM, Google, Chromium, Mozilla, WebKit +AccessModifierOffset: -4 +AlignEscapedNewlinesLeft: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: false # None(false), Inline, All(true) +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: false +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: true +BinPackParameters: true +BreakBeforeBinaryOperators: NonAssignment # None, NonAssignment, All +BreakBeforeBraces: Custom # Attach, Linux, Stroustrup, Custom, GNU +BraceWrapping: + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: false + BeforeCatch: true + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyNamespace: true +BreakBeforeTernaryOperators: false +BreakConstructorInitializersBeforeComma: true +ColumnLimit: 80 +# CommentPragmas +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 2 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: true +# ForEachMacros +IndentCaseLabels: false +IndentWidth: 4 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: false +Language: Cpp # None, Cpp, JavaScript, Proto +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: Inner # None, Inner, All +# ObjCSpaceAfterProperty +# ObjCSpaceBeforeProtocolList +#// PenaltyBreakBeforeFirstCallParameter +#// PenaltyBreakComment +#// PenaltyBreakFirstLessLess +#// PenaltyBreakString +#// PenaltyExcessCharacter +#// PenaltyReturnTypeOnItsOwnLine: +PointerAlignment: Left # Left, Right, Middle +SpaceAfterCStyleCast: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements # Never, ControlStatements, Always +# SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 # Cpp03, Cpp11, Auto +TabWidth: 4 +UseTab: ForIndentation # Never(false), ForIndentation, Always(true) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7488b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/ +compile_commands.json +Release/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c9b4558 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.0) +project(fakeirc LANGUAGES CXX VERSION 1.0) + +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wall -Werror") + +add_subdirectory(src) diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c6e57e --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# fakeIRC – a simple IRC-like server to use with netcat + +fakeIRC is a simple IRC-like server, implementing a very limited protocol to be used to show participants in a workshop how they can chat using simple tools like netcat. + +## Requirements + + * Build: + * CMake + * a C++ compiler + * Linux + * Runtime: + * Linux + * a network to chat on + + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..48464ac --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,14 @@ +#add_libary(fakeirc-common STATIC +#) + +add_executable(fakeirc + main.cpp + vprint.cpp + ConnectionHandler.cpp + ClientHandler.cpp +) +target_compile_features(fakeirc + PUBLIC + cxx_std_17 +) + diff --git a/src/ClientHandler.cpp b/src/ClientHandler.cpp new file mode 100644 index 0000000..b2f1757 --- /dev/null +++ b/src/ClientHandler.cpp @@ -0,0 +1,166 @@ +#include +#include +#include +#include + +#include + +#include "ClientHandler.hpp" +#include "ConnectionHandler.hpp" +#include "vprint.hpp" + +namespace +{ +constexpr size_t READ_BUF_SIZE{4096}; + +constexpr std::array RICKROLL{ + "We're no strangers to love", + "You know the rules and so do I", + "A full commitment's what I'm thinking of", + "You wouldn't get this from any other guy", + "I just wanna tell you how I'm feeling", + "Gotta make you understand", + "Never gonna give you up", + "Never gonna let you down", + "Never gonna run around and desert you", + "Never gonna make you cry", + "Never gonna say goodbye", + "Never gonna tell a lie and hurt you", + "We've known each other for so long", + "Your heart's been aching but you're too shy to say it", + "Inside we both know what's been going on", + "We know the game and we're gonna play it", + "And if you ask me how I'm feeling", + "Don't tell me you're too blind to see", + "Never gonna give you up", + "Never gonna let you down", + "Never gonna run around and desert you", + "Never gonna make you cry", + "Never gonna say goodbye", + "Never gonna tell a lie and hurt you", + "Never gonna give you up", + "Never gonna let you down", + "Never gonna run around and desert you", + "Never gonna make you cry", + "Never gonna say goodbye", + "Never gonna tell a lie and hurt you", + "Never gonna give, never gonna give", + "(Give you up)", + "We've known each other for so long", + "Your heart's been aching but you're too shy to say it", + "Inside we both know what's been going on", + "We know the game and we're gonna play it", + "I just wanna tell you how I'm feeling", + "Gotta make you understand", + "Never gonna give you up", + "Never gonna let you down", + "Never gonna run around and desert you", + "Never gonna make you cry", + "Never gonna say goodbye", + "Never gonna tell a lie and hurt you", + "Never gonna give you up", + "Never gonna let you down", + "Never gonna run around and desert you", + "Never gonna make you cry", + "Never gonna say goodbye", + "Never gonna tell a lie and hurt you", + "Never gonna give you up", + "Never gonna let you down", + "Never gonna run around and desert you", + "Never gonna make you cry", + "Never gonna say goodbye"}; + +bool is_command(std::string_view cmd, std::string_view cmdline) +{ + return cmdline.substr(0, cmd.length()) == cmd; +} +} // namespace + +void ClientHandler::send_motd() const +{ + vprint("Sending MOTD\n"); + send_message(motd); +} + +void ClientHandler::send_join_announcement() const +{ + conn_hdl.broadcast_message("* " + nick + " entered the chat"); +} + +void ClientHandler::send_message(const std::string& message) const +{ + vprint("Sending message:\n", message); + ssize_t res = write(sockfd, message.c_str(), message.length()); + size_t ures = static_cast(res); + + if (res < 0) { + std::clog << "Write error: " << strerror(errno) << '\n'; + } + + if (ures < message.length()) { + res = write(sockfd, message.c_str() + res, message.length() - res); + ures = static_cast(res); + if (res < 0) { + std::clog << "Write error: " << strerror(errno) << '\n'; + } else if (ures < message.length() - res) { + std::cout << "Partial write!\n"; + } + } +} + +void ClientHandler::read_messages() +{ + vprint("reading\n"); + char read_buf[READ_BUF_SIZE]; + + ssize_t res = read(sockfd, read_buf, READ_BUF_SIZE); + + if (res < 0) { + std::clog << "Read error: " << strerror(errno) << '\n'; + return; + } + + for (ssize_t i = 0; i < res; ++i) { + if (read_buf[i] == '\n') { + input_line = input_buffer.str(); + input_buffer.str(""); + input_buffer.clear(); + process_input_line(); + } else { + input_buffer << read_buf[i]; + } + } +} + +void ClientHandler::close() const +{ + conn_hdl.broadcast_message("* " + nick + " disconnected"); + ::close(sockfd); +} + +void ClientHandler::process_input_line() +{ + vprint("Processing input line...\n"); + if (input_line.length() == 0) + return; + if (input_line.at(0) == '/' && input_line.length() > 1) { + std::string_view cmdline = std::string_view{input_line}.substr(1); + vprint("Received command: ", cmdline, "\n"); + + if (is_command("nick ", cmdline)) { + std::string old_nick = std::move(nick); + nick = cmdline.substr(std::string_view{"nick "}.length()); + conn_hdl.broadcast_message("* " + old_nick + " is now known as " + + nick); + } else if (is_command("rick", cmdline)) { + for (const char* msg : RICKROLL) + conn_hdl.broadcast_message(std::string{"* "} + msg); + } else { + send_message("Invalid command line: " + std::string{cmdline} + + "\n"); + } + } else { + vprint("Received message: ", input_line, "\n"); + conn_hdl.broadcast_message("<" + nick + "> " + input_line); + } +} diff --git a/src/ClientHandler.hpp b/src/ClientHandler.hpp new file mode 100644 index 0000000..c53f72d --- /dev/null +++ b/src/ClientHandler.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include + +#include + +class ConnectionHandler; + +class ClientHandler +{ +public: + ClientHandler(ConnectionHandler& conn_hdl, int sockfd, std::string& motd, + std::string_view initial_nick) + : conn_hdl(conn_hdl), sockfd(sockfd), motd(motd), nick(initial_nick) + {} + ClientHandler(const ClientHandler& other) = delete; + ClientHandler(ClientHandler&&) = default; + ~ClientHandler() = default; + + void send_motd() const; + void send_join_announcement() const; + + void read_messages(); + + void send_message(const std::string& message) const; + + void close() const; + +private: + void process_input_line(); + + ConnectionHandler& conn_hdl; + int sockfd; + std::string& motd; + + std::stringstream input_buffer; + std::string input_line; + + std::string nick; +}; diff --git a/src/ConnectionHandler.cpp b/src/ConnectionHandler.cpp new file mode 100644 index 0000000..a8f8eb1 --- /dev/null +++ b/src/ConnectionHandler.cpp @@ -0,0 +1,135 @@ +#include + +#include + +#include +#include +#include + +#include + +#include "ConnectionHandler.hpp" +#include "vprint.hpp" + +void ConnectionHandler::start() +{ + run = true; + int res; + + struct sockaddr_in client_sin4; + struct sockaddr_in6 client_sin6; + struct sockaddr* client_sin; + socklen_t client_socklen; + if (address_family == AF_INET) { + client_sin = reinterpret_cast(&client_sin4); + client_socklen = sizeof(client_sin4); + } else if (address_family == AF_INET6) { + client_sin = reinterpret_cast(&client_sin6); + client_socklen = sizeof(client_sin6); + } else { + std::cerr << "Unexpected address family " << address_family + << " in ConnectionHandler\n"; + return; + } + + int epollfd = epoll_create1(0); + + if (epollfd < 0) { + std::cerr << "Failed setting up epollfd: " << strerror(errno) << '\n'; + return; + } + + vprint("setting up epoll for sockfd\n"); + struct epoll_event sockfd_epe = {.events = EPOLLIN, .data = {.fd = sockfd}}; + res = epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &sockfd_epe); + if (res < 0) { + std::cerr << "Failed setting up epoll for sockfd: " << strerror(errno) + << '\n'; + return; + } + + struct epoll_event next_event; + + while (run) { + res = epoll_wait(epollfd, &next_event, 1, -1); + + if (res == 0) { + vprint("epoll_wait() returned without events\n"); + continue; + } else if (res < 0 && errno == EINTR) { + std::cout << "Exiting...\n"; + broadcast_message("* this server is shutting down"); + + for (auto& [fd, peer] : peers) + peer.close(); + peers.clear(); + + run = false; + goto epoll_uninit; + } else if (res < 0) { + std::cerr << "epoll_wait() failed: " << strerror(errno) << '\n'; + goto epoll_uninit; + } + + int event_fd = next_event.data.fd; + vprint("epoll event for fd ", event_fd, "\n"); + + if (event_fd == sockfd) { + vprint("accepting new connection\n"); + res = accept(sockfd, client_sin, &client_socklen); + + if (res < 0) { + std::cerr << "accept() failed with error: " << strerror(errno) + << '\n'; + continue; + } + + std::cout << "New client!\n"; + int client_fd = res; + auto peer = peers.emplace(std::make_pair( + client_fd, + ClientHandler{*this, client_fd, motd, + "user" + std::to_string(connections_accepted)})); + peer.first->second.send_motd(); + peer.first->second.send_join_announcement(); + + vprint("setting up epoll for new client ", client_fd, "\n"); + struct epoll_event client_epe = {.events = EPOLLIN, + .data = {.fd = client_fd}}; + res = epoll_ctl(epollfd, EPOLL_CTL_ADD, client_fd, &client_epe); + if (res < 0) { + std::cerr << "Failed setting up epoll for client fd: " + << strerror(errno) << '\n'; + goto socket_uninit; + } + + ++connections_accepted; + } else { + vprint("Client epoll event!\n"); + char buf; + ssize_t peek_res = recv(event_fd, &buf, 1, MSG_PEEK); + if (peek_res == 0) { + vprint("Client disconnect\n"); + peers.at(event_fd).close(); + peers.erase(event_fd); + } else { + peers.at(event_fd).read_messages(); + } + } + } + +socket_uninit: + for (const auto& [fd, _] : peers) { + close(fd); + } + +epoll_uninit: + close(epollfd); +} + +void ConnectionHandler::broadcast_message(const std::string& msg) +{ + std::string nl_msg = msg + '\n'; + for (const auto& [_, peer] : peers) + peer.send_message(nl_msg); +} diff --git a/src/ConnectionHandler.hpp b/src/ConnectionHandler.hpp new file mode 100644 index 0000000..e177d05 --- /dev/null +++ b/src/ConnectionHandler.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include + +#include "ClientHandler.hpp" + +class ConnectionHandler +{ +public: + ConnectionHandler(int sockfd, int address_family, std::string& motd) : sockfd(sockfd), address_family(address_family), motd(motd) + {} + ConnectionHandler(const ConnectionHandler&) = delete; + ~ConnectionHandler() = default; + + void start(); + + void broadcast_message(const std::string& msg); + +private: + int sockfd; + int address_family; + volatile bool run{false}; + + std::map peers; + std::string& motd; + + size_t connections_accepted{0}; +}; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..75eeaf7 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,152 @@ +#include + +#include +#include + +#include +#include +#include +#include + +#include + +#include "ConnectionHandler.hpp" +#include "vprint.hpp" + +namespace +{ +constexpr const char* OPTSTRING{"hp:m:v46"}; +constexpr int LISTEN_CONNLIMIT{1024}; + +std::string motd{""}; +uint16_t port{1337}; + +int sock_domain{-1}; +sa_family_t sa_family; + +void print_usage(); +bool assert_args(); +} // namespace + +int main(int argc, char** argv) +{ + signal(SIGPIPE, SIG_IGN); + + int optchar; + while ((optchar = getopt(argc, argv, OPTSTRING)) > 0) { + switch (static_cast(optchar)) { + case 'h': + print_usage(); + return 0; + break; + case 'p': + port = atoi(optarg); + break; + case 'm': + motd = optarg; + motd += '\n'; + break; + case 'v': + vprint_verbose = true; + break; + case '4': + sock_domain = AF_INET; + sa_family = AF_INET; + break; + case '6': + sock_domain = AF_INET6; + sa_family = AF_INET6; + break; + + case '?': + print_usage(); + return 1; + break; + } + } + + if (!assert_args()) + return 1; + + int res; + + vprint("fakeirc starting up\n"); + vprint("setting up socket\n"); + + res = socket(sock_domain, SOCK_STREAM, 0); + if (res < 0) { + std::clog << "Failed setting up socket: " << strerror(errno) << '\n'; + return 1; + } + + int sockfd = res; + + struct sockaddr_in sin4; + struct sockaddr_in6 sin6; + + vprint("binding socket\n"); + if (sa_family == AF_INET) { + sin4 = {.sin_family = sa_family, + .sin_port = htons(port), + .sin_addr = {INADDR_ANY}}; + + res = bind(sockfd, reinterpret_cast(&sin4), + sizeof(sockaddr_in)); + } else if (sa_family == AF_INET6) { + sin6 = {.sin6_family = sa_family, + .sin6_port = htons(port), + .sin6_flowinfo = 0, + .sin6_addr = IN6ADDR_ANY_INIT, + .sin6_scope_id = 0}; + + res = bind(sockfd, reinterpret_cast(&sin6), + sizeof(sockaddr_in6)); + } else { + std::cerr << "Unexpected error: Invalid sa_family\n"; + return 1; + } + + if (res < 0) { + std::clog << "Failed binding socket: " << strerror(errno) << '\n'; + return 1; + } + + vprint("listening on socket\n"); + res = listen(sockfd, LISTEN_CONNLIMIT); + + if (res < 0) { + std::clog << "Failed listening on socket: " << strerror(errno) << '\n'; + return 1; + } + + ConnectionHandler conn_handler{sockfd, sa_family, motd}; + conn_handler.start(); + + return 0; +} + +namespace +{ +void print_usage() +{ + std::cout << "fakeirc\tCopyright (C) 2021 Adrian Schollmeyer\n\n" + << "Usage: fakeirc [Options]\n\n" + << "Options:\n" + << "\t-h \tPrint this help and exit\n" + << "\t-v \tEnable verbose output\n" + << "\t-p [Port] \tListen on this port\n" + << "\t-m [MOTD] \tSet message of the day\n" + << "\t-4 \tEnable a legacy version of IP\n" + << "\t-6 \tEnable the current version of IP\n"; +} + +bool assert_args() +{ + if (sock_domain == -1) { + std::cerr << "Missing IP version!\n"; + return false; + } + + return true; +} +} // namespace diff --git a/src/vprint.cpp b/src/vprint.cpp new file mode 100644 index 0000000..e5bb3e0 --- /dev/null +++ b/src/vprint.cpp @@ -0,0 +1,3 @@ +#include "vprint.hpp" + +bool vprint_verbose{false}; diff --git a/src/vprint.hpp b/src/vprint.hpp new file mode 100644 index 0000000..6d79d4e --- /dev/null +++ b/src/vprint.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +extern bool vprint_verbose; + +namespace +{ +template +void vprint(T&... args) +{ + if (vprint_verbose) { + (std::cout << ... << std::forward(args)); + } +} +} // namespace