Compare commits
No commits in common. "main" and "0.2.1" have entirely different histories.
|
@ -1,19 +0,0 @@
|
|||
#!/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
|
|
@ -1 +0,0 @@
|
|||
/nix/store/gr8ifjf51b4w3v62vvinq4s8w97pn3ag-nix-shell-env
|
|
@ -1,56 +0,0 @@
|
|||
name: Docker Build & Publish
|
||||
|
||||
on: [push, workflow_dispatch]
|
||||
#test11111
|
||||
env:
|
||||
CTR_REGISTRY: git.nickorlow.com
|
||||
IMAGE_NAME: "anthracite"
|
||||
|
||||
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.CTR_REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
|
||||
with:
|
||||
registry: ${{ env.CTR_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||
with:
|
||||
images: ${{ env.CTR_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
|
2
.github/workflows/docker-publish.yml
vendored
|
@ -28,7 +28,7 @@ jobs:
|
|||
- name: Write Version
|
||||
run: |
|
||||
echo Building with version number $RELEASE_VERSION
|
||||
echo $RELEASE_VERSION > build_supp/version.txt
|
||||
echo $RELEASE_VERSION > src/build/version.txt
|
||||
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
|
|
6
.gitignore
vendored
|
@ -1,3 +1,3 @@
|
|||
build/
|
||||
build_supp/version.cpp
|
||||
.cache/
|
||||
anthracite
|
||||
src/error_pages/
|
||||
src/build/version.cpp
|
||||
|
|
30
CHANGELOG.md
|
@ -1,33 +1,3 @@
|
|||
# 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
|
||||
improvements made between 0.2.0 and 0.3.0, as well as comparison to boost's
|
||||
parsing library.
|
||||
|
||||
It should probably be noted that Boost's parser can do a lot more than mine
|
||||
and is likely slower for good reason. Also, these were single runs but
|
||||
subsequent runs showed similar results.
|
||||
|
||||
| Parser Tested | RPS |
|
||||
|--------------------|--------------|
|
||||
| Anthracite 0.2.0 | 688,042 |
|
||||
| Anthracite 0.3.0 | 27,027,000 |
|
||||
| Boost Beast | 1,023,230 |
|
||||
|
||||
# 0.2.0 Fifth Pre-Release
|
||||
- Added true HTTP/1.1 support with persistent connections
|
||||
- Added HTTP/1.0 vs HTTP/1.1 test to benchmarking suite
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
project(anthracite)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "-O3")
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
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/
|
||||
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)"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
googletest
|
||||
URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(googletest)
|
||||
|
||||
file(GLOB TESTS_SRC CONFIGURE_DEPENDS "tests/*.cpp")
|
||||
enable_testing()
|
||||
|
||||
add_custom_target(test_files
|
||||
COMMAND cp -r ../tests/test_files .
|
||||
DEPENDS ../tests/test_files/*
|
||||
COMMENT "Copied test resource files"
|
||||
)
|
||||
|
||||
add_executable(
|
||||
tests
|
||||
${TESTS_SRC}
|
||||
)
|
||||
add_dependencies(tests anthracite)
|
||||
add_dependencies(tests test_files)
|
||||
|
||||
target_link_libraries(
|
||||
tests
|
||||
GTest::gtest_main
|
||||
)
|
||||
target_link_libraries(
|
||||
tests
|
||||
anthracite
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(tests)
|
29
Dockerfile
|
@ -1,23 +1,12 @@
|
|||
FROM alpine AS build-env
|
||||
FROM alpine as build-env
|
||||
|
||||
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 .
|
||||
|
||||
|
||||
RUN mkdir build
|
||||
WORKDIR build
|
||||
RUN cmake -DCMAKE_BUILD_TYPE=Release ..
|
||||
RUN make anthracite-bin
|
||||
RUN apk add --no-cache build-base python3
|
||||
COPY ./src/ .
|
||||
RUN make build-release
|
||||
|
||||
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"]
|
||||
RUN apk add --no-cache build-base
|
||||
COPY --from=build-env /anthracite /anthracite
|
||||
COPY --from=build-env /www /www
|
||||
COPY --from=build-env /error_pages /error_pages
|
||||
CMD ["/anthracite"]
|
||||
|
|
38
README.md
|
@ -1,28 +1,28 @@
|
|||
# Anthracite
|
||||
|
||||
Anthracite is an extensible, low-dependency, fast web server.
|
||||
A simple web server written in C++. Supports HTTP 1.0 & 1.1.
|
||||
|
||||
## Developing
|
||||
|
||||
To build/develop Anthracite, you must have C++20, OpenSSL, CMake, Make, and Python3 installed.
|
||||
To build/develop Anthracite, you must have C++20, Make, and Python3 installed.
|
||||
|
||||
Create a `build/` directory, run `cmake ..`, and then `make` to build.
|
||||
You can run Anthracite with: `make run`
|
||||
|
||||
## 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
|
||||
## 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
|
||||
- [ ] Improve benchmarking infrastructure
|
||||
- [ ] Faster parsing
|
||||
- [ ] Fix glaring security issues
|
||||
- [ ] Proper error handling
|
||||
- [ ] User configuration
|
||||
- [ ] Build out module-based backend system for handling requests
|
||||
- [ ] HTTP/2
|
||||
- [ ] Enhance logging
|
||||
- [ ] Cleanup (this one will never truly be done)
|
||||
|
||||
## Screenshots
|
||||
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
log_level INFO
|
||||
|
||||
http 8080 1000 blocking
|
||||
|
||||
event_loop 6 10000
|
||||
www_dir ./www
|
|
@ -1 +0,0 @@
|
|||
0.3.0
|
|
@ -1,9 +0,0 @@
|
|||
<head>
|
||||
<title>Anthracite</title>
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
</head>
|
||||
<center>
|
||||
<h1>Anthracite is Running in Docker!</h1>
|
||||
<p>If you are seeing this page, then Anthracite is configured correctly!</p>
|
||||
<p>Add files to the "www" directory in your build step to begin serving your website.</p>
|
||||
</center>
|
Before Width: | Height: | Size: 319 B |
|
@ -1,9 +0,0 @@
|
|||
<head>
|
||||
<title>Anthracite</title>
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
</head>
|
||||
<center>
|
||||
<h1>Anthracite is Running in Docker Compose!</h1>
|
||||
<p>If you are seeing this page, then Anthracite is configured correctly!</p>
|
||||
<p>Bind-mount a directory to "/www" to begin serving your website.</p>
|
||||
</center>
|
Before Width: | Height: | Size: 319 B |
|
@ -1,12 +0,0 @@
|
|||
services:
|
||||
anthracite-web:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./default_www/docker_compose/
|
||||
target: /www
|
||||
- type: bind
|
||||
source: ./build_supp/default_config.cfg
|
||||
target: /anthracite.cfg
|
|
@ -1,2 +0,0 @@
|
|||
git ls-files -- '*.cpp' '*.h' | xargs clang-format -i -style=file
|
||||
git diff --exit-code --color
|
|
@ -1,91 +0,0 @@
|
|||
#include "./file_backend.hpp"
|
||||
#include "../log/log.hpp"
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
namespace anthracite::backends {
|
||||
|
||||
std::unique_ptr<http::response> file_backend::handle_request_cache(http::request& req)
|
||||
{
|
||||
std::string filename = req.path() == "/" ? "/index.html" : req.path();
|
||||
filename = file_dir + filename;
|
||||
auto file_info = file_cache.find(filename);
|
||||
|
||||
int status = http::status_codes::OK;
|
||||
if (file_info == file_cache.end()) {
|
||||
return handle_error(http::status_codes::NOT_FOUND);
|
||||
}
|
||||
|
||||
std::unique_ptr<http::response> resp = std::make_unique<http::response>();
|
||||
|
||||
std::string file_extension = file_info->first.substr(file_info->first.rfind('.') + 1);
|
||||
std::string content_type = "text/html";
|
||||
|
||||
auto mime_type = http::mime_types.find(file_extension);
|
||||
if (mime_type != http::mime_types.end()) {
|
||||
content_type = mime_type->second;
|
||||
}
|
||||
|
||||
resp->add_body_ref(&file_info->second);
|
||||
resp->add_status(http::status_codes::OK);
|
||||
resp->add_header(http::header("Content-Type", content_type), false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
void file_backend::populate_cache_dir(std::string dir)
|
||||
{
|
||||
std::filesystem::recursive_directory_iterator cur = begin(std::filesystem::recursive_directory_iterator(dir));
|
||||
std::filesystem::recursive_directory_iterator fin = end(std::filesystem::recursive_directory_iterator(dir));
|
||||
|
||||
while (cur != fin) {
|
||||
auto p = cur->path();
|
||||
std::string filename = p.string();
|
||||
std::stringstream buffer;
|
||||
std::ifstream stream(filename);
|
||||
buffer << stream.rdbuf();
|
||||
file_cache[filename] = buffer.str();
|
||||
log::verbose << "File at " << filename << " cached (" << file_cache[filename].size() << " bytes)" << std::endl;
|
||||
++cur;
|
||||
}
|
||||
}
|
||||
|
||||
void file_backend::populate_cache()
|
||||
{
|
||||
populate_cache_dir(file_dir);
|
||||
populate_cache_dir("./error_pages/");
|
||||
}
|
||||
|
||||
file_backend::file_backend(std::string dir)
|
||||
: file_dir(std::move(dir))
|
||||
{
|
||||
populate_cache();
|
||||
}
|
||||
|
||||
std::unique_ptr<http::response> file_backend::handle_request(http::request& req)
|
||||
{
|
||||
return handle_request_cache(req);
|
||||
}
|
||||
|
||||
std::unique_ptr<http::response> file_backend::handle_error(const http::status_codes& error)
|
||||
{
|
||||
std::string filename = "./error_pages/" + std::to_string(error) + ".html";
|
||||
auto file_info = file_cache.find(filename);
|
||||
|
||||
http::status_codes status = error;
|
||||
if (file_info == file_cache.end()) {
|
||||
status = http::status_codes::NOT_FOUND;
|
||||
filename = "./error_pages/404.html";
|
||||
file_info = file_cache.find(filename);
|
||||
}
|
||||
|
||||
std::unique_ptr<http::response> resp = std::make_unique<http::response>();
|
||||
|
||||
resp->add_body_ref(&file_info->second);
|
||||
resp->add_status(error);
|
||||
resp->add_header(http::header("Content-Type", "text/html"), false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
};
|
|
@ -1,22 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include "backend.hpp"
|
||||
|
||||
namespace anthracite::backends {
|
||||
|
||||
class file_backend : public backend {
|
||||
private:
|
||||
std::unordered_map<std::string,std::string> file_cache;
|
||||
std::string file_dir;
|
||||
|
||||
std::unique_ptr<http::response> handle_request_cache(http::request& req);
|
||||
void populate_cache_dir(std::string dir);
|
||||
void populate_cache();
|
||||
public:
|
||||
file_backend(std::string dir = "./www") ;
|
||||
|
||||
std::unique_ptr<http::response> handle_request(http::request& req) override;
|
||||
std::unique_ptr<http::response> handle_error(const http::status_codes& error);
|
||||
};
|
||||
|
||||
};
|
|
@ -1,200 +0,0 @@
|
|||
#include "request.hpp"
|
||||
#include "../log/log.hpp"
|
||||
#include "constants.hpp"
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <stdio.h>
|
||||
|
||||
namespace anthracite::http {
|
||||
|
||||
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);
|
||||
|
||||
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)
|
||||
{
|
||||
auto delim_pos = raw_param.find_first_of('=');
|
||||
auto value_pos = delim_pos + 1;
|
||||
|
||||
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)
|
||||
{
|
||||
char* saveptr = nullptr;
|
||||
char* tok = strtok_r(raw_path, "?", &saveptr);
|
||||
|
||||
if (tok) {
|
||||
_path = tok;
|
||||
}
|
||||
|
||||
tok = strtok_r(nullptr, "?", &saveptr);
|
||||
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;
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
tok = strtok_r(nullptr, " \r", &saveptr);
|
||||
}
|
||||
}
|
||||
|
||||
request::request(std::string& raw_data, const std::string& client_ip)
|
||||
: _path("")
|
||||
, _client_ipaddr(client_ip)
|
||||
{
|
||||
|
||||
parser_state state = REQUEST_LINE;
|
||||
|
||||
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);
|
||||
tok = strtok_r(nullptr, "\n", &saveptr);
|
||||
}
|
||||
break;
|
||||
};
|
||||
case BODY_CONTENT:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
tok = strtok_r(nullptr, "", &saveptr);
|
||||
if (tok) {
|
||||
_body_content = std::string(tok);
|
||||
}
|
||||
// if (getline(line_stream, line, '\0')) {
|
||||
// _body_content = line;
|
||||
// }
|
||||
}
|
||||
|
||||
std::string request::path() { return _path; }
|
||||
|
||||
method request::get_method() { return _method; }
|
||||
|
||||
std::string request::client_ip() { return _client_ipaddr; }
|
||||
|
||||
version request::get_http_version()
|
||||
{
|
||||
return _http_version;
|
||||
}
|
||||
|
||||
bool request::is_supported_version()
|
||||
{
|
||||
return _http_version == HTTP_1_1 || _http_version == HTTP_1_0;
|
||||
}
|
||||
|
||||
bool request::close_connection()
|
||||
{
|
||||
const auto& header = _headers.find("Connection");
|
||||
const bool found = header != _headers.end();
|
||||
|
||||
if (found && header->second == "close") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string request::to_string()
|
||||
{
|
||||
std::string response = "";
|
||||
response += reverse_method_map.find(_method)->second + " " + _path;
|
||||
|
||||
if (_query_params.size() > 0) {
|
||||
response += "?";
|
||||
}
|
||||
|
||||
auto qp_map = std::map(_query_params.begin(), _query_params.end());
|
||||
auto qp = qp_map.begin();
|
||||
while (qp != qp_map.end()) {
|
||||
response += qp->first + "=" + qp->second;
|
||||
if (++qp != qp_map.end()) {
|
||||
response += "&";
|
||||
}
|
||||
}
|
||||
|
||||
response += " " + reverse_version_map.find(_http_version)->second + "\r\n";
|
||||
|
||||
if (_headers.size() == 0) {
|
||||
response += "\r\n";
|
||||
}
|
||||
|
||||
auto hd_map = std::map(_headers.begin(), _headers.end());
|
||||
auto hd = hd_map.begin();
|
||||
while (hd != hd_map.end()) {
|
||||
response += hd->first + ": " + hd->second + "\r\n";
|
||||
if (++hd == hd_map.end()) {
|
||||
response += "\r\n";
|
||||
}
|
||||
}
|
||||
|
||||
response += _body_content;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
};
|
|
@ -1,46 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include "./header_query.hpp"
|
||||
#include "./constants.hpp"
|
||||
|
||||
namespace anthracite::http {
|
||||
|
||||
class request {
|
||||
private:
|
||||
enum request_line_parser_state {
|
||||
METHOD, PATH, VERSION
|
||||
};
|
||||
|
||||
enum parser_state {
|
||||
REQUEST_LINE,
|
||||
HEADERS,
|
||||
BODY_CONTENT
|
||||
};
|
||||
|
||||
method _method;
|
||||
version _http_version;
|
||||
std::string _path;
|
||||
std::string _client_ipaddr;
|
||||
std::string _body_content;
|
||||
std::unordered_map<std::string, std::string> _headers;
|
||||
std::unordered_map<std::string, std::string> _query_params;
|
||||
|
||||
void parse_request_line(char* raw_line);
|
||||
void parse_header(std::string& raw_line);
|
||||
void parse_path(char* raw_path);
|
||||
void parse_query_param(std::string& raw_param);
|
||||
|
||||
public:
|
||||
request(std::string& raw_data, const std::string& client_ip);
|
||||
std::string path();
|
||||
method get_method();
|
||||
std::string client_ip();
|
||||
version get_http_version();
|
||||
bool is_supported_version();
|
||||
bool close_connection();
|
||||
std::string to_string();
|
||||
};
|
||||
|
||||
};
|
|
@ -1,62 +0,0 @@
|
|||
#include "response.hpp"
|
||||
#include "../version.hpp"
|
||||
#include "./constants.hpp"
|
||||
|
||||
namespace anthracite::http {
|
||||
|
||||
response::response() {};
|
||||
|
||||
int response::status_code() { return _status_code; }
|
||||
|
||||
void response::add_body(const std::string body)
|
||||
{
|
||||
_content_noref = body;
|
||||
_content = &_content_noref;
|
||||
}
|
||||
|
||||
void response::add_body_ref(std::string* body)
|
||||
{
|
||||
_content = body;
|
||||
}
|
||||
|
||||
void response::add_header(header header, bool override_existing)
|
||||
{
|
||||
if (override_existing || _headers.find(header.name()) == _headers.end()) {
|
||||
_headers[header.name()] = header;
|
||||
}
|
||||
}
|
||||
|
||||
void response::add_status(int code)
|
||||
{
|
||||
_status_code = code;
|
||||
}
|
||||
|
||||
std::string& response::content()
|
||||
{
|
||||
return *_content;
|
||||
}
|
||||
|
||||
std::string response::header_to_string()
|
||||
{
|
||||
std::string response = "";
|
||||
response += "HTTP/1.1 " + std::to_string(_status_code) + " " + status_map.find(_status_code)->second + "\r\n";
|
||||
|
||||
add_header(header("Content-Length", std::to_string(_content->length())), false);
|
||||
add_header(header("Server", ANTHRACITE_FULL_VERSION_STRING), false);
|
||||
add_header(header("Origin-Server", ANTHRACITE_FULL_VERSION_STRING), false);
|
||||
|
||||
for (auto header : _headers) {
|
||||
response += header.second.to_string();
|
||||
}
|
||||
|
||||
response += "\r\n";
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
std::string response::to_string()
|
||||
{
|
||||
return header_to_string() + *_content;
|
||||
}
|
||||
|
||||
};
|
|
@ -1,29 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include "header_query.hpp"
|
||||
|
||||
namespace anthracite::http {
|
||||
|
||||
class response {
|
||||
private:
|
||||
int _status_code;
|
||||
std::string* _content;
|
||||
std::string _content_noref;
|
||||
std::unordered_map<std::string, header> _headers;
|
||||
|
||||
public:
|
||||
response();
|
||||
int status_code();
|
||||
|
||||
void add_body(const std::string body);
|
||||
void add_body_ref(std::string* body);
|
||||
void add_status(int);
|
||||
void add_header(header header, bool override_existing = true);
|
||||
std::string& content();
|
||||
std::string header_to_string();
|
||||
std::string to_string();
|
||||
};
|
||||
|
||||
};
|
|
@ -1,36 +0,0 @@
|
|||
#include "./log.hpp"
|
||||
|
||||
namespace anthracite::log {
|
||||
enum LOG_LEVEL Logger::_level = LOG_LEVEL_NONE;
|
||||
|
||||
// TODO: implement logger as a singleton to prevent duplicates
|
||||
Logger::Logger() = default;
|
||||
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)
|
||||
, _level(level)
|
||||
{
|
||||
}
|
||||
|
||||
int LogBuf::sync()
|
||||
{
|
||||
if (this->_level <= logger._level) {
|
||||
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;
|
||||
}
|
||||
};
|
|
@ -1,60 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#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 {
|
||||
LOG_LEVEL_NONE = 0,
|
||||
LOG_LEVEL_ERROR = 1,
|
||||
LOG_LEVEL_WARN = 2,
|
||||
LOG_LEVEL_INFO = 3,
|
||||
LOG_LEVEL_VERBOSE = 4,
|
||||
LOG_LEVEL_DEBUG = 5
|
||||
};
|
||||
|
||||
class Logger {
|
||||
friend class LogBuf;
|
||||
static enum LOG_LEVEL _level;
|
||||
public:
|
||||
|
||||
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;
|
||||
std::ostream& _output_stream;
|
||||
enum LOG_LEVEL _level;
|
||||
public:
|
||||
LogBuf(std::ostream& output_stream, const std::string& tag, enum LOG_LEVEL level);
|
||||
int sync() override;
|
||||
};
|
||||
|
||||
static class Logger logger{};
|
||||
|
||||
static class LogBuf errBuf{std::cerr, "EROR", LOG_LEVEL_ERROR};
|
||||
static std::ostream err(&errBuf);
|
||||
|
||||
static class LogBuf warnBuf{std::cerr, "WARN", LOG_LEVEL_WARN};
|
||||
static std::ostream warn(&warnBuf);
|
||||
|
||||
static class LogBuf infoBuf{std::cout, "INFO", LOG_LEVEL_INFO};
|
||||
static std::ostream info(&infoBuf);
|
||||
|
||||
static class LogBuf verboseBuf{std::cout, "VERB", LOG_LEVEL_VERBOSE};
|
||||
static std::ostream verbose(&verboseBuf);
|
||||
|
||||
static class LogBuf debugBuf{std::cout, "DEBG", LOG_LEVEL_DEBUG};
|
||||
static std::ostream debug(&debugBuf);
|
||||
|
||||
|
||||
};
|
|
@ -1,126 +0,0 @@
|
|||
#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() };
|
||||
}
|
||||
|
||||
};
|
|
@ -1,29 +0,0 @@
|
|||
#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;
|
||||
};
|
||||
};
|
|
@ -1,144 +0,0 @@
|
|||
#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>
|
||||
#include <vector>
|
||||
|
||||
namespace anthracite::socket {
|
||||
|
||||
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_addr.s_addr = INADDR_ANY;
|
||||
|
||||
int reuse_opt = 1;
|
||||
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();
|
||||
}
|
||||
|
||||
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(_sock_fd, response.data(), buffer_size + 1, 0);
|
||||
|
||||
if (result < 1) {
|
||||
return "";
|
||||
}
|
||||
|
||||
response[buffer_size] = '\0';
|
||||
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;
|
||||
}
|
||||
|
||||
};
|
|
@ -1,51 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <malloc.h>
|
||||
#include <netinet/in.h>
|
||||
#include <string>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace anthracite::socket {
|
||||
|
||||
class socket {
|
||||
protected:
|
||||
bool _nonblocking;
|
||||
socket(bool nonblocking);
|
||||
public:
|
||||
socket(){}
|
||||
virtual ~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();
|
||||
|
||||
virtual void send_message(const std::string& msg);
|
||||
virtual std::string recv_message(int buffer_size);
|
||||
const std::string& client_ip();
|
||||
|
||||
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; }
|
||||
};
|
||||
|
||||
};
|
|
@ -1,138 +0,0 @@
|
|||
#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;
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
#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;
|
||||
};
|
||||
};
|
|
@ -1,16 +0,0 @@
|
|||
#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;
|
||||
};
|
||||
};
|
|
@ -1,6 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
extern const std::string ANTHRACITE_VERSION_STRING;
|
||||
extern const std::string ANTHRACITE_FULL_VERSION_STRING;
|
11
shell.nix
|
@ -1,11 +0,0 @@
|
|||
{ 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"
|
||||
'';
|
||||
}
|
34
src/Makefile
Normal file
|
@ -0,0 +1,34 @@
|
|||
.PHONY: format lint build build-release build-docker run debug
|
||||
|
||||
build-supplemental:
|
||||
cd ./build && ./version.sh && python3 ./error_gen.py
|
||||
|
||||
build: build-supplemental
|
||||
g++ main.cpp --std=c++20 -g -o ./anthracite
|
||||
|
||||
build-release: build-supplemental
|
||||
g++ main.cpp --std=c++20 -O3 -o ./anthracite
|
||||
|
||||
build-docker:
|
||||
docker build . -t anthracite
|
||||
|
||||
run: build
|
||||
./anthracite 8080
|
||||
|
||||
run-test: build
|
||||
./anthracite 8080 ./test_www
|
||||
|
||||
debug: build
|
||||
gdb --args ./anthracite 8080
|
||||
|
||||
debug-test: build
|
||||
gdb --args ./anthracite 8080 ./test_www
|
||||
|
||||
format:
|
||||
clang-format *.cpp -i
|
||||
|
||||
lint:
|
||||
clang-tidy *.cpp
|
||||
|
||||
lint-fix:
|
||||
clang-tidy *.cpp -fix -fix-errors
|
|
@ -1,10 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include "../http/request.hpp"
|
||||
#include "../http/response.hpp"
|
||||
|
||||
namespace anthracite::backends {
|
||||
#include "../http/http_request.cpp"
|
||||
#include "../http/http_response.cpp"
|
||||
|
||||
class backend {
|
||||
public:
|
||||
|
@ -14,7 +11,5 @@ public:
|
|||
backend& operator = (backend const&) = delete;
|
||||
backend(backend&&) = delete;
|
||||
backend& operator=(backend&&) = delete;
|
||||
virtual std::unique_ptr<http::response> handle_request(http::request& req) = 0;
|
||||
};
|
||||
|
||||
virtual std::unique_ptr<http_response> handle_request(http_request& req) = 0;
|
||||
};
|
67
src/backends/file_backend.cpp
Normal file
|
@ -0,0 +1,67 @@
|
|||
#include <filesystem>
|
||||
#include <string>
|
||||
#include "backend.cpp"
|
||||
|
||||
class file_backend : public backend {
|
||||
private:
|
||||
std::unordered_map<std::string,std::string> file_cache;
|
||||
std::string file_dir;
|
||||
|
||||
std::unique_ptr<http_response> handle_request_cache(http_request& req) {
|
||||
std::string filename = req.path() == "/" ? "/index.html" : req.path();
|
||||
filename = file_dir + filename;
|
||||
auto file_info = file_cache.find(filename);
|
||||
|
||||
int status = http_status_codes::OK;
|
||||
if (file_info == file_cache.end()) {
|
||||
return handle_error(http_status_codes::NOT_FOUND);
|
||||
}
|
||||
|
||||
return std::make_unique<http_response>(file_info->second, filename, status);
|
||||
}
|
||||
|
||||
void populate_cache_dir(std::string dir) {
|
||||
std::filesystem::recursive_directory_iterator cur = begin(std::filesystem::recursive_directory_iterator(dir));
|
||||
std::filesystem::recursive_directory_iterator fin = end(std::filesystem::recursive_directory_iterator(dir));
|
||||
|
||||
while (cur != fin) {
|
||||
auto p = cur->path();
|
||||
std::string filename = p.string();
|
||||
std::stringstream buffer;
|
||||
std::ifstream stream(filename);
|
||||
buffer << stream.rdbuf();
|
||||
file_cache[filename] = buffer.str();
|
||||
std::cout << "File at " << filename << " cached (" << file_cache[filename].size() << " bytes)" << std::endl;
|
||||
++cur;
|
||||
}
|
||||
}
|
||||
|
||||
void populate_cache() {
|
||||
populate_cache_dir(file_dir);
|
||||
populate_cache_dir("./error_pages/");
|
||||
}
|
||||
|
||||
public:
|
||||
file_backend(std::string dir = "./www") : file_dir(std::move(dir)) {
|
||||
populate_cache();
|
||||
}
|
||||
|
||||
|
||||
std::unique_ptr<http_response> handle_request(http_request& req) override {
|
||||
return handle_request_cache(req);
|
||||
}
|
||||
|
||||
std::unique_ptr<http_response> handle_error(const http_status_codes& error) {
|
||||
std::string filename = "./error_pages/" + std::to_string(error) + ".html";
|
||||
auto file_info = file_cache.find(filename);
|
||||
|
||||
http_status_codes status = error;
|
||||
if (file_info == file_cache.end()) {
|
||||
status = http_status_codes::NOT_FOUND;
|
||||
filename = "./error_pages/404.html";
|
||||
file_info = file_cache.find(filename);
|
||||
}
|
||||
|
||||
return std::make_unique<http_response>(file_info->second, filename, status);
|
||||
}
|
||||
};
|
|
@ -16,7 +16,7 @@ def generate_error_page(error_code, error_title):
|
|||
<h1>{error_code} - {error_title}</h1>
|
||||
<hr>
|
||||
<p>Anthracite/{version}</p>
|
||||
<p><small><a href="https://git.nickorlow.com/nickorlow/anthracite">This is Open Source Software</small></a></p>
|
||||
<p><small><a href="https://github.com/nickorlow/anthracite">This is Open Source Software</small></a></p>
|
||||
</center>
|
||||
</body>
|
||||
</html>"""
|
||||
|
@ -66,7 +66,7 @@ error_codes = {
|
|||
}
|
||||
|
||||
|
||||
error_dir = '../build/error_pages'
|
||||
error_dir = '../error_pages'
|
||||
os.makedirs(error_dir, exist_ok=True)
|
||||
|
||||
for code, title in error_codes.items():
|
|
@ -1,4 +1,3 @@
|
|||
echo "#include <string>" > version.cpp
|
||||
echo "#include \"../lib/version.hpp\"" >> version.cpp
|
||||
echo "const std::string ANTHRACITE_VERSION_STRING = \"$(cat version.txt)\";" >> version.cpp
|
||||
echo "const std::string ANTHRACITE_FULL_VERSION_STRING = \"Anthracite/$(cat version.txt)\";" >> version.cpp
|
1
src/build/version.txt
Normal file
|
@ -0,0 +1 @@
|
|||
0.2.0
|
91
src/datastructures/smart_map.cpp
Normal file
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
*
|
||||
* smart_map.cpp
|
||||
* -------------
|
||||
* Class that wraps unordered_map. Chooses whether to do O(1) hash lookup
|
||||
* or O(N) linear lookup based on benchmark. This is done as O(smallN) can
|
||||
* be faster than O(1)
|
||||
*
|
||||
*/
|
||||
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <chrono>
|
||||
#include <algorithm>
|
||||
#include <bits/stdc++.h>
|
||||
|
||||
constexpr int benchmark_loops = 100;
|
||||
constexpr int SEED = 570;
|
||||
|
||||
template <typename KeyType, typename ValueType>
|
||||
class smart_map {
|
||||
private:
|
||||
bool use_hmap = false;
|
||||
std::unordered_map<KeyType, ValueType> hmap;
|
||||
|
||||
double assess_hmap(const std::vector<std::pair<KeyType, ValueType>> check) {
|
||||
const auto& start = std::chrono::high_resolution_clock::now();
|
||||
for(int i = 0; i < benchmark_loops; i++) {
|
||||
for(const auto& check_item : check) {
|
||||
assert(check_item.second == hmap[check_item.first]);
|
||||
}
|
||||
}
|
||||
const auto& end = std::chrono::high_resolution_clock::now();
|
||||
auto duration = std::chrono::duration<double, std::nano>(end - start).count();
|
||||
return duration;
|
||||
}
|
||||
|
||||
double assess_vmap(const std::vector<std::pair<KeyType, ValueType>> check) {
|
||||
const auto& start = std::chrono::high_resolution_clock::now();
|
||||
for(int i = 0; i < benchmark_loops; i++) {
|
||||
for(const auto& check_item : check) {
|
||||
for(const auto& item : hmap) {
|
||||
if(check_item.first == item.first) {
|
||||
assert(check_item.second == item.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const auto& end = std::chrono::high_resolution_clock::now();
|
||||
auto duration = std::chrono::duration<double, std::nano>(end - start).count();
|
||||
return duration;
|
||||
}
|
||||
|
||||
public:
|
||||
smart_map () = default;
|
||||
|
||||
void assess_datastructure() {
|
||||
std::vector<std::pair<KeyType, ValueType>> vals(hmap.begin(), hmap.end());
|
||||
std::shuffle(vals.begin(), vals.end(), std::default_random_engine(SEED));
|
||||
use_hmap = assess_hmap(vals) > assess_vmap(vals);
|
||||
}
|
||||
|
||||
bool will_use_hmap() {
|
||||
return use_hmap;
|
||||
}
|
||||
|
||||
ValueType* get(const KeyType& key) {
|
||||
if(use_hmap) {
|
||||
if(hmap.find(key) != hmap.end()) {
|
||||
return &hmap[key];
|
||||
} else {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
for(auto& item : hmap) {
|
||||
if(item.first == key) {
|
||||
std::string& ref = item.second;
|
||||
return &ref;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void insert(const KeyType key, const ValueType value) {
|
||||
hmap[key] = value;
|
||||
}
|
||||
};
|
|
@ -1,304 +0,0 @@
|
|||
#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>
|
||||
|
||||
struct event_loop_config {
|
||||
int max_workers;
|
||||
int max_clients;
|
||||
};
|
||||
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
|
@ -1,13 +1,9 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace anthracite::http {
|
||||
constexpr int HTTP_HEADER_BYTES = 8190;
|
||||
|
||||
constexpr int HEADER_BYTES = 8190;
|
||||
|
||||
enum method {
|
||||
enum http_method {
|
||||
GET,
|
||||
POST,
|
||||
DELETE,
|
||||
|
@ -28,7 +24,7 @@ enum method {
|
|||
UNKNOWN
|
||||
};
|
||||
|
||||
enum status_codes {
|
||||
enum http_status_codes {
|
||||
CONTINUE = 100,
|
||||
SWITCHING_PROTOCOLS = 101,
|
||||
PROCESSING = 102,
|
||||
|
@ -94,49 +90,49 @@ enum status_codes {
|
|||
NETWORK_AUTHENTICATION_REQUIRED = 511
|
||||
};
|
||||
|
||||
static std::unordered_map<std::string, method> const method_map = {
|
||||
{ "GET", method::GET },
|
||||
{ "POST", method::POST },
|
||||
{ "DELETE", method::DELETE },
|
||||
{ "PUT", method::PUT },
|
||||
{ "PATCH", method::PATCH },
|
||||
{ "HEAD", method::HEAD },
|
||||
{ "OPTIONS", method::OPTIONS },
|
||||
{ "CONNECT", method::CONNECT },
|
||||
{ "TRACE", method::TRACE },
|
||||
{ "COPY", method::COPY },
|
||||
{ "LINK", method::LINK },
|
||||
{ "UNLINK", method::UNLINK },
|
||||
{ "PURGE", method::PURGE },
|
||||
{ "LOCK", method::LOCK },
|
||||
{ "UNLOCK", method::UNLOCK },
|
||||
{ "PROPFIND", method::PROPFIND },
|
||||
{ "VIEW", method::VIEW },
|
||||
{ "UNKNOWN", method::UNKNOWN }
|
||||
static std::unordered_map<std::string, http_method> const http_method_map = {
|
||||
{ "GET", http_method::GET },
|
||||
{ "POST", http_method::POST },
|
||||
{ "DELETE", http_method::DELETE },
|
||||
{ "PUT", http_method::PUT },
|
||||
{ "PATCH", http_method::PATCH },
|
||||
{ "HEAD", http_method::HEAD },
|
||||
{ "OPTIONS", http_method::OPTIONS },
|
||||
{ "CONNECT", http_method::CONNECT },
|
||||
{ "TRACE", http_method::TRACE },
|
||||
{ "COPY", http_method::COPY },
|
||||
{ "LINK", http_method::LINK },
|
||||
{ "UNLINK", http_method::UNLINK },
|
||||
{ "PURGE", http_method::PURGE },
|
||||
{ "LOCK", http_method::LOCK },
|
||||
{ "UNLOCK", http_method::UNLOCK },
|
||||
{ "PROPFIND", http_method::PROPFIND },
|
||||
{ "VIEW", http_method::VIEW },
|
||||
{ "UNKNOWN", http_method::UNKNOWN }
|
||||
};
|
||||
|
||||
static std::unordered_map<method, std::string> const reverse_method_map = {
|
||||
{ method::GET, "GET" },
|
||||
{ method::POST, "POST" },
|
||||
{ method::DELETE, "DELETE" },
|
||||
{ method::PUT, "PUT" },
|
||||
{ method::PATCH, "PATCH" },
|
||||
{ method::HEAD, "HEAD" },
|
||||
{ method::OPTIONS, "OPTIONS" },
|
||||
{ method::CONNECT, "CONNECT" },
|
||||
{ method::TRACE, "TRACE" },
|
||||
{ method::COPY, "COPY" },
|
||||
{ method::LINK, "LINK" },
|
||||
{ method::UNLINK, "UNLINK" },
|
||||
{ method::PURGE, "PURGE" },
|
||||
{ method::LOCK, "LOCK" },
|
||||
{ method::UNLOCK, "UNLOCK" },
|
||||
{ method::PROPFIND, "PROPFIND" },
|
||||
{ method::VIEW, "VIEW" },
|
||||
{ method::UNKNOWN, "UNKNOWN" }
|
||||
static std::unordered_map<http_method, std::string> const http_reverse_method_map = {
|
||||
{ http_method::GET, "GET" },
|
||||
{ http_method::POST, "POST" },
|
||||
{ http_method::DELETE, "DELETE" },
|
||||
{ http_method::PUT, "PUT" },
|
||||
{ http_method::PATCH, "PATCH" },
|
||||
{ http_method::HEAD, "HEAD" },
|
||||
{ http_method::OPTIONS, "OPTIONS" },
|
||||
{ http_method::CONNECT, "CONNECT" },
|
||||
{ http_method::TRACE, "TRACE" },
|
||||
{ http_method::COPY, "COPY" },
|
||||
{ http_method::LINK, "LINK" },
|
||||
{ http_method::UNLINK, "UNLINK" },
|
||||
{ http_method::PURGE, "PURGE" },
|
||||
{ http_method::LOCK, "LOCK" },
|
||||
{ http_method::UNLOCK, "UNLOCK" },
|
||||
{ http_method::PROPFIND, "PROPFIND" },
|
||||
{ http_method::VIEW, "VIEW" },
|
||||
{ http_method::UNKNOWN, "UNKNOWN" }
|
||||
};
|
||||
|
||||
static std::unordered_map<int, std::string> const status_map = {
|
||||
static std::unordered_map<int, std::string> const http_status_map = {
|
||||
{ 100, "CONTINUE" },
|
||||
{ 101, "SWITCHING PROTOCOLS" },
|
||||
{ 200, "OK" },
|
||||
|
@ -200,7 +196,6 @@ static std::unordered_map<std::string, std::string> const mime_types = {
|
|||
{ "css", "text/css" },
|
||||
|
||||
{ "js", "application/javascript" },
|
||||
{ "json", "application/json" },
|
||||
{ "pdf", "application/pdf" },
|
||||
|
||||
{ "ico", "image/x-icon" },
|
||||
|
@ -217,13 +212,13 @@ static std::unordered_map<std::string, std::string> const mime_types = {
|
|||
{ "wmv", "video/x-ms-wmv" },
|
||||
};
|
||||
|
||||
enum version { HTTP_0_9,
|
||||
enum http_version { HTTP_0_9,
|
||||
HTTP_1_0,
|
||||
HTTP_1_1,
|
||||
HTTP_2_0,
|
||||
HTTP_3_0 };
|
||||
|
||||
static std::unordered_map<std::string, version> const version_map = {
|
||||
static std::unordered_map<std::string, http_version> const http_version_map = {
|
||||
// This is because HTTP 0.9 didn't specify version in the header
|
||||
{ "", HTTP_0_9 },
|
||||
{ "HTTP/0.9", HTTP_0_9 },
|
||||
|
@ -233,7 +228,7 @@ static std::unordered_map<std::string, version> const version_map = {
|
|||
{ "HTTP/3.0", HTTP_3_0 }
|
||||
};
|
||||
|
||||
static std::unordered_map<version, std::string> const reverse_version_map = {
|
||||
static std::unordered_map<http_version, std::string> const http_reverse_version_map = {
|
||||
{ HTTP_0_9, "HTTP/0.9" },
|
||||
{ HTTP_1_0, "HTTP/1.0" },
|
||||
{ HTTP_1_1, "HTTP/1.1" },
|
||||
|
@ -241,4 +236,6 @@ static std::unordered_map<version, std::string> const reverse_version_map = {
|
|||
{ HTTP_3_0, "HTTP/3.0" }
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
|
||||
namespace anthracite::http {
|
||||
|
||||
class name_value {
|
||||
private:
|
||||
std::string _name;
|
||||
|
@ -29,13 +26,13 @@ public:
|
|||
virtual std::string to_string() { return ""; }
|
||||
};
|
||||
|
||||
class header : public name_value {
|
||||
class http_header : public name_value {
|
||||
public:
|
||||
header()
|
||||
http_header()
|
||||
: name_value()
|
||||
{
|
||||
}
|
||||
header(std::string name, std::string value)
|
||||
http_header(std::string name, std::string value)
|
||||
: name_value(name, value)
|
||||
{
|
||||
}
|
||||
|
@ -56,5 +53,3 @@ public:
|
|||
|
||||
std::string to_string() override { return name() + "=" + value(); }
|
||||
};
|
||||
|
||||
};
|
8
src/http/http.hpp
Normal file
|
@ -0,0 +1,8 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "constants.cpp"
|
||||
#include "header_query.cpp"
|
||||
#include "../socket.cpp"
|
||||
#include "../build/version.cpp"
|
177
src/http/http_request.cpp
Normal file
|
@ -0,0 +1,177 @@
|
|||
#include "http.hpp"
|
||||
|
||||
class http_request {
|
||||
private:
|
||||
enum parser_state { METHOD,
|
||||
PATH,
|
||||
QUERY_PARAM_NAME,
|
||||
QUERY_PARAM_VALUE,
|
||||
VERSION,
|
||||
HEADER_NAME,
|
||||
HEADER_VALUE,
|
||||
BODY_CONTENT };
|
||||
http_method _method;
|
||||
http_version _http_version;
|
||||
std::string _path;
|
||||
std::string _client_ipaddr;
|
||||
std::string _body_content;
|
||||
std::unordered_map<std::string, http_header> _headers; // kinda goofy, whatever
|
||||
std::unordered_map<std::string, query_param> _query_params; // kinda goofy, whatever
|
||||
|
||||
public:
|
||||
http_request(std::string& raw_data, std::string client_ip)
|
||||
: _path(""), _client_ipaddr(client_ip)
|
||||
{
|
||||
|
||||
parser_state state = METHOD;
|
||||
|
||||
std::string scratch = "";
|
||||
std::string scratch_2 = "";
|
||||
for (int i = 0; i < raw_data.length(); i++) {
|
||||
switch (state) {
|
||||
case METHOD: {
|
||||
if (raw_data[i] == ' ') {
|
||||
if (http_method_map.find(scratch) == http_method_map.end()) {
|
||||
_method = http_method::UNKNOWN;
|
||||
} else {
|
||||
_method = http_method_map.find(scratch)->second;
|
||||
}
|
||||
scratch = "";
|
||||
state = PATH;
|
||||
} else {
|
||||
scratch += raw_data[i];
|
||||
}
|
||||
} break;
|
||||
|
||||
case PATH: {
|
||||
switch (raw_data[i]) {
|
||||
case ' ':
|
||||
state = VERSION;
|
||||
break;
|
||||
case '?':
|
||||
state = QUERY_PARAM_NAME;
|
||||
break;
|
||||
default:
|
||||
_path += raw_data[i];
|
||||
break;
|
||||
}
|
||||
} break;
|
||||
|
||||
case QUERY_PARAM_NAME: {
|
||||
if (raw_data[i] == ' ') {
|
||||
scratch = "";
|
||||
state = VERSION;
|
||||
} else if (raw_data[i] == '=') {
|
||||
state = QUERY_PARAM_VALUE;
|
||||
} else {
|
||||
scratch += raw_data[i];
|
||||
}
|
||||
} break;
|
||||
|
||||
case QUERY_PARAM_VALUE: {
|
||||
if (raw_data[i] == ' ') {
|
||||
_query_params[scratch] = query_param(scratch, scratch_2);
|
||||
scratch = "";
|
||||
scratch_2 = "";
|
||||
state = VERSION;
|
||||
} else if (raw_data[i] == '&') {
|
||||
_query_params[scratch] = query_param(scratch, scratch_2);
|
||||
scratch = "";
|
||||
scratch_2 = "";
|
||||
state = QUERY_PARAM_NAME;
|
||||
} else {
|
||||
scratch_2 += raw_data[i];
|
||||
}
|
||||
} break;
|
||||
|
||||
case VERSION: {
|
||||
if (raw_data[i] == '\n') {
|
||||
_http_version = http_version_map.find(scratch)->second;
|
||||
scratch = "";
|
||||
state = HEADER_NAME;
|
||||
} else if (raw_data[i] != '\r') {
|
||||
scratch += raw_data[i];
|
||||
}
|
||||
} break;
|
||||
|
||||
case HEADER_NAME: {
|
||||
if (raw_data[i] == '\n') {
|
||||
scratch = "";
|
||||
scratch_2 = "";
|
||||
state = BODY_CONTENT;
|
||||
break;
|
||||
} else if (raw_data[i] == ' ') {
|
||||
scratch = "";
|
||||
break;
|
||||
} else if (raw_data[i] == ':') {
|
||||
state = HEADER_VALUE;
|
||||
i++;
|
||||
} else {
|
||||
scratch += raw_data[i];
|
||||
}
|
||||
} break;
|
||||
|
||||
case HEADER_VALUE: {
|
||||
if (raw_data[i] == '\n') {
|
||||
_headers[scratch] = http_header(scratch, scratch_2);
|
||||
scratch = "";
|
||||
scratch_2 = "";
|
||||
state = HEADER_NAME;
|
||||
} else if (raw_data[i] != '\r') {
|
||||
scratch_2 += raw_data[i];
|
||||
}
|
||||
} break;
|
||||
|
||||
case BODY_CONTENT: {
|
||||
_body_content += raw_data[i];
|
||||
} break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string path() { return _path; }
|
||||
|
||||
http_method method() { return _method; }
|
||||
|
||||
std::string client_ip() { return _client_ipaddr; }
|
||||
|
||||
http_version get_http_version() {
|
||||
return _http_version;
|
||||
}
|
||||
|
||||
bool is_supported_version() {
|
||||
return _http_version == HTTP_1_1 || _http_version == HTTP_1_0;
|
||||
}
|
||||
|
||||
bool close_connection() {
|
||||
const auto& header = _headers.find("Connection");
|
||||
const bool found = header != _headers.end();
|
||||
|
||||
if(found && header->second.value() == "keep-alive") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string to_string()
|
||||
{
|
||||
std::string response = "";
|
||||
response += http_reverse_method_map.find(_method)->second + " " + _path + "?";
|
||||
|
||||
for (auto qp : _query_params) {
|
||||
response += qp.second.to_string() + "&";
|
||||
}
|
||||
|
||||
response += " " + http_reverse_version_map.find(_http_version)->second + "\r\n";
|
||||
|
||||
for (auto header : _headers) {
|
||||
response += header.second.to_string();
|
||||
}
|
||||
|
||||
response += "\r\n";
|
||||
response += _body_content;
|
||||
|
||||
return response;
|
||||
}
|
||||
};
|
61
src/http/http_response.cpp
Normal file
|
@ -0,0 +1,61 @@
|
|||
#include "http.hpp"
|
||||
|
||||
class http_response {
|
||||
private:
|
||||
int _status_code;
|
||||
std::string& _content;
|
||||
std::string _filename;
|
||||
std::unordered_map<std::string, http_header> _headers; // kinda goofy, whatever
|
||||
|
||||
public:
|
||||
http_response(std::string& content, std::string filename, int status_code = http_status_codes::OK)
|
||||
: _content(content)
|
||||
, _status_code(status_code)
|
||||
, _filename(std::move(filename))
|
||||
{
|
||||
}
|
||||
|
||||
int status_code() { return _status_code; }
|
||||
|
||||
void add_header(http_header header, bool override_existing = true)
|
||||
{
|
||||
if (override_existing || _headers.find(header.name()) == _headers.end()) {
|
||||
_headers[header.name()] = header;
|
||||
}
|
||||
}
|
||||
|
||||
std::string& content()
|
||||
{
|
||||
return _content;
|
||||
}
|
||||
|
||||
std::string header_to_string()
|
||||
{
|
||||
std::string response = "";
|
||||
response += "HTTP/1.1 " + std::to_string(_status_code) + " " + http_status_map.find(_status_code)->second + "\r\n";
|
||||
std::string content_type = "text/html";
|
||||
std::string file_extension = _filename.substr(_filename.rfind('.') + 1);
|
||||
auto mime_type = mime_types.find(file_extension);
|
||||
if (mime_type != mime_types.end()) {
|
||||
content_type = mime_type->second;
|
||||
}
|
||||
|
||||
add_header(http_header("Content-Type", content_type), false);
|
||||
add_header(http_header("Content-Length", std::to_string(_content.length())), false);
|
||||
add_header(http_header("Server", ANTHRACITE_FULL_VERSION_STRING), false);
|
||||
add_header(http_header("Origin-Server", ANTHRACITE_FULL_VERSION_STRING), false);
|
||||
|
||||
for (auto header : _headers) {
|
||||
response += header.second.to_string();
|
||||
}
|
||||
|
||||
response += "\r\n";
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
std::string to_string()
|
||||
{
|
||||
return header_to_string() + _content;
|
||||
}
|
||||
};
|
79
src/main.cpp
Normal file
|
@ -0,0 +1,79 @@
|
|||
#include "backends/file_backend.cpp"
|
||||
#include <condition_variable>
|
||||
#include <exception>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <netinet/in.h>
|
||||
#include <span>
|
||||
#include <sstream>
|
||||
#include <sys/socket.h>
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
#include <unordered_map>
|
||||
|
||||
void log_request_and_response(http_request& req, std::unique_ptr<http_response>& resp);
|
||||
|
||||
constexpr int default_port = 80;
|
||||
constexpr int max_worker_threads = 128;
|
||||
|
||||
void handle_client(anthracite_socket s, backend& b, 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);
|
||||
if(raw_request == "") {
|
||||
break;
|
||||
}
|
||||
http_request req(raw_request, s.get_client_ip());
|
||||
std::unique_ptr<http_response> resp = req.is_supported_version() ? b.handle_request(req) : fb.handle_error(http_status_codes::HTTP_VERSION_NOT_SUPPORTED);
|
||||
log_request_and_response(req, resp);
|
||||
std::string header = resp->header_to_string();
|
||||
s.send_message(header);
|
||||
s.send_message(resp->content());
|
||||
resp.reset();
|
||||
if(req.close_connection()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
s.close_conn();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(thread_wait_mutex);
|
||||
active_threads--;
|
||||
}
|
||||
thread_wait_condvar.notify_one();
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
auto args = std::span(argv, size_t(argc));
|
||||
int port_number = default_port;
|
||||
|
||||
if (argc > 1) {
|
||||
port_number = atoi(args[1]);
|
||||
}
|
||||
|
||||
std::cout << "Initializing Anthracite" << std::endl;
|
||||
anthracite_socket s(port_number);
|
||||
file_backend fb(argc > 2 ? args[2] : "./www");
|
||||
std::cout << "Initialization Complete" << std::endl;
|
||||
std::cout << "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(fb), 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)
|
||||
{
|
||||
std::cout << "[" << resp->status_code() << " " + http_status_map.find(resp->status_code())->second + "] " + req.client_ip() + " " + http_reverse_method_map.find(req.method())->second + " " + req.path() << std::endl;
|
||||
}
|
88
src/socket.cpp
Normal file
|
@ -0,0 +1,88 @@
|
|||
#include <arpa/inet.h>
|
||||
#include <exception>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <malloc.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sstream>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#include <unordered_map>
|
||||
#include <sys/time.h>
|
||||
|
||||
constexpr int MAX_QUEUE_LENGTH = 100;
|
||||
|
||||
class anthracite_socket {
|
||||
private:
|
||||
int server_socket;
|
||||
int client_socket {};
|
||||
std::string client_ip;
|
||||
struct sockaddr_in client_addr {};
|
||||
socklen_t client_addr_len {};
|
||||
|
||||
public:
|
||||
anthracite_socket(int port, int max_queue = MAX_QUEUE_LENGTH)
|
||||
: server_socket(socket(AF_INET, SOCK_STREAM, 0))
|
||||
, client_ip("")
|
||||
{
|
||||
struct sockaddr_in address {};
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_port = htons(port);
|
||||
address.sin_addr.s_addr = INADDR_ANY;
|
||||
|
||||
int x = 1;
|
||||
setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &x, sizeof(x));
|
||||
bind(server_socket, (struct sockaddr*)&address, sizeof(address));
|
||||
|
||||
::listen(server_socket, max_queue);
|
||||
}
|
||||
|
||||
void wait_for_conn()
|
||||
{
|
||||
client_ip = "";
|
||||
client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len);
|
||||
char ip_str[INET_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET, &client_addr.sin_addr, ip_str, INET_ADDRSTRLEN);
|
||||
client_ip = std::string(ip_str);
|
||||
}
|
||||
|
||||
std::string get_client_ip()
|
||||
{
|
||||
return client_ip;
|
||||
}
|
||||
|
||||
void close_conn()
|
||||
{
|
||||
close(client_socket);
|
||||
client_socket = -1;
|
||||
}
|
||||
|
||||
void send_message(std::string& msg)
|
||||
{
|
||||
if (client_socket == -1) {
|
||||
return;
|
||||
}
|
||||
send(client_socket, &msg[0], msg.length(), 0);
|
||||
}
|
||||
|
||||
std::string recv_message(int buffer_size)
|
||||
{
|
||||
if (client_socket == -1) {
|
||||
return "";
|
||||
}
|
||||
|
||||
struct timeval tv;
|
||||
tv.tv_sec = 5;
|
||||
tv.tv_usec = 0;
|
||||
setsockopt(client_socket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv);
|
||||
char response[buffer_size + 1];
|
||||
int result = recv(client_socket, response, sizeof(response), 0);
|
||||
|
||||
if (result < 1) {
|
||||
return "";
|
||||
}
|
||||
|
||||
response[buffer_size] = '\0';
|
||||
return std::string(response);
|
||||
}
|
||||
};
|
BIN
src/test_www/images/emma.bmp
Normal file
After Width: | Height: | Size: 496 KiB |
Before Width: | Height: | Size: 319 B After Width: | Height: | Size: 319 B |
BIN
src/test_www/images/favicon_anim.ico
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
src/test_www/images/lola.jpeg
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
src/test_www/images/tini.png
Normal file
After Width: | Height: | Size: 42 KiB |
|
@ -1,6 +1,6 @@
|
|||
<head>
|
||||
<title>Anthracite</title>
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
<link rel="icon" type="image/x-icon" href="/images/favicon.ico">
|
||||
</head>
|
||||
<center>
|
||||
<h1>Anthracite is Running!</h1>
|
18
src/test_www/test.css
Normal file
|
@ -0,0 +1,18 @@
|
|||
.cool-style {
|
||||
background: linear-gradient(to right, #ef5350, #f48fb1, #7e57c2, #2196f3, #26c6da, #43a047, #eeff41, #f9a825, #ff5722);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 10px;
|
||||
}
|
25
src/test_www/test.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Anthracite</title>
|
||||
<link rel="icon" type="image/x-icon" href="/images/favicon_anim.ico">
|
||||
<link rel="stylesheet" href="test.css">
|
||||
</head>
|
||||
<body>
|
||||
<center>
|
||||
<h1 class="cool-style">Test Page!</h1>
|
||||
<h2>Dogs</h2>
|
||||
<div class="content">
|
||||
<img src="/images/tini.png" alt="A border collie" />
|
||||
<img src="/images/lola.jpeg" alt="A border collie" />
|
||||
<img src="/images/emma.bmp" alt="A corgi" />
|
||||
</div>
|
||||
<h2>Trains</h2>
|
||||
<div>
|
||||
<video controls>
|
||||
<source src="videos/train_vid.mp4" />
|
||||
</video>
|
||||
</div>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
BIN
src/test_www/videos/train_vid.mp4
Normal file
9
src/www/index.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
<head>
|
||||
<title>Anthracite</title>
|
||||
<link rel="icon" type="image/x-icon" href="/images/favicon.ico">
|
||||
</head>
|
||||
<center>
|
||||
<h1>Anthracite is Running!</h1>
|
||||
<p>If you are seeing this page, then Anthracite is configured correctly!</p>
|
||||
<p>Add files to the "www" directory to begin serving your website.</p>
|
||||
</center>
|
|
@ -1,67 +0,0 @@
|
|||
#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::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)
|
||||
{
|
||||
std::ifstream t("./test_files/test_request.http");
|
||||
std::stringstream buffer;
|
||||
buffer << t.rdbuf();
|
||||
std::string raw_req = buffer.str();
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
auto end = high_resolution_clock::now();
|
||||
|
||||
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 << " at " << m_rps << " Million RPS " << std::endl;
|
||||
|
||||
ASSERT_LT(ms_int.count(), 2000);
|
||||
}
|
||||
|
||||
#ifdef SPEEDTEST_COMPARE_BOOST
|
||||
TEST(speed_tests, boost)
|
||||
{
|
||||
std::ifstream t("./test_files/test_request.http");
|
||||
std::stringstream buffer;
|
||||
buffer << t.rdbuf();
|
||||
std::string raw_req = buffer.str();
|
||||
|
||||
auto start = high_resolution_clock::now();
|
||||
|
||||
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);
|
||||
boost::beast::http::request<boost::beast::http::string_body> r = p.get();
|
||||
}
|
||||
|
||||
auto end = high_resolution_clock::now();
|
||||
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 << " at " << m_rps << " Million RPS " << std::endl;
|
||||
}
|
||||
#endif
|
|
@ -1,15 +0,0 @@
|
|||
GET /foo/bar?test=a&test2=b HTTP/1.1
|
||||
Accept: */*
|
||||
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
|
||||
Accept-Encoding: gzip,deflate
|
||||
Accept-Language: fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3
|
||||
Connection: keep-alive
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
Cookie: foo=bar; lorem=ipsum;
|
||||
Host: example.org
|
||||
Keep-Alive: 115
|
||||
Referer: http://example.org/test
|
||||
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; fr; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8
|
||||
X-Requested-With: XMLHttpRequest
|
||||
|
||||
{ "test": "content" }
|
|
@ -1,18 +0,0 @@
|
|||
#include "../lib/http/request.hpp"
|
||||
#include <boost/beast.hpp>
|
||||
#include <fstream>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
TEST(unit_tests, single_request_parse)
|
||||
{
|
||||
std::ifstream t("./test_files/test_request.http");
|
||||
std::stringstream buffer;
|
||||
buffer << t.rdbuf();
|
||||
|
||||
std::string raw_req = buffer.str();
|
||||
std::string expected = buffer.str();
|
||||
|
||||
anthracite::http::request req(raw_req, "0.0.0.0");
|
||||
|
||||
ASSERT_EQ(expected, req.to_string());
|
||||
}
|