Compare commits

..

14 commits

Author SHA1 Message Date
efd19d3bba fix version build step
Some checks failed
Docker Build & Publish / build (push) Failing after 1h39m43s
2025-02-24 21:34:54 -05:00
0fc8400066 add version.txt
Some checks failed
Docker Build & Publish / build (push) Failing after 1h19m14s
2025-02-24 19:53:38 -05:00
f48eda81e8 add version.txt fix
Some checks failed
Docker Build & Publish / build (push) Has been cancelled
2025-02-24 19:41:41 -05:00
5c196c05fc small changes before 0.3.0
Some checks failed
Docker Build & Publish / build (push) Has been cancelled
2025-02-24 19:37:23 -05:00
9b5719f9be Release v0.3.0
Some checks failed
Docker Build & Publish / build (push) Has been cancelled
2025-02-24 19:29:43 -05:00
c07f3ebf81 faster nonblocking io
Some checks failed
Docker Build & Publish / build (push) Failing after 1h24m16s
2025-02-23 17:06:32 -05:00
6c5feb8675 epoll per thd 2025-02-22 00:52:30 -05:00
409024e04a polished up event loop changes
Some checks failed
Docker Build & Publish / build (push) Failing after 55m49s
2025-02-21 18:24:28 -05:00
058c395095 event loop with threadmgr
Some checks failed
Docker Build & Publish / build (push) Failing after 1h11m38s
2025-02-21 14:09:16 -05:00
ca05aa1e5a event loop with threadmgr
Some checks failed
Docker Build & Publish / build (push) Has been cancelled
2025-02-21 14:09:01 -05:00
f09b261b62 ssl fix
Some checks failed
Docker Build & Publish / build (push) Failing after 1h12m36s
2025-02-20 13:48:30 -05:00
f1195d1f04 add shell.nix 2025-02-19 23:54:15 -05:00
a63d9d1e65
broken ssl
Some checks failed
Docker Build & Publish / build (push) Failing after 1h8m3s
2025-02-17 18:46:52 -05:00
da9f2f2d51
broken ssl
Some checks failed
Docker Build & Publish / build (push) Has been cancelled
2025-02-17 18:46:33 -05:00
31 changed files with 3254 additions and 741 deletions

