Some update

This commit is contained in:
Minecon724 2025-07-19 18:40:55 +02:00
commit 63a1499977
Signed by untrusted user who does not match committer: m724
GPG key ID: A02E6E67AB961189
22 changed files with 12037 additions and 53 deletions

View file

@ -1 +1,2 @@
run/
run/
control-server/build/

3
.gitignore vendored
View file

@ -1 +1,2 @@
run/
run/
control-server/build/

76
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,76 @@
{
"files.associations": {
"ostream": "cpp",
"array": "cpp",
"atomic": "cpp",
"bit": "cpp",
"cctype": "cpp",
"charconv": "cpp",
"chrono": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"codecvt": "cpp",
"compare": "cpp",
"concepts": "cpp",
"csignal": "cpp",
"cstdarg": "cpp",
"cstddef": "cpp",
"cstdint": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"ctime": "cpp",
"cwchar": "cpp",
"cwctype": "cpp",
"deque": "cpp",
"string": "cpp",
"unordered_map": "cpp",
"vector": "cpp",
"exception": "cpp",
"algorithm": "cpp",
"functional": "cpp",
"iterator": "cpp",
"memory": "cpp",
"memory_resource": "cpp",
"numeric": "cpp",
"optional": "cpp",
"random": "cpp",
"ratio": "cpp",
"string_view": "cpp",
"system_error": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"utility": "cpp",
"format": "cpp",
"initializer_list": "cpp",
"iomanip": "cpp",
"iosfwd": "cpp",
"iostream": "cpp",
"istream": "cpp",
"limits": "cpp",
"new": "cpp",
"numbers": "cpp",
"ranges": "cpp",
"span": "cpp",
"sstream": "cpp",
"stdexcept": "cpp",
"streambuf": "cpp",
"typeinfo": "cpp",
"variant": "cpp",
"text_encoding": "cpp",
"queue": "cpp",
"filesystem": "cpp",
"any": "cpp",
"condition_variable": "cpp",
"coroutine": "cpp",
"source_location": "cpp",
"future": "cpp",
"mutex": "cpp",
"stop_token": "cpp",
"thread": "cpp",
"list": "cpp",
"semaphore": "cpp",
"stdfloat": "cpp",
"fstream": "cpp"
}
}

View file

@ -1,30 +1,44 @@
# Remember to update!
FROM docker.io/nginx:1.29.0-alpine-slim
ARG NGINX_TAG=1.29.0-alpine-slim
#----------------------------------------#
FROM docker.io/nginx:${NGINX_TAG} AS builder
WORKDIR /usr/control-server
COPY control-server /usr/control-server
RUN apk add --no-cache \
build-base \
cmake \
&& mkdir -p build \
&& cd build \
&& cmake .. \
&& make -j$(nproc) \
&& make install \
&& du -h /usr/local/bin/control_server
#----------------------------------------#
FROM docker.io/nginx:${NGINX_TAG}
ENV DOMAIN="example.localhost"
ENV SERVER_ID="server"
ENV RELOAD_FILE="/var/run/nginx-reload"
ENV ACME_CHALLENGE_URL="http://acme-challenge.${DOMAIN}/.well-known/acme-challenge/"
ENV CONTROL_DOMAIN="control.localhost"
ENV CONTROL_TOKEN="Tr0ub4dor&3"
RUN apk add --no-cache \
inotify-tools # For reloading
# Copy the configuration files
COPY nginx /etc/nginx/
# Copy the dummy certificate files
COPY certificates /etc/ssl/
# Copy the entrypoint scripts
COPY --chmod=0755 docker-entrypoint.d /docker-entrypoint.d/
COPY --from=builder /usr/local/bin/control_server /usr/local/bin/control_server
# Copy the scripts
COPY --chmod=0755 scripts/ /opt/scripts/
RUN ln -s /opt/scripts/reload.sh /usr/local/bin/reload
# Create the volumes for certificates and website files
VOLUME /etc/ssl/certs
VOLUME /var/www/html
# Expose the ports for HTTP and HTTPS
EXPOSE 80/tcp 443/tcp 443/udp

View file

