Compare commits

...

48 commits
v0.1.1 ... main

Author SHA1 Message Date
Nicholas Orlowsky efd19d3bba fix version build step
Some checks failed
Docker Build & Publish / build (push) Failing after 1h39m43s
2025-02-24 21:34:54 -05:00
Nicholas Orlowsky 0fc8400066 add version.txt
Some checks failed
Docker Build & Publish / build (push) Failing after 1h19m14s
2025-02-24 19:53:38 -05:00
Nicholas Orlowsky f48eda81e8 add version.txt fix
Some checks failed
Docker Build & Publish / build (push) Has been cancelled
2025-02-24 19:41:41 -05:00
Nicholas Orlowsky 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
Nicholas Orlowsky 9b5719f9be Release v0.3.0
Some checks failed
Docker Build & Publish / build (push) Has been cancelled
2025-02-24 19:29:43 -05:00
Nicholas Orlowsky c07f3ebf81 faster nonblocking io
Some checks failed
Docker Build & Publish / build (push) Failing after 1h24m16s
2025-02-23 17:06:32 -05:00
Nicholas Orlowsky 6c5feb8675 epoll per thd 2025-02-22 00:52:30 -05:00
Nicholas Orlowsky 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
Nicholas Orlowsky 058c395095 event loop with threadmgr
Some checks failed
Docker Build & Publish / build (push) Failing after 1h11m38s
2025-02-21 14:09:16 -05:00
Nicholas Orlowsky ca05aa1e5a event loop with threadmgr
Some checks failed
Docker Build & Publish / build (push) Has been cancelled
2025-02-21 14:09:01 -05:00
Nicholas Orlowsky f09b261b62 ssl fix
Some checks failed
Docker Build & Publish / build (push) Failing after 1h12m36s
2025-02-20 13:48:30 -05:00
Nicholas Orlowsky f1195d1f04 add shell.nix 2025-02-19 23:54:15 -05:00
Nicholas Orlowsky a63d9d1e65
broken ssl
Some checks failed
Docker Build & Publish / build (push) Failing after 1h8m3s
2025-02-17 18:46:52 -05:00
Nicholas Orlowsky da9f2f2d51
broken ssl
Some checks failed
Docker Build & Publish / build (push) Has been cancelled
2025-02-17 18:46:33 -05:00
Nicholas Orlowsky 10ca7f9f51
add turnaround time logging
Some checks failed
Docker Build & Publish / build (push) Failing after 1s
2025-02-05 09:32:21 -05:00
Nicholas Orlowsky f930792a11
request parser speed improvements and test updates
Some checks failed
Docker Build & Publish / build (push) Failing after 2s
2025-02-05 09:07:21 -05:00
Nicholas Orlowsky 95430a5dc3
strtok in more places 2025-02-04 23:38:57 -05:00
Nicholas Orlowsky 236f7399fe
request parser rewrite + added tests
Some checks failed
Docker Build & Publish / build (push) Failing after 11m2s
* rewrote request parser, now more simplified and theoretically faster
* added gtest and an example test to measure parser times
2025-02-04 18:27:33 -05:00
Nicholas Orlowsky f962f5796d
moving ci/cd over to forgejo
Some checks failed
Docker Build & Publish / build (push) Failing after 1h7m13s
2025-02-04 14:53:51 -05:00
Nicholas Orlowsky d952cae3f6
moving ci/cd over to forgejo
Some checks failed
Docker Build & Publish / build (push) Failing after 1s
2025-02-04 14:46:17 -05:00
Nicholas Orlowsky ce74fa1d3c
moving ci/cd over to forgejo
Some checks failed
Docker Build & Publish / build (push) Failing after 6m52s
2025-02-04 14:36:28 -05:00
Nicholas Orlowsky 46bb91864f
moving ci/cd over to forgejo
Some checks failed
Docker Build & Publish / build (push) Failing after 6m49s
2025-02-04 14:22:48 -05:00
Nicholas Orlowsky 88e53b2b46
moving ci/cd over to forgejo
Some checks failed
Docker Build & Publish / build (push) Failing after 3m27s
2025-02-04 14:10:46 -05:00
Nicholas Orlowsky 94d2b55d29
moving ci/cd over to forgejo
Some checks failed
Docker Build & Publish / build (push) Failing after 10m25s
2025-02-04 13:54:50 -05:00
Nicholas Orlowsky 6c99bee466
moving ci/cd over to forgejo
Some checks failed
Docker Build & Publish / build (push) Has been cancelled
2025-02-04 13:48:13 -05:00
Nicholas Orlowsky b8ec23a776
moving ci/cd over to forgejo
Some checks failed
Docker Build & Publish / build (push) Failing after 3m36s
2025-02-04 13:38:12 -05:00
Nicholas Orlowsky 678d742da8
moving ci/cd over to forgejo 2025-02-04 13:35:18 -05:00
Nicholas Orlowsky 36a11110a5
moving ci/cd over to forgejo 2025-02-04 13:33:06 -05:00
Nicholas Orlowsky 1fb5daf4dd
moving ci/cd over to forgejo 2025-02-04 13:29:19 -05:00
Nicholas Orlowsky 3c9edb1e0b
moving ci/cd over to forgejo 2025-02-04 13:26:54 -05:00
Nicholas Orlowsky 0ebdb34601
format + README 2025-02-04 11:44:34 -05:00
Nicholas Orlowsky 71be773d49
split into src and lib and socket changes
* libanthracite files are now all in lib/
* anthracite-bin + anthracite-api-bin files are now all in src/
* socket split into header and source properly
2025-02-04 11:37:46 -05:00
Nicholas Orlowsky fba87f3fbb
Updated directory structure and build steps
* seperate build-supplemental from build-version
* version.cpp now included in cmake config, not directly imported
* socket moved into its own directory
* test_www removed from project
2025-02-04 11:29:15 -05:00
Nicholas Orlowsky 54d82b8c66
cleanup of source + build system
* seperated most parts into headers & source files
* seperated anthracite into libanthracite and anthracite-bin for modules
* built demo webAPI module
* rewrote some code to do things in the
* changed cmakelists to not build in src directory
2025-02-04 02:03:27 -05:00
Nicholas Orlowsky c4540a1397
cmake fix 2024-05-10 06:33:40 -05:00
Nicholas Orlowsky 207015c944
add cmake files 2024-05-10 06:13:31 -05:00
Nicholas Orlowsky 734e37de37
move build system to cmake 2024-05-10 06:09:40 -05:00
Nicholas Orlowsky c31db4d2a8
remove march native for now 2023-10-20 13:35:18 -04:00
Nicholas Orlowsky c507903ca4
cleanup 2023-10-20 12:54:17 -04:00
Nicholas Orlowsky b8c388dd5c
fix production build 2023-10-20 12:52:01 -04:00
Nicholas Orlowsky bdd12adfb4
fix production build 2023-10-20 12:51:01 -04:00
Nicholas Orlowsky 3dddee43f7
version 0.2.0 2023-10-20 12:46:30 -04:00
Nicholas Orlowsky d19c4efad3
remove error pages directory 2023-10-20 00:46:04 -04:00
Nicholas Orlowsky b28891adb5
update build process 2023-10-20 00:45:20 -04:00
Nicholas Orlowsky dea773366e
cleanup 2023-10-20 00:27:35 -04:00
Nicholas Orlowsky f1868ceea5
minor changes 2023-10-18 14:17:44 -04:00
Nicholas Orlowsky 6c9f7f7c49
restructuring 2023-10-18 14:09:26 -04:00
Nicholas Orlowsky ac669ba504
v0.1.2 2023-10-17 13:33:22 -04:00
82 changed files with 4523 additions and 1021088 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