19
.direnv/bin/nix-direnv-reload Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -e
if [[ ! -d "/home/nickorlow/programming/personal/anthracite" ]]; then
echo "Cannot find source directory; Did you move it?"
echo "(Looking for "/home/nickorlow/programming/personal/anthracite")"
echo 'Cannot force reload with this script - use "direnv reload" manually and then try again'
exit 1
fi
# rebuild the cache forcefully
_nix_direnv_force_reload=1 direnv exec "/home/nickorlow/programming/personal/anthracite" true
# Update the mtime for .envrc.
# This will cause direnv to reload again - but without re-building.
touch "/home/nickorlow/programming/personal/anthracite/.envrc"
# Also update the timestamp of whatever profile_rc we have.
# This makes sure that we know we are up to date.
touch -r "/home/nickorlow/programming/personal/anthracite/.envrc" "/home/nickorlow/programming/personal/anthracite/.direnv"/*.rc

View file

@ -0,0 +1 @@
/nix/store/gr8ifjf51b4w3v62vvinq4s8w97pn3ag-nix-shell-env

File diff suppressed because it is too large Load diff

1
.envrc Normal file
View file

@ -0,0 +1 @@
use nix

56
.github/workflows/docker-publish.yml vendored Normal file
View file

@ -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 > build_supp/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

View file

@ -1,13 +1,17 @@
# 0.3.0
- SSL support via OpenSSL
- Added "Thread Manager" class to allow for multiple (or custom) threading models (process per thread, 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
- Revamped build system to use CMake properly
- Moved CI/CD over to Forgejo
- Added simple config file system (will be completely replaced by v1.0)
- 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

View file

@ -2,42 +2,41 @@ cmake_minimum_required(VERSION 3.10)
project(anthracite)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
set(CMAKE_CXX_FLAGS_RELEASE "-O3")
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
add_custom_target(build-version
COMMAND cd ../build_supp && ./version.sh
DEPENDS build_supp/version.txt
find_package(OpenSSL REQUIRED)
configure_file(build_supp/version.txt version.txt COPYONLY)
add_custom_command(
COMMAND ../build_supp/version.sh
DEPENDS version.txt
OUTPUT ${CMAKE_BINARY_DIR}/version.cpp
COMMENT "Generated supplemental build files (version)"
)
add_custom_target(build-supplemental
COMMAND cd ../build_supp && python3 ./error_gen.py
COMMAND mkdir -p www && cp -r ../default_www/regular/* ./www/
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)"
COMMAND cp ../build_supp/default_config.cfg ./anthracite.cfg
DEPENDS ../default_www/regular/* build_supp/error_gen.py ${CMAKE_BINARY_DIR}/version.cpp
COMMENT "Generated supplemental build files (default www dir + default config + error pages)"
)
add_custom_target(run
COMMAND anthracite-bin
DEPENDS anthracite-bin
WORKING_DIRECTORY ${CMAKE_PROJECT_DIR}
)
FILE(GLOB LIB_SOURCES lib/*.cpp lib/**/*.cpp build_supp/version.cpp)
add_library(anthracite ${LIB_SOURCES})
add_dependencies(anthracite build-version)
FILE(GLOB LIB_SOURCES lib/*.cpp lib/**/*.cpp)
add_library(anthracite ${LIB_SOURCES} ${CMAKE_BINARY_DIR}/version.cpp)
add_dependencies(anthracite build-supplemental)
target_link_libraries(anthracite OpenSSL::SSL OpenSSL::Crypto)
target_include_directories(anthracite PUBLIC ${OPENSSL_INCLUDE_DIR})
add_executable(anthracite-bin src/file_main.cpp)
target_link_libraries(anthracite-bin anthracite)
add_dependencies(anthracite-bin build-supplemental)
add_dependencies(anthracite-bin anthracite)
add_executable(anthracite-api-bin src/api_main.cpp)
target_link_libraries(anthracite-api-bin anthracite)
include(FetchContent)
FetchContent_Declare(
googletest

View file

@ -1,8 +1,9 @@
FROM alpine as build-env
FROM alpine AS build-env
RUN apk add --no-cache build-base python3 cmake
RUN apk add --no-cache build-base python3 cmake openssl-dev
COPY ./src ./src
COPY ./lib ./lib
COPY ./tests ./tests
COPY ./default_www ./default_www
COPY ./build_supp ./build_supp
COPY ./CMakeLists.txt .
@ -17,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"]

View file

@ -1,28 +1,28 @@
# Anthracite
A simple web server written in C++. Supports HTTP 1.0 & 1.1.
Anthracite is an extensible, low-dependency, fast web server.
## Developing
To build/develop Anthracite, you must have C++23, CMake, Make, and Python3 installed.
To build/develop Anthracite, you must have C++20, OpenSSL, CMake, Make, and Python3 installed.
Create a `build/` directory, run `cmake ..`, and then `make` to build.
## Todo
- [x] HTTP/1.0
- [x] Serve HTML Pages
- [x] Properly parse HTTP requests
- [x] Add module-based backend system for handling requests
- [x] Multithreading
- [x] HTTP/1.1
- [x] Enhance logging
- [x] Create library that can be used to implement custom backends (i.e. webapi, fileserver, etc)
- [ ] Faster parsing
- [ ] HTTP/2
- [ ] Improve benchmarking infrastructure
- [ ] Fix glaring security issues
- [ ] Proper error handling
- [ ] User configuration
- [ ] Cleanup (this one will never truly be done)
## Features
- HTTP/1.0 & HTTP/1.1 Support
- SSL via OpenSSL
- Event loop thread management
- libanthracite library for reating custom webservers
- Configuration through configuration file
- Minimal dependencies (only OpenSSL & stantart library so far)
## Roadmap
- HTTP/2
- HTTP/3
- More threading modes
- Proxy backend
- Security/Error handling audit
## Screenshots

View file

@ -0,0 +1,6 @@
log_level INFO
http 8080 1000 blocking
event_loop 6 10000
www_dir ./www

View file

@ -1 +1 @@
0.3.0-dev
0.3.0

View file

@ -2,8 +2,11 @@ services:
anthracite-web:
build: .
ports:
- "8080:80"
- "8080:8080"
volumes:
- type: bind
source: ./default_www/docker_compose/
target: /www
- type: bind
source: ./build_supp/default_config.cfg
target: /anthracite.cfg

View file

@ -1,84 +0,0 @@
#include "./anthracite.hpp"
#include "./log/log.hpp"
#include "./socket/socket.hpp"
#include "./socket/tls_socket.hpp"
#include "backends/file_backend.hpp"
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <netinet/in.h>
#include <span>
#include <sys/socket.h>
#include <thread>
#include <unistd.h>
using namespace anthracite;
void log_request_and_response(http::request& req, std::unique_ptr<http::response>& resp, uint32_t micros);
constexpr int default_port = 80;
constexpr int max_worker_threads = 128;
using std::chrono::high_resolution_clock;
using std::chrono::duration_cast;
using std::chrono::duration;
using std::chrono::milliseconds;
void handle_client(socket::tls_socket s, backends::backend& b, backends::file_backend& fb, std::mutex& thread_wait_mutex, std::condition_variable& thread_wait_condvar, int& active_threads)
{
while (true) {
std::string raw_request = s.recv_message(http::HEADER_BYTES);
// We're doing the start here even though it would ideally be done
// before the first line since if we leave the connection open for
// HTTP 1.1, we can spend a bit of time waiting
auto start = high_resolution_clock::now();
if (raw_request == "") {
break;
}
continue;
}
s.close_conn();
{
std::lock_guard<std::mutex> lock(thread_wait_mutex);
active_threads--;
}
thread_wait_condvar.notify_one();
}
int anthracite_main(int argc, char** argv, backends::backend& be)
{
log::logger.initialize(log::LOG_LEVEL_DEBUG);
auto args = std::span(argv, size_t(argc));
int port_number = default_port;
if (argc > 1) {
port_number = atoi(args[1]);
}
socket::tls_socket s(port_number);
backends::file_backend fb(argc > 2 ? args[2] : "./www");
log::info << "Listening for HTTP connections on port " << port_number << std::endl;
int active_threads = 0;
std::mutex thread_wait_mutex;
std::condition_variable thread_wait_condvar;
while (true) {
s.wait_for_conn();
std::unique_lock<std::mutex> lock(thread_wait_mutex);
thread_wait_condvar.wait(lock, [active_threads] { return active_threads < max_worker_threads; });
active_threads++;
std::thread(handle_client, s, std::ref(be), std::ref(fb), std::ref(thread_wait_mutex), std::ref(thread_wait_condvar), std::ref(active_threads)).detach();
}
exit(0);
}
void log_request_and_response(http::request& req, std::unique_ptr<http::response>& resp, uint32_t micros)
{
log::info << "[" << resp->status_code() << " " + http::status_map.find(resp->status_code())->second + "] " + req.client_ip() + " " + http::reverse_method_map.find(req.get_method())->second + " " + req.path() << " in " << micros << " usecs" << std::endl;
}

View file

@ -1,4 +0,0 @@
#include "backends/backend.hpp"
using namespace anthracite;
int anthracite_main(int argc, char** argv, backends::backend& be);

View file

@ -1,87 +1,91 @@
#include "request.hpp"
#include "../log/log.hpp"
#include "constants.hpp"
#include <map>
#include <cstring>
#include <map>
#include <stdio.h>
namespace anthracite::http {
void request::parse_header(std::string& raw_line) {
void request::parse_header(std::string& raw_line)
{
auto delim_pos = raw_line.find_first_of(':');
auto value_pos = raw_line.find_first_not_of(' ', delim_pos+1);
auto value_pos = raw_line.find_first_not_of(' ', delim_pos + 1);
std::string header_name = raw_line.substr(0,delim_pos);
std::string header_name = raw_line.substr(0, delim_pos);
std::string header_val = raw_line.substr(value_pos);
_headers[header_name] = header_val;
}
void request::parse_query_param(std::string& raw_param) {
void request::parse_query_param(std::string& raw_param)
{
auto delim_pos = raw_param.find_first_of('=');
auto value_pos = delim_pos+1;
auto value_pos = delim_pos + 1;
std::string query_name = raw_param.substr(0,delim_pos);
std::string query_name = raw_param.substr(0, delim_pos);
std::string query_val = raw_param.substr(value_pos);
_query_params[query_name] = query_val;
}
void request::parse_path(char* raw_path) {
void request::parse_path(char* raw_path)
{
char* saveptr = nullptr;
char* tok = strtok_r(raw_path, "?", &saveptr);
if (tok){
if (tok) {
_path = tok;
}
tok = strtok_r(nullptr, "?", &saveptr);
while(tok) {
while (tok) {
std::string rtok(tok);
parse_query_param(rtok);
tok = strtok_r(nullptr, "?", &saveptr);
}
}
void request::parse_request_line(char* raw_line) {
request_line_parser_state state = METHOD;
void request::parse_request_line(char* raw_line)
{
request_line_parser_state state = METHOD;
char* saveptr = nullptr;
char* tok = strtok_r(raw_line, " \r", &saveptr);
char* saveptr = nullptr;
char* tok = strtok_r(raw_line, " \r", &saveptr);
while(tok){
switch(state) {
case METHOD: {
auto search = method_map.find(tok);
if (search != method_map.end()) {
_method = search->second;
} else {
_method = method::UNKNOWN;
}
state = PATH;
break;
};
case PATH: {
std::string str_tok(tok);
parse_path(tok);
state = VERSION;
break;
};
case VERSION: {
auto search = version_map.find(tok);
if (search != version_map.end()) {
_http_version = search->second;
} else {
_http_version = version::HTTP_1_0;
}
return;
};
while (tok) {
switch (state) {
case METHOD: {
auto search = method_map.find(tok);
if (search != method_map.end()) {
_method = search->second;
} else {
_method = method::UNKNOWN;
}
tok = strtok_r(nullptr, " \r", &saveptr);
state = PATH;
break;
};
case PATH: {
std::string str_tok(tok);
parse_path(tok);
state = VERSION;
break;
};
case VERSION: {
auto search = version_map.find(tok);
if (search != version_map.end()) {
_http_version = search->second;
} else {
_http_version = version::HTTP_1_0;
}
return;
};
}
tok = strtok_r(nullptr, " \r", &saveptr);
}
}
request::request(std::string& raw_data, const std::string& client_ip)
@ -94,26 +98,27 @@ request::request(std::string& raw_data, const std::string& client_ip)
char* saveptr = nullptr;
char* tok = strtok_r(raw_data.data(), "\r\n", &saveptr);
while(tok && state != BODY_CONTENT){
switch(state) {
case REQUEST_LINE: {
parse_request_line(tok);
state = HEADERS;
tok = strtok_r(nullptr, "\n", &saveptr);
break;
};
case HEADERS: {
if (tok[0] == '\r') {
state = BODY_CONTENT;
} else {
std::string rtok(tok);
rtok.pop_back();
parse_header(rtok);
while (tok && state != BODY_CONTENT) {
switch (state) {
case REQUEST_LINE: {
parse_request_line(tok);
state = HEADERS;
tok = strtok_r(nullptr, "\n", &saveptr);
break;
};
case HEADERS: {
if (tok[0] == '\r') {
state = BODY_CONTENT;
} else {
std::string rtok(tok);
rtok.pop_back();
parse_header(rtok);
tok = strtok_r(nullptr, "\n", &saveptr);
}
break;
};
case BODY_CONTENT: break;
}
break;
};
case BODY_CONTENT:
break;
}
}
@ -121,9 +126,9 @@ request::request(std::string& raw_data, const std::string& client_ip)
if (tok) {
_body_content = std::string(tok);
}
//if (getline(line_stream, line, '\0')) {
// _body_content = line;
//}
// if (getline(line_stream, line, '\0')) {
// _body_content = line;
// }
}
std::string request::path() { return _path; }
@ -147,11 +152,11 @@ bool request::close_connection()
const auto& header = _headers.find("Connection");
const bool found = header != _headers.end();
if (found && header->second == "keep-alive") {
return false;
if (found && header->second == "close") {
return true;
}
return true;
return false;
}
std::string request::to_string()

View file

@ -10,6 +10,11 @@ void Logger::initialize(enum LOG_LEVEL level)
_level = level;
}
void Logger::log_request_and_response(http::request& req, std::unique_ptr<http::response>& resp, uint32_t micros)
{
log::info << "[" << resp->status_code() << " " + http::status_map.find(resp->status_code())->second + "] " + req.client_ip() + " " + http::reverse_method_map.find(req.get_method())->second + " " + req.path() << " in " << micros << " usecs" << std::endl;
}
LogBuf::LogBuf(std::ostream& output_stream, const std::string& tag, enum LOG_LEVEL level)
: _output_stream(output_stream)
, _tag(tag)
@ -20,8 +25,10 @@ LogBuf::LogBuf(std::ostream& output_stream, const std::string& tag, enum LOG_LEV
int LogBuf::sync()
{
if (this->_level <= logger._level) {
std::cout << "[" << this->_tag << "] " << this->str();
std::cout.flush();
char thread_name[100];
pthread_getname_np(pthread_self(), thread_name, 100);
_output_stream << "[" << this->_tag << "] [" << syscall(SYS_gettid) << ":" << thread_name << "] " << this->str();
_output_stream.flush();
}
this->str("");
return 0;

View file

@ -3,6 +3,11 @@
#include <iostream>
#include <ostream>
#include <sstream>
#include <inttypes.h>
#include <memory>
#include <string>
#include "../http/request.hpp"
#include "../http/response.hpp"
namespace anthracite::log {
enum LOG_LEVEL {
@ -21,9 +26,9 @@ namespace anthracite::log {
Logger();
void initialize(enum LOG_LEVEL level);
void log_request_and_response(http::request& req, std::unique_ptr<http::response>& resp, uint32_t micros);
};
class LogBuf : public std::stringbuf
{
std::string _tag;
@ -50,4 +55,6 @@ namespace anthracite::log {
static class LogBuf debugBuf{std::cout, "DEBG", LOG_LEVEL_DEBUG};
static std::ostream debug(&debugBuf);
};

View file

@ -0,0 +1,126 @@
#include "./openssl_socket.hpp"
#include "../log/log.hpp"
#include <arpa/inet.h>
#include <array>
#include <exception>
#include <iostream>
#include <malloc.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <openssl/err.h>
#include <openssl/ssl.h>
#include <string>
#include <sys/socket.h>
#include <sys/time.h>
#include <unistd.h>
#include <vector>
namespace anthracite::socket {
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();
_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_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_path.c_str(), SSL_FILETYPE_PEM) <= 0) {
log::err << "Unable to open Key file at: " << key_path << std::endl;
throw std::exception();
}
}
bool openssl_listener::wait_for_conn(server** client_sock_p)
{
struct sockaddr_in client_addr {};
socklen_t client_addr_len;
int csock = accept(_sock_fd, reinterpret_cast<struct sockaddr*>(&client_addr), &client_addr_len);
if (csock > 0) {
std::array<char, INET6_ADDRSTRLEN> 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;
}
}
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);
}
void openssl_server::send_message(const std::string& msg)
{
SSL_write(_ssl, &msg[0], msg.length());
}
std::string openssl_server::recv_message(int buffer_size)
{
// Ignored because it's nonfatal, just slower
int nodelay_opt = 1;
(void)setsockopt(_sock_fd, SOL_TCP, TCP_NODELAY, &nodelay_opt, sizeof(nodelay_opt));
std::vector<char> response(buffer_size + 1);
ssize_t result = SSL_read(_ssl, response.data(), buffer_size + 1);
if (result < 1) {
return "";
}
response[buffer_size] = '\0';
return { response.data() };
}
};

View file

@ -0,0 +1,29 @@
#pragma once
#include "./socket.hpp"
#include <openssl/ssl.h>
#include <openssl/err.h>
namespace anthracite::socket {
class openssl_server : public server{
private:
SSL* _ssl;
public:
openssl_server(int sock_fd, std::string client_ip, bool nonblocking, SSL* ssl);
~openssl_server();
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;
};
};

View file

@ -1,9 +1,16 @@
#include "./socket.hpp"
#include "../log/log.hpp"
#include "assert.h"
#include <arpa/inet.h>
#include <array>
#include <exception>
#include <fcntl.h>
#include <iostream>
#include <malloc.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <string>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <unistd.h>
@ -11,61 +18,103 @@
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)
: server_socket(::socket(AF_INET, SOCK_STREAM, 0))
, client_ip("")
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<struct sockaddr*>(&address), sizeof(address));
listen(server_socket, max_queue);
}
void anthracite_socket::wait_for_conn()
{
client_ip = "";
client_socket = accept(server_socket, reinterpret_cast<struct sockaddr*>(&client_addr), &client_addr_len);
std::array<char, INET_ADDRSTRLEN> ip_str { 0 };
inet_ntop(AF_INET, &client_addr.sin_addr, ip_str.data(), INET_ADDRSTRLEN);
client_ip = std::string(ip_str.data());
}
const std::string& anthracite_socket::get_client_ip()
{
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;
}
send(client_socket, &msg[0], msg.length(), 0);
}
std::string anthracite_socket::recv_message(int buffer_size)
{
if (client_socket == -1) {
return "";
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();
}
setsockopt(client_socket, SOL_SOCKET, SO_RCVTIMEO, &timeout_tv, sizeof timeout_tv);
if (bind(_sock_fd, reinterpret_cast<struct sockaddr*>(&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 listener::wait_for_conn(server** client_sock_p)
{
struct sockaddr_in client_addr {};
socklen_t client_addr_len;
int csock = accept(_sock_fd, reinterpret_cast<struct sockaddr*>(&client_addr), &client_addr_len);
if (csock > 0) {
std::array<char, INET6_ADDRSTRLEN> 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::string client_ip = std::string(ip_str.data());
*client_sock_p = new server(csock, client_ip, _nonblocking);
return true;
} else {
return false;
}
}
server::server(int sock_fd, std::string client_ip, bool nonblocking)
: _sock_fd(sock_fd)
, _client_ip(std::move(client_ip))
, socket(nonblocking)
{
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();
}
}
}
void server::send_message(const std::string& msg)
{
// 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;
(void)setsockopt(_sock_fd, SOL_TCP, TCP_NODELAY, &nodelay_opt, sizeof(nodelay_opt));
std::vector<char> 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 "";
@ -75,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;
}
};

View file

@ -10,25 +10,42 @@
namespace anthracite::socket {
class socket {
protected:
bool _nonblocking;
socket(bool nonblocking);
public:
socket(){}
virtual ~socket(){}
};
class anthracite_socket {
protected:
static const int MAX_QUEUE_LENGTH = 100;
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;
class server : public socket {
protected:
int _sock_fd;
std::string _client_ip;
public:
server(int sock_fd, std::string client_ip, bool nonblocking);
~server();
public:
anthracite_socket(int port, int max_queue = MAX_QUEUE_LENGTH);
virtual void send_message(const std::string& msg);
virtual std::string recv_message(int buffer_size);
const std::string& client_ip();
virtual void wait_for_conn();
virtual const std::string& get_client_ip();
virtual void close_conn();
virtual void send_message(std::string& msg);
virtual std::string recv_message(int buffer_size);
int fd() { return _sock_fd; }
};
class listener : public socket {
protected:
uint16_t _port;
int _sock_fd;
public:
listener(int port, int max_queue_length, bool nonblocking);
~listener();
virtual bool wait_for_conn(server** client_sock_p);
int fd() { return _sock_fd; }
int port() { return _port; }
};
};

View file

@ -1,106 +0,0 @@
#include "./tls_socket.hpp"
#include <arpa/inet.h>
#include <array>
#include <malloc.h>
#include <netinet/in.h>
#include <string>
#include <sys/socket.h>
#include <sys/time.h>
#include <unistd.h>
#include <vector>
#include "../log/log.hpp"
namespace anthracite::socket {
tls_socket::tls_socket(int port, int max_queue) : anthracite_socket(port, max_queue), _handshakeDone(false) {
}
void tls_socket::wait_for_conn()
{
client_ip = "";
client_socket = accept(server_socket, reinterpret_cast<struct sockaddr*>(&client_addr), &client_addr_len);
std::array<char, INET_ADDRSTRLEN> ip_str { 0 };
inet_ntop(AF_INET, &client_addr.sin_addr, ip_str.data(), INET_ADDRSTRLEN);
client_ip = std::string(ip_str.data());
}
void tls_socket::close_conn()
{
close(client_socket);
client_socket = -1;
}
void tls_socket::send_message(std::string& msg)
{
if (client_socket == -1) {
return;
}
send(client_socket, &msg[0], msg.length(), 0);
}
void tls_socket::perform_handshake() {
struct tls_msg_hdr hdr{};
ssize_t result = recv(client_socket, &hdr, sizeof(hdr), 0);
if (result < 1) {
return;
}
log::info << "MsgType " << unsigned(hdr.msg_type);
log::info << " MsgLen " << hdr.length << std::endl;
char hhdr[4];
result = recv(client_socket, &hhdr, sizeof(hhdr), 0);
if (result < 1) {
return;
}
uint16_t msg_size = ClientHello::deserialize_uint16(hhdr + 2);
log::debug << "TLS ClientHello Size: " << msg_size << std::endl;
char* client_hello_data = (char*) malloc(msg_size);
result = recv(client_socket, client_hello_data, msg_size, 0);
std::cout << result << " Bytes rxd" << std::endl;
ClientHello hello_msg(client_hello_data, result);
char *ptr;
ServerHello hello_retmsg(hello_msg.session_id);
int size = hello_retmsg.get_buf(&ptr);
log::debug << "Sending message of length " << size << std::endl;
send(client_socket, ptr , size, 0);
for(;;){}
_handshakeDone = true;
}
std::string tls_socket::recv_message(int buffer_size)
{
if (client_socket == -1) {
return "";
}
setsockopt(client_socket, SOL_SOCKET, SO_RCVTIMEO, &timeout_tv, sizeof timeout_tv);
if (!_handshakeDone) {
perform_handshake();
return "";
}
std::vector<char> response(buffer_size + 1);
ssize_t result = recv(client_socket, response.data(), buffer_size + 1, 0);
if (result < 1) {
return "";
}
response[buffer_size] = '\0';
return { response.data() };
}
};

View file

@ -1,235 +0,0 @@
#pragma once
#include <arpa/inet.h>
#include <cstdlib>
#include <malloc.h>
#include <netinet/in.h>
#include <string>
#include <sys/socket.h>
#include <sys/time.h>
#include <unistd.h>
#include "./socket.hpp"
#include <vector>
#include <array>
#include <iostream>
#include "../log/log.hpp"
namespace anthracite::socket {
constexpr uint32_t TLS_MSGHDR_RXSIZE = 4;
struct __attribute__((packed)) tls_version {
uint8_t major;
uint8_t minor;
};
struct __attribute__((packed)) tls_msg_hdr {
uint8_t msg_type;
tls_version version;
uint16_t length;
};
struct tls_extension {
uint16_t extension_type;
std::vector<char> data;
};
class ServerHello {
public:
struct tls_version server_version;
std::array<uint8_t, 32> random_bytes;
std::array<uint8_t, 32> _session_id;
uint16_t cipher;
uint8_t compression;
ServerHello(std::array<uint8_t, 32> session_id) {
srand(time(nullptr));
server_version.major = 3;
server_version.minor = 3;
for (int i = 0; i < 32; i++) {
random_bytes[i] = rand() % 256;
}
_session_id = session_id;
// TLS_RSA_WITH_NULL_MD5
cipher = 1;
// None
compression = 0;
}
int get_buf(char** bufptr) {
constexpr int msgsize = 2 + 32 + 1 + 32 + 2 + 1 + 7;
constexpr int mmsgsize = msgsize + 4;
constexpr int bufsize = mmsgsize + 5;
*bufptr = (char*) malloc(bufsize);
char* buf = *bufptr;
buf[0] = 0x16;
buf[1] = 3;
buf[2] = 1;
buf[3] = (mmsgsize >> 8) & 0xFF;
buf[4] = (mmsgsize) & 0xFF;
buf[5] = 0x02;
buf[6] = (msgsize >> 16) & 0xFF;
buf[7] = (msgsize >> 8) & 0xFF;
buf[8] = (msgsize) & 0xFF;
buf[9] = server_version.major;
buf[10] = server_version.minor;
for(int i = 0; i < 32; i++) {
buf[i+11] = random_bytes[i];
}
buf[43] = 32;
for(int i = 0; i < 32; i++) {
buf[i+44] = _session_id[i];
}
// Cipher
buf[76] = 00;
buf[77] = 0x33;
// Compression
buf[78] = 00;
// Extensions Length
buf[79] = 00;
buf[80] = 01;
// Renegotiation
buf[81] = 0xFF;
buf[82] = 01;
// Disabled
buf[83] = 00;
buf[84] = 01;
buf[85] = 00;
return bufsize;
}
};
class ClientHello {
public:
struct tls_version client_version;
std::array<uint8_t, 32> random_bytes;
std::array<uint8_t, 32> session_id;
std::vector<uint16_t> cipher_suites;
std::vector<uint8_t> compression_methods;
std::vector<tls_extension> extensions;
static uint32_t deserialize_uint32(char *buffer)
{
uint32_t value = 0;
value |= buffer[0] << 24;
value |= buffer[1] << 16;
value |= buffer[2] << 8;
value |= buffer[3];
return value;
}
static uint32_t deserialize_uint24(char *buffer)
{
uint32_t value = 0;
value |= buffer[0] << 16;
value |= buffer[1] << 8;
value |= buffer[2];
return value;
}
static uint16_t deserialize_uint16(char *buffer)
{
uint32_t value = 0;
value |= buffer[0] << 8;
value |= buffer[1];
return value;
}
// TODO: Note that the security of this funciton is terrible and absolutely
// can cause nasty things to happen with a malformed message
ClientHello(char* buffer, ssize_t size) {
int bufptr = 0;
// Get version data
client_version.major = (uint8_t) buffer[bufptr++];
client_version.minor = (uint8_t) buffer[bufptr++];
log::debug << "TLS Version : maj " << unsigned(client_version.major) << " min " << unsigned(client_version.minor) << std::endl;
log::debug << "TLS Random Data: ";
for(int i = 0; i < 32; i++) {
random_bytes[i] = buffer[bufptr++];
log::debug << std::hex << unsigned(random_bytes[i]) << std::hex << " ";
}
log::debug << std::endl;
// Get session id
int session_id_length = (uint8_t) buffer[bufptr++];
log::debug << "TLS SesId Data : ";
for(int i = 0; i < session_id_length; i++) {
session_id[i] = buffer[bufptr++];
log::debug << std::hex << unsigned(session_id[i]) << " ";
}
log::debug << std::dec;
log::debug << std::endl;
// Get cipher suites
uint16_t cipher_suites_length = deserialize_uint16(&buffer[bufptr]);
bufptr += 2;
log::debug << cipher_suites_length << " cipher suites supported" << std::endl;
for(uint16_t i = 0; i < cipher_suites_length; i++) {
cipher_suites.push_back(deserialize_uint16(&buffer[bufptr]));
bufptr += 2;
}
// Get compression methods
uint16_t compression_methods_length = buffer[bufptr++];
log::debug << compression_methods_length << " compression methods supported" << std::endl;
for(uint16_t i = 0; i < compression_methods_length; i++) {
cipher_suites.push_back(buffer[bufptr]);
bufptr ++;
}
}
};
class tls_socket : anthracite_socket {
private:
bool _handshakeDone;
void perform_handshake();
public:
tls_socket(int port, int max_queue = MAX_QUEUE_LENGTH);
void wait_for_conn() override;
void close_conn() override;
void send_message(std::string& msg) override;
std::string recv_message(int buffer_size) override;
};
};

View file

@ -0,0 +1,138 @@
#include "./event_loop.hpp"
#include "../log/log.hpp"
#include "assert.h"
#include "signal.h"
#include "sys/epoll.h"
#include <chrono>
#include <mutex>
#include <pthread.h>
#include <sstream>
#include <syncstream>
#include <thread>
using std::chrono::duration;
using std::chrono::duration_cast;
using std::chrono::high_resolution_clock;
using std::chrono::milliseconds;
namespace anthracite::thread_mgr {
event_loop::event_loop(std::vector<socket::listener*>& 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::server* sock)
{
std::string raw_request = sock->recv_message(http::HEADER_BYTES);
if (raw_request == "") {
return false;
}
http::request req(raw_request, sock->client_ip());
std::unique_ptr<http::response> 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);
sock->send_message(resp->content());
if (req.close_connection()) {
return false;
}
return true;
}
void event_loop::worker_thread_loop(int threadno)
{
std::stringstream ss;
ss << "worker " << threadno;
pthread_setname_np(pthread_self(), ss.str().c_str());
struct epoll_event* events = new struct epoll_event[_max_clients];
int timeout_ms = 1000;
if (_nonblocking) {
timeout_ms = 0;
}
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, _max_clients, timeout_ms);
if (ready_fds > 0) {
for (int i = 0; i < ready_fds; i++) {
socket::socket* sockptr = reinterpret_cast<socket::socket*>(events[i].data.ptr);
socket::server* server_ptr = dynamic_cast<socket::server*>(sockptr);
if (server_ptr != nullptr) {
if (!event_handler(server_ptr)) {
delete server_ptr;
}
} else {
socket::listener* listen_ptr = dynamic_cast<socket::listener*>(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;
}
}
}
}
}
delete[] events;
std::osyncstream(log::info) << "Stopping worker thread " << threadno << std::endl;
}
void event_loop::start()
{
std::lock_guard<std::mutex> lg(_run_lock);
signal(SIGPIPE, SIG_IGN);
log::info << "Starting event_loop Thread Manager" << std::endl;
_run = true;
std::vector<std::thread> worker_threads;
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));
}
for (std::thread& t : worker_threads) {
t.join();
}
}
void event_loop::stop()
{
_run = false;
}
}

View file

@ -0,0 +1,26 @@
#include "./thread_mgr.hpp"
#include "../socket/socket.hpp"
#include "../backends/file_backend.hpp"
#include <mutex>
#include <vector>
#include "../socket/socket.hpp"
namespace anthracite::thread_mgr {
class event_loop : public virtual thread_mgr {
std::mutex _event_mtx;
backends::file_backend _error_backend;
std::vector<socket::listener*>& _listen_sockets;
bool _nonblocking;
std::mutex _run_lock;
int _max_threads;
int _max_clients;
void worker_thread_loop(int threadno);
bool event_handler(socket::server*);
public:
event_loop(std::vector<socket::listener*>&, backends::backend& backend, int max_workers, int max_clients);
void start() override;
void stop() override;
};
};

View file

@ -0,0 +1,16 @@
#pragma once
#include "../backends/backend.hpp"
namespace anthracite::thread_mgr {
class thread_mgr {
protected:
bool _run;
backends::backend& _backend;
public:
thread_mgr(backends::backend& backend): _backend(backend) {}
virtual ~thread_mgr() = default;
virtual void start() = 0;
virtual void stop() = 0;
};
};

11
shell.nix Normal file
View file

@ -0,0 +1,11 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
nativeBuildInputs = [ pkgs.pkg-config pkgs.openssl pkgs.libgcc pkgs.boost pkgs.cmake pkgs.python312 pkgs.cmake pkgs.gnumake ];
shellHook = ''
export OPENSSL_DIR="${pkgs.openssl.dev}"
export PKG_CONFIG_PATH="${pkgs.openssl.dev}/lib/pkgconfig"
export OPENSSL_NO_VENDOR=1
export OPENSSL_LIB_DIR="${pkgs.lib.getLib pkgs.openssl}/lib"
'';
}

View file

@ -1,111 +0,0 @@
#include "../lib/anthracite.hpp"
#include "../lib/backends/backend.hpp"
#include "../lib/http/constants.hpp"
#include <iostream>
#include <memory>
#include <optional>
#include <sstream>
#include <unordered_map>
#include <vector>
using namespace anthracite;
using CallbackType = std::unique_ptr<http::response> (*)(http::request&);
class api_backend : public backends::backend {
class RouteNode {
public:
std::optional<CallbackType> callback;
RouteNode()
: callback(std::nullopt)
{
}
std::unordered_map<std::string, RouteNode> routes;
};
RouteNode root;
std::unique_ptr<http::response> default_route(http::request& req)
{
std::unique_ptr<http::response> resp = std::make_unique<http::response>();
resp->add_body("Not Found");
resp->add_header(http::header("Content-Type", "application/json"));
resp->add_status(http::status_codes::NOT_FOUND);
return resp;
}
std::unique_ptr<http::response> find_handler(http::request& req)
{
std::string filename = req.path().substr(1);
std::vector<std::string> result;
std::stringstream ss(filename);
std::string item;
RouteNode* cur = &root;
while (getline(ss, item, '/')) {
if (cur->routes.find(item) == cur->routes.end()) {
if (cur->routes.find("*") == cur->routes.end()) {
break;
} else {
cur = &cur->routes["*"];
}
} else {
cur = &cur->routes[item];
}
}
if (cur->callback.has_value()) {
return cur->callback.value()(req);
} else {
return default_route(req);
}
}
std::unique_ptr<http::response> handle_request(http::request& req) override
{
return find_handler(req);
}
public:
api_backend()
{
root.routes = std::unordered_map<std::string, RouteNode>();
}
void register_endpoint(std::string pathspec, CallbackType callback)
{
std::vector<std::string> result;
std::stringstream ss(pathspec);
std::string item;
RouteNode* cur = &root;
while (getline(ss, item, '/')) {
cur->routes[item] = RouteNode {};
cur = &cur->routes[item];
}
cur->callback = callback;
}
};
std::unique_ptr<http::response> handle_request(http::request& req)
{
std::unique_ptr<http::response> resp = std::make_unique<http::response>();
resp->add_body(R"({"user": "endpoint"}")");
resp->add_header(http::header("Content-Type", "application/json"));
resp->add_status(http::status_codes::OK);
return resp;
}
int main(int argc, char** argv)
{
auto args = std::span(argv, size_t(argc));
api_backend ab;
ab.register_endpoint("users/*", handle_request);
anthracite_main(argc, argv, ab);
}

View file

@ -1,11 +1,304 @@
#include "../lib/anthracite.hpp"
#include "../lib/backends/file_backend.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 "getopt.h"
#include <fstream>
#include <memory>
#include <optional>
#include <sstream>
#include <string>
#include <unordered_map>
#include <vector>
using namespace anthracite;
struct event_loop_config {
int max_workers;
int max_clients;
};
int main(int argc, char** argv)
struct server_config {
std::unordered_map<int, anthracite::socket::listener*> listeners;
std::optional<event_loop_config> event_loop;
std::string serve_dir = "./www";
enum anthracite::log::LOG_LEVEL log_level = anthracite::log::LOG_LEVEL_INFO;
};
std::shared_ptr<anthracite::thread_mgr::thread_mgr> server = nullptr;
extern "C" void signalHandler(int signum)
{
auto args = std::span(argv, size_t(argc));
backends::file_backend fb(argc > 2 ? args[2] : "./www");
anthracite_main(argc, argv, fb);
anthracite::log::warn << "Caught signal SIGIN, exiting Anthracite" << std::endl;
if (server != nullptr) {
server->stop();
}
}
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[])
{
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;
}
int opt_index = 0;
option options[] = {
{ "help", no_argument, 0, 'h' },
{ "config", required_argument, 0, 'c' }
};
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 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<anthracite::socket::listener*> listeners;
for (auto lp : cfg.listeners) {
listeners.push_back(lp.second);
}
server = std::make_shared<anthracite::thread_mgr::event_loop>(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();
for (auto listener : listeners) {
delete listener;
}
anthracite::log::info << "Stopping Anthracite, a very high performance web server" << std::endl;
}

