servery 1.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- servery-1.1.1/.github/dependabot.yml +17 -0
- servery-1.1.1/.github/workflows/ci.yml +131 -0
- servery-1.1.1/.github/workflows/release.yml +52 -0
- servery-1.1.1/.github/workflows/security.yml +54 -0
- servery-1.1.1/.gitignore +26 -0
- servery-1.1.1/.pre-commit-config.yaml +36 -0
- servery-1.1.1/CHANGELOG.md +178 -0
- servery-1.1.1/CONTRIBUTING.md +56 -0
- servery-1.1.1/LICENSE +21 -0
- servery-1.1.1/Makefile +33 -0
- servery-1.1.1/PKG-INFO +139 -0
- servery-1.1.1/README.md +110 -0
- servery-1.1.1/docs/ARCHITECTURE.md +677 -0
- servery-1.1.1/docs/BEST-PRACTICES.md +725 -0
- servery-1.1.1/docs/PRINCIPLES.md +305 -0
- servery-1.1.1/docs/REFERENCES.md +348 -0
- servery-1.1.1/docs/REQUIREMENTS.md +1011 -0
- servery-1.1.1/docs/ROADMAP.md +605 -0
- servery-1.1.1/docs/STANDARDS.md +460 -0
- servery-1.1.1/docs/TRANSPORTS.md +321 -0
- servery-1.1.1/docs/VISION.md +159 -0
- servery-1.1.1/pyproject.toml +149 -0
- servery-1.1.1/scripts/bench.py +126 -0
- servery-1.1.1/scripts/check_zero_deps.py +47 -0
- servery-1.1.1/scripts/microbench.py +81 -0
- servery-1.1.1/src/servery/__init__.py +26 -0
- servery-1.1.1/src/servery/__main__.py +5 -0
- servery-1.1.1/src/servery/_log.py +28 -0
- servery-1.1.1/src/servery/_oscrypto.py +161 -0
- servery-1.1.1/src/servery/_version.py +3 -0
- servery-1.1.1/src/servery/archive.py +53 -0
- servery-1.1.1/src/servery/auth.py +81 -0
- servery-1.1.1/src/servery/cli.py +207 -0
- servery-1.1.1/src/servery/config.py +114 -0
- servery-1.1.1/src/servery/handler.py +710 -0
- servery-1.1.1/src/servery/http2/__init__.py +1 -0
- servery-1.1.1/src/servery/http2/connection.py +297 -0
- servery-1.1.1/src/servery/http2/frames.py +640 -0
- servery-1.1.1/src/servery/http2/hpack.py +839 -0
- servery-1.1.1/src/servery/http3.py +136 -0
- servery-1.1.1/src/servery/listing.py +775 -0
- servery-1.1.1/src/servery/py.typed +0 -0
- servery-1.1.1/src/servery/ranges.py +89 -0
- servery-1.1.1/src/servery/security.py +53 -0
- servery-1.1.1/src/servery/server.py +120 -0
- servery-1.1.1/src/servery/upload.py +229 -0
- servery-1.1.1/tests/__init__.py +0 -0
- servery-1.1.1/tests/_harness.py +71 -0
- servery-1.1.1/tests/test_archive.py +64 -0
- servery-1.1.1/tests/test_auth.py +73 -0
- servery-1.1.1/tests/test_h2frames.py +455 -0
- servery-1.1.1/tests/test_handler_helpers.py +77 -0
- servery-1.1.1/tests/test_hpack.py +555 -0
- servery-1.1.1/tests/test_http1_conformance.py +250 -0
- servery-1.1.1/tests/test_http2_conformance.py +233 -0
- servery-1.1.1/tests/test_http2_server.py +243 -0
- servery-1.1.1/tests/test_http3.py +57 -0
- servery-1.1.1/tests/test_listing.py +230 -0
- servery-1.1.1/tests/test_oscrypto.py +60 -0
- servery-1.1.1/tests/test_performance.py +90 -0
- servery-1.1.1/tests/test_ranges.py +62 -0
- servery-1.1.1/tests/test_request_parsing.py +107 -0
- servery-1.1.1/tests/test_security.py +64 -0
- servery-1.1.1/tests/test_security_regression.py +125 -0
- servery-1.1.1/tests/test_server.py +654 -0
- servery-1.1.1/tests/test_smoke.py +123 -0
- servery-1.1.1/tests/test_upload.py +104 -0
- servery-1.1.1/tests/test_upload_robustness.py +153 -0
- servery-1.1.1/uv.lock +938 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
# Keep GitHub Actions current (and enable SHA-pin maintenance).
|
|
4
|
+
- package-ecosystem: github-actions
|
|
5
|
+
directory: "/"
|
|
6
|
+
schedule:
|
|
7
|
+
interval: weekly
|
|
8
|
+
commit-message:
|
|
9
|
+
prefix: ci
|
|
10
|
+
|
|
11
|
+
# Dev/CI tooling only (runtime deps are zero by mandate).
|
|
12
|
+
- package-ecosystem: uv
|
|
13
|
+
directory: "/"
|
|
14
|
+
schedule:
|
|
15
|
+
interval: weekly
|
|
16
|
+
commit-message:
|
|
17
|
+
prefix: deps
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
# Least privilege by default; jobs opt into more if needed.
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
concurrency:
|
|
14
|
+
group: ci-${{ github.ref }}
|
|
15
|
+
cancel-in-progress: true
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
lint:
|
|
19
|
+
name: Lint & format (ruff)
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
23
|
+
with:
|
|
24
|
+
persist-credentials: false
|
|
25
|
+
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
|
26
|
+
with:
|
|
27
|
+
enable-cache: true
|
|
28
|
+
- run: uv run ruff check --output-format=github .
|
|
29
|
+
- run: uv run ruff format --check .
|
|
30
|
+
|
|
31
|
+
type:
|
|
32
|
+
name: Type check (ty)
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
36
|
+
with:
|
|
37
|
+
persist-credentials: false
|
|
38
|
+
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
|
39
|
+
with:
|
|
40
|
+
enable-cache: true
|
|
41
|
+
# httpx is a dev-only interop dependency used in tests; give ty access to
|
|
42
|
+
# it (in its isolated env) so it resolves and type-checks that code.
|
|
43
|
+
- run: uvx --with "httpx[http2]" ty check src tests
|
|
44
|
+
|
|
45
|
+
security:
|
|
46
|
+
name: SAST (bandit)
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
50
|
+
with:
|
|
51
|
+
persist-credentials: false
|
|
52
|
+
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
|
53
|
+
with:
|
|
54
|
+
enable-cache: true
|
|
55
|
+
- run: uv run bandit -c pyproject.toml -r src
|
|
56
|
+
|
|
57
|
+
test:
|
|
58
|
+
name: Test (${{ matrix.os }}, py${{ matrix.python }})
|
|
59
|
+
needs: [lint, type, security]
|
|
60
|
+
strategy:
|
|
61
|
+
fail-fast: false
|
|
62
|
+
matrix:
|
|
63
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
64
|
+
python: ["3.13", "3.14"]
|
|
65
|
+
runs-on: ${{ matrix.os }}
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
68
|
+
with:
|
|
69
|
+
persist-credentials: false
|
|
70
|
+
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
|
71
|
+
with:
|
|
72
|
+
enable-cache: true
|
|
73
|
+
python-version: ${{ matrix.python }}
|
|
74
|
+
- run: uv run --no-default-groups --group test --python ${{ matrix.python }} coverage run -m unittest discover -s tests -v
|
|
75
|
+
- run: uv run --no-default-groups --group test --python ${{ matrix.python }} coverage report
|
|
76
|
+
|
|
77
|
+
test-freethreaded:
|
|
78
|
+
name: Test free-threaded (py${{ matrix.python }})
|
|
79
|
+
needs: [lint, type, security]
|
|
80
|
+
strategy:
|
|
81
|
+
fail-fast: false
|
|
82
|
+
matrix:
|
|
83
|
+
python: ["3.13t", "3.14t"]
|
|
84
|
+
runs-on: ubuntu-latest
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
87
|
+
with:
|
|
88
|
+
persist-credentials: false
|
|
89
|
+
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
|
90
|
+
with:
|
|
91
|
+
enable-cache: true
|
|
92
|
+
python-version: ${{ matrix.python }}
|
|
93
|
+
# PYTHON_GIL=0 ensures the GIL stays disabled so concurrency is exercised.
|
|
94
|
+
# --no-default-groups --group test avoids the publish tooling (cffi has no free-threaded build).
|
|
95
|
+
- run: PYTHON_GIL=0 uv run --no-default-groups --group test --python ${{ matrix.python }} python -m unittest discover -s tests -v
|
|
96
|
+
|
|
97
|
+
test-experimental:
|
|
98
|
+
name: Test (py${{ matrix.python }}, allowed to fail)
|
|
99
|
+
needs: [lint, type, security]
|
|
100
|
+
runs-on: ubuntu-latest
|
|
101
|
+
continue-on-error: true
|
|
102
|
+
strategy:
|
|
103
|
+
fail-fast: false
|
|
104
|
+
matrix:
|
|
105
|
+
python: ["3.15", "3.15t"]
|
|
106
|
+
steps:
|
|
107
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
108
|
+
with:
|
|
109
|
+
persist-credentials: false
|
|
110
|
+
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
|
111
|
+
with:
|
|
112
|
+
enable-cache: true
|
|
113
|
+
python-version: ${{ matrix.python }}
|
|
114
|
+
- run: uv run --no-default-groups --group test --python ${{ matrix.python }} python -m unittest discover -s tests -v
|
|
115
|
+
|
|
116
|
+
build:
|
|
117
|
+
name: Build & zero-dependency gate
|
|
118
|
+
needs: [lint, type, security]
|
|
119
|
+
runs-on: ubuntu-latest
|
|
120
|
+
steps:
|
|
121
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
122
|
+
with:
|
|
123
|
+
persist-credentials: false
|
|
124
|
+
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
|
125
|
+
with:
|
|
126
|
+
enable-cache: true
|
|
127
|
+
- run: uv build
|
|
128
|
+
- name: Enforce zero runtime dependencies
|
|
129
|
+
run: uv run python scripts/check_zero_deps.py
|
|
130
|
+
- name: Validate distribution metadata
|
|
131
|
+
run: uvx twine check dist/*
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Publishes on a version tag (vX.Y.Z), or manually via the Actions tab. Uses
|
|
4
|
+
# PyPI Trusted Publishing (OIDC): no API token is ever stored — the publish job
|
|
5
|
+
# mints a short-lived token scoped to this repo + the `pypi` environment.
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags: ["v*"]
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
build:
|
|
16
|
+
name: Build & verify distributions
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
20
|
+
with:
|
|
21
|
+
persist-credentials: false
|
|
22
|
+
# No build cache in the publish path: a poisoned cache could taint the
|
|
23
|
+
# released artifacts (zizmor cache-poisoning). Releases build clean.
|
|
24
|
+
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
|
25
|
+
with:
|
|
26
|
+
enable-cache: false
|
|
27
|
+
- run: uv build
|
|
28
|
+
- name: Enforce zero runtime dependencies
|
|
29
|
+
run: uv run python scripts/check_zero_deps.py
|
|
30
|
+
- name: Validate distribution metadata
|
|
31
|
+
run: uvx twine check dist/*
|
|
32
|
+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
33
|
+
with:
|
|
34
|
+
name: dist
|
|
35
|
+
path: dist/
|
|
36
|
+
if-no-files-found: error
|
|
37
|
+
|
|
38
|
+
publish:
|
|
39
|
+
name: Publish to PyPI (Trusted Publishing)
|
|
40
|
+
needs: build
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
environment:
|
|
43
|
+
name: pypi
|
|
44
|
+
url: https://pypi.org/p/servery
|
|
45
|
+
permissions:
|
|
46
|
+
id-token: write # OIDC token for Trusted Publishing — the only privilege needed
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
49
|
+
with:
|
|
50
|
+
name: dist
|
|
51
|
+
path: dist/
|
|
52
|
+
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: Security
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
schedule:
|
|
8
|
+
- cron: "0 6 * * 1" # weekly, Monday 06:00 UTC
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
zizmor:
|
|
16
|
+
name: Audit workflows (zizmor)
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
20
|
+
with:
|
|
21
|
+
persist-credentials: false
|
|
22
|
+
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
|
23
|
+
# Audit our own GitHub Actions for injection / privilege / pinning issues.
|
|
24
|
+
# Fails on medium-or-higher findings. Actions are SHA-pinned (Dependabot
|
|
25
|
+
# maintains the pins), so unpinned-uses stays clean.
|
|
26
|
+
- run: uvx zizmor --min-severity=medium --format plain .github/workflows
|
|
27
|
+
|
|
28
|
+
bandit:
|
|
29
|
+
name: SAST (bandit)
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
33
|
+
with:
|
|
34
|
+
persist-credentials: false
|
|
35
|
+
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
|
36
|
+
- run: uv run bandit -c pyproject.toml -r src
|
|
37
|
+
|
|
38
|
+
secrets:
|
|
39
|
+
name: Secret scan (gitleaks)
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
# Dependabot PR runs get a read-only token that can't list PR commits via the
|
|
42
|
+
# API (gitleaks-action 403s). Skip them — push, schedule, and human PRs still scan.
|
|
43
|
+
if: github.actor != 'dependabot[bot]'
|
|
44
|
+
permissions:
|
|
45
|
+
contents: read
|
|
46
|
+
pull-requests: read
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
|
49
|
+
with:
|
|
50
|
+
fetch-depth: 0
|
|
51
|
+
persist-credentials: false
|
|
52
|
+
- uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2
|
|
53
|
+
env:
|
|
54
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
servery-1.1.1/.gitignore
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
|
|
7
|
+
# Build artifacts
|
|
8
|
+
build/
|
|
9
|
+
dist/
|
|
10
|
+
*.whl
|
|
11
|
+
*.tar.gz
|
|
12
|
+
|
|
13
|
+
# Test / tooling caches
|
|
14
|
+
.coverage
|
|
15
|
+
.coverage.*
|
|
16
|
+
htmlcov/
|
|
17
|
+
.mypy_cache/
|
|
18
|
+
.ruff_cache/
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
|
|
21
|
+
# Environments
|
|
22
|
+
.venv/
|
|
23
|
+
venv/
|
|
24
|
+
|
|
25
|
+
# uv
|
|
26
|
+
# (uv.lock IS committed; the virtualenv is not)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Local commit gates. Install once with: uv run pre-commit install
|
|
2
|
+
# Keep revs current with: uv run pre-commit autoupdate
|
|
3
|
+
# (mypy runs via CI / `make type`, not here — its isolated env can't resolve the
|
|
4
|
+
# package import graph cleanly.)
|
|
5
|
+
repos:
|
|
6
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
7
|
+
rev: v5.0.0
|
|
8
|
+
hooks:
|
|
9
|
+
- id: trailing-whitespace
|
|
10
|
+
- id: end-of-file-fixer
|
|
11
|
+
- id: check-yaml
|
|
12
|
+
- id: check-toml
|
|
13
|
+
- id: check-merge-conflict
|
|
14
|
+
- id: check-added-large-files
|
|
15
|
+
- id: mixed-line-ending
|
|
16
|
+
- id: detect-private-key
|
|
17
|
+
|
|
18
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
19
|
+
rev: v0.8.6
|
|
20
|
+
hooks:
|
|
21
|
+
- id: ruff
|
|
22
|
+
args: [--fix]
|
|
23
|
+
- id: ruff-format
|
|
24
|
+
|
|
25
|
+
- repo: https://github.com/PyCQA/bandit
|
|
26
|
+
rev: 1.8.0
|
|
27
|
+
hooks:
|
|
28
|
+
- id: bandit
|
|
29
|
+
args: ["-c", "pyproject.toml", "-r", "src"]
|
|
30
|
+
additional_dependencies: ["bandit[toml]"]
|
|
31
|
+
|
|
32
|
+
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
|
33
|
+
rev: v1.0.0
|
|
34
|
+
hooks:
|
|
35
|
+
- id: zizmor
|
|
36
|
+
args: ["--min-severity=medium"]
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to servery are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/), and the project uses
|
|
5
|
+
[semantic versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [1.1.1] — 2026-06-22
|
|
8
|
+
|
|
9
|
+
First release published to PyPI.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Directory listing on touch devices**: the per-file download button was
|
|
14
|
+
hover-only (invisible/untappable on phones); it is now shown via
|
|
15
|
+
`@media (hover: none)`, which also enlarges the facet chips, theme toggle, and
|
|
16
|
+
pager to finger-sized tap targets. Long filenames get `overflow-wrap` so they
|
|
17
|
+
can't force horizontal scroll on a narrow screen.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **Publishing**: releases go to PyPI via GitHub Actions **Trusted Publishing**
|
|
22
|
+
(OIDC) — no API token is stored anywhere.
|
|
23
|
+
- **Packaging**: the version is single-sourced from `servery/_version.py`; added
|
|
24
|
+
the `Changelog` project URL and the `Programming Language :: Python :: Free
|
|
25
|
+
Threading` classifier.
|
|
26
|
+
- **CI/dev**: bumped `actions/checkout` (v7), `astral-sh/setup-uv`,
|
|
27
|
+
`gitleaks-action`, and the `bandit` floor (`>=1.9.4`).
|
|
28
|
+
|
|
29
|
+
## [1.1.0] — 2026-06-22
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **Directory-listing UI/UX pass** (still zero-dependency, server-side, **no
|
|
34
|
+
JavaScript**, and safe under the existing strict CSP):
|
|
35
|
+
- Clickable **breadcrumb** trail in the heading.
|
|
36
|
+
- Per-type **file icons** (extension-based, with a stdlib `mimetypes` fallback
|
|
37
|
+
for long-tail extensions — a pure lookup, no file content is read) and
|
|
38
|
+
**relative timestamps** ("3h ago", exact time on hover).
|
|
39
|
+
- Inline **size bars** and an aggregate **metrics strip** (file/dir counts,
|
|
40
|
+
total size, largest, newest).
|
|
41
|
+
- **`?ext=` file-type facet** chips alongside the existing `?q=` filter.
|
|
42
|
+
- Pure-**SVG modification timeline** histogram.
|
|
43
|
+
- **Per-file download** affordance (`?download=1` forces
|
|
44
|
+
`Content-Disposition: attachment`).
|
|
45
|
+
- **Pagination** for large directories (`?page=`, 1000 rows/page).
|
|
46
|
+
- Cookie-backed **light/dark/auto theme** toggle (`?theme=`).
|
|
47
|
+
- Friendly **empty / no-match** states, sticky table header, `aria-sort`, and
|
|
48
|
+
visible focus styles.
|
|
49
|
+
|
|
50
|
+
### Performance
|
|
51
|
+
|
|
52
|
+
A second profiling-driven pass (cProfile / strace / timeit, benchmarked
|
|
53
|
+
before/after each change):
|
|
54
|
+
|
|
55
|
+
- **HTTP/2 HPACK**: Huffman coding is now opt-in on the encoder (raw literals by
|
|
56
|
+
default — for a file server the CPU it costs outweighs the few header bytes it
|
|
57
|
+
saves). Also fixed an O(n²) bit accumulator in `huffman_encode`. **+20%** h2
|
|
58
|
+
throughput.
|
|
59
|
+
- **HTTP/2 framing**: pack the 9-octet frame header in a single `struct` call
|
|
60
|
+
(was two packs + concatenations, ~4 allocations), byte-for-byte identical.
|
|
61
|
+
**+9%** h2; combined h2 throughput **+31%** (~11.6k → ~15k req/s, 1-core).
|
|
62
|
+
- **Path containment**: `security.is_contained` uses a separator-anchored prefix
|
|
63
|
+
test on POSIX (≈15× faster than `os.path.commonpath`, exact-match verified;
|
|
64
|
+
Windows keeps `commonpath`). Runs on every request. **+3%** small-file.
|
|
65
|
+
- **Listing render**: quote each entry name once (not twice) and cache file-type
|
|
66
|
+
extension lookups. **+5–6%** render.
|
|
67
|
+
|
|
68
|
+
## [1.0.2] — 2026-06-22
|
|
69
|
+
|
|
70
|
+
A profiling-driven performance pass (cProfile + strace, benchmarked before/after
|
|
71
|
+
each change with a new single-core `scripts/microbench.py`).
|
|
72
|
+
|
|
73
|
+
### Performance
|
|
74
|
+
|
|
75
|
+
- **Fast request-header parser**: the stdlib's email-based
|
|
76
|
+
`http.client.parse_headers` dominated per-request CPU (MIME/multipart work HTTP
|
|
77
|
+
never needs). Replaced with a line-based reader + a minimal case-insensitive
|
|
78
|
+
header map. Faithful `parse_request` (limits, versions, 0.9, expect-100, obs-fold
|
|
79
|
+
per RFC 9112 §5.2 preserved). Small-file serving **8,896 → 10,766 req/s (+21%)**
|
|
80
|
+
single-core; **~42k → ~52k req/s (+23%)** at 16-way concurrency.
|
|
81
|
+
- **Listing render**: `time.localtime` + manual date formatting instead of
|
|
82
|
+
`strftime`, and dropped a redundant `html.escape` on the already-percent-encoded
|
|
83
|
+
href. 50-entry listing **2,486 → 2,843 req/s (+14%)** single-core.
|
|
84
|
+
- **Fewer syscalls per file request**: send the body in one `sendfile` (was two);
|
|
85
|
+
skip the SPA `os.path.exists` stat when SPA is off (the default); drop a
|
|
86
|
+
`tell()` `lseek`. Per small-file GET: `sendfile` 2→1, `stat` 2.2→1.2,
|
|
87
|
+
`lseek` 3→2 (≈13→11 syscalls).
|
|
88
|
+
- Cached the constant `Server` header; guard access logging on the log level so a
|
|
89
|
+
disabled (quiet) logger does no per-request formatting.
|
|
90
|
+
|
|
91
|
+
Cumulative: small-file throughput **+24%** single-core. The large-file `sendfile`
|
|
92
|
+
path was already ~2.5 GB/s. No API or behavior changes; 295+ tests still pass.
|
|
93
|
+
|
|
94
|
+
### Added (tests)
|
|
95
|
+
|
|
96
|
+
- `test_request_parsing.py`: the fast parser (case-insensitivity, first-wins,
|
|
97
|
+
obs-fold, no-colon, EOF-termination, bad version, HTTP/2.0-in-line, HTTP/0.9).
|
|
98
|
+
- Listing: an XSS guard proving a hostile filename cannot break out of the href
|
|
99
|
+
now that it is no longer html-escaped, plus an mtime-format check.
|
|
100
|
+
- `scripts/microbench.py` (single-core attribution) and a warmup in
|
|
101
|
+
`scripts/bench.py`.
|
|
102
|
+
|
|
103
|
+
## [1.0.1] — 2026-06-22
|
|
104
|
+
|
|
105
|
+
Fixes surfaced by a large test-suite expansion (RFC reads + cross-checking
|
|
106
|
+
against httpx, curl, and h2spec).
|
|
107
|
+
|
|
108
|
+
### Performance
|
|
109
|
+
|
|
110
|
+
- **TCP_NODELAY**: every small response previously incurred a ~40 ms
|
|
111
|
+
Nagle/delayed-ACK stall. Disabling Nagle takes small-file throughput from
|
|
112
|
+
~390 to ~41,600 req/s (p50 41 ms → 0.28 ms) and listings from ~380 to
|
|
113
|
+
~14,100 req/s on loopback. The sendfile large-file path was already ~2.5 GB/s.
|
|
114
|
+
|
|
115
|
+
### Fixed
|
|
116
|
+
|
|
117
|
+
- Upload: RFC 5987/8187 `filename*` (non-ASCII filenames) was silently dropped;
|
|
118
|
+
now decoded (charset-validated) and preferred over plain `filename`.
|
|
119
|
+
- Upload: a plain non-ASCII `filename="naïve.txt"` was mojibake'd — part headers
|
|
120
|
+
are decoded as UTF-8 (RFC 7578 §5.1.1).
|
|
121
|
+
- Upload: a zero-part body (just the close-delimiter) is accepted as empty.
|
|
122
|
+
|
|
123
|
+
### Added (tests)
|
|
124
|
+
|
|
125
|
+
- HTTP/1.1 conformance (methods, 9 path-traversal vectors, conditional
|
|
126
|
+
precedence, MIME, HEAD==GET, multi-range, empty file) + httpx interop.
|
|
127
|
+
- Upload robustness (filename*, multiple files, boundary-in-content, chunked
|
|
128
|
+
rejection) + httpx multipart interop.
|
|
129
|
+
- HTTP/2 conformance (h2spec generic 50/52 + hpack 8/8 validated; padded
|
|
130
|
+
HEADERS, HPACK continuity, concurrent streams, malformed-request RST) + httpx
|
|
131
|
+
h2-over-TLS interop.
|
|
132
|
+
- Security regression + abuse cases (URI/header limits, Slowloris timeout, CRLF
|
|
133
|
+
injection e2e, upload containment, auth bypass).
|
|
134
|
+
- Performance/load smoke (Nagle regression guard, concurrent-correctness under
|
|
135
|
+
free-threading, large-file integrity) + a runnable `scripts/bench.py`.
|
|
136
|
+
|
|
137
|
+
281 tests total.
|
|
138
|
+
|
|
139
|
+
## [1.0.0] — 2026-06-21
|
|
140
|
+
|
|
141
|
+
First stable release. A zero-dependency, pure-Python HTTP file server.
|
|
142
|
+
|
|
143
|
+
### Added
|
|
144
|
+
|
|
145
|
+
- **HTTP/1.1 file serving** (pure stdlib): rich, sortable (`?C=&O=`), searchable
|
|
146
|
+
(`?q=`) directory listings with sizes and modified times; index documents.
|
|
147
|
+
- **RFC 9110 downloads**: `Range`/`206`/`416`, strong `ETag`s, the conditional
|
|
148
|
+
ladder (`If-None-Match`/`If-Modified-Since`/`If-Range` → `304`/`412`), and
|
|
149
|
+
zero-copy `socket.sendfile()` with a userspace fallback.
|
|
150
|
+
- **TLS/HTTPS** via `--tls-cert`/`--tls-key`, ALPN, HSTS over TLS, `--tls-help`.
|
|
151
|
+
- **HTTP Basic Auth** (`--auth`): single credential or pre-hashed
|
|
152
|
+
`user:sha256:…`/`sha512`, constant-time comparison, no-TLS warning.
|
|
153
|
+
- **Upload** (`--upload`): streaming `multipart/form-data` parser (no `cgi`),
|
|
154
|
+
atomic `os.replace`, bounded size (`--max-upload-size`), `--allow-overwrite`.
|
|
155
|
+
- **Archive download**: stream any directory as `tar.gz`/`zip` (`?archive=`),
|
|
156
|
+
chunked, `Content-Disposition` with `filename*`, symlink-safe.
|
|
157
|
+
- **CORS** (`--cors` + preflight), **SPA fallback** (`--spa`), **cache control**
|
|
158
|
+
(`--cache`), and secure headers (`nosniff` everywhere, scoped CSP +
|
|
159
|
+
Referrer-Policy on generated pages; `--no-security-headers`).
|
|
160
|
+
- **Hardening**: `logging`-module access logs, default socket timeout
|
|
161
|
+
(`--timeout`), optional bounded concurrency (`--max-workers`).
|
|
162
|
+
- **HTTP/2** (`--http2`): a pure-stdlib HTTP/2 server — HPACK (RFC 7541) and the
|
|
163
|
+
frame codec (RFC 9113) implemented from the RFCs, ALPN `h2` + h2c, per-stream
|
|
164
|
+
flow control, and DoS limits (concurrent-stream cap, header-block cap, RST
|
|
165
|
+
budget). Verified against `curl --http2`.
|
|
166
|
+
- **HTTP/3** via the optional `servery[http3]` (aioquic) extra; the core stays
|
|
167
|
+
zero-dependency. `servery._oscrypto` provides AES-256-GCM via `ctypes` → OS
|
|
168
|
+
OpenSSL (NIST-vector verified) as the zero-PyPI-dependency crypto foundation.
|
|
169
|
+
- **Safe defaults**: localhost bind, path-traversal + symlink-escape containment
|
|
170
|
+
(`realpath` + `commonpath`), exposure/cleartext-auth warnings.
|
|
171
|
+
- **Free-threading** support (3.13t/3.14t), full type hints (`ty`-checked), and a
|
|
172
|
+
CI gate that enforces zero runtime dependencies in the core wheel.
|
|
173
|
+
|
|
174
|
+
[1.1.1]: https://github.com/mjbommar/servery/releases/tag/v1.1.1
|
|
175
|
+
[1.1.0]: https://github.com/mjbommar/servery/releases/tag/v1.1.0
|
|
176
|
+
[1.0.2]: https://github.com/mjbommar/servery/releases/tag/v1.0.2
|
|
177
|
+
[1.0.1]: https://github.com/mjbommar/servery/releases/tag/v1.0.1
|
|
178
|
+
[1.0.0]: https://github.com/mjbommar/servery/releases/tag/v1.0.0
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Contributing to servery
|
|
2
|
+
|
|
3
|
+
## The one rule that overrides everything
|
|
4
|
+
|
|
5
|
+
**Zero third-party *runtime* dependencies.** The shipped wheel must declare no
|
|
6
|
+
`Requires-Dist`. This is enforced in CI by `scripts/check_zero_deps.py` — if a
|
|
7
|
+
runtime dependency ever appears, the build fails. Dev/CI tooling (ruff, mypy,
|
|
8
|
+
bandit, coverage, build, twine, pre-commit) is fine: it lives in the `dev`
|
|
9
|
+
[dependency group](pyproject.toml) and never ships in the wheel.
|
|
10
|
+
|
|
11
|
+
Tests use the standard-library `unittest` only — no pytest, no hypothesis.
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv sync # create the env with dev tooling
|
|
17
|
+
uv run pre-commit install # local commit gates
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## The gates
|
|
21
|
+
|
|
22
|
+
Run everything locally before pushing:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
make check # lint + type + security + test
|
|
26
|
+
make build # build wheel/sdist + zero-dependency gate
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or individually:
|
|
30
|
+
|
|
31
|
+
| Gate | Command | What it enforces |
|
|
32
|
+
|------|---------|------------------|
|
|
33
|
+
| Lint & format | `make lint` / `make format` | `ruff check` + `ruff format` |
|
|
34
|
+
| Types | `make type` | `ty check` over `src` and `tests` |
|
|
35
|
+
| Security (SAST) | `make security` | `bandit` over `src` |
|
|
36
|
+
| Tests + coverage | `make test` | `unittest` discovery, ≥90% coverage |
|
|
37
|
+
| Packaging | `make build` | builds, asserts zero runtime deps, `twine check` |
|
|
38
|
+
|
|
39
|
+
CI (`.github/workflows/`) runs the same gates plus a test matrix
|
|
40
|
+
(Linux/macOS/Windows × Python 3.13/3.14, free-threaded 3.13t/3.14t, and
|
|
41
|
+
3.15/3.15t allowed to fail), a GitHub Actions audit (`zizmor`), and a secret
|
|
42
|
+
scan (`gitleaks`). servery targets free-threaded builds as a first-class
|
|
43
|
+
configuration — no module-level mutable state, no reliance on the GIL.
|
|
44
|
+
|
|
45
|
+
## Standards
|
|
46
|
+
|
|
47
|
+
servery aims to be a conformant HTTP/1.1 origin server (RFC 9110/9111/9112);
|
|
48
|
+
see [`docs/STANDARDS.md`](docs/STANDARDS.md). New behavior should cite the RFC
|
|
49
|
+
section it implements and ship with a test. Security-relevant changes should
|
|
50
|
+
keep the safe-by-default posture described in
|
|
51
|
+
[`docs/PRINCIPLES.md`](docs/PRINCIPLES.md).
|
|
52
|
+
|
|
53
|
+
## Commits
|
|
54
|
+
|
|
55
|
+
Small, focused commits. Reference the milestone from
|
|
56
|
+
[`docs/ROADMAP.md`](docs/ROADMAP.md) where relevant.
|
servery-1.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Bommarito
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
servery-1.1.1/Makefile
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
.PHONY: install lint format type security test build check all clean
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
uv sync
|
|
5
|
+
|
|
6
|
+
lint:
|
|
7
|
+
uv run ruff check .
|
|
8
|
+
uv run ruff format --check .
|
|
9
|
+
|
|
10
|
+
format:
|
|
11
|
+
uv run ruff format .
|
|
12
|
+
uv run ruff check --fix .
|
|
13
|
+
|
|
14
|
+
type:
|
|
15
|
+
uvx --with "httpx[http2]" ty check src tests
|
|
16
|
+
|
|
17
|
+
security:
|
|
18
|
+
uv run bandit -c pyproject.toml -r src
|
|
19
|
+
|
|
20
|
+
test:
|
|
21
|
+
uv run coverage run -m unittest discover -s tests -v
|
|
22
|
+
uv run coverage report
|
|
23
|
+
|
|
24
|
+
build:
|
|
25
|
+
uv build
|
|
26
|
+
uv run python scripts/check_zero_deps.py
|
|
27
|
+
|
|
28
|
+
check: lint type security test
|
|
29
|
+
|
|
30
|
+
all: check build
|
|
31
|
+
|
|
32
|
+
clean:
|
|
33
|
+
rm -rf dist build .coverage .mypy_cache .ruff_cache *.egg-info src/*.egg-info
|