View file

@ -0,0 +1,56 @@
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

View file

@ -1,16 +1,14 @@
name: Build and Publish Docker Image
on:
push:
branches: [ "main" ]
release:
types: [created]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
@ -24,6 +22,14 @@ jobs:
- 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

4
.gitignore vendored
View file

@ -1 +1,3 @@
anthracite
build/
build_supp/version.cpp
.cache/

View file

@ -1,3 +1,104 @@
# 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
- Slight improvements to benchmarking
- Added version number information to binaries at build-time
- Added error page generation build step
- GitHub CI pipeline will now tag the version when publishing the container to the registry
- General system stability improvements were made to enhance the user's experience
## HTTP/1.1 Speed improvements
The following benchmark shows the speed improvements created by implementing
persistent connections with HTTP/1.1. This test measures the time it takes to
request a 222 byte file 10,000 times by one user
```
=====[ Anthracite Benchmarking Tool ]=====
Requests : 10000
Test : HTTP 1.0 vs HTTP 1.1
HTTP/1.0 Total Time: 3.1160 seconds
HTTP/1.1 Total Time: 0.3621 seconds
```
## Benchmark Results
Each benchmark makes 1000 requests requesting a 50MB file using
100 users to the webserver running in a Docker container.
This is a change from previous benchmarks which used a large html
file.
```
=====[ Anthracite Benchmarking Tool ]=====
Requests : 1000
Users/Threads: 100
Test : Load Test
====[ anthracite ]=====
Average Response Time: 19.5831 seconds
p995 Response Time : 38.9563 seconds
p99 Response Time : 37.1518 seconds
p90 Response Time : 27.5117 seconds
p75 Response Time : 21.4345 seconds
p50 Response Time : 17.7999 seconds
Total Response Time : 19583.1491 seconds
====[ nginx ]=====
Average Response Time: 19.5464 seconds
p995 Response Time : 49.9527 seconds
p99 Response Time : 47.5037 seconds
p90 Response Time : 29.7642 seconds
p75 Response Time : 21.4559 seconds
p50 Response Time : 17.1338 seconds
Total Response Time : 19546.4399 seconds
====[ apache ]=====
Average Response Time: 20.8133 seconds
p995 Response Time : 42.5797 seconds
p99 Response Time : 39.8580 seconds
p90 Response Time : 30.1892 seconds
p75 Response Time : 22.3492 seconds
p50 Response Time : 19.0437 seconds
Total Response Time : 20813.3035 seconds
==========
Total Test Time : 612.3112 seconds
```
# 0.1.2 Fourth Pre-Release
- Fixed bug with mapping / to index.html
- Addex Origin-Server header
# 0.1.1 Third Pre-Release
- Add mappings for common MIME types

