First feature for v1.1: public rate-limited access. This allows select paths to be made publicly accessible without TOTP authentication, with separate per-IP rate limiting to protect server resources from bot traffic.
Practical example: Allow anyone to visit https://code.devgnome.com/public/* in Gitea, but limit them to 100 requests/minute and 500 requests/hour per IP. Authenticated users bypass the public rate limiter entirely.
Built on top of fix/v1.0-must-fix (PR #4). This PR should be merged after #4 is merged into main.
How It Works
A new PublicAccessListener (priority 84) sits in the request pipeline after the auth listeners (AcceptListener at 99, AllowListener at 88) but before the login rate limiter (RejectListener at 77). This means:
Authenticated users never reach PublicAccessListener — they get 200 from AcceptListener/AllowListener first.
Unauthenticated users visiting a configured public path get rate-limited by a separate public_limiter (independent from the login attempt limiter). Within limit → 200 OK (no Remote-User header). Over limit → 429 with Retry-After header.
Unauthenticated users visiting a non-public path fall through to the normal auth flow (login page or redirect).
Public path accessible without auth, query string ignored, deep paths
Non-public path shows login, exact path without slash not matched
Rate limit enforced after burst exceeded
Authenticated user bypasses public rate limit
Security headers on public responses, error template on 429
Documentation
README.md — New "Public Rate-Limited Access" section with config table, pattern syntax, and examples. Updated Features list and listener pipeline.
CHANGELOG.md — v1.1 section with all new features.
ROADMAP.md — Phase 1 marked as completed. Updated listener pipeline, test counts (293/605), and source→test mapping.
docs/Caddyfile — Added example of public + protected service config.
docs/example.env — All new env vars documented with comments.
docs/v1.1-plan.md — Full design plan document for reference.
## Summary
First feature for v1.1: **public rate-limited access**. This allows select paths to be made publicly accessible without TOTP authentication, with separate per-IP rate limiting to protect server resources from bot traffic.
**Practical example:** Allow anyone to visit `https://code.devgnome.com/public/*` in Gitea, but limit them to 100 requests/minute and 500 requests/hour per IP. Authenticated users bypass the public rate limiter entirely.
**Tests:** 293 passing (605 assertions) · **Linting:** PHP CS Fixer clean (63/63 files) · **Working tree:** clean
Built on top of `fix/v1.0-must-fix` (PR #4). This PR should be merged after #4 is merged into main.
---
## How It Works
A new `PublicAccessListener` (priority 84) sits in the request pipeline after the auth listeners (AcceptListener at 99, AllowListener at 88) but before the login rate limiter (RejectListener at 77). This means:
1. **Authenticated users** never reach `PublicAccessListener` — they get 200 from AcceptListener/AllowListener first.
2. **Unauthenticated users** visiting a configured public path get rate-limited by a separate `public_limiter` (independent from the login attempt limiter). Within limit → 200 OK (no `Remote-User` header). Over limit → 429 with `Retry-After` header.
3. **Unauthenticated users** visiting a non-public path fall through to the normal auth flow (login page or redirect).
## New Components
### `PublicPathMatcher` (`src/Service/PublicPathMatcher.php`)
- Parses the `PUBLIC_PATHS` env var into pattern entries
- Supports `*` (single segment) and `**` (cross-segment) wildcards
- Supports optional host prefix (e.g., `code.example.com/public/**`)
- Query strings are ignored — matching is against path only
- Hosts are case-insensitive; paths are case-sensitive
### `PublicAccessListener` (`src/Listener/PublicAccessListener.php`)
- Priority 84 — after auth checks, before login rate limiter
- Skips when `PUBLIC_PATHS` is empty (feature disabled)
- Skips requests to the auth subdomain
- Consumes 1 token from `public_limiter` per request
- 200 OK (text/plain, no Remote-User) when within limit
- 429 Too Many Requests (text/html, Retry-After) when over limit
## Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `PUBLIC_PATHS` | `''` (disabled) | Comma-separated path patterns with wildcards |
| `PUBLIC_BURST_COUNT` | `100` | Max requests per burst window per IP |
| `PUBLIC_BURST_TIME` | `60` | Burst window in seconds |
| `PUBLIC_UPPER_COUNT` | `500` | Max requests per sustained window per IP |
| `PUBLIC_UPPER_TIME` | `3600` | Sustained window in seconds |
**Path pattern syntax:**
- `/public` — exact match
- `/public/*` — matches one segment after /public/
- `/public/**` — matches any depth after /public/
- `host.com/public/**` — restricts to specific host
When `PUBLIC_PATHS` is empty (default), the feature is completely disabled and has zero impact on existing behavior.
## Tests (52 new)
### `PublicPathMatcherTest` (29 unit tests)
- Empty/whitespace strings, exact matching, single/double wildcards
- Mid-path wildcards, multiple patterns, whitespace handling
- Domain-prefixed patterns, mixed domain/plain patterns
- Case sensitivity, invalid patterns, special regex characters
### `PublicAccessListenerTest` (12 unit tests)
- Feature disabled, non-public path, public path within/over rate limit
- Auth subdomain skipped, query string ignored, domain-scoped paths
- Wildcard matching, Retry-After header
### `PublicAccessFlowTest` (11 functional tests)
- Public path accessible without auth, query string ignored, deep paths
- Non-public path shows login, exact path without slash not matched
- Rate limit enforced after burst exceeded
- Authenticated user bypasses public rate limit
- Security headers on public responses, error template on 429
## Documentation
- **README.md** — New "Public Rate-Limited Access" section with config table, pattern syntax, and examples. Updated Features list and listener pipeline.
- **CHANGELOG.md** — v1.1 section with all new features.
- **ROADMAP.md** — Phase 1 marked as completed. Updated listener pipeline, test counts (293/605), and source→test mapping.
- **docs/Caddyfile** — Added example of public + protected service config.
- **docs/example.env** — All new env vars documented with comments.
- **docs/v1.1-plan.md** — Full design plan document for reference.
andrew
changed target branch from fix/v1.0-must-fix to main2026-08-12 22:32:58 -04:00
Add PublicAccessListener (priority 84) that allows rate-limited
unauthenticated access to configured public paths. Authenticated users
bypass this listener entirely via AcceptListener/AllowListener.
New components:
- PublicPathMatcher service with wildcard path matching (* and **)
and optional host-prefix scoping
- PublicAccessListener applying per-IP rate limiting to public paths
- Separate public_limiter compound rate limiter (burst + sustained)
- publicRateLimitCache pool (APCu in prod, array in tests)
New env vars:
- PUBLIC_PATHS (comma-separated path patterns, empty = disabled)
- PUBLIC_BURST_COUNT/PUBLIC_BURST_TIME (default 100/60s)
- PUBLIC_UPPER_COUNT/PUBLIC_UPPER_TIME (default 500/3600s)
Tests: 52 new tests (29 unit for PublicPathMatcher, 12 unit for
PublicAccessListener, 11 functional for PublicAccessFlowTest).
Total: 293 tests, 605 assertions, all passing.
PHP CS Fixer: 0 of 63 files need fixing.
Documentation: README, CHANGELOG, ROADMAP, Caddyfile, example.env
all updated with public access configuration and examples.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
First feature for v1.1: public rate-limited access. This allows select paths to be made publicly accessible without TOTP authentication, with separate per-IP rate limiting to protect server resources from bot traffic.
Practical example: Allow anyone to visit
https://code.devgnome.com/public/*in Gitea, but limit them to 100 requests/minute and 500 requests/hour per IP. Authenticated users bypass the public rate limiter entirely.Tests: 293 passing (605 assertions) · Linting: PHP CS Fixer clean (63/63 files) · Working tree: clean
Built on top of
fix/v1.0-must-fix(PR #4). This PR should be merged after #4 is merged into main.How It Works
A new
PublicAccessListener(priority 84) sits in the request pipeline after the auth listeners (AcceptListener at 99, AllowListener at 88) but before the login rate limiter (RejectListener at 77). This means:PublicAccessListener— they get 200 from AcceptListener/AllowListener first.public_limiter(independent from the login attempt limiter). Within limit → 200 OK (noRemote-Userheader). Over limit → 429 withRetry-Afterheader.New Components
PublicPathMatcher(src/Service/PublicPathMatcher.php)PUBLIC_PATHSenv var into pattern entries*(single segment) and**(cross-segment) wildcardscode.example.com/public/**)PublicAccessListener(src/Listener/PublicAccessListener.php)PUBLIC_PATHSis empty (feature disabled)public_limiterper requestConfiguration
PUBLIC_PATHS''(disabled)PUBLIC_BURST_COUNT100PUBLIC_BURST_TIME60PUBLIC_UPPER_COUNT500PUBLIC_UPPER_TIME3600Path pattern syntax:
/public— exact match/public/*— matches one segment after /public//public/**— matches any depth after /public/host.com/public/**— restricts to specific hostWhen
PUBLIC_PATHSis empty (default), the feature is completely disabled and has zero impact on existing behavior.Tests (52 new)
PublicPathMatcherTest(29 unit tests)PublicAccessListenerTest(12 unit tests)PublicAccessFlowTest(11 functional tests)Documentation