Compare commits
48 commits
Author | SHA1 | Date | |
---|---|---|---|
|
efd19d3bba | ||
|
0fc8400066 | ||
|
f48eda81e8 | ||
|
5c196c05fc | ||
|
9b5719f9be | ||
|
c07f3ebf81 | ||
|
6c5feb8675 | ||
|
409024e04a | ||
|
058c395095 | ||
|
ca05aa1e5a | ||
|
f09b261b62 | ||
|
f1195d1f04 | ||
|
a63d9d1e65 | ||
|
da9f2f2d51 | ||
|
10ca7f9f51 | ||
|
f930792a11 | ||
|
95430a5dc3 | ||
|
236f7399fe | ||
|
f962f5796d | ||
|
d952cae3f6 | ||
|
ce74fa1d3c | ||
|
46bb91864f | ||
|
88e53b2b46 | ||
|
94d2b55d29 | ||
|
6c99bee466 | ||
|
b8ec23a776 | ||
|
678d742da8 | ||
|
36a11110a5 | ||
|
1fb5daf4dd | ||
|
3c9edb1e0b | ||
|
0ebdb34601 | ||
|
71be773d49 | ||
|
fba87f3fbb | ||
|
54d82b8c66 | ||
|
c4540a1397 | ||
|
207015c944 | ||
|
734e37de37 | ||
![]() |
c31db4d2a8 | ||
![]() |
c507903ca4 | ||
![]() |
b8c388dd5c | ||
![]() |
bdd12adfb4 | ||
![]() |
3dddee43f7 | ||
![]() |
d19c4efad3 | ||
![]() |
b28891adb5 | ||
![]() |
dea773366e | ||
![]() |
f1868ceea5 | ||
![]() |
6c9f7f7c49 | ||
![]() |
ac669ba504 |
19
.direnv/bin/nix-direnv-reload
Executable 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
|
1
.direnv/nix-profile-24.11-mma8n3yfap91nw44
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/gr8ifjf51b4w3v62vvinq4s8w97pn3ag-nix-shell-env
|
2219
.direnv/nix-profile-24.11-mma8n3yfap91nw44.rc
Normal file
56
.forgejo/workflows/docker-publish.yml
Normal 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
|
14
.github/workflows/docker-publish.yml
vendored
|
@ -1,16 +1,14 @@
|
||||||
name: Build and Publish Docker Image
|
name: Build and Publish Docker Image
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
release:
|
||||||
branches: [ "main" ]
|
types: [created]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
@ -24,6 +22,14 @@ jobs:
|
||||||
- name: Setup Docker buildx
|
- name: Setup Docker buildx
|
||||||
uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf
|
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 }}
|
- name: Log into registry ${{ env.REGISTRY }}
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
|
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
|
||||||
|
|
4
.gitignore
vendored
|
@ -1 +1,3 @@
|
||||||
anthracite
|
build/
|
||||||
|
build_supp/version.cpp
|
||||||
|
.cache/
|
||||||
|
|
101
CHANGELOG.md
|
@ -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
|
# 0.1.1 Third Pre-Release
|
||||||
|
|
||||||
- Add mappings for common MIME types
|
- Add mappings for common MIME types
|
||||||
|
|
74
CMakeLists.txt
Normal 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)
|
29
Dockerfile
|
@ -1,12 +1,23 @@
|
||||||
FROM alpine as build-env
|
FROM alpine AS build-env
|
||||||
|
|
||||||
RUN apk add --no-cache build-base
|
RUN apk add --no-cache build-base python3 cmake openssl-dev
|
||||||
COPY ./src/ .
|
COPY ./src ./src
|
||||||
RUN make build-release
|
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
|
FROM alpine
|
||||||
RUN apk add --no-cache build-base
|
RUN apk add --no-cache libgcc libstdc++
|
||||||
COPY --from=build-env /anthracite /anthracite
|
COPY --from=build-env /build/anthracite-bin /anthracite-bin
|
||||||
COPY --from=build-env /www /www
|
COPY --from=build-env /build/error_pages /error_pages
|
||||||
COPY --from=build-env /error_pages /error_pages
|
COPY --from=build-env /build_supp/default_config.cfg /anthracite.cfg
|
||||||
CMD ["/anthracite"]
|
COPY /default_www/docker /www
|
||||||
|
CMD ["/anthracite-bin"]
|
||||||
|
|
65
README.md
|
@ -1,59 +1,28 @@
|
||||||
# Anthracite
|
# Anthracite
|
||||||
A simple web server written in C++
|
|
||||||
|
|
||||||
## Module-Based Backends
|
Anthracite is an extensible, low-dependency, fast web server.
|
||||||
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:
|
|
||||||
|
|
||||||
- File: Return files from a directory
|
## Developing
|
||||||
- Reverse Proxy: Pass the request to another server
|
|
||||||
- Web Framework: Pass the request into an application built on your favorite web framework
|
|
||||||
|
|
||||||
## 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
|
## Features
|
||||||
make build
|
|
||||||
```
|
|
||||||
|
|
||||||
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`:
|
## Roadmap
|
||||||
|
- HTTP/2
|
||||||
```shell
|
- HTTP/3
|
||||||
make run
|
- More threading modes
|
||||||
```
|
- Proxy backend
|
||||||
|
- Security/Error handling audit
|
||||||
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
|
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
cd ..
|
|
||||||
docker build . -t anthracite:latest
|
|
||||||
cd benchmark
|
|
||||||
docker build . -t benchmark-anthracite -f anthracite.Dockerfile
|
|
||||||
docker compose up -d
|
|
1020251
benchmark/www/large.html
36
benchmarks/http_1_v_11/benchmark.py
Normal 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')
|
7
benchmarks/http_1_v_11/docker-compose.yaml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
services:
|
||||||
|
anthracite:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: anthracite.Dockerfile
|
||||||
|
ports:
|
||||||
|
- "8091:80"
|
4
benchmarks/http_1_v_11/rebuild-container.sh
Executable 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
|
@ -0,0 +1 @@
|
||||||
|
docker-compose stop && ./rebuild-container.sh && docker compose up -d && clear && python3 benchmark.py && docker-compose stop
|
2
benchmarks/http_1_v_11/www/test.html
Normal 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>
|
2
benchmarks/load_test/anthracite.Dockerfile
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
FROM anthracite:latest
|
||||||
|
COPY ./www/ /www/
|
|
@ -5,7 +5,7 @@ from concurrent.futures import ThreadPoolExecutor
|
||||||
from http.client import HTTPConnection
|
from http.client import HTTPConnection
|
||||||
|
|
||||||
HTTPConnection._http_vsn_str = 'HTTP/1.0'
|
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_requests = 1000
|
||||||
num_users = 100 # number of threads
|
num_users = 100 # number of threads
|
||||||
response_times = {}
|
response_times = {}
|
||||||
|
@ -37,7 +37,8 @@ def make_request(request_number, server_name):
|
||||||
|
|
||||||
print('=====[ Anthracite Benchmarking Tool ]=====')
|
print('=====[ Anthracite Benchmarking Tool ]=====')
|
||||||
print(f'Requests : {num_requests}')
|
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()
|
start_all_time = time.time()
|
||||||
|
|
||||||
futures = []
|
futures = []
|
5
benchmarks/load_test/rebuild-container.sh
Executable 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
|
@ -0,0 +1 @@
|
||||||
|
docker-compose stop && ./rebuild-container.sh && docker compose up -d && clear && python3 benchmark.py && docker-compose stop
|
BIN
benchmarks/load_test/www/50MB.zip
Normal file
6
build_supp/default_config.cfg
Normal 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
|
@ -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
|
@ -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
|
@ -0,0 +1 @@
|
||||||
|
0.3.0
|
Before Width: | Height: | Size: 319 B After Width: | Height: | Size: 319 B |
9
default_www/docker/index.html
Normal 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>
|
BIN
default_www/docker_compose/favicon.ico
Normal file
After Width: | Height: | Size: 319 B |
9
default_www/docker_compose/index.html
Normal 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>
|
BIN
default_www/regular/favicon.ico
Normal file
After Width: | Height: | Size: 319 B |
|
@ -1,6 +1,6 @@
|
||||||
<head>
|
<head>
|
||||||
<title>Anthracite</title>
|
<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>
|
</head>
|
||||||
<center>
|
<center>
|
||||||
<h1>Anthracite is Running!</h1>
|
<h1>Anthracite is Running!</h1>
|
12
docker-compose.yml
Normal 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
|
@ -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
|
@ -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;
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
91
lib/backends/file_backend.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
22
lib/backends/file_backend.hpp
Normal 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
|
@ -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
|
@ -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(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
0
lib/http/http_request.cpp
Normal file
200
lib/http/request.cpp
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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);
|
||||||
|
|
||||||
|
|
||||||
|
};
|
126
lib/socket/openssl_socket.cpp
Normal 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() };
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
29
lib/socket/openssl_socket.hpp
Normal 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
|
@ -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
|
@ -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; }
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
138
lib/thread_mgr/event_loop.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
26
lib/thread_mgr/event_loop.hpp
Normal 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;
|
||||||
|
};
|
||||||
|
};
|
16
lib/thread_mgr/thread_mgr.hpp
Normal 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
|
@ -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
|
@ -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"
|
||||||
|
'';
|
||||||
|
}
|
28
src/Makefile
|
@ -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
|
|
|
@ -1,7 +0,0 @@
|
||||||
#include "../http.cpp"
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
class backend {
|
|
||||||
public:
|
|
||||||
virtual unique_ptr<http_response> handle_request(http_request& req) = 0;
|
|
||||||
};
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -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>
|
|
|
@ -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
|
@ -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;
|
||||||
|
}
|
455
src/http.cpp
|
@ -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;
|
|
||||||
}
|
|
||||||
};
|
|
70
src/main.cpp
|
@ -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;
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
Before Width: | Height: | Size: 496 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 42 KiB |
|
@ -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;
|
|
||||||
}
|
|
|
@ -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>
|
|
|
@ -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
|
@ -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
|
15
tests/test_files/test_request.http
Normal 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
|
@ -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());
|
||||||
|
}
|