diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..565eda4 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,56 @@ +name: Build and Publish Docker Image + +on: + release: + types: [created] +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf + + - name: Get Release Tag + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + + - name: Write Version + run: | + echo Building with version number $RELEASE_VERSION + echo $RELEASE_VERSION > src/build/version.txt + + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/CHANGELOG.md b/CHANGELOG.md index c3e9edb..d34c86f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ # 0.3.0 - SSL support via OpenSSL - Added "Thread Manager" class to allow for multiple (or custom) threading models (process per thread, event loop) -- Defaul threading model is now event loop +- Default (and only included) threading model is now event loop - Rewrote request parser for readability and speed +- Rewrote socket system for readability and speed - Added improved logging with different log levels - Separated anthracite into libanthracite and anthracite-bin to allow for other projects to implement anthracite (example in ./src/api_main.cpp) - Cleaned up code and seperated most code into headers & source @@ -10,7 +11,6 @@ - Moved CI/CD over to Forgejo - General system stability improvements were made to enhance the user's experience - ## HTTP Request Parser Rewrite The following benchmark (source in ./tests/speed_tests.cpp) shows the speed diff --git a/CMakeLists.txt b/CMakeLists.txt index a1fbdf8..9af19e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,8 +18,9 @@ add_custom_target(build-version add_custom_target(build-supplemental COMMAND cd ../build_supp && python3 ./error_gen.py COMMAND mkdir -p www && cp -r ../default_www/regular/* ./www/ + COMMAND cp ../build_supp/default_config.cfg ./anthracite.cfg DEPENDS build_supp/version.txt ../default_www/regular/* build_supp/error_gen.py build-version - COMMENT "Generated supplemental build files (default www dir + error pages)" + COMMENT "Generated supplemental build files (default www dir + default config + error pages)" ) add_custom_target(run diff --git a/Dockerfile b/Dockerfile index b7247e4..3c5c6c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,5 +18,6 @@ FROM alpine RUN apk add --no-cache libgcc libstdc++ COPY --from=build-env /build/anthracite-bin /anthracite-bin COPY --from=build-env /build/error_pages /error_pages +COPY --from=build-env /build_supp/default_config.cfg /anthracite.cfg COPY /default_www/docker /www CMD ["/anthracite-bin"] diff --git a/build_supp/default_config.cfg b/build_supp/default_config.cfg new file mode 100644 index 0000000..380e4fd --- /dev/null +++ b/build_supp/default_config.cfg @@ -0,0 +1,6 @@ +log_level INFO + +http 8080 1000 blocking + +event_loop 6 10000 +www_dir ./www diff --git a/docker-compose.yml b/docker-compose.yml index ff74b34..3d71054 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,3 +7,6 @@ services: - type: bind source: ./default_www/docker_compose/ target: /www + - type: bind + source: ./build_supp/default_config.cfg + target: /anthracite.cfg diff --git a/lib/config/config.hpp b/lib/config/config.hpp deleted file mode 100644 index f353a21..0000000 --- a/lib/config/config.hpp +++ /dev/null @@ -1,59 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace anthracite::config { - class http_config { - uint16_t _port; - public: - http_config(uint16_t port) : _port(port) {}; - virtual ~http_config() {}; - - uint16_t port() { return _port; } - }; - - class https_config : public http_config { - std::string _cert_path; - std::string _key_path; - public: - https_config(uint16_t port, std::string cert_path, std::string key_path) : - http_config(port), _cert_path(cert_path), _key_path(key_path) {}; - }; - - class config { - int _worker_threads; - int _max_clients; - std::optional _http_config; - std::optional _https_config; - - public: - config(int worker_threads, int max_clients) : _worker_threads(worker_threads), _max_clients(max_clients) { - } - - void add_http_config(http_config config) { - _http_config = config; - } - - void add_https_config(https_config config) { - _https_config = config; - } - - int worker_threads() { - return _worker_threads; - } - - int max_clients() { - return _max_clients; - } - - std::optional& http_cfg() { - return _http_config; - } - - std::optional& https_cfg() { - return _https_config; - } - }; -}; diff --git a/lib/log/log.cpp b/lib/log/log.cpp index 8095903..2fba0fd 100644 --- a/lib/log/log.cpp +++ b/lib/log/log.cpp @@ -1,5 +1,4 @@ #include "./log.hpp" -#include namespace anthracite::log { enum LOG_LEVEL Logger::_level = LOG_LEVEL_NONE; @@ -28,8 +27,8 @@ int LogBuf::sync() if (this->_level <= logger._level) { char thread_name[100]; pthread_getname_np(pthread_self(), thread_name, 100); - std::osyncstream(std::cout) << "[" << this->_tag << "] [" << syscall(SYS_gettid) << ":" << thread_name << "] "<< this->str(); - std::cout.flush(); + _output_stream << "[" << this->_tag << "] [" << syscall(SYS_gettid) << ":" << thread_name << "] " << this->str(); + _output_stream.flush(); } this->str(""); return 0; diff --git a/lib/log/log.hpp b/lib/log/log.hpp index 6a99749..d9fd1e4 100644 --- a/lib/log/log.hpp +++ b/lib/log/log.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "../http/request.hpp" #include "../http/response.hpp" diff --git a/lib/socket/openssl_socket.cpp b/lib/socket/openssl_socket.cpp index 9bb4639..775f170 100644 --- a/lib/socket/openssl_socket.cpp +++ b/lib/socket/openssl_socket.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -16,85 +17,101 @@ namespace anthracite::socket { -SSL_CTX* openssl_socket::_context = nullptr; - -openssl_socket::openssl_socket(int port, int max_queue) - : anthracite_socket(port, max_queue) +openssl_listener::openssl_listener(std::string& key_path, std::string& cert_path, int port, int max_queue, bool nonblocking) + : listener(port, max_queue, nonblocking) { const SSL_METHOD* method = TLS_server_method(); - if (_context == nullptr) { - _context = SSL_CTX_new(method); - } + _context = SSL_CTX_new(method); if (!_context) { log::err << "Unable to initialize SSL" << std::endl; throw std::exception(); } - if (SSL_CTX_use_certificate_file(_context, "cert.pem", SSL_FILETYPE_PEM) <= 0) { - log::err << "Unable to open cert.pem" << std::endl; + if (SSL_CTX_use_certificate_file(_context, cert_path.c_str(), SSL_FILETYPE_PEM) <= 0) { + log::err << "Unable to open Cert file at: " << cert_path << std::endl; throw std::exception(); } - if (SSL_CTX_use_PrivateKey_file(_context, "key.pem", SSL_FILETYPE_PEM) <= 0) { - log::err << "Unable to open key.pem" << std::endl; + if (SSL_CTX_use_PrivateKey_file(_context, key_path.c_str(), SSL_FILETYPE_PEM) <= 0) { + log::err << "Unable to open Key file at: " << key_path << std::endl; throw std::exception(); } } -openssl_socket::~openssl_socket() = default; - -bool openssl_socket::wait_for_conn() +bool openssl_listener::wait_for_conn(server** client_sock_p) { - client_ip = ""; - struct timeval tv = { .tv_sec = 1, .tv_usec = 0 }; - fd_set read_fd; - FD_ZERO(&read_fd); - FD_SET(server_socket, &read_fd); - if (select(server_socket + 1, &read_fd, NULL, NULL, &wait_timeout)) { - client_socket = accept(server_socket, reinterpret_cast(&client_addr), &client_addr_len); - std::array ip_str { 0 }; - inet_ntop(AF_INET, &client_addr.sin_addr, ip_str.data(), INET_ADDRSTRLEN); - client_ip = std::string(ip_str.data()); - _ssl = SSL_new(_context); - SSL_set_fd(_ssl, client_socket); - if (SSL_accept(_ssl) <= 0) { - log::warn << "Unable to open SSL connection with client" << std::endl; - client_ip = ""; - close(client_socket); - client_socket = -1; + struct sockaddr_in client_addr {}; + socklen_t client_addr_len; + + int csock = accept(_sock_fd, reinterpret_cast(&client_addr), &client_addr_len); + + if (csock > 0) { + std::array ip_str { 0 }; + + if (inet_ntop(AF_INET, &client_addr.sin_addr, ip_str.data(), INET_ADDRSTRLEN) == NULL) { + if (inet_ntop(AF_INET6, &client_addr.sin_addr, ip_str.data(), INET6_ADDRSTRLEN) == NULL) { + log::warn << "Unable to decode client's IP address" << std::endl; + } + } + + SSL* ssl = SSL_new(_context); + + if (ssl == NULL) { + for (int i = 0; i < 5 && close(csock) != 0; ++i) + ; return false; } + + if (SSL_set_fd(ssl, csock) == 0) { + SSL_free(ssl); + for (int i = 0; i < 5 && close(csock) != 0; ++i) + ; + return false; + } + + if (SSL_accept(ssl) <= 0) { + log::warn << "Unable to open SSL connection with client" << std::endl; + SSL_free(ssl); + for (int i = 0; i < 5 && close(csock) != 0; ++i) + ; + return false; + } + + std::string client_ip = std::string(ip_str.data()); + *client_sock_p = new openssl_server(csock, client_ip, _nonblocking, ssl); return true; } else { return false; } } -void openssl_socket::close_conn() +openssl_listener::~openssl_listener() {} + +openssl_server::openssl_server(int sock_fd, std::string client_ip, bool nonblocking, SSL* ssl) + : server(sock_fd, client_ip, nonblocking) + , _ssl(ssl) +{ +} + +openssl_server::~openssl_server() { SSL_shutdown(_ssl); SSL_free(_ssl); - close(client_socket); - client_socket = -1; } -void openssl_socket::send_message(std::string& msg) +void openssl_server::send_message(const std::string& msg) { - if (client_socket == -1) { - return; - } SSL_write(_ssl, &msg[0], msg.length()); } -std::string openssl_socket::recv_message(int buffer_size) +std::string openssl_server::recv_message(int buffer_size) { - if (client_socket == -1) { - return ""; - } + // Ignored because it's nonfatal, just slower + int nodelay_opt = 1; + (void)setsockopt(_sock_fd, SOL_TCP, TCP_NODELAY, &nodelay_opt, sizeof(nodelay_opt)); - setsockopt(client_socket, SOL_SOCKET, SO_RCVTIMEO, &timeout_tv, sizeof timeout_tv); std::vector response(buffer_size + 1); ssize_t result = SSL_read(_ssl, response.data(), buffer_size + 1); diff --git a/lib/socket/openssl_socket.hpp b/lib/socket/openssl_socket.hpp index 4d4df6f..24d135a 100644 --- a/lib/socket/openssl_socket.hpp +++ b/lib/socket/openssl_socket.hpp @@ -5,18 +5,25 @@ #include namespace anthracite::socket { -class openssl_socket : public anthracite_socket { +class openssl_server : public server{ private: - static SSL_CTX* _context; SSL* _ssl; - public: - openssl_socket(int port, int max_queue = MAX_QUEUE_LENGTH); - ~openssl_socket(); + openssl_server(int sock_fd, std::string client_ip, bool nonblocking, SSL* ssl); + ~openssl_server(); - bool wait_for_conn() override; - void close_conn() override; - void send_message(std::string& msg) override; + void send_message(const std::string& msg) override; std::string recv_message(int buffer_size) override; }; + +class openssl_listener : public listener { + private: + SSL_CTX* _context; + + public: + openssl_listener(std::string& key_path, std::string& cert_path, int port, int max_queue_length, bool nonblocking); + ~openssl_listener(); + + bool wait_for_conn(server** client_sock_) override; +}; }; diff --git a/lib/socket/socket.cpp b/lib/socket/socket.cpp index 0c2046d..acf9bf4 100644 --- a/lib/socket/socket.cpp +++ b/lib/socket/socket.cpp @@ -1,98 +1,120 @@ #include "./socket.hpp" +#include "../log/log.hpp" +#include "assert.h" #include #include +#include +#include +#include #include #include +#include #include #include #include #include #include #include -#include -#include -#include "assert.h" -#include -#include - namespace anthracite::socket { -const struct timeval anthracite_socket::timeout_tv = { .tv_sec = 5, .tv_usec = 0 }; - -anthracite_socket::anthracite_socket(int port, int max_queue, bool nonblocking) - : server_socket(::socket(AF_INET, SOCK_STREAM, 0)) - , client_ip("") - , _nonblocking(nonblocking) +socket::socket(bool nonblocking) + : _nonblocking(nonblocking) { +} + +listener::listener(int port, int max_queue, bool nonblocking) + : socket(nonblocking) + , _port(port) +{ + _sock_fd = ::socket(AF_INET, SOCK_STREAM, 0); + + if (_sock_fd == -1) { + log::err << "Listener was unable to open a socket" << std::endl; + throw std::exception(); + } + struct sockaddr_in address {}; address.sin_family = AF_INET; - address.sin_port = htons(port); + address.sin_port = htons(_port); address.sin_addr.s_addr = INADDR_ANY; int reuse_opt = 1; - setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &reuse_opt, sizeof(reuse_opt)); - bind(server_socket, reinterpret_cast(&address), sizeof(address)); - - if (_nonblocking) { - fcntl(server_socket, F_SETFL, O_NONBLOCK); + if (setsockopt(_sock_fd, SOL_SOCKET, SO_REUSEADDR, &reuse_opt, sizeof(reuse_opt)) < 0) { + log::err << "Listener was unable to set SO_REUSEADDR" << std::endl; + throw std::exception(); } - listen(server_socket, max_queue); + if (bind(_sock_fd, reinterpret_cast(&address), sizeof(address)) != 0) { + log::err << "Listener was unable to bind to address" << std::endl; + throw std::exception(); + } + + if (fcntl(_sock_fd, F_SETFL, O_NONBLOCK) == -1) { + log::err << "Listener was unable to fcntl(O_NONBLOCK)" << std::endl; + throw std::exception(); + } + + if (listen(_sock_fd, max_queue) == -1) { + log::err << "Listener was unable to begin listening" << std::endl; + throw std::exception(); + } } -bool anthracite_socket::wait_for_conn() +bool listener::wait_for_conn(server** client_sock_p) { - client_ip = ""; - client_socket = accept(server_socket, reinterpret_cast(&client_addr), &client_addr_len); - if (client_socket > 0) { - if (_nonblocking) { - fcntl(client_socket, F_SETFL, O_NONBLOCK); + struct sockaddr_in client_addr {}; + socklen_t client_addr_len; + + int csock = accept(_sock_fd, reinterpret_cast(&client_addr), &client_addr_len); + + if (csock > 0) { + std::array ip_str { 0 }; + + if (inet_ntop(AF_INET, &client_addr.sin_addr, ip_str.data(), INET_ADDRSTRLEN) == NULL) { + if (inet_ntop(AF_INET6, &client_addr.sin_addr, ip_str.data(), INET6_ADDRSTRLEN) == NULL) { + log::warn << "Unable to decode client's IP address" << std::endl; + } } - std::array ip_str { 0 }; - inet_ntop(AF_INET, &client_addr.sin_addr, ip_str.data(), INET_ADDRSTRLEN); - client_ip = std::string(ip_str.data()); + + std::string client_ip = std::string(ip_str.data()); + *client_sock_p = new server(csock, client_ip, _nonblocking); + return true; } else { return false; } } -const std::string& anthracite_socket::get_client_ip() +server::server(int sock_fd, std::string client_ip, bool nonblocking) + : _sock_fd(sock_fd) + , _client_ip(std::move(client_ip)) + , socket(nonblocking) { - return client_ip; -} - -void anthracite_socket::close_conn() { - close(client_socket); - client_socket = -1; -} - -void anthracite_socket::send_message(std::string& msg) -{ - if (client_socket == -1) { - return; + if (_nonblocking) { + if (fcntl(_sock_fd, F_SETFL, O_NONBLOCK) == -1) { + log::err << "Server was unable to fcntl(O_NONBLOCK)" << std::endl; + throw std::exception(); + } } - send(client_socket, &msg[0], msg.length(), 0); } -bool anthracite_socket::has_client() { - return client_socket > 0; -} - -std::string anthracite_socket::recv_message(int buffer_size) +void server::send_message(const std::string& msg) { - if (client_socket == -1) { - return ""; - } - - //setsockopt(client_socket, SOL_SOCKET, SO_RCVTIMEO, &timeout_tv, sizeof timeout_tv); + // Ignored because if we fail to send, it probably means + // a HUP will occur and it'll be closed. TODO: Just close + // it here and add a return value + (void)send(_sock_fd, &msg[0], msg.length(), 0); +} +std::string server::recv_message(int buffer_size) +{ + // Ignored because it's nonfatal, just slower int nodelay_opt = 1; - assert(setsockopt(client_socket, SOL_TCP, TCP_NODELAY, &nodelay_opt, sizeof(nodelay_opt)) == 0); + (void)setsockopt(_sock_fd, SOL_TCP, TCP_NODELAY, &nodelay_opt, sizeof(nodelay_opt)); std::vector response(buffer_size + 1); - ssize_t result = recv(client_socket, response.data(), buffer_size + 1, 0); + ssize_t result = recv(_sock_fd, response.data(), buffer_size + 1, 0); if (result < 1) { return ""; @@ -102,4 +124,21 @@ std::string anthracite_socket::recv_message(int buffer_size) return { response.data() }; } +server::~server() +{ + for (int i = 0; i < 5 && close(_sock_fd) != 0; ++i) + ; +} + +listener::~listener() +{ + for (int i = 0; i < 5 && close(_sock_fd) != 0; ++i) + ; +} + +const std::string& server::client_ip() +{ + return _client_ip; +} + }; diff --git a/lib/socket/socket.hpp b/lib/socket/socket.hpp index ea8622d..18590d3 100644 --- a/lib/socket/socket.hpp +++ b/lib/socket/socket.hpp @@ -10,32 +10,42 @@ namespace anthracite::socket { +class socket { + protected: + bool _nonblocking; + socket(bool nonblocking); + public: + socket(){} + virtual ~socket(){} +}; -class anthracite_socket { +class server : public socket { + protected: + int _sock_fd; + std::string _client_ip; + public: + server(int sock_fd, std::string client_ip, bool nonblocking); + ~server(); -protected: - bool _nonblocking; - struct timeval wait_timeout = { .tv_sec = 1, .tv_usec = 0}; - int server_socket; - int client_socket {}; - std::string client_ip; - struct sockaddr_in client_addr {}; - socklen_t client_addr_len {}; - static const struct timeval timeout_tv; - static const int MAX_QUEUE_LENGTH = 100; + virtual void send_message(const std::string& msg); + virtual std::string recv_message(int buffer_size); + const std::string& client_ip(); -public: - anthracite_socket(int port, int max_queue = MAX_QUEUE_LENGTH, bool nonblocking = false); + int fd() { return _sock_fd; } +}; - virtual const std::string& get_client_ip() final; +class listener : public socket { + protected: + uint16_t _port; + int _sock_fd; + public: + listener(int port, int max_queue_length, bool nonblocking); + ~listener(); - virtual bool has_client(); - virtual bool wait_for_conn(); - virtual void close_conn(); - virtual void send_message(std::string& msg); - virtual std::string recv_message(int buffer_size); + virtual bool wait_for_conn(server** client_sock_p); - int csock() { return client_socket; } + int fd() { return _sock_fd; } + int port() { return _port; } }; }; diff --git a/lib/thread_mgr/event_loop.cpp b/lib/thread_mgr/event_loop.cpp index 92c5bfb..a78956e 100644 --- a/lib/thread_mgr/event_loop.cpp +++ b/lib/thread_mgr/event_loop.cpp @@ -1,14 +1,14 @@ #include "./event_loop.hpp" #include "../log/log.hpp" -#include "../socket/openssl_socket.hpp" #include "assert.h" +#include "signal.h" #include "sys/epoll.h" #include #include #include #include #include -#include "signal.h" +#include using std::chrono::duration; using std::chrono::duration_cast; @@ -16,29 +16,18 @@ using std::chrono::high_resolution_clock; using std::chrono::milliseconds; namespace anthracite::thread_mgr { -event_loop::event::event(socket::anthracite_socket* socket, std::chrono::time_point timestamp) - : _socket(socket) - , _ts(timestamp) -{ -} -socket::anthracite_socket* event_loop::event::socket() -{ - return _socket; -} - -std::chrono::time_point& event_loop::event::timestamp() -{ - return _ts; -} - -event_loop::event_loop(backends::backend& backend, config::config& config) - : thread_mgr(backend, config) +event_loop::event_loop(std::vector& listen_sockets, backends::backend& backend, int max_threads, int max_clients) + : thread_mgr(backend) , _error_backend("./www") + , _max_threads(max_threads) + , _listen_sockets(listen_sockets) + , _max_clients(max_clients) + , _nonblocking(false) { } -bool event_loop::event_handler(socket::anthracite_socket* sock) +bool event_loop::event_handler(socket::server* sock) { std::string raw_request = sock->recv_message(http::HEADER_BYTES); @@ -46,7 +35,7 @@ bool event_loop::event_handler(socket::anthracite_socket* sock) return false; } - http::request req(raw_request, sock->get_client_ip()); + http::request req(raw_request, sock->client_ip()); std::unique_ptr resp = req.is_supported_version() ? _backend.handle_request(req) : _error_backend.handle_error(http::status_codes::HTTP_VERSION_NOT_SUPPORTED); std::string header = resp->header_to_string(); sock->send_message(header); @@ -65,107 +54,81 @@ void event_loop::worker_thread_loop(int threadno) ss << "worker " << threadno; pthread_setname_np(pthread_self(), ss.str().c_str()); - struct epoll_event* events = new struct epoll_event[_config.max_clients()]; + struct epoll_event* events = new struct epoll_event[_max_clients]; int timeout_ms = 1000; if (_nonblocking) { timeout_ms = 0; } - log::info << "Starting worker thread " << threadno << std::endl; + std::osyncstream(log::info) << "Starting worker thread " << threadno << std::endl; + + int epoll_fd = epoll_create(1); + + for (socket::listener* sl : _listen_sockets) { + struct epoll_event event; + event.events = EPOLLIN | EPOLLEXCLUSIVE; + event.data.ptr = sl; + epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sl->fd(), &event); + if (threadno == 0) { + std::osyncstream(log::info) << "Listening started on port " << sl->port() << std::endl; + } + } while (_run) { - int ready_fds = epoll_wait(_epoll_fd, events, _config.max_clients(), timeout_ms); + int ready_fds = epoll_wait(epoll_fd, events, _max_clients, timeout_ms); if (ready_fds > 0) { for (int i = 0; i < ready_fds; i++) { - socket::anthracite_socket* sockptr = reinterpret_cast(events[i].data.ptr); + socket::socket* sockptr = reinterpret_cast(events[i].data.ptr); + socket::server* server_ptr = dynamic_cast(sockptr); - if (!event_handler(sockptr)) { - epoll_ctl(_epoll_fd, EPOLL_CTL_DEL, sockptr->csock(), &events[i]); - sockptr->close_conn(); - delete sockptr; + if (server_ptr != nullptr) { + if (!event_handler(server_ptr)) { + delete server_ptr; + } + } else { + socket::listener* listen_ptr = dynamic_cast(sockptr); + if (listen_ptr != nullptr) { + socket::server* server_sock; + while (listen_ptr->wait_for_conn(&server_sock)) { + struct epoll_event event; + event.events = EPOLLIN | EPOLLEXCLUSIVE; + event.data.ptr = server_sock; + epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_sock->fd(), &event); + } + } else { + std::osyncstream(log::err) << "Had socket type that wasn't listener or server" << std::endl; + } } } } } - log::info << "Stopping worker thread " << threadno << std::endl; -} + delete[] events; -void event_loop::listener_thread_loop(config::http_config& http_config) -{ - socket::anthracite_socket* socket; - - config::http_config* http_ptr = &http_config; - config::https_config* https_ptr = dynamic_cast(http_ptr); - - bool is_tls = https_ptr != nullptr; - - if (is_tls) { - socket = new socket::openssl_socket(https_ptr->port()); - } else { - socket = new socket::anthracite_socket(http_ptr->port()); - } - - std::osyncstream(log::info) << "Listening for " << (is_tls ? "HTTPS" : "HTTP") << " connections on port " << http_ptr->port() << std::endl; - - while (_run) { - if (socket->wait_for_conn()) { - socket::anthracite_socket* client_sock; - - if (is_tls) { - socket::openssl_socket* ssl_sock = dynamic_cast(socket); - client_sock = new socket::openssl_socket(*ssl_sock); - } else { - client_sock = new socket::anthracite_socket(*socket); - } - - struct epoll_event event; - event.events = EPOLLIN | EPOLLEXCLUSIVE;// | EPOLLET; - event.data.ptr = client_sock; - epoll_ctl(_epoll_fd, EPOLL_CTL_ADD, client_sock->csock(), &event); - } - } - - std::osyncstream(log::info) << "Stopping listening for " << (is_tls ? "HTTPS" : "HTTP") << " connections on port " << http_ptr->port() << std::endl; - - delete socket; + std::osyncstream(log::info) << "Stopping worker thread " << threadno << std::endl; } void event_loop::start() { + std::lock_guard lg(_run_lock); + signal(SIGPIPE, SIG_IGN); log::info << "Starting event_loop Thread Manager" << std::endl; _run = true; - _epoll_fd = epoll_create(1); - std::vector listener_threads; std::vector worker_threads; - for (int i = 0; i < _config.worker_threads(); i++) { + for (int i = 0; i < _max_threads; i++) { auto thread = std::thread(&event_loop::worker_thread_loop, this, i); worker_threads.push_back(std::move(thread)); } - if (_config.http_cfg().has_value()) { - auto thread = std::thread(&event_loop::listener_thread_loop, this, std::ref(_config.http_cfg().value())); - listener_threads.push_back(std::move(thread)); - } - - if (_config.https_cfg().has_value()) { - auto thread = std::thread(&event_loop::listener_thread_loop, this, std::ref(_config.https_cfg().value())); - listener_threads.push_back(std::move(thread)); - } - for (std::thread& t : worker_threads) { t.join(); } - - for (std::thread& t : listener_threads) { - t.join(); - } } void event_loop::stop() diff --git a/lib/thread_mgr/event_loop.hpp b/lib/thread_mgr/event_loop.hpp index 3fa8ea5..41c3a18 100644 --- a/lib/thread_mgr/event_loop.hpp +++ b/lib/thread_mgr/event_loop.hpp @@ -1,34 +1,25 @@ #include "./thread_mgr.hpp" #include "../socket/socket.hpp" #include "../backends/file_backend.hpp" -#include -#include #include -#include +#include +#include "../socket/socket.hpp" namespace anthracite::thread_mgr { class event_loop : public virtual thread_mgr { - class event { - socket::anthracite_socket* _socket; - std::chrono::time_point _ts; - public: - event(socket::anthracite_socket* socket, std::chrono::time_point timestamp); - socket::anthracite_socket* socket(); - std::chrono::time_point& timestamp(); - }; - - int _epoll_fd; std::mutex _event_mtx; backends::file_backend _error_backend; + std::vector& _listen_sockets; bool _nonblocking; + std::mutex _run_lock; + int _max_threads; + int _max_clients; void worker_thread_loop(int threadno); - void listener_thread_loop(config::http_config& http_config); - void eventer_thread_loop(); - bool event_handler(socket::anthracite_socket*); + bool event_handler(socket::server*); public: - event_loop(backends::backend& backend, config::config& config); + event_loop(std::vector&, backends::backend& backend, int max_workers, int max_clients); void start() override; void stop() override; }; diff --git a/lib/thread_mgr/thread_mgr.hpp b/lib/thread_mgr/thread_mgr.hpp index faae7ec..cbb7614 100644 --- a/lib/thread_mgr/thread_mgr.hpp +++ b/lib/thread_mgr/thread_mgr.hpp @@ -1,16 +1,14 @@ #pragma once #include "../backends/backend.hpp" -#include "../config/config.hpp" namespace anthracite::thread_mgr { class thread_mgr { protected: bool _run; backends::backend& _backend; - config::config& _config; public: - thread_mgr(backends::backend& backend, config::config& config): _backend(backend), _config(config) {} + thread_mgr(backends::backend& backend): _backend(backend) {} virtual ~thread_mgr() = default; virtual void start() = 0; virtual void stop() = 0; diff --git a/src/file_main.cpp b/src/file_main.cpp index ad3dcdd..3e7285b 100644 --- a/src/file_main.cpp +++ b/src/file_main.cpp @@ -1,35 +1,306 @@ #include "../lib/backends/file_backend.hpp" -#include "../lib/config/config.hpp" #include "../lib/log/log.hpp" +#include "../lib/socket/openssl_socket.hpp" +#include "../lib/socket/socket.hpp" #include "../lib/thread_mgr/event_loop.hpp" -#include "signal.h" +#include "../lib/version.hpp" +#include "getopt.h" #include "string.h" +#include "sys/signal.h" +#include #include +#include +#include +#include +#include +#include + +struct event_loop_config { + int max_workers; + int max_clients; +}; + +struct server_config { + std::unordered_map listeners; + std::optional event_loop; + std::string serve_dir = "./www"; + enum anthracite::log::LOG_LEVEL log_level = anthracite::log::LOG_LEVEL_INFO; +}; std::shared_ptr server = nullptr; extern "C" void signalHandler(int signum) { - //anthracite::log::warn << "Caught signal SIG" << sigabbrev_np(signum) << ", exiting Anthracite" << std::endl; + anthracite::log::warn << "Caught signal SIGIN, exiting Anthracite" << std::endl; if (server != nullptr) { server->stop(); } } -int main(int argc, char** argv) +bool read_config(server_config& config, const std::string& config_path) +{ + std::ifstream config_stream(config_path); + if (!config_stream.is_open()) { + anthracite::log::err << "Unable to open configuration file at '" << config_path << "', ensure that the file exists and permissions are set correctly" << std::endl; + return false; + } + + std::string line; + for (int lineno = 1; std::getline(config_stream, line); lineno++) { + bool parse_failed = false; + + std::stringstream ss_line(line); + std::string directive; + ss_line >> directive; + + if (ss_line.fail()) { + continue; + } + + if (directive == "http" || directive == "https") { + // http PORT QUEUE_LEN NONBLOCK + // https PORT QUEUE_LEN NONBLOCK CRT_PATH KEY_PATH + int port; + int queue_len; + std::string block; + std::string crt_path; + std::string key_path; + ss_line >> port >> queue_len >> block; + + if (directive == "https") { + ss_line >> crt_path >> key_path; + } + + bool nonblocking = false; + if (block == "blocking") { + nonblocking = false; + } else if (block == "nonblocking") { + nonblocking = true; + } else { + anthracite::log::err << "BLOCK is not a string of value blocking or nonblocking"; + parse_failed = true; + } + + parse_failed |= ss_line.fail(); + parse_failed |= !ss_line.eof(); + + if (parse_failed) { + anthracite::log::err << "Invalid http/https config on line " << lineno << " of configuration file" << std::endl; + anthracite::log::err << std::endl; + anthracite::log::err << "Format is: " << std::endl; + anthracite::log::err << "http PORT QUEUE_LENGTH BLOCK" << std::endl; + anthracite::log::err << "https PORT QUEUE_LENGTH BLOCK CRT_PATH KEY_PATH" << std::endl; + anthracite::log::err << std::endl; + anthracite::log::err << "PORT, QUEUE_LENGTH are integers" << std::endl; + anthracite::log::err << "BLOCK is a string of value blocking or nonblocking" << std::endl; + anthracite::log::err << "CRT_PATH and KEY_PATH are strings for the path or the certificate and key files respectively" << std::endl; + anthracite::log::err << std::endl + << "Line was: " << std::endl + << line << std::endl; + anthracite::log::err << "Check for trailing whitespace!" << std::endl; + return false; + } + + if (config.listeners.contains(port)) { + anthracite::log::err << "Invalid http/https config on line " << lineno << " of configuration file" << std::endl; + anthracite::log::err << "Port " << port << " is already being used" << std::endl; + return false; + } + + if (directive == "https") { + config.listeners[port] = new anthracite::socket::openssl_listener(key_path, crt_path, port, queue_len, nonblocking); + } else { + config.listeners[port] = new anthracite::socket::listener(port, queue_len, nonblocking); + } + } else if (directive == "log_level") { + // log_level LEVEL + std::string log_level; + ss_line >> log_level; + + parse_failed |= ss_line.fail(); + parse_failed |= !ss_line.eof(); + + if (log_level == "DEBUG") { + config.log_level = anthracite::log::LOG_LEVEL::LOG_LEVEL_DEBUG; + } else if (log_level == "VERBOSE") { + config.log_level = anthracite::log::LOG_LEVEL::LOG_LEVEL_VERBOSE; + } else if (log_level == "INFO") { + config.log_level = anthracite::log::LOG_LEVEL::LOG_LEVEL_INFO; + } else if (log_level == "WARN") { + config.log_level = anthracite::log::LOG_LEVEL::LOG_LEVEL_WARN; + } else if (log_level == "ERROR") { + config.log_level = anthracite::log::LOG_LEVEL::LOG_LEVEL_ERROR; + } else { + parse_failed = true; + } + + if (parse_failed) { + anthracite::log::err << "Invalid log_level config on line " << lineno << " of configuration file" << std::endl; + anthracite::log::err << std::endl; + anthracite::log::err << "Format is: " << std::endl; + anthracite::log::err << "log_level LEVEL" << std::endl; + anthracite::log::err << std::endl; + anthracite::log::err << "LEVEL is string of value DEBUG, VERBOSE, INFO, WARN, ERROR" << std::endl; + anthracite::log::err << std::endl + << "Line was: " << std::endl + << line << std::endl; + anthracite::log::err << "Check for trailing whitespace!" << std::endl; + return false; + } + } else if (directive == "event_loop") { + // event_loop MAX_WORKERS MAX_CLIENS + int max_workers; + int max_clients; + + ss_line >> max_workers >> max_clients; + + parse_failed |= ss_line.fail(); + parse_failed |= !ss_line.eof(); + + if (parse_failed) { + anthracite::log::err << "Invalid event_loop config on line " << lineno << " of configuration file" << std::endl; + anthracite::log::err << std::endl; + anthracite::log::err << "Format is: " << std::endl; + anthracite::log::err << "event_loop MAX_WORKERS MAX_CLIENTS" << std::endl; + anthracite::log::err << std::endl; + anthracite::log::err << "MAX_WORKERS is the maximum number of worker threads" << std::endl; + anthracite::log::err << "MAX_CLIENTS is the maximum number of concurrent clients" << std::endl; + anthracite::log::err << std::endl + << "Line was: " << std::endl + << line << std::endl; + anthracite::log::err << "Check for trailing whitespace!" << std::endl; + return false; + } + + if (max_workers <= 0) { + anthracite::log::err << "Invalid event_loop config on line " << lineno << " of configuration file" << std::endl; + anthracite::log::err << "MAX_WORKERS must be a positive, nonzero number" << std::endl; + return false; + } + + if (max_clients <= 0) { + anthracite::log::err << "Invalid event_loop config on line " << lineno << " of configuration file" << std::endl; + anthracite::log::err << "MAX_CLIENTS must be a positive, nonzero number" << std::endl; + return false; + } + + if (config.event_loop.has_value()) { + anthracite::log::err << "Invalid event_loop config on line " << lineno << " of configuration file" << std::endl; + anthracite::log::err << "A thread manager was already specified. You may only specify one at a time as of now." << std::endl; + return false; + } + + // Eww + config.event_loop = { .max_workers = max_workers, .max_clients = max_clients }; + } else if (directive == "www_dir") { + std::string www_dir; + ss_line >> www_dir; + + parse_failed |= ss_line.fail(); + parse_failed |= !ss_line.eof(); + + if (parse_failed) { + anthracite::log::err << "Invalid www_dir config on line " << lineno << " of configuration file" << std::endl; + anthracite::log::err << std::endl; + anthracite::log::err << "Format is: " << std::endl; + anthracite::log::err << "www_dir PATH" << std::endl; + anthracite::log::err << std::endl; + anthracite::log::err << "PATH is a path to a directory containing files to serve" << std::endl; + anthracite::log::err << std::endl + << "Line was: " << std::endl + << line << std::endl; + anthracite::log::err << "Check for trailing whitespace!" << std::endl; + return false; + } + } else { + anthracite::log::err << "Invalid configuration. Unknown directive " << directive << " on line " << lineno << std::endl; + return false; + } + } + + if (!config.event_loop.has_value()) { + anthracite::log::err << "Invalid configuration. Missing a thread manager. Try adding an event_loop directive." << std::endl; + return false; + } + + if (config.listeners.size() == 0) { + anthracite::log::err << "Invalid configuration. Missing listeners. Try adding a http or https directive." << std::endl; + return false; + } + + return true; +} + +int main(int argc, char* argv[]) { - anthracite::log::logger.initialize(anthracite::log::LOG_LEVEL_INFO); - anthracite::log::info << "Starting Anthracite, a higher performance web server" << std::endl; signal(SIGINT, signalHandler); + anthracite::log::logger.initialize(anthracite::log::LOG_LEVEL_INFO); + if (pthread_setname_np(pthread_self(), "main") != 0) { + anthracite::log::err << "Failed to set thread name via pthread_setname_np" << std::endl; + } - anthracite::backends::file_backend fb("./www"); - anthracite::config::config cfg(1, 10); - cfg.add_http_config(anthracite::config::http_config(8080)); - // cfg.add_https_config(config::https_config(8081, "", "")); + int opt_index = 0; + option options[] = { + { "help", no_argument, 0, 'h' }, + { "config", required_argument, 0, 'c' } + }; - server = std::make_shared(fb, cfg); + char c; + std::string config_path = "./anthracite.cfg"; + bool config_set = false; + while ((c = getopt_long(argc, argv, "hc:", options, &opt_index)) != -1) { + switch (c) { + case 'h': { + std::cerr << "Anthracite " << ANTHRACITE_VERSION_STRING << " Help" << std::endl; + std::cerr << std::endl; + std::cerr << "-h, --help Prints this help menu " << std::endl; + std::cerr << std::endl; + std::cerr << "-c, --config string (optional) Specifies the path of the configuration" << std::endl; + std::cerr << " file. Default is './anthracite.cfg'" << std::endl; + std::cerr << std::endl; + return 0; + break; + }; + case 'c': { + if (config_set) { + anthracite::log::err << "You cannot specify multiple configuration files" << std::endl; + return 1; + } + config_set = true; + config_path = std::string(optarg); + break; + }; + } + } + + anthracite::log::info << "Loading configuration file at path '" << config_path << "'" << std::endl; + + server_config cfg; + if (!read_config(cfg, config_path)) { + anthracite::log::err << "Failed reading configuration file at path '" << config_path << "'" << std::endl; + return 1; + } + + anthracite::log::logger.initialize(cfg.log_level); + + anthracite::log::info << "Serving files in directory " << cfg.serve_dir << std::endl; + anthracite::backends::file_backend fb(cfg.serve_dir); + + std::vector listeners; + + for (auto lp : cfg.listeners) { + listeners.push_back(lp.second); + } + + server = std::make_shared(listeners, fb, cfg.event_loop->max_workers, cfg.event_loop->max_clients); + + anthracite::log::info << "Starting Anthracite, a very high performance web server" << std::endl; server->start(); - anthracite::log::info << "Stopping Anthracite, a higher performance web server" << std::endl; + for (auto listener : listeners) { + delete listener; + } + + anthracite::log::info << "Stopping Anthracite, a very high performance web server" << std::endl; }