diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/misc/FileDescriptor.cpp | 86 | ||||
| -rw-r--r-- | src/misc/FileDescriptor.hpp | 35 | ||||
| -rw-r--r-- | src/misc/macros.hpp | 4 | ||||
| -rw-r--r-- | src/misc/memory.hpp | 10 | ||||
| -rw-r--r-- | src/server/client.cpp | 156 | ||||
| -rw-r--r-- | src/server/clients.cpp | 131 | ||||
| -rw-r--r-- | src/server/server.cpp | 38 |
7 files changed, 460 insertions, 0 deletions
diff --git a/src/misc/FileDescriptor.cpp b/src/misc/FileDescriptor.cpp new file mode 100644 index 0000000..92505f5 --- /dev/null +++ b/src/misc/FileDescriptor.cpp @@ -0,0 +1,86 @@ +#include "FileDescriptor.hpp" +#include <cstdlib> +#include <fcntl.h> +#include <sys/poll.h> +#include <unistd.h> +#include <utility> + +CFileDescriptor::CFileDescriptor(int const fd) + : m_fd(fd) {} + +CFileDescriptor::CFileDescriptor(CFileDescriptor &&other) noexcept + : m_fd(std::exchange(other.m_fd, -1)) {} + +CFileDescriptor &CFileDescriptor::operator=(CFileDescriptor &&other) noexcept { + if (this == &other) // Shit will go haywire if there is duplicate ownership + abort(); + + reset(); + m_fd = std::exchange(other.m_fd, -1); + return *this; +} + +CFileDescriptor::~CFileDescriptor() { + reset(); +} + +bool CFileDescriptor::isValid() const { + return m_fd != -1; +} + +int CFileDescriptor::get() const { + return m_fd; +} + +int CFileDescriptor::getFlags() const { + return fcntl(m_fd, F_GETFD); +} + +bool CFileDescriptor::setFlags(int flags) { + return fcntl(m_fd, F_SETFD, flags) != -1; +} + +int CFileDescriptor::take() { + return std::exchange(m_fd, -1); +} + +void CFileDescriptor::reset() { + if (m_fd != -1) { + close(m_fd); + m_fd = -1; + } +} + +CFileDescriptor CFileDescriptor::duplicate(int flags) const { + if (m_fd == -1) + return {}; + + return CFileDescriptor{fcntl(m_fd, flags, 0)}; +} + +bool CFileDescriptor::isClosed() const { + return isClosed(m_fd); +} + +bool CFileDescriptor::isReadable() const { + return isReadable(m_fd); +} + +bool CFileDescriptor::isClosed(int fd) { + pollfd pfd = { + .fd = fd, + .events = POLLIN, + .revents = 0, + }; + + if (poll(&pfd, 1, 0) < 0) + return true; + + return pfd.revents & (POLLHUP | POLLERR); +} + +bool CFileDescriptor::isReadable(int fd) { + pollfd pfd = {.fd = fd, .events = POLLIN, .revents = 0}; + + return poll(&pfd, 1, 0) > 0 && (pfd.revents & POLLIN); +} diff --git a/src/misc/FileDescriptor.hpp b/src/misc/FileDescriptor.hpp new file mode 100644 index 0000000..75aeb73 --- /dev/null +++ b/src/misc/FileDescriptor.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include <fcntl.h> +class CFileDescriptor { + public: + CFileDescriptor() = default; + explicit CFileDescriptor(int const fd); + CFileDescriptor(CFileDescriptor &&) noexcept; + CFileDescriptor &operator=(CFileDescriptor &&) noexcept; + ~CFileDescriptor(); + + CFileDescriptor(const CFileDescriptor &) = delete; + CFileDescriptor &operator=(const CFileDescriptor &) = delete; + + bool operator==(const CFileDescriptor &rhs) const { + return m_fd == rhs.m_fd; + } + + bool isValid() const; + int get() const; + int getFlags() const; + bool setFlags(int flags); + int take(); + void reset(); + CFileDescriptor duplicate(int flags = F_DUPFD_CLOEXEC) const; + + bool isReadable() const; + bool isClosed() const; + + static bool isReadable(int fd); + static bool isClosed(int fd); + + private: + int m_fd = -1; +}; diff --git a/src/misc/macros.hpp b/src/misc/macros.hpp new file mode 100644 index 0000000..02f5048 --- /dev/null +++ b/src/misc/macros.hpp @@ -0,0 +1,4 @@ +#pragma once + +#define UNLIKELY(expr) (expr) [[unlikely]] +#define LIKELY(expr) (expr) [[likely]] diff --git a/src/misc/memory.hpp b/src/misc/memory.hpp new file mode 100644 index 0000000..a90368e --- /dev/null +++ b/src/misc/memory.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include <memory> + +template <typename T> +using SP = std::shared_ptr<T>; +template <typename T> +using WP = std::weak_ptr<T>; +template <typename T> +using UP = std::unique_ptr<T>; diff --git a/src/server/client.cpp b/src/server/client.cpp new file mode 100644 index 0000000..f99cb97 --- /dev/null +++ b/src/server/client.cpp @@ -0,0 +1,156 @@ +#include "libsock/server/client.hpp" +#include "../misc/memory.hpp" +// #include "chatManager.hpp" +#include "libsock/server/server.hpp" +// #include "sessionManager.hpp" +#include <algorithm> +#include <arpa/inet.h> +#include <cerrno> +#include <cstring> +#include <format> +#include <optional> +#include <sys/socket.h> + +using namespace LibSock::Client; + +Client::Client(bool track, bool oneShot) : m_track(track), m_oneShot(oneShot) { + m_sockfd = std::make_shared<CFileDescriptor>( + accept(LibSock::Server::pServer->getSocket()->get(), + reinterpret_cast<sockaddr *>(&m_addr), + &m_addrLen)); // if this is in the init list, it will run before + // m_addrLen, so it won't work :/ + + if (!m_sockfd->isValid()) + throw std::runtime_error("session: Failed to create socket"); + + m_ip.resize(INET_ADDRSTRLEN); + inet_ntop(AF_INET, &m_addr.sin_addr, &m_ip[0], INET_ADDRSTRLEN); + m_ip.resize(strlen(m_ip.c_str())); + m_port = ntohs(m_addr.sin_port); +} + +Client::~Client() { +#ifdef DEBUG + onDisconnect(); +#endif + m_sockfd.reset(); +} + +void Client::recvLoop() { + while (true) { + auto recvData = read(std::format("{}: ", m_name)); + if (!recvData->good) + break; + + recvData->sanitize(); + if (recvData->isEmpty()) + continue; + + // pChatManager->newMessage({.msg = recvData->data, .username = m_name, + // .sender = self, .admin = isCommand}); + } +} + +bool SRecvData::isEmpty() const { + return data.empty() || + std::ranges::all_of(data, [](char c) { return std::isspace(c); }); +} + +void SRecvData::sanitize() { + if (!data.empty() && data.back() == '\n') + data.pop_back(); + + data.erase(std::remove(data.begin(), data.end(), asciiEscape), + data.end()); // don't accept ASCII code, might mess up terminal + // // NOLINT + + if (const auto start = data.find_first_not_of(" \t\r\n"); + start != std::string::npos) { + const auto end = data.find_last_not_of(" \t\r\n"); + data = data.substr(start, end - start + 1); + } else + data.clear(); +} + +UP<SRecvData> +Client::read(std::optional<std::function<void(const SRecvData &)>> cb) { + auto recvData = std::make_unique<SRecvData>(); + + recvData->data.resize(recvData->size); + ssize_t size = recv(m_sockfd->get(), &recvData->data[0], recvData->size, 0); + + if (size < 0) { + } else if (size == 0) { + recvData->good = false; + if (cb) + (*cb)(*recvData); + } else { + recvData->data.resize(size); + recvData->good = true; + } + +#ifdef DEBUG + onRecv(*recvData); +#endif + m_szReading.reset(); + return recvData; +} + +UP<SRecvData> +Client::read(const std::string &msg, + std::optional<std::function<void(const SRecvData &)>> cb) { + write("{}", msg); + m_szReading = msg; + return read(); +} + +void Client::run() { + for (const auto &chat : pClients->getDatas()) { + write(chat.msg); + recvLoop(); + + pClients->kick(self); + } +} + +bool Client::isValid() { + if (!m_sockfd->isValid() || m_name.empty()) + return false; + + int err = 0; + socklen_t size = sizeof(err); + return getsockopt(m_sockfd->get(), SOL_SOCKET, SO_ERROR, &err, &size) >= + 0 && + err == 0; +} + +bool Client::write(const std::string &msg, + std::optional<std::function<void(const SRecvData &)>> cb) { + const auto r = write("{}", msg); + if (cb) { + auto recvData = std::make_unique<SRecvData>(); + recvData->data = msg; + recvData->good = r; + (*cb)(*recvData); + } + return r; +} + +const std::string &Client::getName() const { return m_name; } + +const std::string &Client::getIp() const { return m_ip; } + +template <typename... Args> +bool Client::write(std::format_string<Args...> fmt, Args &&...args) { + std::string msg = std::format(fmt, std::forward<Args>(args)...); + if (m_szReading) { + msg.insert(0, "\r"); + msg.append(*m_szReading); + } + + if (send(m_sockfd->get(), msg.c_str(), msg.size(), 0) < 0) { + // cb somehow + return false; + } + return true; +} diff --git a/src/server/clients.cpp b/src/server/clients.cpp new file mode 100644 index 0000000..0327ea3 --- /dev/null +++ b/src/server/clients.cpp @@ -0,0 +1,131 @@ +#include "libsock/server/client.hpp" +#include <algorithm> +#include <format> +#include <ranges> +#include <unistd.h> +#include <utility> + +using namespace LibSock::Client; + +void Clients::shutdownClients() { + if (m_vClients.empty()) + return; + + for (auto &[thread, client] : m_vClients) { + if (client) { + client->write("Server shutting down"); + if (!client->getName().empty()) + pClients->broadcast(std::format("{} has left the chat", client->getName()), WP<Client>(client)); + } + } + + for (auto &[thread, client] : m_vClients) { + if (thread.joinable()) + thread.detach(); + client.reset(); + } + + m_vClients.clear(); +} + +Clients::Clients() { + if (std::atexit([]() { + if (pClients) + pClients->shutdownClients(); + })) + void(); // failed to register exit handler... +}; + +Clients::~Clients() { + for (auto &[thread, client] : m_vClients) { + if (thread.joinable()) + thread.detach(); + kick(WP<Client>(client)); + } +} + +void Clients::newClient(std::optional<std::function<void()>> cb) { + if (cb) + (*cb)(); + SP client = std::make_shared<Client>(); + auto &instance = m_vClients.emplace_back(std::jthread([client]() { client->run(); }), client); + instance.second->self = std::weak_ptr<Client>(instance.second); +} + +void Clients::run(std::optional<std::function<void()>> cb) { + while (true) + newClient(cb); +} + +void Clients::broadcast(const std::string &msg, std::optional<std::weak_ptr<Client>> self) { + broadcast(SData{msg, self, false}); +} + +void Clients::broadcast(const SData &msg) { + for (const auto &[thread, client] : m_vClients) { + if (!client) + continue; + + client->write(msg.msg); + } + + m_vDatas.push_back(msg); +} + +bool Clients::nameExists(const std::string &name) { + auto it = std::ranges::find_if(m_vClients, [&name](const auto &s) { return s.second->getName() == name; }); + + if (it != m_vClients.end()) { + if (!it->second->isValid()) { + kick(std::weak_ptr<Client>(it->second), true, "Connection lost"); + return false; + } + return true; + } + return false; +} + +SP<Client> Clients::getByName(const std::string &name) const { + auto it = std::ranges::find_if(m_vClients, [&name](const auto &s) { return s.second->getName() == name; }); + return it != m_vClients.end() ? it->second : nullptr; +} + +SP<Client> Clients::getByIp(const std::string &ip) const { + auto it = std::ranges::find_if(m_vClients, [&ip](const auto &s) { return s.second->getIp() == ip; }); + return it != m_vClients.end() ? it->second : nullptr; +} + +std::vector<SP<Client>> Clients::getClients() const { + return m_vClients | std::views::transform([](const auto &s) { return s.second; }) | std::ranges::to<std::vector>(); +} + +void Clients::kick(WP<Client> clientWeak, const bool kill, const std::string &reason) { + auto client = clientWeak.lock(); + if (!client) + return; + + for (auto it = m_vClients.begin(); it != m_vClients.end(); ++it) { + if (it->second == client) { + if (client && !client->getName().empty()) + // this only exist when the client is registered + // not sure why the second is null, but m_name definately is since it's setted during register + // but we don't need to notify people if the client didn't even "join"/register anyways + pClients->broadcast(std::format("{} has left the chat", client->getName()), clientWeak); + + if (!reason.empty()) + client->write(reason); + + const auto native_handle = it->first.native_handle(); + if (it->first.joinable()) + it->first.detach(); // .detach the thread since it's removing itself + it->second.reset(); + m_vClients.erase(it); + if (kill && native_handle != 0) + // cancel(terminal/kill) the thread if it doesn't exit on its own + // pthread_cancel cuz it's the safest https://stackoverflow.com/a/3438576 + // need a condition because it might crash if the thread is already dead + pthread_cancel(native_handle); + return; + } + } +} diff --git a/src/server/server.cpp b/src/server/server.cpp new file mode 100644 index 0000000..0692a74 --- /dev/null +++ b/src/server/server.cpp @@ -0,0 +1,38 @@ +#include "libsock/server/server.hpp" +#include "misc/memory.hpp" +#include <cstring> +#include <mutex> +#include <sys/socket.h> + +using namespace LibSock::Server; + +Server::Server(uint16_t port, bool reuseaddr, bool keepalive) + : m_sockfd( + std::make_shared<CFileDescriptor>(socket(AF_INET, SOCK_STREAM, 0))) { + std::lock_guard<std::mutex> lk(m_mutex); + if (!m_sockfd->isValid() || m_sockfd->get() < 0) + throw std::runtime_error("Failed to create socket"); + + constexpr int opt = 1; + constexpr int size = sizeof(opt); + if (keepalive && + setsockopt(m_sockfd->get(), SOL_SOCKET, SO_REUSEADDR, &opt, size) < 0) + throw std::runtime_error("setsockopt(SO_REUSEADDR) failed"); + + if (reuseaddr && + setsockopt(m_sockfd->get(), SOL_SOCKET, SO_KEEPALIVE, &opt, size) < 0) + throw std::runtime_error("setsockopt(SO_KEEPALIVE) failed"); + + memset(&m_addr, 0, sizeof(m_addr)); + m_addr.sin_family = AF_INET; + m_addr.sin_port = htons(port); + m_addr.sin_addr.s_addr = INADDR_ANY; + if (bind(m_sockfd->get(), reinterpret_cast<sockaddr *>(&m_addr), + sizeof(m_addr)) || + listen(m_sockfd->get(), 5)) + throw std::runtime_error("Failed to bind or listen on socket"); +}; + +Server::~Server() { m_sockfd.reset(); } + +std::shared_ptr<CFileDescriptor> Server::getSocket() const { return m_sockfd; } |
