summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/misc/FileDescriptor.cpp86
-rw-r--r--src/misc/FileDescriptor.hpp35
-rw-r--r--src/misc/macros.hpp4
-rw-r--r--src/misc/memory.hpp10
-rw-r--r--src/server/client.cpp156
-rw-r--r--src/server/clients.cpp131
-rw-r--r--src/server/server.cpp38
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; }