74
CMakeLists.txt Normal file
View file

@ -0,0 +1,74 @@
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)

View file

@ -1,12 +1,23 @@
FROM alpine as build-env
FROM alpine AS build-env
RUN apk add --no-cache build-base
COPY ./src/ .
RUN make build-release
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
FROM alpine
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"]
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,59 +1,28 @@
# Anthracite
A simple web server written in C++
## Module-Based Backends
Anthracite includes (read: will include) system for allowing different "backend modules" to handle requests.
This allows for anthracite to be extended for additional use-cases. For example, the following
backends could be implemented:
Anthracite is an extensible, low-dependency, fast web server.
- File: Return files from a directory
- Reverse Proxy: Pass the request to another server
- Web Framework: Pass the request into an application built on your favorite web framework
## Developing
## Building & Debugging
To build/develop Anthracite, you must have C++20, OpenSSL, CMake, Make, and Python3 installed.
Once you have the repository cloned, you can run the following command to build Anthracite:
Create a `build/` directory, run `cmake ..`, and then `make` to build.
```shell
make build
```
## Features
It will create a binary file named `./anthracite`
- 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)
To save time, you can use the following command to build and run anthracite on port `8080`:
```shell
make run
```
To save time again, you can use the following command to build and debug anthracite in gdb:
```shell
make debug
```
## Usage
Run the following commands to serve all files located in `./www/`:
```shell
./anthracite [PORT_NUMBER]
```
## Todo
- [x] Serve HTML Pages
- [x] Properly parse HTTP requests
- [x] Add module-based backend system for handling requests
- [x] Multithreading
- [ ] Cleanup (this one will never truly be done)
- [ ] Proper error handling
- [ ] Build out module-based backend system for handling requests
- [ ] Fix glaring security issues
- [ ] Faster parsing
- [ ] Speed optimizations such as keeping the most visited html pages in memory
- [ ] Cleanup codebase
- [ ] Enable cache support
- [ ] Support newer HTTP versions
## Roadmap
- HTTP/2
- HTTP/3
- More threading modes
- Proxy backend
- Security/Error handling audit
## Screenshots

View file

@ -1,5 +0,0 @@
cd ..
docker build . -t anthracite:latest
cd benchmark
docker build . -t benchmark-anthracite -f anthracite.Dockerfile
docker compose up -d

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,36 @@
import socket
import time
num_requests = 10000
http_1_times = []
http_11_times = []
print('=====[ Anthracite Benchmarking Tool ]=====')
print(f'Requests : {num_requests}')
print(f'Test : HTTP 1.0 vs HTTP 1.1\n\n')
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(("localhost" , 8091))
for i in range(num_requests):
start_time = time.time()
s.sendall(b"GET /test.html HTTP/1.1\r\nAccept: text/html\r\nConnection: keep-alive\r\n\r\n")
data = s.recv(220)
end_time = time.time()
http_11_times.append((end_time - start_time))
s.close()
for i in range(num_requests):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
start_time = time.time()
s.connect(("localhost" , 8091))
s.sendall(b"GET /test.html HTTP/1.0\r\nAccept: text/html\r\n\r\n")
data = s.recv(220)
end_time = time.time()
http_1_times.append((end_time - start_time))
s.close()
run_time_1 = sum(http_1_times)
run_time_11 = sum(http_11_times)
print(f'HTTP/1.0 Total Time: {run_time_1:.4f} seconds')
print(f'HTTP/1.1 Total Time: {run_time_11:.4f} seconds')

View file

@ -0,0 +1,7 @@
services:
anthracite:
build:
context: .
dockerfile: anthracite.Dockerfile
ports:
- "8091:80"

View file

@ -0,0 +1,4 @@
cd ../..
docker build . -t anthracite:latest
cd benchmarks/http_1_v_11
docker compose build

1
benchmarks/http_1_v_11/run.sh Executable file
View file

@ -0,0 +1 @@
docker-compose stop && ./rebuild-container.sh && docker compose up -d && clear && python3 benchmark.py && docker-compose stop

View file

@ -0,0 +1,2 @@
<h1>Anthracite Benchmarking</h1>
<p>Test document to test the speed of HTTP/1.0 and HTTP/1.1</p>

View file

@ -0,0 +1,2 @@
FROM anthracite:latest
COPY ./www/ /www/

View file

@ -5,7 +5,7 @@ from concurrent.futures import ThreadPoolExecutor
from http.client import HTTPConnection
HTTPConnection._http_vsn_str = 'HTTP/1.0'
urls = { 'anthracite': 'http://localhost:8081/large.html','nginx': 'http://localhost:8082/large.html', 'apache': 'http://localhost:8083/large.html' }
urls = { 'anthracite': 'http://localhost:8081/50MB.zip','nginx': 'http://localhost:8082/50MB.zip', 'apache': 'http://localhost:8083/50MB.zip' }
num_requests = 1000
num_users = 100 # number of threads
response_times = {}
@ -37,7 +37,8 @@ def make_request(request_number, server_name):
print('=====[ Anthracite Benchmarking Tool ]=====')
print(f'Requests : {num_requests}')
print(f'Users/Threads: {num_users}\n\n')
print(f'Users/Threads: {num_users}')
print(f'Test : Load Test\n\n')
start_all_time = time.time()
futures = []

