Skip to content

Commit bb42a74

Browse files
etrclaude
andcommitted
Merge feature/ip-access-control-naming: symmetric allow/deny IP API
Renames block_ip/unblock_ip -> deny_ip/remove_denied_ip, adds the allow_ip/remove_allowed_ip pair (making default_policy(REJECT) usable), ban_system -> ip_access_control. make check green (doxygen gate fixed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NpysYDDJac63yz2mZKKiDf
2 parents 5c84039 + 5443f69 commit bb42a74

31 files changed

Lines changed: 616 additions & 243 deletions

README.md

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ thread-safe by contract.
4242
- External event-loop integration (`run`, `run_wait`, `get_fdset`, `add_connection`)
4343
- Daemon introspection (bound port, active connections, listen FD)
4444
- Turbo mode, TCP_FASTOPEN, suppressed `Date` headers, configurable backlog
45-
- IP blocking with wildcard ranges (`block_ip` / `unblock_ip`), IPv4 & IPv6
45+
- IP access control with wildcard ranges — deny list (`deny_ip`) and allow list (`allow_ip`), IPv4 & IPv6
4646
- Architecture contracts you can rely on: threading (DR-008 / §5.1) and error propagation (DR-009 / §5.2)
4747

4848
This README is a guided reference: it walks the v2.0 API surface section by
@@ -568,8 +568,10 @@ The rest of this section is a reference of every group of options.
568568
* **`.pedantic(bool = true)`** — strict HTTP-RFC parsing.
569569
* **`.regex_checking(bool = true)`** — validate path regexes at
570570
registration. Default `true`.
571-
* **`.ban_system(bool = true)`** — enable the IP block-list machinery
572-
used by `block_ip` / `unblock_ip`. Default `true`.
571+
* **`.ip_access_control(bool = true)`** — enable the IP access-control
572+
machinery used by `deny_ip` / `allow_ip` (and their `remove_*`
573+
counterparts). Default `true`. When `false`, every connection is
574+
admitted and both lists are ignored.
573575
* **`.post_process(bool = true)`** — auto-parse `application/x-www-form-urlencoded`
574576
and `multipart/form-data` bodies into the request's argument map.
575577
Default `true`.
@@ -691,11 +693,13 @@ httpserver::webserver ws{cfg};
691693

692694
These dovetail with the runtime methods covered under [Daemon
693695
introspection and external event loops](#daemon-introspection-and-external-event-loops).
694-
The `default_policy` controls the IP block list semantics:
696+
The `default_policy` selects which list is the exception list — i.e. what
697+
happens to an address on neither list:
695698

696699
* **`.default_policy(http::http_utils::policy_T p)`**`ACCEPT` (default)
697-
treats `block_ip` as a deny list; `REJECT` flips the semantics so
698-
`block_ip` becomes an allow list.
700+
admits every address except those on the deny list (`deny_ip`); `REJECT`
701+
refuses every address except those on the allow list (`allow_ip`). Under
702+
either policy an allow entry overrides a matching deny entry.
699703

700704
### Runtime operations on `webserver`
701705

@@ -1089,16 +1093,22 @@ The only requirement for IPv6 is that it is enabled on the underlying
10891093
server — set `use_ipv6(true)` on `create_webserver` (or `use_dual_stack(true)`
10901094
for both stacks on the same socket).
10911095

1092-
You can explicitly block or unblock IP addresses (or wildcard ranges)
1093-
at runtime using these methods on `webserver`:
1094-
1095-
* **`void block_ip(std::string_view ip)`** — add one IP (or a range,
1096-
e.g. `"127.0.0.*"`) to the block list. Connections from a matching
1097-
address are refused at the policy callback. Intended for use under the
1098-
default `ACCEPT` policy.
1099-
* **`void unblock_ip(std::string_view ip)`** — remove one IP (or a
1100-
range) from the block list. Idempotent: removing an entry that is not
1101-
currently blocked is a no-op.
1096+
You can populate the deny list and the allow list (with individual IPs or
1097+
wildcard ranges) at runtime using these methods on `webserver`:
1098+
1099+
* **`void deny_ip(std::string_view ip)`** — add one IP (or a range,
1100+
e.g. `"127.0.0.*"`) to the deny list. Connections from a matching
1101+
address are refused at the policy callback. This is the exception list
1102+
under the default `ACCEPT` policy.
1103+
* **`void remove_denied_ip(std::string_view ip)`** — remove one IP (or a
1104+
range) from the deny list. Idempotent: removing an entry that is not
1105+
currently denied is a no-op.
1106+
* **`void allow_ip(std::string_view ip)`** — add one IP (or a range) to
1107+
the allow list. This is the exception list under the `REJECT` policy
1108+
(permit only these). Under `ACCEPT`, an allow entry overrides a
1109+
matching `deny_ip` entry.
1110+
* **`void remove_allowed_ip(std::string_view ip)`** — remove one IP (or a
1111+
range) from the allow list. Idempotent.
11021112

11031113
### IP string format
11041114

@@ -1120,19 +1130,19 @@ Examples of valid IPs include:
11201130

11211131
### Allow-list mode
11221132

1123-
By default, `block_ip` defines a deny list under the `ACCEPT` policy.
1124-
To flip the semantics so `block_ip` defines an *allow* list under a
1125-
default-deny policy, set the policy on the builder:
1133+
By default (`ACCEPT` policy) the deny list is the exception list: use
1134+
`deny_ip` to refuse specific addresses and admit everyone else. To invert
1135+
this into an allow list — refuse everyone except specific addresses — set
1136+
the default policy to `REJECT` and populate the allow list with `allow_ip`:
11261137

11271138
```cpp
11281139
webserver ws{create_webserver(8080)
11291140
.default_policy(http::http_utils::REJECT)};
1130-
ws.block_ip("192.168.0.*"); // permits 192.168.0.0/24, blocks everything else
1141+
ws.allow_ip("192.168.0.*"); // permits 192.168.0.0/24, refuses everything else
11311142
```
11321143
1133-
See [`examples/minimal_ip_ban.cpp`](examples/minimal_ip_ban.cpp) for a
1134-
worked example of `block_ip` and `unblock_ip` under the default
1135-
`ACCEPT` policy.
1144+
See [`examples/minimal_ip_access_control.cpp`](examples/minimal_ip_access_control.cpp)
1145+
for a worked example of both modes.
11361146
11371147
[Back to TOC](#table-of-contents)
11381148
@@ -1605,8 +1615,8 @@ worked example.
16051615

16061616
### Examples
16071617

1608-
* [`examples/banned_ip_log.cpp`](examples/banned_ip_log.cpp) — log every
1609-
banned-IP rejection via a single `accept_decision` hook.
1618+
* [`examples/denied_ip_log.cpp`](examples/denied_ip_log.cpp) — log every
1619+
denied-IP rejection via a single `accept_decision` hook.
16101620
* [`examples/early_413.cpp`](examples/early_413.cpp) — return 413 from
16111621
a `request_received` hook before the body is consumed.
16121622
* [`examples/clf_access_log.cpp`](examples/clf_access_log.cpp) — write
@@ -1780,10 +1790,11 @@ try {
17801790
}
17811791
```
17821792

1783-
Block lists, IP-allow handling, and similar features that do not depend
1784-
on external libraries are always available: `webserver::block_ip(addr)`
1785-
and `webserver::unblock_ip(addr)` install and clear per-server blocks at
1786-
runtime regardless of build flags.
1793+
Deny lists, IP-allow handling, and similar features that do not depend
1794+
on external libraries are always available: `webserver::deny_ip(addr)` /
1795+
`webserver::remove_denied_ip(addr)` and `webserver::allow_ip(addr)` /
1796+
`webserver::remove_allowed_ip(addr)` install and clear per-server access
1797+
rules at runtime regardless of build flags.
17871798

17881799
[Back to TOC](#table-of-contents)
17891800

@@ -1864,8 +1875,8 @@ to the canonical example for each topic covered in this manual.
18641875

18651876
### Lifecycle hooks
18661877

1867-
* [`examples/banned_ip_log.cpp`](examples/banned_ip_log.cpp) — log every
1868-
banned-IP rejection from a single `accept_decision` hook (issue #332).
1878+
* [`examples/denied_ip_log.cpp`](examples/denied_ip_log.cpp) — log every
1879+
denied-IP rejection from a single `accept_decision` hook (issue #332).
18691880
* [`examples/early_413.cpp`](examples/early_413.cpp) — short-circuit
18701881
oversized uploads with 413 before any body bytes are consumed via a
18711882
`request_received` hook (issue #273).
@@ -1885,8 +1896,8 @@ to the canonical example for each topic covered in this manual.
18851896
drive `EXTERNAL_SELECT` from the application's loop via `run_wait`.
18861897
* [`examples/custom_access_log.cpp`](examples/custom_access_log.cpp)
18871898
server-wide access-log callback.
1888-
* [`examples/minimal_ip_ban.cpp`](examples/minimal_ip_ban.cpp)
1889-
`block_ip` / `unblock_ip` under the default `ACCEPT` policy.
1899+
* [`examples/minimal_ip_access_control.cpp`](examples/minimal_ip_access_control.cpp)
1900+
`deny_ip` / `allow_ip` under the `ACCEPT` and `REJECT` policies.
18901901
* [`examples/turbo_mode.cpp`](examples/turbo_mode.cpp) — turbo,
18911902
suppressed Date header, fastopen queue, listen backlog.
18921903
* [`examples/service.cpp`](examples/service.cpp) — kitchen-sink

RELEASE_NOTES.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,13 @@ v1.x is end-of-life on the day v2.0 ships.
3434
smart-pointer overload.
3535
- **`sweet_kill`.** Removed. Use `stop_and_wait()` (or `stop()` for the
3636
signal-only form).
37-
- **IP allow/deny verbs.** `ban_ip`, `unban_ip`, `allow_ip`,
38-
`disallow_ip` are removed. Use `block_ip` / `unblock_ip`.
37+
- **IP allow/deny verbs.** The v1 `ban_ip` / `unban_ip` / `allow_ip` /
38+
`disallow_ip` quartet is renamed for a symmetric, unambiguous surface:
39+
`deny_ip` / `remove_denied_ip` operate the deny list, and `allow_ip` /
40+
`remove_allowed_ip` operate the allow list. `default_policy(ACCEPT)`
41+
makes the deny list the exception list (block only these); `REJECT`
42+
makes the allow list the exception list (permit only these). An allow
43+
entry overrides a matching deny entry. See "What's renamed".
3944
- **Paired `no_*` boolean setters.** `no_basic_auth`, `no_digest_auth`,
4045
`no_ssl`, `no_debug`, `no_pedantic`, `no_deferred`, `no_regex_checking`,
4146
`no_ban_system`, `no_post_process`, `no_single_resource`, `no_ipv6`,
@@ -92,7 +97,7 @@ v1.x is end-of-life on the day v2.0 ships.
9297
configured validator callback is dead code. The
9398
v2 replacement is `webserver::add_hook(hook_phase::request_received,
9499
...)` returning `hook_action::respond_with(http_response)` to reject
95-
a request, or `webserver::block_ip`/`unblock_ip` for a connection-scoped
100+
a request, or `webserver::deny_ip`/`remove_denied_ip` for a connection-scoped
96101
veto; `hook_phase::accept_decision` is observation-only (Short-circuit:
97102
no, per `specs/architecture/04-components/hooks.md`) and cannot reject
98103
a connection. The legacy
@@ -166,7 +171,7 @@ v1.x is end-of-life on the day v2.0 ships.
166171
`hook_handle::detach()` keeps the registration alive for the
167172
webserver's lifetime. See `specs/architecture/04-components/hooks.md`
168173
and [`README.md#lifecycle-hooks`](README.md#lifecycle-hooks). Closes:
169-
#332 (banned-IP log, `examples/banned_ip_log.cpp`), #281 + #69
174+
#332 (denied-IP log, `examples/denied_ip_log.cpp`), #281 + #69
170175
(CLF / time-taken access log, `examples/clf_access_log.cpp`), #273
171176
(early 413, `examples/early_413.cpp`); partially closes #272
172177
(per-chunk observation; body steal deferred to v2.1).
@@ -179,10 +184,10 @@ and see the v2 replacement.
179184
| v1 | v2 |
180185
|---|---|
181186
| `sweet_kill` | `stop_and_wait` |
182-
| `ban_ip` | `block_ip` |
183-
| `unban_ip` | `unblock_ip` |
184-
| `allow_ip` | `unblock_ip` (or `block_ip` to deny) |
185-
| `disallow_ip` | `block_ip` (or `unblock_ip` to allow) |
187+
| `ban_ip` | `deny_ip` |
188+
| `unban_ip` | `remove_denied_ip` |
189+
| `allow_ip` | `allow_ip` (unchanged; allow list) |
190+
| `disallow_ip` | `remove_allowed_ip` |
186191
| `not_found_resource` (setter) | `not_found_handler` |
187192
| `method_not_allowed_resource` (setter) | `method_not_allowed_handler` |
188193
| `internal_error_resource` (setter) | `internal_error_handler` |

doxyconfig.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ TAB_SIZE = 8
179179
# will result in a user-defined paragraph with heading "Side Effects:".
180180
# You can put \n's in the value part of an alias to insert newlines.
181181

182-
ALIASES =
182+
ALIASES = "security=\par Security:^^"
183183

184184
# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C
185185
# sources only. Doxygen will then generate output that is more tailored for C.

examples/Makefile.am

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
LDADD = $(top_builddir)/src/libhttpserver.la
2020
AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/
2121
METASOURCES = AUTO
22-
noinst_PROGRAMS = hello_world shared_state service custom_error allowing_disallowing_methods handlers hello_with_get_arg args_processing setting_headers custom_access_log clf_access_log minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban banned_ip_log early_413 per_route_auth benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload file_upload_with_callback empty_response_example iovec_response_example pipe_response_example daemon_info external_event_loop turbo_mode binary_buffer_response
22+
noinst_PROGRAMS = hello_world shared_state service custom_error allowing_disallowing_methods handlers hello_with_get_arg args_processing setting_headers custom_access_log clf_access_log minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_access_control denied_ip_log early_413 per_route_auth benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload file_upload_with_callback empty_response_example iovec_response_example pipe_response_example daemon_info external_event_loop turbo_mode binary_buffer_response
2323

2424
hello_world_SOURCES = hello_world.cpp
2525
shared_state_SOURCES = shared_state.cpp
@@ -42,8 +42,8 @@ minimal_file_response_SOURCES = minimal_file_response.cpp
4242
minimal_deferred_SOURCES = minimal_deferred.cpp
4343
deferred_with_accumulator_SOURCES = deferred_with_accumulator.cpp
4444
url_registration_SOURCES = url_registration.cpp
45-
minimal_ip_ban_SOURCES = minimal_ip_ban.cpp
46-
banned_ip_log_SOURCES = banned_ip_log.cpp
45+
minimal_ip_access_control_SOURCES = minimal_ip_access_control.cpp
46+
denied_ip_log_SOURCES = denied_ip_log.cpp
4747
early_413_SOURCES = early_413.cpp
4848
# TASK-052: per-route auth via http_resource::add_hook(before_handler).
4949
# Demonstrates that a hook registered on one resource fires only when

examples/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ every interesting point in the request lifecycle. The four examples
7878
below demonstrate the user-visible resolution of issues #332, #273,
7979
#281, #69, and the per-route hook contract introduced in TASK-051.
8080

81-
* `banned_ip_log.cpp` — a single `accept_decision` hook logs every
82-
banned-IP rejection to stderr. Resolves issue #332.
81+
* `denied_ip_log.cpp` — a single `accept_decision` hook logs every
82+
denied-IP rejection to stderr. Resolves issue #332.
8383
* `early_413.cpp` — a single `request_received` hook returns
8484
`respond_with(http_response::empty().with_status(413))` when
8585
`Content-Length` exceeds the configured cap. The request body never
@@ -99,8 +99,8 @@ Operations
9999
* `external_event_loop.cpp` — drive `EXTERNAL_SELECT` from the
100100
application's loop via `run_wait`.
101101
* `custom_access_log.cpp` — server-wide access-log callback.
102-
* `minimal_ip_ban.cpp``block_ip` / `unblock_ip` under the default
103-
ACCEPT policy.
102+
* `minimal_ip_access_control.cpp``deny_ip` / `allow_ip` under the
103+
`ACCEPT` and `REJECT` policies.
104104
* `turbo_mode.cpp` — turbo, suppressed Date header, fastopen queue,
105105
listen backlog.
106106
* `service.cpp` — the kitchen-sink reference example: CLI args,
Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,18 @@
1818
USA
1919
*/
2020

21-
// Demonstrates the solution to issue #332: log every banned-IP rejection.
21+
// Demonstrates the solution to issue #332: log every denied-IP rejection.
2222
//
23-
// Configure the daemon with an ACCEPT default policy, block the IPs you
24-
// want denied via ws.block_ip(), and register a connection-level
23+
// Configure the daemon with an ACCEPT default policy, deny the IPs you
24+
// want refused via ws.deny_ip(), and register a connection-level
2525
// `accept_decision` hook that logs every rejection to stderr.
2626
//
2727
// The hook is observation-only: throwing from it does not flip the
2828
// decision (the daemon still rejects the connection per the policy
2929
// callback's return), so it is safe to do I/O here.
3030
//
31-
// Run, then attempt to connect from one of the blocked IPs to see
32-
// stderr show a "[BANNED] ..." line per attempt.
31+
// Run, then attempt to connect from one of the denied IPs to see
32+
// stderr show a "[DENIED] ..." line per attempt.
3333

3434
#include <functional>
3535
#include <iostream>
@@ -51,15 +51,15 @@ int main() {
5151
hs::webserver ws{hs::create_webserver(8080)
5252
.default_policy(hs::http::http_utils::ACCEPT)};
5353

54-
// Block whatever client IP you want denied. The shipping example
54+
// Deny whatever client IP you want refused. The shipping example
5555
// uses an RFC-1918 placeholder; swap with your client's address.
56-
ws.block_ip("10.0.0.1");
56+
ws.deny_ip("10.0.0.1");
5757

5858
auto h = ws.add_hook(hs::hook_phase::accept_decision,
5959
std::function<void(const hs::accept_ctx&)>(
6060
[](const hs::accept_ctx& ctx) {
6161
if (!ctx.accepted) {
62-
std::cerr << "[BANNED] peer=" << ctx.peer.to_string()
62+
std::cerr << "[DENIED] peer=" << ctx.peer.to_string()
6363
<< " reason="
6464
<< (ctx.reason.has_value()
6565
? std::string(*ctx.reason)
Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,31 @@
1818
USA
1919
*/
2020

21-
// minimal_ip_ban.cpp - demonstrate the v2.0 IP block API.
21+
// minimal_ip_access_control.cpp - demonstrate the IP access-control API.
2222
//
23-
// TASK-029: the v2.0 public IP-control surface is the pair block_ip /
24-
// unblock_ip, usable under the default ACCEPT policy. The historical
25-
// allow_ip / disallow_ip pair (under REJECT) was dropped.
23+
// Two lists, selected by create_webserver::default_policy():
24+
// - deny_ip / remove_denied_ip : the exception list under the default
25+
// ACCEPT policy (permit everyone except these).
26+
// - allow_ip / remove_allowed_ip : the exception list under the REJECT
27+
// policy (permit only these). Under ACCEPT, an allow entry also
28+
// overrides a matching deny entry (allow wins).
2629

2730
#include <httpserver.hpp>
2831

2932
int main() {
33+
// Default ACCEPT policy: block-list mode. Everyone is admitted except
34+
// addresses on the deny list.
3035
httpserver::webserver ws{httpserver::create_webserver(8080)};
31-
32-
ws.block_ip("10.0.0.1");
36+
ws.deny_ip("10.0.0.1");
37+
38+
// Allow-list mode: flip the default to REJECT, then permit only the
39+
// addresses you allow_ip. Everyone else is refused at the policy
40+
// callback.
41+
//
42+
// httpserver::webserver ws{
43+
// httpserver::create_webserver(8080)
44+
// .default_policy(httpserver::http::http_utils::REJECT)};
45+
// ws.allow_ip("127.0.0.1"); // only localhost may connect
3346

3447
ws.on_get("/hello", [](const httpserver::http_request&) {
3548
return httpserver::http_response::string("Hello, World!");

scripts/check-examples.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,13 +184,13 @@ if [ "$unlisted" -gt 0 ]; then
184184
fi
185185

186186
# ---- TASK-052: hook-example documentation coverage --------------------------
187-
# The four lifecycle-hook examples (banned_ip_log, early_413, clf_access_log,
187+
# The four lifecycle-hook examples (denied_ip_log, early_413, clf_access_log,
188188
# per_route_auth) must be listed in both examples/README.md and the top-level
189189
# README.md so the user-visible resolution of issues #332, #281, #69, #273 is
190190
# discoverable. Per TASK-052 / Phase 3.
191191
EXAMPLES_README="$REPO_ROOT/examples/README.md"
192192
TOP_README="$REPO_ROOT/README.md"
193-
HOOK_EXAMPLES="banned_ip_log early_413 clf_access_log per_route_auth"
193+
HOOK_EXAMPLES="denied_ip_log early_413 clf_access_log per_route_auth"
194194

195195
for f in "$EXAMPLES_README" "$TOP_README"; do
196196
[ -f "$f" ] || fail "$(basename "$f") does not exist"

0 commit comments

Comments
 (0)