castrum 0.9.2 → 0.9.4
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.
- package/Cargo.lock +2259 -0
- package/Cargo.toml +138 -0
- package/README.md +6 -0
- package/build.rs +5 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +1348 -1295
- package/dist/index.js +4307 -3776
- package/docs/API.md +1 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CASE_STUDY.md +2 -2
- package/docs/ENVIRONMENT.md +16 -0
- package/docs/FFI_BUN_GUIDE.md +89 -6
- package/docs/REPO_MAP.md +2 -2
- package/index.ts +19 -25
- package/package.json +9 -1
- package/rust/crypto/aead.rs +378 -0
- package/rust/crypto/argon2.rs +265 -0
- package/rust/crypto/base64.rs +420 -0
- package/rust/crypto/bcrypt.rs +89 -0
- package/rust/crypto/cookie_sign.rs +169 -0
- package/rust/crypto/csrf.rs +106 -0
- package/rust/crypto/ed25519.rs +215 -0
- package/rust/crypto/hashing.rs +140 -0
- package/rust/crypto/hmac_sha256.rs +88 -0
- package/rust/crypto/jwt/api.rs +286 -0
- package/rust/crypto/jwt/mod.rs +22 -0
- package/rust/crypto/jwt/tests.rs +312 -0
- package/rust/crypto/jwt/token.rs +360 -0
- package/rust/crypto/mod.rs +27 -0
- package/rust/crypto/pbkdf2.rs +96 -0
- package/rust/crypto/random_token.rs +85 -0
- package/rust/crypto/session.rs +222 -0
- package/rust/ffi/crypto.rs +708 -0
- package/rust/ffi/hashing.rs +285 -0
- package/rust/ffi/http.rs +752 -0
- package/rust/ffi/ingress.rs +303 -0
- package/rust/ffi/json.rs +517 -0
- package/rust/ffi/jwt.rs +419 -0
- package/rust/ffi/metrics.rs +373 -0
- package/rust/ffi/mod.rs +68 -0
- package/rust/ffi/payload.rs +275 -0
- package/rust/ffi/probe.rs +60 -0
- package/rust/ffi/rate_limit.rs +77 -0
- package/rust/ffi/route.rs +85 -0
- package/rust/ffi/tests.rs +2333 -0
- package/rust/ffi/util.rs +152 -0
- package/rust/ffi/validators.rs +123 -0
- package/rust/http/accept.rs +606 -0
- package/rust/http/cookie_parser.rs +140 -0
- package/rust/http/etag.rs +289 -0
- package/rust/http/form.rs +132 -0
- package/rust/http/headers.rs +267 -0
- package/rust/http/http_date.rs +286 -0
- package/rust/http/http_parser.rs +252 -0
- package/rust/http/media_type.rs +355 -0
- package/rust/http/method.rs +200 -0
- package/rust/http/mime_lookup.rs +130 -0
- package/rust/http/mod.rs +32 -0
- package/rust/http/multipart.rs +685 -0
- package/rust/http/query_parser.rs +180 -0
- package/rust/http/url_codec.rs +296 -0
- package/rust/http/url_join.rs +324 -0
- package/rust/ingress/api.rs +355 -0
- package/rust/ingress/cors.rs +403 -0
- package/rust/ingress/ingress_constants.rs +209 -0
- package/rust/ingress/ip_trust.rs +312 -0
- package/rust/ingress/mod.rs +48 -0
- package/rust/ingress/native_route.rs +779 -0
- package/rust/ingress/options.rs +176 -0
- package/rust/ingress/output.rs +406 -0
- package/rust/ingress/packed.rs +191 -0
- package/rust/ingress/pipeline.rs +534 -0
- package/rust/ingress/proxy.rs +138 -0
- package/rust/ingress/rate_limit.rs +538 -0
- package/rust/ingress/terminal.rs +114 -0
- package/rust/ingress/tests.rs +566 -0
- package/rust/ingress/time.rs +70 -0
- package/rust/json/fast_schema/capture.rs +336 -0
- package/rust/json/fast_schema/compile.rs +556 -0
- package/rust/json/fast_schema/cursor.rs +368 -0
- package/rust/json/fast_schema/email.rs +282 -0
- package/rust/json/fast_schema/errors.rs +247 -0
- package/rust/json/fast_schema/mod.rs +57 -0
- package/rust/json/fast_schema/tests.rs +1143 -0
- package/rust/json/fast_schema/types.rs +140 -0
- package/rust/json/fast_schema/validate.rs +931 -0
- package/rust/json/json_ops.rs +121 -0
- package/rust/json/json_schema.rs +837 -0
- package/rust/json/json_ser.rs +881 -0
- package/rust/json/mod.rs +19 -0
- package/rust/json/napi_marshal.rs +318 -0
- package/rust/json/patch/api.rs +60 -0
- package/rust/json/patch/engine.rs +153 -0
- package/rust/json/patch/mod.rs +47 -0
- package/rust/json/patch/ops.rs +270 -0
- package/rust/json/patch/pointer.rs +49 -0
- package/rust/json/patch/tests.rs +499 -0
- package/rust/lib.rs +72 -0
- package/rust/metrics/api.rs +148 -0
- package/rust/metrics/mod.rs +21 -0
- package/rust/metrics/registry.rs +961 -0
- package/rust/panic_safety.rs +40 -0
- package/rust/payload/compress.rs +512 -0
- package/rust/payload/mod.rs +13 -0
- package/rust/payload/sse.rs +214 -0
- package/rust/payload/template.rs +253 -0
- package/rust/payload/websocket.rs +69 -0
- package/rust/payload/ws_frames.rs +354 -0
- package/rust/proptest_suite.rs +139 -0
- package/rust/selection.rs +95 -0
- package/rust/src/bin/native-bench.rs +495 -0
- package/rust/test_support.rs +71 -0
- package/rust/util/batch/api.rs +266 -0
- package/rust/util/batch/core.rs +473 -0
- package/rust/util/batch/mod.rs +23 -0
- package/rust/util/batch/tests.rs +445 -0
- package/rust/util/batch_core.rs +184 -0
- package/rust/util/bytes.rs +407 -0
- package/rust/util/mod.rs +34 -0
- package/rust/util/packed.rs +473 -0
- package/rust/util/text.rs +130 -0
- package/rust/util/threadpool.rs +128 -0
- package/rust/util/validation.rs +419 -0
- package/rust-toolchain.toml +17 -0
- package/scripts/postinstall.mjs +199 -0
- package/src/ingress/constants.ts +1 -1
- package/src/ingress/context.ts +2 -2
- package/src/ingress/decode/baked-result.ts +13 -13
- package/src/ingress/decode/fast-result.ts +17 -17
- package/src/ingress/errors.ts +7 -7
- package/src/ingress/headers/baked-templates.ts +1 -1
- package/src/ingress/headers/fast-templates.ts +3 -3
- package/src/ingress/index.ts +1 -1
- package/src/ingress/native-route.ts +9 -5
- package/src/ingress/options.ts +1 -1
- package/src/ingress/packing/gather-raw-headers.ts +1 -1
- package/src/ingress/packing/route-wire.ts +1 -5
- package/src/ingress/response/error-bodies.ts +5 -5
- package/src/ingress/response/terminal.ts +1 -1
- package/src/ingress/routes/echo.ts +4 -4
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +2 -2
- package/src/ingress/routes/native.ts +2 -1
- package/src/ingress/routes/options.ts +1 -1
- package/src/ingress/routes/read.ts +2 -2
- package/src/ingress/server.ts +10 -6
- package/src/ingress/status.ts +7 -7
- package/src/integration/batch.ts +1 -1
- package/src/integration/index.ts +9 -9
- package/src/integration/pipeline.ts +6 -6
- package/src/integration/websocket.ts +2 -2
- package/src/loader/index.ts +1 -5
- package/src/loader/ops.ts +1 -1
- package/src/native/ffi/build/codecs.ts +104 -8
- package/src/native/ffi/build/compress.ts +14 -10
- package/src/native/ffi/build/instances.ts +40 -3
- package/src/native/ffi/build/metrics.ts +224 -0
- package/src/native/ffi/build/parse.ts +59 -23
- package/src/native/ffi/build/util.ts +14 -1
- package/src/native/ffi/build.ts +2 -0
- package/src/native/ffi/selftest.ts +10 -3
- package/src/native/ffi/types.ts +111 -9
- package/src/native/ffi.ts +87 -1
- package/src/native/index.ts +1 -1
- package/src/native/loader.ts +2 -2
- package/src/native/types/instances.ts +34 -0
- package/src/native/types.ts +11 -0
- package/src/runtime/codec.ts +53 -9
- package/src/runtime/types.ts +7 -0
- package/src/rust-ffi/batch/build.ts +16 -9
- package/src/rust-ffi/index.ts +7 -6
- package/src/rust-ffi/options.ts +1 -3
- package/src/rust-ffi/proven.ts +3 -3
- package/src/rust-ffi/scalar/crypto.ts +1 -1
- package/src/rust-ffi/scalar/factories.ts +73 -0
- package/src/rust-ffi/scalar/hashing.ts +1 -1
- package/src/rust-ffi/scalar/http.ts +17 -11
- package/src/rust-ffi/scalar/index.ts +2 -0
- package/src/rust-ffi/scalar/interface.ts +20 -0
- package/src/rust-ffi/scalar/payload.ts +11 -3
- package/src/rust-ffi/scalar/validation.ts +121 -0
- package/src/selection.ts +1 -1
- package/src/shared/bytes.ts +5 -4
- package/src/shared/codec.ts +18 -3
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
// rust/ingress/cors.rs — CORS engine extracted from ingress.rs
|
|
2
|
+
// Pre-computes method bitmask for O(1) method matching
|
|
3
|
+
|
|
4
|
+
use crate::http::headers::HeaderRefs;
|
|
5
|
+
use crate::http::method::MethodKind;
|
|
6
|
+
use crate::util::bytes::ascii_eq_ignore_case;
|
|
7
|
+
use crate::util::trim_ascii_whitespace;
|
|
8
|
+
use napi::{Error, Result, Status};
|
|
9
|
+
use napi_derive::napi;
|
|
10
|
+
|
|
11
|
+
// ── CORS engine ───────────────────────────────────────────────────
|
|
12
|
+
#[derive(Clone)]
|
|
13
|
+
pub enum CorsMode {
|
|
14
|
+
Disabled,
|
|
15
|
+
Wildcard,
|
|
16
|
+
Allowlist(Vec<Box<[u8]>>),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[derive(Clone)]
|
|
20
|
+
pub struct CorsEngine {
|
|
21
|
+
mode: CorsMode,
|
|
22
|
+
credentials: bool,
|
|
23
|
+
methods_wildcard: bool,
|
|
24
|
+
// Bitmask of allowed methods for O(1) lookup
|
|
25
|
+
method_bits: u16,
|
|
26
|
+
headers_wildcard: bool,
|
|
27
|
+
headers: Vec<Box<[u8]>>,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pub struct CorsEvaluation {
|
|
31
|
+
pub allowed: bool,
|
|
32
|
+
pub preflight: bool,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
impl CorsEngine {
|
|
36
|
+
pub fn disabled() -> Self {
|
|
37
|
+
Self {
|
|
38
|
+
mode: CorsMode::Disabled,
|
|
39
|
+
credentials: false,
|
|
40
|
+
methods_wildcard: false,
|
|
41
|
+
method_bits: 0,
|
|
42
|
+
headers_wildcard: false,
|
|
43
|
+
headers: Vec::new(),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pub fn from_options(opts: Option<CorsOptions>) -> Result<Self> {
|
|
48
|
+
let Some(opts) = opts else {
|
|
49
|
+
return Ok(Self::disabled());
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
let credentials = opts.allow_credentials.unwrap_or(false);
|
|
53
|
+
|
|
54
|
+
let mode = match opts.allow_origin {
|
|
55
|
+
Some(list) if !list.is_empty() => {
|
|
56
|
+
if list.iter().any(|o| o == "*") {
|
|
57
|
+
if credentials {
|
|
58
|
+
return Err(Error::new(
|
|
59
|
+
Status::InvalidArg,
|
|
60
|
+
"CORS allowCredentials cannot be used with allowOrigin ['*']",
|
|
61
|
+
));
|
|
62
|
+
}
|
|
63
|
+
CorsMode::Wildcard
|
|
64
|
+
} else {
|
|
65
|
+
CorsMode::Allowlist(
|
|
66
|
+
list.iter()
|
|
67
|
+
.map(|s| s.as_bytes().to_vec().into_boxed_slice())
|
|
68
|
+
.collect(),
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
_ => {
|
|
73
|
+
if credentials {
|
|
74
|
+
return Err(Error::new(
|
|
75
|
+
Status::InvalidArg,
|
|
76
|
+
"CORS allowCredentials requires an explicit allowOrigin list",
|
|
77
|
+
));
|
|
78
|
+
}
|
|
79
|
+
CorsMode::Wildcard
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
let (methods_wildcard, method_bits) = parse_allowed_methods_bitset(opts.allow_methods);
|
|
84
|
+
let (headers_wildcard, headers) = parse_allowed_headers(opts.allow_headers);
|
|
85
|
+
|
|
86
|
+
Ok(Self {
|
|
87
|
+
mode,
|
|
88
|
+
credentials,
|
|
89
|
+
methods_wildcard,
|
|
90
|
+
method_bits,
|
|
91
|
+
headers_wildcard,
|
|
92
|
+
headers,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[inline]
|
|
97
|
+
pub fn evaluate(&self, method: MethodKind, headers: &HeaderRefs) -> CorsEvaluation {
|
|
98
|
+
let origin = match headers.origin() {
|
|
99
|
+
Some(o) => o,
|
|
100
|
+
None => {
|
|
101
|
+
return CorsEvaluation {
|
|
102
|
+
allowed: false,
|
|
103
|
+
preflight: false,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
let preflight = method == MethodKind::Options && headers.has_acrm();
|
|
109
|
+
|
|
110
|
+
let mut allowed = match &self.mode {
|
|
111
|
+
CorsMode::Disabled => false,
|
|
112
|
+
CorsMode::Wildcard => !self.credentials,
|
|
113
|
+
CorsMode::Allowlist(list) => {
|
|
114
|
+
let origin_trim = trim_ascii_whitespace(origin);
|
|
115
|
+
list.iter().any(|allowed| &**allowed == origin_trim)
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if allowed {
|
|
120
|
+
if preflight {
|
|
121
|
+
allowed = self.preflight_method_allowed(headers.acrm())
|
|
122
|
+
&& self.headers_allowed(headers.acrh());
|
|
123
|
+
} else {
|
|
124
|
+
allowed = self.method_allowed_kind(method);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
CorsEvaluation { allowed, preflight }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#[inline(always)]
|
|
132
|
+
fn method_allowed_kind(&self, kind: MethodKind) -> bool {
|
|
133
|
+
if self.methods_wildcard {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if self.method_bits == 0 {
|
|
138
|
+
return matches!(kind, MethodKind::Get | MethodKind::Head | MethodKind::Post);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
(self.method_bits & kind.bit()) != 0
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#[inline]
|
|
145
|
+
fn preflight_method_allowed(&self, acrm: Option<&[u8]>) -> bool {
|
|
146
|
+
let Some(acrm) = acrm else {
|
|
147
|
+
return false;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
let trimmed = trim_ascii_whitespace(acrm);
|
|
151
|
+
if trimmed.is_empty() {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let kind = MethodKind::from_bytes_ignore_case(trimmed);
|
|
156
|
+
self.method_allowed_kind(kind)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#[inline]
|
|
160
|
+
fn headers_allowed(&self, acrh: Option<&[u8]>) -> bool {
|
|
161
|
+
let Some(acrh) = acrh else {
|
|
162
|
+
return true;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if self.headers_wildcard {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for requested in acrh.split(|&b| b == b',') {
|
|
170
|
+
let name = trim_ascii_whitespace(requested);
|
|
171
|
+
if name.is_empty() {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if self.headers.is_empty() {
|
|
176
|
+
if !is_cors_safelisted_request_header(name) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
} else if !self
|
|
180
|
+
.headers
|
|
181
|
+
.iter()
|
|
182
|
+
.any(|allowed| ascii_eq_ignore_case(name, allowed.as_ref()))
|
|
183
|
+
{
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
true
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fn is_cors_safelisted_request_header(name: &[u8]) -> bool {
|
|
193
|
+
ascii_eq_ignore_case(name, b"accept")
|
|
194
|
+
|| ascii_eq_ignore_case(name, b"accept-language")
|
|
195
|
+
|| ascii_eq_ignore_case(name, b"content-language")
|
|
196
|
+
|| ascii_eq_ignore_case(name, b"content-type")
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
fn parse_allowed_methods_bitset(list: Option<Vec<String>>) -> (bool, u16) {
|
|
200
|
+
let Some(list) = list else {
|
|
201
|
+
return (false, 0);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
let mut bits = 0u16;
|
|
205
|
+
|
|
206
|
+
for raw in list {
|
|
207
|
+
let trimmed = raw.trim();
|
|
208
|
+
|
|
209
|
+
if trimmed == "*" {
|
|
210
|
+
return (true, 0);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if trimmed.is_empty() {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let upper = trimmed.to_ascii_uppercase();
|
|
218
|
+
let kind = MethodKind::from_str(&upper);
|
|
219
|
+
|
|
220
|
+
if kind != MethodKind::Other {
|
|
221
|
+
bits |= kind.bit();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
(false, bits)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fn parse_allowed_headers(list: Option<Vec<String>>) -> (bool, Vec<Box<[u8]>>) {
|
|
229
|
+
let Some(list) = list else {
|
|
230
|
+
return (false, Vec::new());
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
let mut headers = Vec::new();
|
|
234
|
+
|
|
235
|
+
for raw in list {
|
|
236
|
+
let trimmed = raw.trim();
|
|
237
|
+
|
|
238
|
+
if trimmed == "*" {
|
|
239
|
+
return (true, Vec::new());
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if trimmed.is_empty() {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
headers.push(trimmed.to_ascii_lowercase().into_bytes().into_boxed_slice());
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
(false, headers)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── NAPI option struct ───────────────────────────────────────────
|
|
253
|
+
#[napi(object)]
|
|
254
|
+
#[derive(Clone)]
|
|
255
|
+
pub struct CorsOptions {
|
|
256
|
+
pub allow_origin: Option<Vec<String>>,
|
|
257
|
+
pub allow_methods: Option<Vec<String>>,
|
|
258
|
+
pub allow_headers: Option<Vec<String>>,
|
|
259
|
+
pub allow_credentials: Option<bool>,
|
|
260
|
+
// NOTE: expose_headers / max_age are intentionally NOT here — the Rust
|
|
261
|
+
// engine only EVALUATES origin/method/header/credentials. The emitted
|
|
262
|
+
// `Access-Control-Expose-Headers` / `Access-Control-Max-Age` headers are
|
|
263
|
+
// produced by the TS header templates (both ingress paths), which read the
|
|
264
|
+
// same `cors` option object.
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#[cfg(test)]
|
|
268
|
+
mod tests {
|
|
269
|
+
use super::{CorsEngine, CorsOptions};
|
|
270
|
+
use crate::http::headers::HeaderRefs;
|
|
271
|
+
use crate::http::method::MethodKind;
|
|
272
|
+
use crate::test_support::pack_headers;
|
|
273
|
+
|
|
274
|
+
fn cors_options(
|
|
275
|
+
origin: Vec<String>,
|
|
276
|
+
methods: Vec<String>,
|
|
277
|
+
headers: Vec<String>,
|
|
278
|
+
creds: bool,
|
|
279
|
+
) -> CorsOptions {
|
|
280
|
+
CorsOptions {
|
|
281
|
+
allow_origin: Some(origin),
|
|
282
|
+
allow_methods: Some(methods),
|
|
283
|
+
allow_headers: Some(headers),
|
|
284
|
+
allow_credentials: Some(creds),
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#[test]
|
|
289
|
+
fn cors_wildcard_with_credentials_rejected() {
|
|
290
|
+
let opts = cors_options(vec!["*".to_string()], vec![], vec![], true);
|
|
291
|
+
assert!(CorsEngine::from_options(Some(opts)).is_err());
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#[test]
|
|
295
|
+
fn cors_wildcard_without_credentials_ok() {
|
|
296
|
+
let opts = cors_options(vec!["*".to_string()], vec![], vec![], false);
|
|
297
|
+
let engine = CorsEngine::from_options(Some(opts)).unwrap();
|
|
298
|
+
let packed = pack_headers([("origin", "https://anywhere.com")]);
|
|
299
|
+
let h = HeaderRefs::parse(&packed, false, 100).unwrap();
|
|
300
|
+
let ev = engine.evaluate(MethodKind::Get, &h);
|
|
301
|
+
assert!(ev.allowed);
|
|
302
|
+
assert!(!ev.preflight);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
#[test]
|
|
306
|
+
fn cors_allowlist_matching_origin() {
|
|
307
|
+
let opts = cors_options(
|
|
308
|
+
vec!["https://app.example.com".to_string()],
|
|
309
|
+
vec!["GET".to_string(), "POST".to_string()],
|
|
310
|
+
vec![],
|
|
311
|
+
true,
|
|
312
|
+
);
|
|
313
|
+
let engine = CorsEngine::from_options(Some(opts)).unwrap();
|
|
314
|
+
let packed = pack_headers([("origin", "https://app.example.com")]);
|
|
315
|
+
let h = HeaderRefs::parse(&packed, false, 100).unwrap();
|
|
316
|
+
let ev = engine.evaluate(MethodKind::Get, &h);
|
|
317
|
+
assert!(ev.allowed);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
#[test]
|
|
321
|
+
fn cors_allowlist_non_matching_origin() {
|
|
322
|
+
let opts = cors_options(
|
|
323
|
+
vec!["https://app.example.com".to_string()],
|
|
324
|
+
vec!["GET".to_string()],
|
|
325
|
+
vec![],
|
|
326
|
+
true,
|
|
327
|
+
);
|
|
328
|
+
let engine = CorsEngine::from_options(Some(opts)).unwrap();
|
|
329
|
+
let packed = pack_headers([("origin", "https://evil.example.net")]);
|
|
330
|
+
let h = HeaderRefs::parse(&packed, false, 100).unwrap();
|
|
331
|
+
let ev = engine.evaluate(MethodKind::Get, &h);
|
|
332
|
+
assert!(!ev.allowed);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
#[test]
|
|
336
|
+
fn cors_no_origin_never_allowed() {
|
|
337
|
+
let opts = cors_options(vec!["*".to_string()], vec![], vec![], false);
|
|
338
|
+
let engine = CorsEngine::from_options(Some(opts)).unwrap();
|
|
339
|
+
let packed = pack_headers([]);
|
|
340
|
+
let h = HeaderRefs::parse(&packed, false, 100).unwrap();
|
|
341
|
+
let ev = engine.evaluate(MethodKind::Get, &h);
|
|
342
|
+
assert!(!ev.allowed);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
#[test]
|
|
346
|
+
fn cors_preflight_allowed() {
|
|
347
|
+
let opts = cors_options(
|
|
348
|
+
vec!["https://app.example.com".to_string()],
|
|
349
|
+
vec!["POST".to_string()],
|
|
350
|
+
vec!["Content-Type".to_string()],
|
|
351
|
+
true,
|
|
352
|
+
);
|
|
353
|
+
let engine = CorsEngine::from_options(Some(opts)).unwrap();
|
|
354
|
+
let packed = pack_headers([
|
|
355
|
+
("origin", "https://app.example.com"),
|
|
356
|
+
("access-control-request-method", "POST"),
|
|
357
|
+
("access-control-request-headers", "Content-Type"),
|
|
358
|
+
]);
|
|
359
|
+
let h = HeaderRefs::parse(&packed, true, 100).unwrap();
|
|
360
|
+
let ev = engine.evaluate(MethodKind::Options, &h);
|
|
361
|
+
assert!(ev.preflight);
|
|
362
|
+
assert!(ev.allowed);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
#[test]
|
|
366
|
+
fn cors_preflight_disallowed_method() {
|
|
367
|
+
let opts = cors_options(
|
|
368
|
+
vec!["https://app.example.com".to_string()],
|
|
369
|
+
vec!["GET".to_string()],
|
|
370
|
+
vec![],
|
|
371
|
+
true,
|
|
372
|
+
);
|
|
373
|
+
let engine = CorsEngine::from_options(Some(opts)).unwrap();
|
|
374
|
+
let packed = pack_headers([
|
|
375
|
+
("origin", "https://app.example.com"),
|
|
376
|
+
("access-control-request-method", "DELETE"),
|
|
377
|
+
]);
|
|
378
|
+
let h = HeaderRefs::parse(&packed, true, 100).unwrap();
|
|
379
|
+
let ev = engine.evaluate(MethodKind::Options, &h);
|
|
380
|
+
assert!(ev.preflight);
|
|
381
|
+
assert!(!ev.allowed);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
#[test]
|
|
385
|
+
fn cors_preflight_disallowed_header() {
|
|
386
|
+
let opts = cors_options(
|
|
387
|
+
vec!["https://app.example.com".to_string()],
|
|
388
|
+
vec!["POST".to_string()],
|
|
389
|
+
vec!["Content-Type".to_string()],
|
|
390
|
+
true,
|
|
391
|
+
);
|
|
392
|
+
let engine = CorsEngine::from_options(Some(opts)).unwrap();
|
|
393
|
+
let packed = pack_headers([
|
|
394
|
+
("origin", "https://app.example.com"),
|
|
395
|
+
("access-control-request-method", "POST"),
|
|
396
|
+
("access-control-request-headers", "X-Secret-Token"),
|
|
397
|
+
]);
|
|
398
|
+
let h = HeaderRefs::parse(&packed, true, 100).unwrap();
|
|
399
|
+
let ev = engine.evaluate(MethodKind::Options, &h);
|
|
400
|
+
assert!(ev.preflight);
|
|
401
|
+
assert!(!ev.allowed);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// rust/ingress/ingress_constants.rs — NAPI exports for the ingress binary layout
|
|
2
|
+
//
|
|
3
|
+
// The NUMERIC values live in `output.rs` (the single source of truth). This
|
|
4
|
+
// module re-exports them to JavaScript via NAPI so TypeScript (via
|
|
5
|
+
// `src/ingress/constants.ts`) always reads the same numbers the Rust hot path
|
|
6
|
+
// uses — the two copies can never drift.
|
|
7
|
+
|
|
8
|
+
// The `as u32` on each const is an intentional explicit-width projection of the
|
|
9
|
+
// layout values (which are already u32) so the JS surface reads as a u32 layout
|
|
10
|
+
// contract. Newer clippy (>= 1.97) flags these as redundant casts.
|
|
11
|
+
#![allow(clippy::unnecessary_cast)]
|
|
12
|
+
|
|
13
|
+
use crate::ingress::output;
|
|
14
|
+
use napi_derive::napi;
|
|
15
|
+
|
|
16
|
+
// ── Output buffer layout ───────────────────────────────────
|
|
17
|
+
#[napi]
|
|
18
|
+
pub const INGRESS_OUT_VERDICT: u32 = output::OUT_VERDICT as u32;
|
|
19
|
+
#[napi]
|
|
20
|
+
pub const INGRESS_OUT_ERROR_CODE: u32 = output::OUT_ERROR_CODE as u32;
|
|
21
|
+
#[napi]
|
|
22
|
+
pub const INGRESS_OUT_STATUS: u32 = output::OUT_STATUS as u32;
|
|
23
|
+
#[napi]
|
|
24
|
+
pub const INGRESS_OUT_FLAGS: u32 = output::OUT_FLAGS as u32;
|
|
25
|
+
#[napi]
|
|
26
|
+
pub const INGRESS_OUT_RATE_LIMIT: u32 = output::OUT_RATE_LIMIT as u32;
|
|
27
|
+
#[napi]
|
|
28
|
+
pub const INGRESS_OUT_RATE_REMAINING: u32 = output::OUT_RATE_REMAINING as u32;
|
|
29
|
+
#[napi]
|
|
30
|
+
pub const INGRESS_OUT_RATE_RESET: u32 = output::OUT_RATE_RESET as u32;
|
|
31
|
+
#[napi]
|
|
32
|
+
pub const INGRESS_OUT_RETRY_AFTER: u32 = output::OUT_RETRY_AFTER as u32;
|
|
33
|
+
#[napi]
|
|
34
|
+
pub const INGRESS_OUT_COOKIES_JSON_LEN: u32 = output::OUT_COOKIES_JSON_LEN as u32;
|
|
35
|
+
#[napi]
|
|
36
|
+
pub const INGRESS_OUT_QUERY_JSON_LEN: u32 = output::OUT_QUERY_JSON_LEN as u32;
|
|
37
|
+
#[napi]
|
|
38
|
+
pub const INGRESS_OUT_HEADER_VARIANT: u32 = output::OUT_HEADER_VARIANT as u32;
|
|
39
|
+
#[napi]
|
|
40
|
+
pub const INGRESS_OUT_BODY_JSON_LEN: u32 = output::OUT_BODY_JSON_LEN as u32;
|
|
41
|
+
#[napi]
|
|
42
|
+
pub const INGRESS_OUT_DATA_START: u32 = output::OUT_DATA_START as u32;
|
|
43
|
+
|
|
44
|
+
// ── Flags ──────────────────────────────────────────────────
|
|
45
|
+
#[napi]
|
|
46
|
+
pub const INGRESS_FLAG_HAS_COOKIES: u32 = output::FLAG_HAS_COOKIES;
|
|
47
|
+
#[napi]
|
|
48
|
+
pub const INGRESS_FLAG_HAS_QUERY: u32 = output::FLAG_HAS_QUERY;
|
|
49
|
+
#[napi]
|
|
50
|
+
pub const INGRESS_FLAG_BODY_VALID_JSON: u32 = output::FLAG_BODY_VALID_JSON;
|
|
51
|
+
#[napi]
|
|
52
|
+
pub const INGRESS_FLAG_SCHEMA_VALID: u32 = output::FLAG_SCHEMA_VALID;
|
|
53
|
+
#[napi]
|
|
54
|
+
pub const INGRESS_FLAG_CORS_ALLOWED: u32 = output::FLAG_CORS_ALLOWED;
|
|
55
|
+
#[napi]
|
|
56
|
+
pub const INGRESS_FLAG_IS_PREFLIGHT: u32 = output::FLAG_IS_PREFLIGHT;
|
|
57
|
+
#[napi]
|
|
58
|
+
pub const INGRESS_FLAG_RATE_LIMITED: u32 = output::FLAG_RATE_LIMITED;
|
|
59
|
+
#[napi]
|
|
60
|
+
pub const INGRESS_FLAG_HTTPS: u32 = output::FLAG_HTTPS;
|
|
61
|
+
#[napi]
|
|
62
|
+
pub const INGRESS_FLAG_TRUSTED_PROXY: u32 = output::FLAG_TRUSTED_PROXY;
|
|
63
|
+
#[napi]
|
|
64
|
+
pub const INGRESS_FLAG_BODY_TRUNCATED: u32 = output::FLAG_BODY_TRUNCATED;
|
|
65
|
+
|
|
66
|
+
// ── Header variant bits ────────────────────────────────────
|
|
67
|
+
#[napi]
|
|
68
|
+
pub const INGRESS_HV_JSON: u32 = output::HV_JSON as u32;
|
|
69
|
+
#[napi]
|
|
70
|
+
pub const INGRESS_HV_CORS_SIMPLE: u32 = output::HV_CORS_SIMPLE as u32;
|
|
71
|
+
#[napi]
|
|
72
|
+
pub const INGRESS_HV_CORS_PREFLIGHT: u32 = output::HV_CORS_PREFLIGHT as u32;
|
|
73
|
+
#[napi]
|
|
74
|
+
pub const INGRESS_HV_RATE_ACTIVE: u32 = output::HV_RATE_ACTIVE as u32;
|
|
75
|
+
#[napi]
|
|
76
|
+
pub const INGRESS_HV_RATE_LIMITED: u32 = output::HV_RATE_LIMITED as u32;
|
|
77
|
+
#[napi]
|
|
78
|
+
pub const INGRESS_HV_COUNT: u32 = output::HV_COUNT as u32;
|
|
79
|
+
|
|
80
|
+
// ── Error codes ────────────────────────────────────────────
|
|
81
|
+
#[napi]
|
|
82
|
+
pub const INGRESS_ERR_NONE: u32 = output::ERR_CODE_NONE as u32;
|
|
83
|
+
#[napi]
|
|
84
|
+
pub const INGRESS_ERR_CORS_PREFLIGHT: u32 = output::ERR_CODE_CORS_PREFLIGHT as u32;
|
|
85
|
+
#[napi]
|
|
86
|
+
pub const INGRESS_ERR_RATE_LIMITED: u32 = output::ERR_CODE_RATE_LIMITED as u32;
|
|
87
|
+
#[napi]
|
|
88
|
+
pub const INGRESS_ERR_BODY_TOO_LARGE: u32 = output::ERR_CODE_BODY_TOO_LARGE as u32;
|
|
89
|
+
#[napi]
|
|
90
|
+
pub const INGRESS_ERR_INVALID_JSON: u32 = output::ERR_CODE_INVALID_JSON as u32;
|
|
91
|
+
#[napi]
|
|
92
|
+
pub const INGRESS_ERR_SCHEMA_VALIDATION: u32 = output::ERR_CODE_SCHEMA_VALIDATION as u32;
|
|
93
|
+
#[napi]
|
|
94
|
+
pub const INGRESS_ERR_BAD_REQUEST: u32 = output::ERR_CODE_BAD_REQUEST as u32;
|
|
95
|
+
#[napi]
|
|
96
|
+
pub const INGRESS_ERR_REQUEST_TOO_LARGE: u32 = output::ERR_CODE_REQUEST_TOO_LARGE as u32;
|
|
97
|
+
#[napi]
|
|
98
|
+
pub const INGRESS_ERR_INTERNAL: u32 = output::ERR_CODE_INTERNAL as u32;
|
|
99
|
+
|
|
100
|
+
#[cfg(test)]
|
|
101
|
+
mod tests {
|
|
102
|
+
use super::*;
|
|
103
|
+
use crate::ingress::output;
|
|
104
|
+
|
|
105
|
+
/// The napi projections must never drift from the single numeric source
|
|
106
|
+
/// (`output.rs`). If a layout constant is changed in `output.rs` and the
|
|
107
|
+
/// projection here is not updated, this test fails.
|
|
108
|
+
#[test]
|
|
109
|
+
fn out_constants_match_output_source() {
|
|
110
|
+
assert_eq!(INGRESS_OUT_VERDICT as u32, output::OUT_VERDICT as u32);
|
|
111
|
+
assert_eq!(INGRESS_OUT_ERROR_CODE as u32, output::OUT_ERROR_CODE as u32);
|
|
112
|
+
assert_eq!(INGRESS_OUT_STATUS as u32, output::OUT_STATUS as u32);
|
|
113
|
+
assert_eq!(INGRESS_OUT_FLAGS as u32, output::OUT_FLAGS as u32);
|
|
114
|
+
assert_eq!(INGRESS_OUT_RATE_LIMIT as u32, output::OUT_RATE_LIMIT as u32);
|
|
115
|
+
assert_eq!(
|
|
116
|
+
INGRESS_OUT_RATE_REMAINING as u32,
|
|
117
|
+
output::OUT_RATE_REMAINING as u32
|
|
118
|
+
);
|
|
119
|
+
assert_eq!(INGRESS_OUT_RATE_RESET as u32, output::OUT_RATE_RESET as u32);
|
|
120
|
+
assert_eq!(
|
|
121
|
+
INGRESS_OUT_RETRY_AFTER as u32,
|
|
122
|
+
output::OUT_RETRY_AFTER as u32
|
|
123
|
+
);
|
|
124
|
+
assert_eq!(
|
|
125
|
+
INGRESS_OUT_COOKIES_JSON_LEN as u32,
|
|
126
|
+
output::OUT_COOKIES_JSON_LEN as u32
|
|
127
|
+
);
|
|
128
|
+
assert_eq!(
|
|
129
|
+
INGRESS_OUT_QUERY_JSON_LEN as u32,
|
|
130
|
+
output::OUT_QUERY_JSON_LEN as u32
|
|
131
|
+
);
|
|
132
|
+
assert_eq!(
|
|
133
|
+
INGRESS_OUT_HEADER_VARIANT as u32,
|
|
134
|
+
output::OUT_HEADER_VARIANT as u32
|
|
135
|
+
);
|
|
136
|
+
assert_eq!(
|
|
137
|
+
INGRESS_OUT_BODY_JSON_LEN as u32,
|
|
138
|
+
output::OUT_BODY_JSON_LEN as u32
|
|
139
|
+
);
|
|
140
|
+
assert_eq!(INGRESS_OUT_DATA_START as u32, output::OUT_DATA_START as u32);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#[test]
|
|
144
|
+
fn flag_constants_match_output_source() {
|
|
145
|
+
assert_eq!(INGRESS_FLAG_HAS_COOKIES, output::FLAG_HAS_COOKIES);
|
|
146
|
+
assert_eq!(INGRESS_FLAG_HAS_QUERY, output::FLAG_HAS_QUERY);
|
|
147
|
+
assert_eq!(INGRESS_FLAG_BODY_VALID_JSON, output::FLAG_BODY_VALID_JSON);
|
|
148
|
+
assert_eq!(INGRESS_FLAG_SCHEMA_VALID, output::FLAG_SCHEMA_VALID);
|
|
149
|
+
assert_eq!(INGRESS_FLAG_CORS_ALLOWED, output::FLAG_CORS_ALLOWED);
|
|
150
|
+
assert_eq!(INGRESS_FLAG_IS_PREFLIGHT, output::FLAG_IS_PREFLIGHT);
|
|
151
|
+
assert_eq!(INGRESS_FLAG_RATE_LIMITED, output::FLAG_RATE_LIMITED);
|
|
152
|
+
assert_eq!(INGRESS_FLAG_HTTPS, output::FLAG_HTTPS);
|
|
153
|
+
assert_eq!(INGRESS_FLAG_TRUSTED_PROXY, output::FLAG_TRUSTED_PROXY);
|
|
154
|
+
assert_eq!(INGRESS_FLAG_BODY_TRUNCATED, output::FLAG_BODY_TRUNCATED);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[test]
|
|
158
|
+
fn hv_constants_match_output_source() {
|
|
159
|
+
assert_eq!(INGRESS_HV_JSON as u32, output::HV_JSON as u32);
|
|
160
|
+
assert_eq!(INGRESS_HV_CORS_SIMPLE as u32, output::HV_CORS_SIMPLE as u32);
|
|
161
|
+
assert_eq!(
|
|
162
|
+
INGRESS_HV_CORS_PREFLIGHT as u32,
|
|
163
|
+
output::HV_CORS_PREFLIGHT as u32
|
|
164
|
+
);
|
|
165
|
+
assert_eq!(INGRESS_HV_RATE_ACTIVE as u32, output::HV_RATE_ACTIVE as u32);
|
|
166
|
+
assert_eq!(
|
|
167
|
+
INGRESS_HV_RATE_LIMITED as u32,
|
|
168
|
+
output::HV_RATE_LIMITED as u32
|
|
169
|
+
);
|
|
170
|
+
assert_eq!(INGRESS_HV_COUNT as u32, output::HV_COUNT as u32);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#[test]
|
|
174
|
+
fn err_constants_match_output_source() {
|
|
175
|
+
assert_eq!(INGRESS_ERR_NONE as u32, output::ERR_CODE_NONE as u32);
|
|
176
|
+
assert_eq!(
|
|
177
|
+
INGRESS_ERR_CORS_PREFLIGHT as u32,
|
|
178
|
+
output::ERR_CODE_CORS_PREFLIGHT as u32
|
|
179
|
+
);
|
|
180
|
+
assert_eq!(
|
|
181
|
+
INGRESS_ERR_RATE_LIMITED as u32,
|
|
182
|
+
output::ERR_CODE_RATE_LIMITED as u32
|
|
183
|
+
);
|
|
184
|
+
assert_eq!(
|
|
185
|
+
INGRESS_ERR_BODY_TOO_LARGE as u32,
|
|
186
|
+
output::ERR_CODE_BODY_TOO_LARGE as u32
|
|
187
|
+
);
|
|
188
|
+
assert_eq!(
|
|
189
|
+
INGRESS_ERR_INVALID_JSON as u32,
|
|
190
|
+
output::ERR_CODE_INVALID_JSON as u32
|
|
191
|
+
);
|
|
192
|
+
assert_eq!(
|
|
193
|
+
INGRESS_ERR_SCHEMA_VALIDATION as u32,
|
|
194
|
+
output::ERR_CODE_SCHEMA_VALIDATION as u32
|
|
195
|
+
);
|
|
196
|
+
assert_eq!(
|
|
197
|
+
INGRESS_ERR_BAD_REQUEST as u32,
|
|
198
|
+
output::ERR_CODE_BAD_REQUEST as u32
|
|
199
|
+
);
|
|
200
|
+
assert_eq!(
|
|
201
|
+
INGRESS_ERR_REQUEST_TOO_LARGE as u32,
|
|
202
|
+
output::ERR_CODE_REQUEST_TOO_LARGE as u32
|
|
203
|
+
);
|
|
204
|
+
assert_eq!(
|
|
205
|
+
INGRESS_ERR_INTERNAL as u32,
|
|
206
|
+
output::ERR_CODE_INTERNAL as u32
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|