View file

@ -0,0 +1,5 @@
cd ../..
docker build . -t anthracite:latest
cd benchmarks/load_test
docker compose build
docker compose up -d

1
benchmarks/load_test/run.sh Executable file
View file

@ -0,0 +1 @@
docker-compose stop && ./rebuild-container.sh && docker compose up -d && clear && python3 benchmark.py && docker-compose stop

Binary file not shown.

View file

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

76
build_supp/error_gen.py Normal file
View file

@ -0,0 +1,76 @@
# error_gen.py
# Generates default html error pages for Anthracite
import os
version = "Unknown"
with open("version.txt", "r") as file:
version = file.read().strip()
def generate_error_page(error_code, error_title):
html = f"""<html>
<head><title>{error_title}</title></head>
<body>
<center>
<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>
</center>
</body>
</html>"""
return html
error_codes = {
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Payload Too Large",
414: "URI Too Long",
415: "Unsupported Media Type",
416: "Range Not Satisfiable",
417: "Expectation Failed",
418: "I'm a teapot",
421: "Misdirected Request",
422: "Unprocessable Entity",
423: "Locked",
424: "Failed Dependency",
425: "Too Early",
426: "Upgrade Required",
428: "Precondition Required",
429: "Too Many Requests",
431: "Request Header Fields Too Large",
451: "Unavailable For Legal Reasons",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
505: "HTTP Version Not Supported",
506: "Variant Also Negotiates",
507: "Insufficient Storage",
508: "Loop Detected",
510: "Not Extended",
511: "Network Authentication Required"
}
error_dir = '../build/error_pages'
os.makedirs(error_dir, exist_ok=True)
for code, title in error_codes.items():
error_page = generate_error_page(code, title)
file_path = os.path.join(error_dir, f"{code}.html")
with open(file_path, "w") as file:
file.write(error_page)

4
build_supp/version.sh Executable file
View file

@ -0,0 +1,4 @@
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
build_supp/version.txt Normal file
View file

@ -0,0 +1 @@
0.3.0

View file

Before

Width:  |  Height:  |  Size: 319 B

After

Width:  |  Height:  |  Size: 319 B

View file

@ -0,0 +1,9 @@
<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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

View file

@ -0,0 +1,9 @@
<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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

View file

@ -1,6 +1,6 @@
<head>
<title>Anthracite</title>
<link rel="icon" type="image/x-icon" href="/images/favicon.ico">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
</head>
<center>
<h1>Anthracite is Running!</h1>

12
docker-compose.yml Normal file
View file

@ -0,0 +1,12 @@
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

2
format.sh Executable file
View file

@ -0,0 +1,2 @@
git ls-files -- '*.cpp' '*.h' | xargs clang-format -i -style=file
git diff --exit-code --color

20
lib/backends/backend.hpp Normal file
View file

@ -0,0 +1,20 @@
#pragma once
#include <memory>
#include "../http/request.hpp"
#include "../http/response.hpp"
namespace anthracite::backends {
class backend {
public:
backend() = default;
virtual ~backend() = default;
backend(backend const&) = delete;
backend& operator = (backend const&) = delete;
backend(backend&&) = delete;
backend& operator=(backend&&) = delete;
virtual std::unique_ptr<http::response> handle_request(http::request& req) = 0;
};
};

View file

@ -0,0 +1,91 @@
#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;
}
};

View file

@ -0,0 +1,22 @@
#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);
};
};

244
lib/http/constants.hpp Normal file
View file