View file

@ -1,21 +1,21 @@
#include <gtest/gtest.h>
#include <fstream>
#include <chrono>
#include "../lib/http/request.hpp"
#include <chrono>
#include <fstream>
#include <gtest/gtest.h>
#ifdef SPEEDTEST_COMPARE_BOOST
#include <boost/beast.hpp>
#endif
using std::chrono::high_resolution_clock;
using std::chrono::duration_cast;
using std::chrono::duration;
using std::chrono::duration_cast;
using std::chrono::high_resolution_clock;
using std::chrono::milliseconds;
constexpr uint32_t num_requests = 10000000;
TEST(speed_tests, request_parse) {
TEST(speed_tests, request_parse)
{
std::ifstream t("./test_files/test_request.http");
std::stringstream buffer;
buffer << t.rdbuf();
@ -23,24 +23,25 @@ TEST(speed_tests, request_parse) {
auto start = high_resolution_clock::now();
for(int i = 0; i < num_requests; ++i) {
volatile anthracite::http::request req (raw_req, "0.0.0.0");
for (int i = 0; i < num_requests; ++i) {
volatile anthracite::http::request req(raw_req, "0.0.0.0");
}
auto end = high_resolution_clock::now();
auto ms_int = duration_cast<milliseconds>(end-start);
auto ms_int = duration_cast<milliseconds>(end - start);
double m_rps = ((1000.0 / ms_int.count()) * num_requests) / 1000000;
std::cout << "Parsed " << (num_requests/1000000) << " Million requests in " << ms_int << " ms";
std::cout << "Parsed " << (num_requests / 1000000) << " Million requests in " << ms_int << " ms";
std::cout << " at " << m_rps << " Million RPS " << std::endl;
ASSERT_LT(ms_int.count(), 2000);
}
#ifdef SPEEDTEST_COMPARE_BOOST
TEST(speed_tests, boost) {
TEST(speed_tests, boost)
{
std::ifstream t("./test_files/test_request.http");
std::stringstream buffer;
buffer << t.rdbuf();
@ -48,7 +49,7 @@ TEST(speed_tests, boost) {
auto start = high_resolution_clock::now();
for(int i = 0; i < num_requests; ++i) {
for (int i = 0; i < num_requests; ++i) {
boost::system::error_code ec;
boost::beast::http::request_parser<boost::beast::http::string_body> p;
p.put(boost::asio::buffer(raw_req), ec);
@ -56,11 +57,11 @@ TEST(speed_tests, boost) {
}
auto end = high_resolution_clock::now();
auto ms_int = duration_cast<milliseconds>(end-start);
auto ms_int = duration_cast<milliseconds>(end - start);
double m_rps = ((1000.0 / ms_int.count()) * num_requests) / 1000000;
std::cout << "Parsed " << (num_requests/1000000) << " Million requests in " << ms_int << " ms";
std::cout << "Parsed " << (num_requests / 1000000) << " Million requests in " << ms_int << " ms";
std::cout << " at " << m_rps << " Million RPS " << std::endl;
}
#endif

View file

@ -1,9 +1,10 @@
#include <gtest/gtest.h>
#include <fstream>
#include "../lib/http/request.hpp"
#include <boost/beast.hpp>
#include <fstream>
#include <gtest/gtest.h>
TEST(unit_tests, single_request_parse) {
TEST(unit_tests, single_request_parse)
{
std::ifstream t("./test_files/test_request.http");
std::stringstream buffer;
buffer << t.rdbuf();
@ -11,7 +12,7 @@ TEST(unit_tests, single_request_parse) {
std::string raw_req = buffer.str();
std::string expected = buffer.str();
anthracite::http::request req (raw_req, "0.0.0.0");
anthracite::http::request req(raw_req, "0.0.0.0");
ASSERT_EQ(expected, req.to_string());
}