@ -9,6 +9,7 @@ This is a container that helps host a static website.
**Requires** the following environment variables:
- `DOMAIN`: The domain
- `ACME_CHALLENGE_HOST`: The source of `.well-known/acme-challenge`
- `CONTROL_TOKEN`: Token to access the control server
You're also encouraged to provide your own:
- `/etc/ssl/dhparam.pem`, generated with:
@ -22,5 +23,23 @@ You're also encouraged to provide your own:
- `SERVER_ID`: How to call this server (for info)
- **Mount** `/var/run/nginx-reload`: modify this file to reload nginx
## Control server
Authorize as you normally would with a Bearer token.
If you get an empty response, watch the status code!
Response format:
```json
{
"status": "ok|client_error|server_error",
"message": "Optional, human-readable feedback message"
}
```
Endpoints:
- `/`: Health check
- `/reload`: Reloads nginx
- `/certificate/<domain>`: Uploads a certificate (POST, upload like a form with field names `certificate` and `private_key`)
## TODO
- support for multiple domains

View file

@ -0,0 +1,110 @@
cmake_minimum_required(VERSION 3.25)
# ──────────────────────────────────────────────────────────────
# Project meta data
# ──────────────────────────────────────────────────────────────
project(
control_server
LANGUAGES CXX
)
# ──────────────────────────────────────────────────────────────
# Build type & C++ standard
# ──────────────────────────────────────────────────────────────
if (NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
message(FATAL_ERROR "Unsupported compiler: ${CMAKE_CXX_COMPILER_ID}. Only Clang and GNU compilers are supported.")
endif()
if (NOT CMAKE_CONFIGURATION_TYPES AND NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE MinSizeRel CACHE STRING "Build type" FORCE)
endif()
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# ──────────────────────────────────────────────────────────────
# Output layout (bin/ lib/ inside the build dir)
# ──────────────────────────────────────────────────────────────
include(GNUInstallDirs)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
# ──────────────────────────────────────────────────────────────
# LTO / IPO
# ──────────────────────────────────────────────────────────────
set_property(GLOBAL PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE)
set_property(GLOBAL PROPERTY INTERPROCEDURAL_OPTIMIZATION_MINSIZEREL TRUE)
# ──────────────────────────────────────────────────────────────
# Helper functions
# ──────────────────────────────────────────────────────────────
function(enable_warnings tgt)
target_compile_options(${tgt} PRIVATE
-Wall -Wextra -Wpedantic
-Wconversion -Wshadow
-Wduplicated-cond -Wduplicated-branches
$<$<CXX_COMPILER_ID:Clang>:-Wstrict-aliasing>
)
endfunction()
function(enable_size_optimizations tgt)
# Put the ultra-tiny flags only on Release/MinsizeRel
target_compile_options(${tgt} PRIVATE
$<$<CONFIG:MinSizeRel,Release>:-Os>
-ffunction-sections -fdata-sections
-fno-rtti -fno-unwind-tables
-fno-asynchronous-unwind-tables -fomit-frame-pointer
-fno-ident -pipe
)
target_link_options(${tgt} PRIVATE
$<$<CONFIG:Debug>:-Wl,-Map=${tgt}.map>
$<$<CONFIG:MinSizeRel,Release>:-Wl,--gc-sections>
$<$<CONFIG:MinSizeRel,Release>:-s>
)
endfunction()
function(enable_static tgt)
target_link_options(${tgt} PRIVATE
$<$<CONFIG:MinSizeRel,Release>:-static>
$<$<CONFIG:MinSizeRel,Release>:-static-libstdc++>
$<$<CONFIG:MinSizeRel,Release>:-static-libgcc>
)
endfunction()
# ──────────────────────────────────────────────────────────────
# Position Independent Code (PIC)
# ──────────────────────────────────────────────────────────────
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fPIE -pie")
# ──────────────────────────────────────────────────────────────
# Target
# ──────────────────────────────────────────────────────────────
add_executable(control_server
src/control_server.cpp
src/nginx_process.cpp
src/certificate.cpp
src/main.cpp
)
target_include_directories(control_server PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_link_libraries(control_server PRIVATE
stdc++fs
)
enable_warnings(control_server)
enable_size_optimizations(control_server)
enable_static(control_server)
# ──────────────────────────────────────────────────────────────
# Install
# ──────────────────────────────────────────────────────────────
install(
TARGETS control_server
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

View file

@ -0,0 +1,17 @@
#pragma once
#include <filesystem>
#include <string>
class CertificateManager {
public:
CertificateManager(const std::filesystem::path& certificate_directory_path);
bool certificate_exists(const std::string& domain) const;
void write_private_key(const std::string& domain, const std::string& private_key) const;
void write_certificate(const std::string& domain, const std::string& certificate) const;
private:
std::filesystem::path certificate_root_path_;
inline std::filesystem::path get_certificate_directory(const std::string& domain) const;
};

View file

@ -0,0 +1,22 @@
#pragma once
#include "httplib.hpp"
#include "nginx_process.hpp"
#include "certificate.hpp"
#include <string>
class ControlServer {
public:
ControlServer(const std::string& socket_path, NginxProcess& nginx_process, CertificateManager& certificate_manager);
void start();
void listen();
private:
std::string socket_path_;
NginxProcess& nginx_process_;
CertificateManager& certificate_manager_;
httplib::Server server_;
void setup_routes();
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,13 @@
#pragma once
#include <fstream>
class NginxProcess {
public:
NginxProcess(const std::string& pid_file_path);
pid_t read_pid() const;
void reload() const;
private:
std::string pid_file_path_;
};

View file

@ -0,0 +1,35 @@
#include "certificate.hpp"
#include <fstream>
#include <filesystem>
#include <system_error>
void write_to_file(std::filesystem::path path, const std::string& content) {
std::ofstream private_key_file;
private_key_file.exceptions(std::ios::failbit | std::ios::badbit);
// These lines will throw
private_key_file.open(path);
private_key_file << content;
}
CertificateManager::CertificateManager(const std::filesystem::path& certificate_root_path)
: certificate_root_path_(certificate_root_path) {}
inline std::filesystem::path CertificateManager::get_certificate_directory(const std::string& domain) const {
return certificate_root_path_ / domain;
}
bool CertificateManager::certificate_exists(const std::string& domain) const {
return std::filesystem::exists(get_certificate_directory(domain));
}
void CertificateManager::write_private_key(const std::string& domain, const std::string& private_key) const {
std::filesystem::path path = get_certificate_directory(domain) / "privkey.pem";
write_to_file(domain, private_key.c_str());
}
void CertificateManager::write_certificate(const std::string& domain, const std::string& private_key) const {
std::filesystem::path path = get_certificate_directory(domain) / "fullchain.pem";
write_to_file(domain, private_key.c_str());
}

View file

@ -0,0 +1,101 @@
#include "control_server.hpp"
#include "nginx_process.hpp"
ControlServer::ControlServer(const std::string& socket_path, NginxProcess& nginx_process, CertificateManager& certificate_manager)
: socket_path_(socket_path), nginx_process_(nginx_process), certificate_manager_(certificate_manager) {}
inline void response_ok(httplib::Response &res) {
res.set_content("{\"status\": \"ok\"}", "application/json");
}
inline void response_client_error(httplib::Response &res, const std::string &message, const httplib::StatusCode status) {
res.status = status;
res.set_content("{\"status\": \"client_error\", \"message\": \"" + message + "\"}", "application/json");
}
inline void response_server_error(httplib::Response &res, const std::string &message, const httplib::StatusCode status) {
res.status = status;
res.set_content("{\"status\": \"server_error\", \"message\": \"" + message + "\"}", "application/json");
}
void ControlServer::setup_routes() {
server_.Get("/", [](const httplib::Request& /* req */, httplib::Response &res) {
std::cout << "Received health request" << std::endl;
response_ok(res);
});
server_.Get("/reload", [this](const httplib::Request& /* req */, httplib::Response &res) {
std::cout << "Received reload request" << std::endl;
try
{
nginx_process_.reload();
response_ok(res);
std::cout << "Nginx reloaded successfully" << std::endl;
}
catch(const std::exception& e)
{
response_server_error(res, e.what(), httplib::StatusCode::InternalServerError_500);
std::cout << "Failed to reload Nginx: " << e.what() << std::endl;
}
});
server_.Get("/certificate/:domain", [](const httplib::Request& /* req */, httplib::Response &res) {
res.status = httplib::StatusCode::MethodNotAllowed_405;
// TODO maybe we could do something here?
});
server_.Post("/certificate/:domain", [this](const httplib::Request& req, httplib::Response &res) {
// TODO do we need to sanitize?
std::string domain = req.get_param_value("domain");
if (!req.form.has_file("certificate") || !req.form.has_file("private_key")) {
response_client_error(res, "Missing certificate and/or private_key fields", httplib::StatusCode::BadRequest_400);
return;
}
if (!certificate_manager_.certificate_exists(domain)) {
response_client_error(res, "Invalid domain", httplib::StatusCode::BadRequest_400);
return;
}
httplib::FormData private_key_field = req.form.get_file("private_key");
httplib::FormData certificate_field = req.form.get_file("certificate");
// TODO more safeguards
try {
certificate_manager_.write_private_key(domain, private_key_field.content);
certificate_manager_.write_certificate(domain, certificate_field.content);
} catch (const std::exception& e) {
response_server_error(res, e.what(), httplib::StatusCode::InternalServerError_500);
std::cout << "Failed to load certificates: " << e.what() << std::endl;
}
});
}
void bind_to_socket(httplib::Server &server, const std::string &socket_path) {
unlink(socket_path.c_str()); // Remove existing socket file if it exists
server.set_address_family(AF_UNIX);
if (!server.bind_to_port(socket_path, 80)) {
throw std::runtime_error("Failed to bind to socket");
}
if (chmod(socket_path.c_str(), 0666) == -1) { // TODO Adjust permissions as needed
throw std::runtime_error("Failed to set socket permissions");
}
}
void ControlServer::start() {
setup_routes();
bind_to_socket(server_, socket_path_);
}
void ControlServer::listen() {
if (!server_.listen_after_bind()) {
throw std::runtime_error("Failed to start listening on socket");
}
}

View file

@ -0,0 +1,27 @@
#include "control_server.hpp"
int main(int argc, char* argv[]) {
if (argc < 4) {
std::cerr << "Usage: " << argv[0] << " <socket_path> <pid_file_path> <certificate_path>" << std::endl;
return 1;
}
const char* SOCKET_PATH = argv[1];
const char* NGINX_PID_FILE_PATH = argv[2];
const char* CERTIFICATE_PATH = argv[3];
NginxProcess nginx_process(NGINX_PID_FILE_PATH);
CertificateManager certificate_manager(CERTIFICATE_PATH);
ControlServer control_server(SOCKET_PATH, nginx_process, certificate_manager);
try {
control_server.start();
std::cout << "Control server is listening on " << SOCKET_PATH << std::endl;
control_server.listen();
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
}

View file

@ -0,0 +1,34 @@
#include "nginx_process.hpp"
#include <csignal>
#include <cstring>
NginxProcess::NginxProcess(const std::string& pid_file_path)
: pid_file_path_(pid_file_path) {}
pid_t NginxProcess::read_pid() const {
std::ifstream pid_file(pid_file_path_);
if (!pid_file.is_open()) {
throw std::runtime_error("Failed to open PID file: " + pid_file_path_);
}
pid_t pid = -1;
pid_file >> pid;
if (pid_file.fail() || pid <= 0) {
pid_file.close();
throw std::runtime_error("Invalid PID read from file");
}
pid_file.close();
return pid;
}
void NginxProcess::reload() const {
pid_t pid = read_pid();
if (kill(pid, SIGHUP) == -1) {
throw std::runtime_error("Failed to send SIGHUP to Nginx with PID: " + std::to_string(pid) + ". Reason: " + strerror(errno));
}
}

View file

@ -6,11 +6,13 @@ services:
environment:
DOMAIN: example.localhost
SERVER_ID: development
ACME_CHALLENGE_URL: http://localhost/acme-challenge
ACME_CHALLENGE_URL: https://files.catbox.moe/xpfyfh
CONTROL_DOMAIN: control.localhost
CONTROL_TOKEN: Tr0ub4dor&3
ports:
- "80:80"
- "443:443"
- "443:443/udp"
- "8080:80"
- "8443:443"
- "8443:443/udp"
volumes:
- ./run/html:/var/www/html
- ./run/certs:/etc/ssl/certs

View file

@ -1,17 +1,29 @@
#!/bin/sh
set -euo pipefail
ls -la /etc/ssl
ls -la /etc/nginx
: "${DOMAIN?Error: DOMAIN environment variable is not set.}"
: "${CONTROL_DOMAIN?Error: CONTROL_DOMAIN environment variable is not set.}"
CERTIFICATE_ROOT="/etc/ssl"
CERTIFICATE_DOMAIN_ROOT="$CERTIFICATE_ROOT/certs/$DOMAIN"
mkdir -p "$CERTIFICATE_DOMAIN_ROOT"
setup_snakeoil_cert() {
local domain="$1"
local cert_dir="$CERTIFICATE_ROOT/certs/$domain"
cp -n "$CERTIFICATE_ROOT/snakeoil.pem" "$CERTIFICATE_DOMAIN_ROOT/fullchain.pem"
cp -n "$CERTIFICATE_ROOT/snakeoil.key" "$CERTIFICATE_DOMAIN_ROOT/privkey.pem"
mkdir -p "$cert_dir"
chmod 700 "$CERTIFICATE_DOMAIN_ROOT/fullchain.pem"
chmod 700 "$CERTIFICATE_DOMAIN_ROOT/privkey.pem"
cp -n "$CERTIFICATE_ROOT/snakeoil.pem" "$cert_dir/fullchain.pem"
cp -n "$CERTIFICATE_ROOT/snakeoil.key" "$cert_dir/privkey.pem"
chmod 644 "$cert_dir/fullchain.pem"
chmod 600 "$cert_dir/privkey.pem"
}
for domain_to_setup in "$DOMAIN" "$CONTROL_DOMAIN"; do
echo "Ensuring certificate for domain: $domain_to_setup"
setup_snakeoil_cert "$domain_to_setup"
done
echo "Placeholder certificate setup complete."

View file

@ -0,0 +1,9 @@
#!/bin/sh
set -euo pipefail
CONTROL_SERVER_SOCKET="/var/run/control-server.sock"
NGINX_PID_FILE="/var/run/nginx.pid"
CERTIFICATE_PATH="/etc/ssl/certs"
control_server $CONTROL_SERVER_SOCKET $NGINX_PID_FILE $CERTIFICATE_PATH &

View file

@ -1,7 +0,0 @@
#!/bin/sh
set -euo pipefail
echo "Starting reload watcher..."
/opt/scripts/watch-reload.sh &

View file

@ -17,9 +17,7 @@ http {
'$status $body_bytes_sent bytes "$http_referer" '
'"$http_x_forwarded_for"';
# While I removed PII from the above log format, still better not logging
access_log /dev/null main; # /var/log/nginx/access.log main;
access_log /var/log/nginx/access.log main; # /dev/null to disable
server_tokens off;
@ -83,7 +81,8 @@ http {
server_name _;
return 444;
default_type text/plain;
return 200 "OK"; # 444 makes no sense here because we have sent the certificate already
}
include /etc/nginx/conf.d/*.conf;

View file

@ -0,0 +1,21 @@
server {
listen 443 ssl;
listen 443 quic;
listen [::]:443 ssl;
listen [::]:443 quic;
server_name ${CONTROL_DOMAIN};
ssl_certificate /etc/ssl/certs/${CONTROL_DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/ssl/certs/${CONTROL_DOMAIN}/privkey.pem;
location / {
default_type text/plain;
if ($http_authorization != "Bearer ${CONTROL_TOKEN}") {
return 200 "OK"; # 444 makes no sense here because we have sent the certificate already
}
proxy_pass http://unix:/var/run/control-server.sock;
}
}

View file

@ -12,7 +12,8 @@ server {
root /var/www/html/${DOMAIN};
index index.html;
location .well-known/acme-challenge {
location /.well-known/acme-challenge {
proxy_buffering off;
proxy_pass ${ACME_CHALLENGE_URL};
}
}

View file

@ -1,15 +0,0 @@
#!/bin/sh
set -euo pipefail
touch "$RELOAD_FILE"
# The loop will run once every time the file is saved.
while inotifywait -e close_write "$RELOAD_FILE"; do
echo "File '$RELOAD_FILE' changed. Reloading."
if nginx -t; then
nginx -s reload
else
echo "Nginx configuration is invalid. Skipping reload."
fi
done