@ -0,0 +1,244 @@
#pragma once
#include <string>
#include <unordered_map>
namespace anthracite::http {
constexpr int HEADER_BYTES = 8190;
enum method {
GET,
POST,
DELETE,
PUT,
PATCH,
HEAD,
OPTIONS,
CONNECT,
TRACE,
COPY,
LINK,
UNLINK,
PURGE,
LOCK,
UNLOCK,
PROPFIND,
VIEW,
UNKNOWN
};
enum status_codes {
CONTINUE = 100,
SWITCHING_PROTOCOLS = 101,
PROCESSING = 102,
EARLY_HINTS = 103,
OK = 200,
CREATED = 201,
ACCEPTED = 202,
NON_AUTHORITATIVE_INFORMATION = 203,
NO_CONTENT = 204,
RESET_CONTENT = 205,
PARTIAL_CONTENT = 206,
MULTI_STATUS = 207,
ALREADY_REPORTED = 208,
IM_USED = 226,
MULTIPLE_CHOICES = 300,
MOVED_PERMANENTLY = 301,
FOUND = 302,
SEE_OTHER = 303,
NOT_MODIFIED = 304,
USE_PROXY = 305,
TEMPORARY_REDIRECT = 307,
PERMANENT_REDIRECT = 308,
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
PAYMENT_REQUIRED = 402,
FORBIDDEN = 403,
NOT_FOUND = 404,
METHOD_NOT_ALLOWED = 405,
NOT_ACCEPTABLE = 406,
PROXY_AUTHENTICATION_REQUIRED = 407,
REQUEST_TIMEOUT = 408,
CONFLICT = 409,
GONE = 410,
LENGTH_REQUIRED = 411,
PRECONDITION_FAILED = 412,
PAYLOAD_TOO_LARGE = 413,
URI_TOO_LONG = 414,
UNSUPPORTED_MEDIA_TYPE = 415,
RANGE_NOT_SATISFIABLE = 416,
EXPECTATION_FAILED = 417,
I_AM_A_TEAPOT = 418,
ENHANCE_YOUR_CALM = 420,
MISDIRECTED_REQUEST = 421,
UNPROCESSABLE_ENTITY = 422,
LOCKED = 423,
FAILED_DEPENDENCY = 424,
TOO_EARLY = 425,
UPGRADE_REQUIRED = 426,
PRECONDITION_REQUIRED = 428,
TOO_MANY_REQUESTS = 429,
REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
UNAVAILABLE_FOR_LEGAL_REASONS = 451,
INTERNAL_SERVER_ERROR = 500,
NOT_IMPLEMENTED = 501,
BAD_GATEWAY = 502,
SERVICE_UNAVAILABLE = 503,
GATEWAY_TIMEOUT = 504,
HTTP_VERSION_NOT_SUPPORTED = 505,
VARIANT_ALSO_NEGOTIATES = 506,
INSUFFICIENT_STORAGE = 507,
LOOP_DETECTED = 508,
NOT_EXTENDED = 510,
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<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<int, std::string> const status_map = {
{ 100, "CONTINUE" },
{ 101, "SWITCHING PROTOCOLS" },
{ 200, "OK" },
{ 201, "CREATED" },
{ 202, "ACCEPTED" },
{ 203, "NON-AUTHORITATIVE INFORMATION" },
{ 204, "NO CONTENT" },
{ 205, "RESET CONTENT" },
{ 206, "PARTIAL CONTENT" },
{ 300, "MULTIPLE CHOICES" },
{ 301, "MOVED PERMANENTLY" },
{ 302, "FOUND" },
{ 303, "SEE OTHER" },
{ 304, "NOT MODIFIED" },
{ 305, "USE PROXY" },
{ 307, "TEMPORARY REDIRECT" },
{ 400, "BAD REQUEST" },
{ 401, "UNAUTHORIZED" },
{ 402, "PAYMENT REQUIRED" },
{ 403, "FORBIDDEN" },
{ 404, "NOT FOUND" },
{ 405, "METHOD NOT ALLOWED" },
{ 406, "NOT ACCEPTABLE" },
{ 407, "PROXY AUTHENTICATION REQUIRED" },
{ 408, "REQUEST TIMEOUT" },
{ 409, "CONFLICT" },
{ 410, "GONE" },
{ 411, "LENGTH REQUIRED" },
{ 412, "PRECONDITION FAILED" },
{ 413, "PAYLOAD TOO LARGE" },
{ 414, "URI TOO LONG" },
{ 415, "UNSUPPORTED MEDIA TYPE" },
{ 416, "RANGE NOT SATISFIABLE" },
{ 417, "EXPECTATION FAILED" },
{ 418, "I'M A TEAPOT" },
{ 421, "MISDIRECTED REQUEST" },
{ 422, "UNPROCESSABLE ENTITY" },
{ 423, "LOCKED" },
{ 424, "FAILED DEPENDENCY" },
{ 426, "UPGRADE REQUIRED" },
{ 428, "PRECONDITION REQUIRED" },
{ 429, "TOO MANY REQUESTS" },
{ 431, "REQUEST HEADER FIELDS TOO LARGE" },
{ 451, "UNAVAILABLE FOR LEGAL REASONS" },
{ 500, "INTERNAL SERVER ERROR" },
{ 501, "NOT IMPLEMENTED" },
{ 502, "BAD GATEWAY" },
{ 503, "SERVICE UNAVAILABLE" },
{ 504, "GATEWAY TIMEOUT" },
{ 505, "HTTP VERSION NOT SUPPORTED" },
{ 506, "VARIANT ALSO NEGOTIATES" },
{ 507, "INSUFFICIENT STORAGE" },
{ 508, "LOOP DETECTED" },
{ 510, "NOT EXTENDED" },
{ 511, "NETWORK AUTHENTICATION REQUIRED" },
{ 420, "ENHANCE YOUR CALM" }
};
static std::unordered_map<std::string, std::string> const mime_types = {
{ "html", "text/html" },
{ "css", "text/css" },
{ "js", "application/javascript" },
{ "json", "application/json" },
{ "pdf", "application/pdf" },
{ "ico", "image/x-icon" },
{ "jpg", "image/jpeg" },
{ "jpeg", "image/jpeg" },
{ "png", "image/png" },
{ "gif", "image/gif" },
{ "bmp", "image/bmp" },
{ "mp4", "video/mp4" },
{ "avi", "video/x-msvideo" },
{ "mkv", "video/x-matroska" },
{ "mov", "video/quicktime" },
{ "wmv", "video/x-ms-wmv" },
};
enum 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 = {
// This is because HTTP 0.9 didn't specify version in the header
{ "", HTTP_0_9 },
{ "HTTP/0.9", HTTP_0_9 },
{ "HTTP/1.0", HTTP_1_0 },
{ "HTTP/1.1", HTTP_1_1 },
{ "HTTP/2.0", HTTP_2_0 },
{ "HTTP/3.0", HTTP_3_0 }
};
static std::unordered_map<version, std::string> const reverse_version_map = {
{ HTTP_0_9, "HTTP/0.9" },
{ HTTP_1_0, "HTTP/1.0" },
{ HTTP_1_1, "HTTP/1.1" },
{ HTTP_2_0, "HTTP/2.0" },
{ HTTP_3_0, "HTTP/3.0" }
};
};

60
lib/http/header_query.hpp Normal file
View file

@ -0,0 +1,60 @@
#pragma once
#include <string>
namespace anthracite::http {
class name_value {
private:
std::string _name;
std::string _value;
protected:
name_value() {}
public:
name_value(std::string name, std::string value)
: _name(std::move(name))
, _value(std::move(value))
{
}
virtual ~name_value() = default;
name_value(const name_value&) = default;
name_value& operator=(name_value const&) = default;
name_value(name_value&&) = default;
name_value& operator=(name_value&&) = default;
std::string& name() { return _name; }
std::string& value() { return _value; }
virtual std::string to_string() { return ""; }
};
class header : public name_value {
public:
header()
: name_value()
{
}
header(std::string name, std::string value)
: name_value(name, value)
{
}
std::string to_string() override { return name() + ": " + value() + "\r\n"; }
};
class query_param : public name_value {
public:
query_param()
: name_value()
{
}
query_param(std::string name, std::string value)
: name_value(name, value)
{
}
std::string to_string() override { return name() + "=" + value(); }
};
};

View file

200
lib/http/request.cpp Normal file
View file

@ -0,0 +1,200 @@
#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;
}
};

