1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Praxis Contributors
//! Reserved internal header prefixes for proxy-owned routing metadata.
// -----------------------------------------------------------------------------
// Constants
// -----------------------------------------------------------------------------
/// Built-in reserved header prefixes for Praxis routing metadata.
///
/// Headers with these prefixes are proxy-internal metadata used for
/// body-derived routing decisions. Clients must not be able to inject
/// them directly, and they should not be forwarded to upstream
/// backends or mutated by external processors.
///
/// The `x-ext-protocol-*` and `x-ext-agent-*` prefixes are reserved
/// for the AI extension package (`praxis-ai`). They are stripped to
/// prevent clients from spoofing internal AI routing headers even
/// when the AI filters are not loaded.
///
/// ```
/// use praxis_core::reserved_headers::RESERVED_HEADER_PREFIXES;
///
/// assert!(
/// RESERVED_HEADER_PREFIXES
/// .iter()
/// .any(|p| "x-praxis-foo".starts_with(p))
/// );
/// assert!(
/// !RESERVED_HEADER_PREFIXES
/// .iter()
/// .any(|p| "x-custom-foo".starts_with(p))
/// );
/// ```
// TODO(#186) Spike: consider additive operator-managed reserved prefixes
// once the broader config model defines global vs listener/filter-chain
// scope and additive vs override semantics.
pub const RESERVED_HEADER_PREFIXES: & = &;
/// [RFC 9110] hop-by-hop headers: connection-specific headers that apply to a
/// single transport hop and must not be forwarded across a proxy boundary.
///
/// This is the canonical set shared by sub-request stripping in `praxis-core`
/// and the protocol request handlers in `praxis-protocol`, so the two cannot
/// drift. Response stripping uses this set minus `proxy-authorization`, which
/// is a request-only credential header.
///
/// [RFC 9110]: https://datatracker.ietf.org/doc/html/rfc9110
pub const HOP_BY_HOP_HEADERS: & = &;
// -----------------------------------------------------------------------------
// Reserved Headers
// -----------------------------------------------------------------------------
/// Return whether a header name matches any reserved prefix.
///
/// The comparison is ASCII case-insensitive. Every current caller passes an
/// [`http::HeaderName`] string, which is already lowercase, but matching
/// case-insensitively means a future caller handing this a raw config or user
/// string (e.g. `"X-Praxis-Route"`) cannot slip a reserved header past the
/// check.
///
/// ```
/// assert!(praxis_core::reserved_headers::is_reserved("x-praxis-route"));
/// assert!(praxis_core::reserved_headers::is_reserved("X-Praxis-Route"));
/// assert!(praxis_core::reserved_headers::is_reserved(
/// "x-ext-agent-task"
/// ));
/// assert!(!praxis_core::reserved_headers::is_reserved("authorization"));
/// ```
/// Whether a header must never be removed because a client named it in a
/// `Connection` token.
///
/// Covers proxy-owned trust headers (the `x-forwarded-*` family and the
/// RFC 7239 `Forwarded` header, injected by the forwarded-headers filter),
/// the reserved internal namespaces ([`is_reserved`]), and the headers
/// essential to routing and framing (`Host`, `Content-Length`). Both the
/// main upstream path and filtered sub-requests honor this, so a client
/// cannot use `Connection: host` (a vhost-selection bypass, malformed per
/// RFC 9112) or `Connection: x-forwarded-for` (erasing the client address
/// upstreams rely on) to delete a header the proxy depends on.
///
/// Matching is ASCII case-insensitive.
///
/// ```
/// use praxis_core::reserved_headers::is_connection_token_protected;
/// assert!(is_connection_token_protected("host"));
/// assert!(is_connection_token_protected("X-Forwarded-For"));
/// assert!(is_connection_token_protected("forwarded"));
/// assert!(is_connection_token_protected("content-length"));
/// assert!(is_connection_token_protected("x-praxis-route"));
/// assert!(!is_connection_token_protected("x-app-state"));
/// ```
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------