diff --git a/.github/workflows/changelog.yaml b/.github/workflows/changelog.yaml new file mode 100644 index 0000000..447a84b --- /dev/null +++ b/.github/workflows/changelog.yaml @@ -0,0 +1,40 @@ +name: Changelog + +on: + workflow_call: + push: + tags: + - "v*.*.*" + +jobs: + changelog: + name: Generate and publish changelog + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Generate a changelog + uses: orhun/git-cliff-action@v4 + id: git-cliff + with: + config: cliff.toml + args: --verbose --latest --strip header + env: + OUTPUT: CHANGELOG.md + GITHUB_REPO: ${{ github.repository }} + + - name: Polish changelog + shell: bash + run: sed -i '1,2d' CHANGELOG.md + + - name: Upload the changelog + uses: ncipollo/release-action@v1 + with: + # draft: true + allowUpdates: true + bodyFile: CHANGELOG.md diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml new file mode 100644 index 0000000..11931fc --- /dev/null +++ b/.github/workflows/codeql.yaml @@ -0,0 +1,98 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: + - main + - dev + pull_request: + branches: + - main + - dev + schedule: + - cron: '19 8 * * 2' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: c-cpp + build-mode: autobuild + - language: python + build-mode: none + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/linting.yaml b/.github/workflows/linting.yaml new file mode 100644 index 0000000..787f45c --- /dev/null +++ b/.github/workflows/linting.yaml @@ -0,0 +1,60 @@ +name: Lint and Format + +on: + push: + branches: + - main + - dev + pull_request: + branches: + - main + - dev + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Setup environment + run: | + python3 -m venv venv + ./venv/bin/pip install pylint + + - name: Run pylint + run: | + ./venv/bin/pylint tests + + formatting: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Setup environment + run: | + python3 -m venv venv + ./venv/bin/pip install black + + - name: Run formatters + run: | + ./venv/bin/black tests + + - name: Check for changes + run: | + if [[ -n $(git status --porcelain) ]]; then + echo "Please commit the following formatting changes:\n" + git diff + exit 1 + fi diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..32f5fed --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,39 @@ +on: + push: + branches: + - main + tags: + - 'v*' + +name: Release Packaging + +permissions: + contents: write + +jobs: + tests: + uses: ./.github/workflows/testing.yaml + + changelog: + uses: ./.github/workflows/changelog.yaml + + release: + needs: [tests, changelog] + name: Release Packaging + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Release Build + run: make + + - name: Create Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ github.ref_name }} + run: | + gh release create "$tag" \ + --repo="$GITHUB_REPOSITORY" \ + --title="${tag#v}" \ + --generate-notes diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml new file mode 100644 index 0000000..77e571e --- /dev/null +++ b/.github/workflows/testing.yaml @@ -0,0 +1,61 @@ +name: Testing + +on: + workflow_call: + push: + branches: + - main + - dev + pull_request: + branches: + - main + - dev + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: make + run: | + make + mkdir bin + bash -c 'mv {serve,serve_cov,serve_debug} bin/' + + - name: Temporarily save artifacts + uses: actions/upload-artifact@v4 + with: + name: artifacts + path: bin + retention-days: 1 + + test: + needs: [build] + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Retrieve saved artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts + path: bin + + - name: Move artifacts to project root + run: | + chmod +x bin/* + mv bin/* . + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Run tests + run: | + cd tests + python test.py diff --git a/README.md b/README.md index 654d63a..dc921dc 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,84 @@ # My Blog Technology -This is a minimal web server designed to serve my blog. I'm writing it to be robust enough to face the public internet. You can see it in action at http://playin.coz.is/index.html. You probably can't get it to crash, but feel free to try! And if you manage to do it, send me an email to show off! I'll leave the coolest attempts in `attempts.txt`. + +![Builds](https://github.com/cozis/blogtech/actions/workflows/testing.yaml/badge.svg) +[![GitHub Release](https://img.shields.io/github/release/cozis/blogtech.svg)](https://github.com/cozis/blogtech/releases/latest) +[![License](https://img.shields.io/github/license/cozis/blogtech.svg)](https://github.com/cozis/blogtech/blob/main/UNLICENSE) + +This is a minimal web server designed to host my blog. It's built from scratch to be robust enough to face the public internet. No reverse proxies required! You can see it in action at http://playin.coz.is/index.html. + +I asked [Reddit](https://www.reddit.com/r/C_Programming/comments/1falo3b/using_my_c_web_server_to_host_a_blog_you_cant/) to [hack](https://www.reddit.com/r/hacking/comments/1fcc5hd/im_using_my_custom_c_webserver_to_host_my_blog_no/) me, which resulted in gigabytes of hilarious and malicious request logs. I saved some in `attempts.txt`, and may dig out a few more for fun someday :^) + +There is also a discussion on [Hacker News](https://news.ycombinator.com/item?id=41642151) + +Feel free to help! At this time the main focus is on semantic correctess of HTTP and testing. I try to keep the main branch stable so remember to target the dev branch with PRs. Changes to README are fine to do on main though. + +# But.. Why? +I enjoy making my own tools and I'm a bit tired of hearing that everything needs to be "battle-tested." So what it will crash? Bugs can be fixed :^) # Specs -- Only runs on Linux -- HTTP/1.1 support with pipelining and keep-alive -- HTTPS (TLS 1.2 using BearSSL) -- Uses request and connection timeouts -- Access log, log file rotation, hard disk usage limits -- No `Transfer-Encoding: Chunked` (when receiving a chunked request the server responds with `411 Length Required`, prompting the client to try again with the `Content-Length` header) -- Static file serving utilities +- Linux only +- Implements HTTP/1.1, pipelining, and keep-alive connections +- HTTPS support (up to TLS 1.2 using BearSSL) +- Minimal dependencies (libc and BearSSL when using HTTPS) +- Configurable timeouts +- Access logs, crash logs, log rotation, disk usage limits +- No `Transfer-Encoding: Chunked` (responds with `411 Length Required`, prompting the client to resend with `Content-Length`) - Single core (This will probably change when I get a better VPS) +- No static file caching (yet) + +# Benchmarks +The focus of the project is robustness, but it's definitely not slow. Here's a quick comparison agains nginx (static endpoint, both single-threaded, 1K connection limit) +``` +(blogtech) +$ wrk -c 500 -d 5s http://127.0.0.1:80/hello +Running 5s test @ http://127.0.0.1:80/hello + 2 threads and 500 connections + Thread Stats Avg Stdev Max +/- Stdev + Latency 6.66ms 3.71ms 48.87ms 92.30% + Req/Sec 39.59k 6.43k 50.60k 67.35% + 385975 requests in 5.01s, 30.55MB read +Requests/sec: 76974.24 +Transfer/sec: 6.09MB + +(nginx) +$ wrk -c 500 -d 5s http://127.0.0.1:8080/hello +Running 5s test @ http://127.0.0.1:8080/hello + 2 threads and 500 connections + Thread Stats Avg Stdev Max +/- Stdev + Latency 149.11ms 243.02ms 934.12ms 81.80% + Req/Sec 24.97k 16.87k 57.73k 61.11% + 224790 requests in 5.08s, 42.01MB read +Requests/sec: 44227.78 +Transfer/sec: 8.27MB +``` -# Building -By default the server is built without HTTPS and you can do so by doing: +Nginx uses this configuration: +``` +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + server { + listen 8080; + location /hello { + add_header Content-Type text/plain; + return 200 "Hello, world!"; + } + } +} +``` + +# Build & Run +By default the server build is HTTP-only: ``` $ make ``` -this will generate `serve`, `serve_cov`, and `serve_debug`. These are respectively release, coverage, and debug build. Unless you're modifying the source you need to use `serve`. +this generates the executables: `serve` (release build), `serve_cov` (coverage build), and `serve_debug` (debug build). Release builds listen on port 80; debug builds on port 8080. -If you want to enable HTTPS, you'll need to create a `3p` directory (in the same folder as this README) and clone BearSSL in it. Then you'll need to build it. +To enable HTTPS, you'll need to clone BearSSL and build it. You can do so by running these commands from the root folder of this repository: ``` $ mkdir 3p $ cd 3p @@ -28,12 +88,66 @@ $ make -j $ cd ../../ $ make -B HTTPS=1 ``` -which will produce the same executables but with HTTPS enabled. Your private key `key.pem` and certificate `cert.pem` will need to be stored in the same folder as the executable. +The same executables will be generated, but with secure connections on port 443 (release) or 8081 (debug). + +Place your `cert.pem` and `key.pem` files in the same directory as the executable. You can customiza names and locations by changing: +```c +#define HTTPS_KEY_FILE "key.pem" +#define HTTPS_CERT_FILE "cert.pem" +``` + +For testing locally with HTTPS, generate a self-signed certificate (and private key): +``` +openssl genpkey -algorithm RSA -out key.pem -pkeyopt rsa_keygen_bits:2048 +openssl req -new -x509 -key key.pem -out cert.pem -days 365 +``` + +# Usage +The server serves static content from the `docroot/` folder. You can change this by modifying the `respond` function: +```c +typedef struct { + Method method; + string path; + int major; + int minor; + int nheaders; + Header headers[MAX_HEADERS]; + string content; +} Request; -NOTE: If you already built the files and want to build them again with a different HTTPS setting, you'll need to force the build with the `-B` option +void respond(Request request, ResponseBuilder *b) +{ + if (request.major != 1 || request.minor > 1) { + status_line(b, 505); // HTTP Version Not Supported + return; + } + + if (request.method != M_GET) { + status_line(b, 405); // Method Not Allowed + return; + } + + if (string_match_case_insensitive(request.path, LIT("/hello"))) { + status_line(b, 200); + append_content_s(b, LIT("Hello, world!")); + return; + } + + if (serve_file_or_dir(b, LIT("/"), LIT("docroot/"), request.path, NULLSTR, false)) + return; + + status_line(b, 404); + append_content_s(b, LIT("Nothing here :|")); +} +``` +you can add your endpoints here by switching on the `request.path` field. Note that the path is just a slice into the request buffer. URIs are not parsed. # Testing -I routinely run the server under valgrind or sanitizers (address, undefined) and target it using `wrk`. I'm also adding automatized tests to `tests/test.py` to check compliance with the HTTP/1.1 spec. +I routinely run the server under valgrind and sanitizers (address, undefined) and target it using `wrk`. I'm also adding automatized tests to `tests/test.py` to check compliance with the HTTP/1.1 spec. I also use it to host my website and post it here and there to keep it under stress.Turns out, all of those bots scanning he internet for vulnerable websites make great fuzzers! # Known Issues - Server replies to HTTP/1.0 clients as HTTP/1.1 +- Server rejects HEAD requests + +# Contributing +I usually work on the [DEV](https://github.com/cozis/blogtech/tree/dev) branch and merge into [MAIN](https://github.com/cozis/blogtech/tree/main) once in a while. If you open a pull requests remember to target [DEV](https://github.com/cozis/blogtech/tree/dev). It will make things easier! diff --git a/docroot/index.html b/docroot/index.html index 7f88bb9..f493c21 100644 --- a/docroot/index.html +++ b/docroot/index.html @@ -149,7 +149,7 @@

Projects

-

BPR Renderer

+

PBR Renderer

It's a 3D renderer I built to dive deeper into graphics programming, using as reference the great LearnOpenGL tutorial and Google's Filament. The renderer implements Physically Based Rendering (PBR), a technique that simulates light and shadows to closely mimic real-world behavior. It supports shadow mapping, Image-Based Lighting (IBL), and can load meshes from external files.

diff --git a/tests/test.py b/tests/test.py index c05cb0b..1ec632d 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1,6 +1,7 @@ import time import socket import subprocess +import sys from typing import Optional from dataclasses import dataclass @@ -151,3 +152,6 @@ def run_test(test, addr, port): print("passed: ", passed, "/", total, sep="") p.terminate() p.wait() + +if passed < total: + sys.exit(1)