46
lib/http/request.hpp Normal file
View file

@ -0,0 +1,46 @@
#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();
};
};

62
lib/http/response.cpp Normal file
View file

@ -0,0 +1,62 @@
#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;
}
};

29
lib/http/response.hpp Normal file
View file

@ -0,0 +1,29 @@
#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();
};
};

36
lib/log/log.cpp Normal file
View file

@ -0,0 +1,36 @@
#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;
}
};

60
lib/log/log.hpp Normal file
View file

@ -0,0 +1,60 @@
#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);
};

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;
};
};

144
lib/socket/socket.cpp Normal file
View file

@ -0,0 +1,144 @@
#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;
}
};

51
lib/socket/socket.hpp Normal file
View file

@ -0,0 +1,51 @@
#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; }
};
};

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;
};
};

6
lib/version.hpp Normal file
View file

@ -0,0 +1,6 @@
#pragma once
#include <string>
extern const std::string ANTHRACITE_VERSION_STRING;
extern const std::string ANTHRACITE_FULL_VERSION_STRING;

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,28 +0,0 @@
.PHONY: format lint build build-release build-docker run debug
build:
g++ main.cpp -g -o ./anthracite
build-release:
g++ main.cpp -O3 -march=native -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
format:
clang-format *.cpp -i
lint:
clang-tidy *.cpp
lint-fix:
clang-tidy *.cpp -fix -fix-errors

View file

@ -1,7 +0,0 @@
#include "../http.cpp"
#include <memory>
class backend {
public:
virtual unique_ptr<http_response> handle_request(http_request& req) = 0;
};

View file

@ -1,53 +0,0 @@
#include "backend.cpp"
#include <filesystem>
class file_backend : public backend {
private:
unordered_map<string, string> file_cache;
string file_dir;
unique_ptr<http_response> handle_request_cache(http_request& req) {
string filename = req.path() == "/" ? "index.html" : req.path();
filename = file_dir + filename;
auto file_info = file_cache.find(filename);
int status = 200;
if (file_info == file_cache.end()) {
status = 404;
filename = "./error_pages/404.html";
file_info = file_cache.find(filename);
}
return make_unique<http_response>(file_info->second, filename, status);
}
void populate_cache_dir(string dir) {
filesystem::recursive_directory_iterator cur = begin(filesystem::recursive_directory_iterator(dir));
filesystem::recursive_directory_iterator fin = end(filesystem::recursive_directory_iterator(dir));
while (cur != fin) {
auto p = cur->path();
string filename = p.string();
stringstream buffer;
ifstream stream(filename);
buffer << stream.rdbuf();
file_cache[filename] = buffer.str();
cout << "File at " << filename << " cached (" << file_cache[filename].size() << " bytes)" << endl;
++cur;
}
}
void populate_cache() {
populate_cache_dir(file_dir);
populate_cache_dir("./error_pages/");
}
public:
file_backend(string dir = "./www") : file_dir(dir) {
populate_cache();
}
unique_ptr<http_response> handle_request(http_request& req) override {
return handle_request_cache(req);
}
};

View file

@ -1,12 +0,0 @@
<html>
<head><title>Not Found</title></head>
<body>
<center>
<h1>404 - Not Found</h1>
<hr>
<p>Anthracite/0.0.1</p>
<p><small>Created by Nicholas Orlowsky </small> - <a href="https://github.com/nickorlow/anthracite"><small>This is Open Source Software</small></a></p>
</center>
</body>
</html>

View file

@ -1,12 +0,0 @@
<html>
<head><title>Not Found</title></head>
<body>
<center>
<h1>500 - Internal Server Error</h1>
<hr>
<p>Anthracite/0.0.1</p>
<p><small>Created by Nicholas Orlowsky </small> - <a href="https://github.com/nickorlow/anthracite"><small>This is Open Source Software</small></a></p>
</center>
</body>
</html>

304
src/file_main.cpp Normal file
View file

@ -0,0 +1,304 @@
#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;
}

View file

@ -1,455 +0,0 @@
#include <arpa/inet.h>
#include <malloc.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include "socket.cpp"
#include <exception>
#include <fstream>
#include <iostream>
#include <sstream>
#include <unordered_map>
#include <utility>
#include <vector>
using namespace std;
constexpr int HTTP_HEADER_BYTES = 8190;
enum http_method {
GET,
POST,
DELETE,
PUT,
PATCH,
HEAD,
OPTIONS,
CONNECT,
TRACE,
COPY,
LINK,
UNLINK,
PURGE,
LOCK,
UNLOCK,
PROPFIND,
VIEW,
UNKNOWN
};
static unordered_map<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 unordered_map<http_method, 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 unordered_map<int, string> const http_status_map = {
{ 100, "CONTINUE" },
{ 101, "SWITCHING PROTOCOLS" },
{ 200, "OK" },
{ 201, "CREATED" },
{ 202, "ACCEPTED" },
{ 203, "NON-AUTHORITATIVE INFORMATION" },
{ 204, "NO CONTENT" },
{ 205, "RESET CONTENT" },
{ 206, "PARTIAL CONTENT" },
{ 300, "MULTIPLE CHOICES" },
{ 301, "MOVED PERMANENTLY" },
{ 302, "FOUND" },
{ 303, "SEE OTHER" },
{ 304, "NOT MODIFIED" },
{ 305, "USE PROXY" },
{ 307, "TEMPORARY REDIRECT" },
{ 400, "BAD REQUEST" },
{ 401, "UNAUTHORIZED" },
{ 402, "PAYMENT REQUIRED" },
{ 403, "FORBIDDEN" },
{ 404, "NOT FOUND" },
{ 405, "METHOD NOT ALLOWED" },
{ 406, "NOT ACCEPTABLE" },
{ 407, "PROXY AUTHENTICATION REQUIRED" },
{ 408, "REQUEST TIMEOUT" },
{ 409, "CONFLICT" },
{ 410, "GONE" },
{ 411, "LENGTH REQUIRED" },
{ 412, "PRECONDITION FAILED" },
{ 413, "PAYLOAD TOO LARGE" },
{ 414, "URI TOO LONG" },
{ 415, "UNSUPPORTED MEDIA TYPE" },
{ 416, "RANGE NOT SATISFIABLE" },
{ 417, "EXPECTATION FAILED" },
{ 418, "I'M A TEAPOT" },
{ 421, "MISDIRECTED REQUEST" },
{ 422, "UNPROCESSABLE ENTITY" },
{ 423, "LOCKED" },
{ 424, "FAILED DEPENDENCY" },
{ 426, "UPGRADE REQUIRED" },
{ 428, "PRECONDITION REQUIRED" },
{ 429, "TOO MANY REQUESTS" },
{ 431, "REQUEST HEADER FIELDS TOO LARGE" },
{ 451, "UNAVAILABLE FOR LEGAL REASONS" },
{ 500, "INTERNAL SERVER ERROR" },
{ 501, "NOT IMPLEMENTED" },
{ 502, "BAD GATEWAY" },
{ 503, "SERVICE UNAVAILABLE" },
{ 504, "GATEWAY TIMEOUT" },
{ 505, "HTTP VERSION NOT SUPPORTED" },
{ 506, "VARIANT ALSO NEGOTIATES" },
{ 507, "INSUFFICIENT STORAGE" },
{ 508, "LOOP DETECTED" },
{ 510, "NOT EXTENDED" },
{ 511, "NETWORK AUTHENTICATION REQUIRED" },
{ 420, "ENHANCE YOUR CALM" }
};
static unordered_map<std::string, std::string> const mime_types = {
{ "html", "text/html" },
{ "css", "text/css" },
{ "js", "application/javascript" },
{ "pdf", "application/pdf" },
{ "ico", "image/x-icon" },
{ "jpg", "image/jpeg" },
{ "jpeg", "image/jpeg" },
{ "png", "image/png" },
{ "gif", "image/gif" },
{ "bmp", "image/bmp" },
{ "mp4", "video/mp4" },
{ "avi", "video/x-msvideo" },
{ "mkv", "video/x-matroska" },
{ "mov", "video/quicktime" },
{ "wmv", "video/x-ms-wmv" },
};
class name_value {
private:
string _name;
string _value;
protected:
name_value() {}
public:
name_value(string name, string value)
: _name(std::move(name))
, _value(std::move(value))
{
}
virtual ~name_value() = default;
name_value(const name_value&) = default;
name_value& operator=(name_value const&) = default;
name_value(name_value&&) = default;
name_value& operator=(name_value&&) = default;
string name() { return _name; }
string value() { return _value; }
virtual string to_string() { return ""; }
};
class http_header : public name_value {
public:
http_header()
: name_value()
{
}
http_header(string name, string value)
: name_value(name, value)
{
}
string to_string() override { return name() + ": " + value() + "\r\n"; }
};
class query_param : public name_value {
public:
query_param()
: name_value()
{
}
query_param(string name, string value)
: name_value(name, value)
{
}
string to_string() override { return name() + "=" + value(); }
};
enum http_version { HTTP_0_9,
HTTP_1_0,
HTTP_1_1,
HTTP_2_0,
HTTP_3_0 };
static std::unordered_map<std::string, http_version> const http_version_map = {
{ "HTTP/0.9", HTTP_0_9 },
{ "HTTP/1.0", HTTP_1_0 },
{ "HTTP/1.1", HTTP_1_1 },
{ "HTTP/2.0", HTTP_2_0 },
{ "HTTP/3.0", HTTP_3_0 }
};
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" },
{ HTTP_2_0, "HTTP/2.0" },
{ HTTP_3_0, "HTTP/3.0" }
};
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;
string _path;
string _client_ipaddr;
string _body_content;
unordered_map<string, http_header> _headers; // kinda goofy, whatever
unordered_map<string, query_param> _query_params; // kinda goofy, whatever
public:
http_request(anthracite_socket& s)
: _path("")
{
string raw_data = s.recv_message(HTTP_HEADER_BYTES);
_client_ipaddr = s.get_client_ip();
parser_state state = METHOD;
string scratch = "";
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 = "";
cout << "Error: Whitespace found in header name\n";
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;
}
}
}
string path() { return _path; }
http_method method() { return _method; }
string client_ip() { return _client_ipaddr; }
string to_string()
{
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;
}
};
class http_response {
private:
int _status_code;
string& _content;
string _filename;
unordered_map<string, http_header> _headers; // kinda goofy, whatever
public:
http_response(string& content, string filename, int status_code = 200)
: _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;
}
}
string& content()
{
return _content;
}
string header_to_string()
{
string response = "";
response += "HTTP/1.1 " + ::to_string(_status_code) + " " + http_status_map.find(_status_code)->second + "\r\n";
string content_type = "text/html";
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", ::to_string(_content.length())), false);
add_header(http_header("Server", "Anthracite/0.0.1"), false);
for (auto header : _headers) {
response += header.second.to_string();
}
response += "\r\n";
return response;
}
string to_string()
{
return header_to_string() + _content;
}
};

View file

@ -1,70 +0,0 @@
#include "backends/file_backend.cpp"
#include <condition_variable>
#include <exception>
#include <fstream>
#include <iostream>
#include <mutex>
#include <netinet/in.h>
#include <sstream>
#include <sys/socket.h>
#include <thread>
#include <unistd.h>
#include <unordered_map>
using namespace std;
void log_request_and_response(http_request& req, unique_ptr<http_response>& resp);
constexpr int default_port = 80;
constexpr int max_worker_threads = 128;
int active_threads = 0;
mutex mtx;
condition_variable cv;
void handle_client(anthracite_socket s, file_backend& fb)
{
http_request req(s);
unique_ptr<http_response> resp = fb.handle_request(req);
log_request_and_response(req, resp);
string header = resp->header_to_string();
s.send_message(header);
s.send_message(resp->content());
resp.reset();
s.close_conn();
{
std::lock_guard<std::mutex> lock(mtx);
active_threads--;
}
cv.notify_one();
}
int main(int argc, char** argv)
{
int port_number = default_port;
if (argc > 1) {
port_number = atoi(argv[1]);
}
cout << "Initializing Anthracite" << endl;
anthracite_socket s(port_number);
file_backend fb(argc > 2 ? argv[2] : "./www");
cout << "Initialization Complete" << endl;
cout << "Listening for HTTP connections on port " << port_number << endl;
while (true) {
s.wait_for_conn();
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return active_threads < max_worker_threads; });
active_threads++;
thread(handle_client, s, ref(fb)).detach();
}
exit(0);
}
void log_request_and_response(http_request& req, unique_ptr<http_response>& resp)
{
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() << endl;
}

View file

@ -1,78 +0,0 @@
#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>
using namespace std;
class anthracite_socket {
private:
int server_socket;
int client_socket {};
string client_ip;
struct sockaddr_in client_addr {};
socklen_t client_addr_len {};
public:
anthracite_socket(int port, int max_queue = 10)
: 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 = string(ip_str);
}
string get_client_ip()
{
return client_ip;
}
void close_conn()
{
close(client_socket);
client_socket = -1;
}
void send_message(string& msg)
{
if (client_socket == -1) {
return;
}
send(client_socket, &msg[0], msg.length(), 0);
}
string recv_message(int buffer_size = 1024)
{
if (client_socket == -1) {
return "";
}
char response[buffer_size + 1];
recv(client_socket, response, sizeof(response), 0);
response[buffer_size] = '\0';
return string(response);
}
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View file

@ -1,18 +0,0 @@
.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;
}

View file

@ -1,25 +0,0 @@
<!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>

Binary file not shown.

View file

@ -1,9 +0,0 @@
<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>

67
tests/speed_tests.cpp Normal file
View file

@ -0,0 +1,67 @@
#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

View file

@ -0,0 +1,15 @@
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" }

18
tests/unit_tests.cpp Normal file
View file

@ -0,0 +1,18 @@
#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());
}