castrum 0.9.3 → 0.9.6
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 +2242 -0
- package/Cargo.toml +155 -0
- package/README.md +14 -6
- package/build.rs +5 -0
- package/castrum.darwin-arm64.node +0 -0
- package/castrum.darwin-x64.node +0 -0
- package/castrum.linux-arm64-gnu.node +0 -0
- package/castrum.linux-arm64-musl.node +0 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/castrum.linux-x64-musl.node +0 -0
- package/castrum.linux-x64-v3-gnu.node +0 -0
- package/castrum.win32-x64-msvc.node +0 -0
- package/dist/index.d.ts +1508 -1291
- package/dist/index.js +3391 -2069
- package/docs/API.md +2 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CASE_STUDY.md +5 -3
- package/docs/ENVIRONMENT.md +25 -2
- package/docs/FFI_BUN_GUIDE.md +242 -14
- package/docs/HOW_TO_ADD_AN_OP.md +10 -0
- package/docs/REPO_MAP.md +17 -6
- package/docs/RND-CONCURRENCY.md +772 -0
- package/docs/bun-builtins-decision-matrix.md +4 -1
- package/index.ts +24 -25
- package/package.json +173 -156
- 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 +105 -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 +69 -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/task.rs +172 -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 +136 -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 +211 -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 +770 -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 +567 -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 +883 -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 +73 -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/task/completion.rs +119 -0
- package/rust/task/mod.rs +65 -0
- package/rust/task/ops.rs +503 -0
- package/rust/task/runtime.rs +220 -0
- package/rust/task/tests.rs +231 -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 +594 -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/health.ts +1 -2
- 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 +130 -12
- package/src/native/ffi/build/compress.ts +22 -10
- package/src/native/ffi/build/instances.ts +46 -4
- package/src/native/ffi/build/metrics.ts +247 -0
- package/src/native/ffi/build/parse.ts +59 -23
- package/src/native/ffi/build/task.ts +79 -0
- package/src/native/ffi/build/util.ts +32 -1
- package/src/native/ffi/build.ts +4 -0
- package/src/native/ffi/selftest.ts +12 -3
- package/src/native/ffi/types.ts +151 -9
- package/src/native/ffi.ts +160 -15
- package/src/native/index.ts +1 -1
- package/src/native/loader.ts +55 -5
- 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 +71 -27
- 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/rust-ffi/text.ts +25 -3
- package/src/selection.ts +1 -1
- package/src/shared/bytes.ts +24 -4
- package/src/shared/codec.ts +18 -3
- package/src/task/index.ts +24 -0
- package/src/task/op.ts +171 -0
- package/src/task/runtime.ts +568 -0
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
use std::sync::Arc;
|
|
2
|
+
|
|
3
|
+
use super::*;
|
|
4
|
+
use crate::ingress::cors::{CorsEngine, CorsOptions};
|
|
5
|
+
use crate::ingress::output::{
|
|
6
|
+
ERR_CODE_BAD_REQUEST, ERR_CODE_BODY_TOO_LARGE, ERR_CODE_INVALID_JSON, ERR_CODE_NONE,
|
|
7
|
+
ERR_CODE_RATE_LIMITED, ERR_CODE_REQUEST_TOO_LARGE, ERR_CODE_SCHEMA_VALIDATION,
|
|
8
|
+
FLAG_CORS_ALLOWED, FLAG_HAS_COOKIES, FLAG_HAS_QUERY, FLAG_HTTPS, FLAG_SCHEMA_VALID,
|
|
9
|
+
HV_CORS_SIMPLE, HV_JSON, OUT_COOKIES_JSON_LEN, OUT_DATA_START, OUT_ERROR_CODE, OUT_FLAGS,
|
|
10
|
+
OUT_HEADER_VARIANT, OUT_QUERY_JSON_LEN, OUT_STATUS, OUT_VERDICT,
|
|
11
|
+
};
|
|
12
|
+
use crate::ingress::pipeline::IngressSchema;
|
|
13
|
+
use crate::ingress::rate_limit::{KeyedRateLimiter, RateLimiterState};
|
|
14
|
+
|
|
15
|
+
/// Build a packed input frame: `[method]` then u32le-length-prefixed
|
|
16
|
+
/// url/ip/rid sections, then a u32le-length-prefixed headers section.
|
|
17
|
+
fn packed_input(method: u8, url: &[u8], ip: &[u8], rid: &[u8]) -> Vec<u8> {
|
|
18
|
+
let mut out = Vec::new();
|
|
19
|
+
out.push(method);
|
|
20
|
+
for section in [url, ip, rid] {
|
|
21
|
+
out.extend_from_slice(&(section.len() as u32).to_le_bytes());
|
|
22
|
+
out.extend_from_slice(section);
|
|
23
|
+
}
|
|
24
|
+
// Headers section: empty for these tests.
|
|
25
|
+
out.extend_from_slice(&0u32.to_le_bytes());
|
|
26
|
+
out
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn base_inner() -> IngressInner {
|
|
30
|
+
IngressInner {
|
|
31
|
+
https_fixed: None,
|
|
32
|
+
max_body_bytes: 1_048_576,
|
|
33
|
+
proxy_trust: crate::ingress::ip_trust::ProxyTrustMode::None,
|
|
34
|
+
parse_cookies: false,
|
|
35
|
+
parse_query: false,
|
|
36
|
+
require_json_body: false,
|
|
37
|
+
guard_enabled: true,
|
|
38
|
+
emit_metadata_json: false,
|
|
39
|
+
cors_enabled: false,
|
|
40
|
+
cors: crate::ingress::cors::CorsEngine::disabled(),
|
|
41
|
+
rate: RateLimiterState::Disabled,
|
|
42
|
+
schema: None,
|
|
43
|
+
limits: Limits::default(),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[test]
|
|
48
|
+
fn handle_packed_simple_get_ok() {
|
|
49
|
+
let inner = base_inner();
|
|
50
|
+
let input = packed_input(0, b"/api/users", b"127.0.0.1", b"rid-1");
|
|
51
|
+
let mut out = vec![0u8; 512];
|
|
52
|
+
let written = inner.handle_packed(&input, b"", &mut out).unwrap();
|
|
53
|
+
|
|
54
|
+
assert_eq!(out[OUT_VERDICT], 0);
|
|
55
|
+
assert_eq!(out[OUT_ERROR_CODE], ERR_CODE_NONE);
|
|
56
|
+
assert_eq!(
|
|
57
|
+
u16::from_le_bytes([out[OUT_STATUS], out[OUT_STATUS + 1]]),
|
|
58
|
+
200
|
|
59
|
+
);
|
|
60
|
+
assert_eq!(out[OUT_HEADER_VARIANT], HV_JSON);
|
|
61
|
+
assert_eq!(written, OUT_DATA_START);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#[test]
|
|
65
|
+
fn handle_packed_body_too_large_413() {
|
|
66
|
+
let inner = IngressInner {
|
|
67
|
+
max_body_bytes: 4,
|
|
68
|
+
..base_inner()
|
|
69
|
+
};
|
|
70
|
+
let input = packed_input(2, b"/api", b"127.0.0.1", b"rid");
|
|
71
|
+
let mut out = vec![0u8; 512];
|
|
72
|
+
inner.handle_packed(&input, b"12345", &mut out).unwrap();
|
|
73
|
+
|
|
74
|
+
assert_eq!(out[OUT_VERDICT], 1);
|
|
75
|
+
assert_eq!(out[OUT_ERROR_CODE], ERR_CODE_BODY_TOO_LARGE);
|
|
76
|
+
assert_eq!(
|
|
77
|
+
u16::from_le_bytes([out[OUT_STATUS], out[OUT_STATUS + 1]]),
|
|
78
|
+
413
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[test]
|
|
83
|
+
fn handle_packed_invalid_json_400() {
|
|
84
|
+
let inner = IngressInner {
|
|
85
|
+
require_json_body: true,
|
|
86
|
+
..base_inner()
|
|
87
|
+
};
|
|
88
|
+
let input = packed_input(2, b"/api", b"127.0.0.1", b"rid");
|
|
89
|
+
let mut out = vec![0u8; 512];
|
|
90
|
+
inner.handle_packed(&input, b"not json", &mut out).unwrap();
|
|
91
|
+
|
|
92
|
+
assert_eq!(out[OUT_VERDICT], 1);
|
|
93
|
+
assert_eq!(out[OUT_ERROR_CODE], ERR_CODE_INVALID_JSON);
|
|
94
|
+
assert_eq!(
|
|
95
|
+
u16::from_le_bytes([out[OUT_STATUS], out[OUT_STATUS + 1]]),
|
|
96
|
+
400
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[test]
|
|
101
|
+
fn handle_packed_url_too_long_414() {
|
|
102
|
+
let inner = IngressInner {
|
|
103
|
+
limits: Limits {
|
|
104
|
+
max_url_bytes: 8,
|
|
105
|
+
..Limits::default()
|
|
106
|
+
},
|
|
107
|
+
..base_inner()
|
|
108
|
+
};
|
|
109
|
+
let input = packed_input(0, b"/a/very/long/url/path", b"127.0.0.1", b"rid");
|
|
110
|
+
let mut out = vec![0u8; 512];
|
|
111
|
+
inner.handle_packed(&input, b"", &mut out).unwrap();
|
|
112
|
+
|
|
113
|
+
assert_eq!(out[OUT_VERDICT], 1);
|
|
114
|
+
assert_eq!(out[OUT_ERROR_CODE], ERR_CODE_BAD_REQUEST);
|
|
115
|
+
assert_eq!(
|
|
116
|
+
u16::from_le_bytes([out[OUT_STATUS], out[OUT_STATUS + 1]]),
|
|
117
|
+
414
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── handle_request_full_sync_into / full_sync_into ─────────────
|
|
122
|
+
|
|
123
|
+
/// Build a minimal `IngressOptions` for unit tests (no JS runtime needed).
|
|
124
|
+
fn base_options() -> IngressOptions {
|
|
125
|
+
IngressOptions {
|
|
126
|
+
trust_proxy: None,
|
|
127
|
+
trusted_proxies: None,
|
|
128
|
+
parse_cookies: Some(true),
|
|
129
|
+
parse_query: Some(true),
|
|
130
|
+
require_json_body: None,
|
|
131
|
+
schema: None,
|
|
132
|
+
cors: None,
|
|
133
|
+
rate_limit: None,
|
|
134
|
+
https: Some(true),
|
|
135
|
+
max_body_bytes: None,
|
|
136
|
+
enable_body_size_guard: Some(true),
|
|
137
|
+
emit_metadata_json: Some(true),
|
|
138
|
+
limits: None,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#[test]
|
|
143
|
+
fn build_packed_input_sync_layout_matches_manual_frame() {
|
|
144
|
+
let headers: Vec<(String, String)> = vec![
|
|
145
|
+
(String::from("cookie"), String::from("a=1;b=2")),
|
|
146
|
+
(String::from("origin"), String::from("https://x")),
|
|
147
|
+
];
|
|
148
|
+
let packed = build_packed_input_sync(2, b"/api", b"1.2.3.4", b"rid", &headers);
|
|
149
|
+
|
|
150
|
+
// Manual equivalent: [method] then u32le-length-prefixed url/ip/rid
|
|
151
|
+
// sections, then a u32le-length-prefixed headers section containing a
|
|
152
|
+
// u16le count followed by (u16le name-len, name, u32le value-len, value).
|
|
153
|
+
let mut manual = Vec::new();
|
|
154
|
+
manual.push(2u8);
|
|
155
|
+
for section in [b"/api".as_slice(), b"1.2.3.4".as_slice(), b"rid".as_slice()] {
|
|
156
|
+
manual.extend_from_slice(&(section.len() as u32).to_le_bytes());
|
|
157
|
+
manual.extend_from_slice(section);
|
|
158
|
+
}
|
|
159
|
+
let header_pairs_len: usize = headers.iter().map(|(n, v)| 2 + n.len() + 4 + v.len()).sum();
|
|
160
|
+
manual.extend_from_slice(&((2 + header_pairs_len) as u32).to_le_bytes());
|
|
161
|
+
manual.extend_from_slice(&(headers.len() as u16).to_le_bytes());
|
|
162
|
+
for (n, v) in &headers {
|
|
163
|
+
manual.extend_from_slice(&(n.len() as u16).to_le_bytes());
|
|
164
|
+
manual.extend_from_slice(n.as_bytes());
|
|
165
|
+
manual.extend_from_slice(&(v.len() as u32).to_le_bytes());
|
|
166
|
+
manual.extend_from_slice(v.as_bytes());
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
assert_eq!(packed, manual);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#[test]
|
|
173
|
+
fn full_sync_into_matches_handle_packed_reference() {
|
|
174
|
+
let ingress = Ingress::new(base_options()).unwrap();
|
|
175
|
+
|
|
176
|
+
let headers: Vec<Vec<String>> = vec![
|
|
177
|
+
vec![String::from("cookie"), String::from("a=1;b=2")],
|
|
178
|
+
vec![String::from("x-forwarded-for"), String::from("9.9.9.9")],
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
let mut out = vec![0u8; 131072];
|
|
182
|
+
let written = ingress
|
|
183
|
+
.full_sync_into(
|
|
184
|
+
0,
|
|
185
|
+
"/api/users",
|
|
186
|
+
"127.0.0.1",
|
|
187
|
+
"rid-1",
|
|
188
|
+
headers,
|
|
189
|
+
b"",
|
|
190
|
+
&mut out,
|
|
191
|
+
)
|
|
192
|
+
.expect("full_sync_into should succeed");
|
|
193
|
+
|
|
194
|
+
// Reference: handle_packed on the equivalent manually-built frame,
|
|
195
|
+
// using an inner that mirrors `base_options()` (parse cookies/query,
|
|
196
|
+
// emit metadata, https fixed).
|
|
197
|
+
let inner = IngressInner {
|
|
198
|
+
https_fixed: Some(true),
|
|
199
|
+
parse_cookies: true,
|
|
200
|
+
parse_query: true,
|
|
201
|
+
emit_metadata_json: true,
|
|
202
|
+
..base_inner()
|
|
203
|
+
};
|
|
204
|
+
let mut ref_out = vec![0u8; 131072];
|
|
205
|
+
let header_pairs = vec![
|
|
206
|
+
(String::from("cookie"), String::from("a=1;b=2")),
|
|
207
|
+
(String::from("x-forwarded-for"), String::from("9.9.9.9")),
|
|
208
|
+
];
|
|
209
|
+
let ref_packed =
|
|
210
|
+
build_packed_input_sync(0, b"/api/users", b"127.0.0.1", b"rid-1", &header_pairs);
|
|
211
|
+
let ref_written = inner.handle_packed(&ref_packed, b"", &mut ref_out).unwrap();
|
|
212
|
+
|
|
213
|
+
assert_eq!(written, ref_written);
|
|
214
|
+
assert_eq!(&out[..written], &ref_out[..ref_written]);
|
|
215
|
+
// sanity: 200 ok
|
|
216
|
+
assert_eq!(out[OUT_VERDICT], 0);
|
|
217
|
+
assert_eq!(
|
|
218
|
+
u16::from_le_bytes([out[OUT_STATUS], out[OUT_STATUS + 1]]),
|
|
219
|
+
200
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#[test]
|
|
224
|
+
fn full_sync_into_rejects_tiny_output_buffer() {
|
|
225
|
+
let ingress = Ingress::new(base_options()).unwrap();
|
|
226
|
+
let mut tiny = vec![0u8; OUT_DATA_START - 1];
|
|
227
|
+
let err = ingress.full_sync_into(0, "/x", "1.1.1.1", "rid", vec![], b"", &mut tiny);
|
|
228
|
+
assert!(err.is_err());
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
#[test]
|
|
232
|
+
fn clamp_output_size_bounds() {
|
|
233
|
+
// Regression: a misconfigured huge `outputBufferSize` (e.g. u32::MAX ≈ 4 GiB)
|
|
234
|
+
// must be clamped to MAX_OUTPUT_BUFFER_SIZE instead of OOM-ing the process.
|
|
235
|
+
assert_eq!(clamp_output_size(None), 262_144); // default
|
|
236
|
+
assert_eq!(clamp_output_size(Some(0)), OUT_DATA_START); // floor
|
|
237
|
+
assert_eq!(clamp_output_size(Some(4096)), 4096); // passthrough
|
|
238
|
+
// u32::MAX (≈ 4 GiB) is capped to MAX_OUTPUT_BUFFER_SIZE (64 MiB = 67_108_864).
|
|
239
|
+
// (Literal avoids clippy 0.1.97's const-cast false positive.)
|
|
240
|
+
assert_eq!(clamp_output_size(Some(u32::MAX)), 67_108_864);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── Enabled-feature pipeline e2e (schema / rate limit / CORS / limits) ──
|
|
244
|
+
|
|
245
|
+
/// Build a packed input frame including a headers section:
|
|
246
|
+
/// `[method]` then u32le-length-prefixed url/ip/rid sections, then a
|
|
247
|
+
/// u32le-length-prefixed headers section
|
|
248
|
+
/// (`[u16 count] { [u16 name_len][name][u32 val_len][val] }`).
|
|
249
|
+
fn packed_input_with_headers(
|
|
250
|
+
method: u8,
|
|
251
|
+
url: &[u8],
|
|
252
|
+
ip: &[u8],
|
|
253
|
+
rid: &[u8],
|
|
254
|
+
headers: &[(&str, &str)],
|
|
255
|
+
) -> Vec<u8> {
|
|
256
|
+
let mut out = Vec::new();
|
|
257
|
+
out.push(method);
|
|
258
|
+
for section in [url, ip, rid] {
|
|
259
|
+
out.extend_from_slice(&(section.len() as u32).to_le_bytes());
|
|
260
|
+
out.extend_from_slice(section);
|
|
261
|
+
}
|
|
262
|
+
let header_pairs_len: usize = headers.iter().map(|(n, v)| 2 + n.len() + 4 + v.len()).sum();
|
|
263
|
+
out.extend_from_slice(&((2 + header_pairs_len) as u32).to_le_bytes());
|
|
264
|
+
out.extend_from_slice(&(headers.len() as u16).to_le_bytes());
|
|
265
|
+
for (n, v) in headers {
|
|
266
|
+
out.extend_from_slice(&(n.len() as u16).to_le_bytes());
|
|
267
|
+
out.extend_from_slice(n.as_bytes());
|
|
268
|
+
out.extend_from_slice(&(v.len() as u32).to_le_bytes());
|
|
269
|
+
out.extend_from_slice(v.as_bytes());
|
|
270
|
+
}
|
|
271
|
+
out
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
fn flags_at(out: &[u8]) -> u32 {
|
|
275
|
+
u32::from_le_bytes([
|
|
276
|
+
out[OUT_FLAGS],
|
|
277
|
+
out[OUT_FLAGS + 1],
|
|
278
|
+
out[OUT_FLAGS + 2],
|
|
279
|
+
out[OUT_FLAGS + 3],
|
|
280
|
+
])
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
fn status_at(out: &[u8]) -> u16 {
|
|
284
|
+
u16::from_le_bytes([out[OUT_STATUS], out[OUT_STATUS + 1]])
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
#[test]
|
|
288
|
+
fn schema_valid_200_and_invalid_422() {
|
|
289
|
+
let schema = IngressSchema::compile(&serde_json::json!({
|
|
290
|
+
"type": "object",
|
|
291
|
+
"required": ["id"],
|
|
292
|
+
"properties": { "id": { "type": "number" } },
|
|
293
|
+
}))
|
|
294
|
+
.expect("schema compiles");
|
|
295
|
+
let inner = IngressInner {
|
|
296
|
+
schema: Some(Arc::new(schema)),
|
|
297
|
+
..base_inner()
|
|
298
|
+
};
|
|
299
|
+
let input = packed_input(2, b"/api", b"127.0.0.1", b"rid");
|
|
300
|
+
|
|
301
|
+
// Well-formed but schema-invalid body → 422 (distinct from 400 malformed).
|
|
302
|
+
let mut out = vec![0u8; 512];
|
|
303
|
+
inner
|
|
304
|
+
.handle_packed(&input, br#"{"id":"x"}"#, &mut out)
|
|
305
|
+
.unwrap();
|
|
306
|
+
assert_eq!(out[OUT_VERDICT], 1);
|
|
307
|
+
assert_eq!(out[OUT_ERROR_CODE], ERR_CODE_SCHEMA_VALIDATION);
|
|
308
|
+
assert_eq!(status_at(&out), 422);
|
|
309
|
+
|
|
310
|
+
// Valid body → 200 with FLAG_SCHEMA_VALID (exercises the zero-DOM fast
|
|
311
|
+
// path, since this schema uses only supported keywords).
|
|
312
|
+
let mut out = vec![0u8; 512];
|
|
313
|
+
inner
|
|
314
|
+
.handle_packed(&input, br#"{"id":1}"#, &mut out)
|
|
315
|
+
.unwrap();
|
|
316
|
+
assert_eq!(out[OUT_VERDICT], 0);
|
|
317
|
+
assert_eq!(status_at(&out), 200);
|
|
318
|
+
assert_ne!(flags_at(&out) & FLAG_SCHEMA_VALID, 0);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#[test]
|
|
322
|
+
fn rate_limit_429_after_limit() {
|
|
323
|
+
let inner = IngressInner {
|
|
324
|
+
rate: RateLimiterState::Enabled(Arc::new(KeyedRateLimiter::new(1, 60_000, None))),
|
|
325
|
+
..base_inner()
|
|
326
|
+
};
|
|
327
|
+
let input = packed_input(0, b"/api", b"127.0.0.1", b"rid");
|
|
328
|
+
let mut out1 = vec![0u8; 512];
|
|
329
|
+
let mut out2 = vec![0u8; 512];
|
|
330
|
+
inner.handle_packed(&input, b"", &mut out1).unwrap();
|
|
331
|
+
inner.handle_packed(&input, b"", &mut out2).unwrap();
|
|
332
|
+
|
|
333
|
+
assert_eq!(out1[OUT_VERDICT], 0, "first request is allowed");
|
|
334
|
+
assert_eq!(status_at(&out1), 200);
|
|
335
|
+
assert_eq!(out2[OUT_VERDICT], 1, "second request is rate limited");
|
|
336
|
+
assert_eq!(out2[OUT_ERROR_CODE], ERR_CODE_RATE_LIMITED);
|
|
337
|
+
assert_eq!(status_at(&out2), 429);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
fn cors_engine() -> CorsEngine {
|
|
341
|
+
CorsEngine::from_options(Some(CorsOptions {
|
|
342
|
+
allow_origin: Some(vec!["https://app.example.com".into()]),
|
|
343
|
+
allow_methods: None,
|
|
344
|
+
allow_headers: None,
|
|
345
|
+
allow_credentials: Some(false),
|
|
346
|
+
}))
|
|
347
|
+
.unwrap()
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
#[test]
|
|
351
|
+
fn cors_preflight_allowed_204_forbidden_403() {
|
|
352
|
+
let inner = IngressInner {
|
|
353
|
+
cors_enabled: true,
|
|
354
|
+
cors: cors_engine(),
|
|
355
|
+
..base_inner()
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
// Allowed preflight: OPTIONS (6) with matching origin + ACRM.
|
|
359
|
+
let input = packed_input_with_headers(
|
|
360
|
+
6,
|
|
361
|
+
b"/api",
|
|
362
|
+
b"127.0.0.1",
|
|
363
|
+
b"rid",
|
|
364
|
+
&[
|
|
365
|
+
("origin", "https://app.example.com"),
|
|
366
|
+
("access-control-request-method", "POST"),
|
|
367
|
+
],
|
|
368
|
+
);
|
|
369
|
+
let mut out = vec![0u8; 512];
|
|
370
|
+
inner.handle_packed(&input, b"", &mut out).unwrap();
|
|
371
|
+
assert_eq!(out[OUT_ERROR_CODE], ERR_CODE_NONE);
|
|
372
|
+
assert_eq!(status_at(&out), 204);
|
|
373
|
+
|
|
374
|
+
// Forbidden preflight: non-matching origin.
|
|
375
|
+
let input = packed_input_with_headers(
|
|
376
|
+
6,
|
|
377
|
+
b"/api",
|
|
378
|
+
b"127.0.0.1",
|
|
379
|
+
b"rid",
|
|
380
|
+
&[
|
|
381
|
+
("origin", "https://evil.example.com"),
|
|
382
|
+
("access-control-request-method", "POST"),
|
|
383
|
+
],
|
|
384
|
+
);
|
|
385
|
+
let mut out = vec![0u8; 512];
|
|
386
|
+
inner.handle_packed(&input, b"", &mut out).unwrap();
|
|
387
|
+
assert_eq!(status_at(&out), 403);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
#[test]
|
|
391
|
+
fn cors_simple_sets_flag_and_hv() {
|
|
392
|
+
let inner = IngressInner {
|
|
393
|
+
cors_enabled: true,
|
|
394
|
+
cors: cors_engine(),
|
|
395
|
+
..base_inner()
|
|
396
|
+
};
|
|
397
|
+
let input = packed_input_with_headers(
|
|
398
|
+
0,
|
|
399
|
+
b"/api",
|
|
400
|
+
b"127.0.0.1",
|
|
401
|
+
b"rid",
|
|
402
|
+
&[("origin", "https://app.example.com")],
|
|
403
|
+
);
|
|
404
|
+
let mut out = vec![0u8; 512];
|
|
405
|
+
inner.handle_packed(&input, b"", &mut out).unwrap();
|
|
406
|
+
assert_eq!(status_at(&out), 200);
|
|
407
|
+
assert_ne!(flags_at(&out) & FLAG_CORS_ALLOWED, 0);
|
|
408
|
+
assert_ne!(out[OUT_HEADER_VARIANT] & HV_CORS_SIMPLE, 0);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
#[test]
|
|
412
|
+
fn too_many_headers_431() {
|
|
413
|
+
let inner = IngressInner {
|
|
414
|
+
limits: Limits {
|
|
415
|
+
max_headers: 1,
|
|
416
|
+
..Limits::default()
|
|
417
|
+
},
|
|
418
|
+
..base_inner()
|
|
419
|
+
};
|
|
420
|
+
let input =
|
|
421
|
+
packed_input_with_headers(0, b"/api", b"127.0.0.1", b"rid", &[("a", "1"), ("b", "2")]);
|
|
422
|
+
let mut out = vec![0u8; 512];
|
|
423
|
+
inner.handle_packed(&input, b"", &mut out).unwrap();
|
|
424
|
+
assert_eq!(out[OUT_VERDICT], 1);
|
|
425
|
+
assert_eq!(out[OUT_ERROR_CODE], ERR_CODE_REQUEST_TOO_LARGE);
|
|
426
|
+
assert_eq!(status_at(&out), 431);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
#[test]
|
|
430
|
+
fn headers_section_over_bytes_431() {
|
|
431
|
+
let inner = IngressInner {
|
|
432
|
+
limits: Limits {
|
|
433
|
+
max_headers_bytes: 4,
|
|
434
|
+
..Limits::default()
|
|
435
|
+
},
|
|
436
|
+
..base_inner()
|
|
437
|
+
};
|
|
438
|
+
let input = packed_input_with_headers(0, b"/api", b"127.0.0.1", b"rid", &[("a", "123456")]);
|
|
439
|
+
let mut out = vec![0u8; 512];
|
|
440
|
+
inner.handle_packed(&input, b"", &mut out).unwrap();
|
|
441
|
+
assert_eq!(out[OUT_ERROR_CODE], ERR_CODE_REQUEST_TOO_LARGE);
|
|
442
|
+
assert_eq!(status_at(&out), 431);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
#[test]
|
|
446
|
+
fn query_over_limit_414() {
|
|
447
|
+
let inner = IngressInner {
|
|
448
|
+
parse_query: true,
|
|
449
|
+
limits: Limits {
|
|
450
|
+
max_query_bytes: 4,
|
|
451
|
+
..Limits::default()
|
|
452
|
+
},
|
|
453
|
+
..base_inner()
|
|
454
|
+
};
|
|
455
|
+
let input = packed_input(0, b"/api?long=1234567890", b"127.0.0.1", b"rid");
|
|
456
|
+
let mut out = vec![0u8; 512];
|
|
457
|
+
inner.handle_packed(&input, b"", &mut out).unwrap();
|
|
458
|
+
assert_eq!(out[OUT_ERROR_CODE], ERR_CODE_BAD_REQUEST);
|
|
459
|
+
assert_eq!(status_at(&out), 414);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
#[test]
|
|
463
|
+
fn https_fixed_sets_flag() {
|
|
464
|
+
let inner = IngressInner {
|
|
465
|
+
https_fixed: Some(true),
|
|
466
|
+
..base_inner()
|
|
467
|
+
};
|
|
468
|
+
let input = packed_input(0, b"/api", b"127.0.0.1", b"rid");
|
|
469
|
+
let mut out = vec![0u8; 512];
|
|
470
|
+
inner.handle_packed(&input, b"", &mut out).unwrap();
|
|
471
|
+
assert_ne!(flags_at(&out) & FLAG_HTTPS, 0);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
#[test]
|
|
475
|
+
fn cookies_and_query_json_written() {
|
|
476
|
+
let inner = IngressInner {
|
|
477
|
+
parse_cookies: true,
|
|
478
|
+
parse_query: true,
|
|
479
|
+
..base_inner()
|
|
480
|
+
};
|
|
481
|
+
let input = packed_input_with_headers(
|
|
482
|
+
0,
|
|
483
|
+
b"/api?page=2&q=hello%20world",
|
|
484
|
+
b"127.0.0.1",
|
|
485
|
+
b"rid",
|
|
486
|
+
&[("cookie", "sid=abc123; theme=dark")],
|
|
487
|
+
);
|
|
488
|
+
let mut out = vec![0u8; 4096];
|
|
489
|
+
inner.handle_packed(&input, b"", &mut out).unwrap();
|
|
490
|
+
assert_eq!(status_at(&out), 200);
|
|
491
|
+
assert_ne!(flags_at(&out) & FLAG_HAS_COOKIES, 0);
|
|
492
|
+
assert_ne!(flags_at(&out) & FLAG_HAS_QUERY, 0);
|
|
493
|
+
|
|
494
|
+
let cookies_len = u32::from_le_bytes([
|
|
495
|
+
out[OUT_COOKIES_JSON_LEN],
|
|
496
|
+
out[OUT_COOKIES_JSON_LEN + 1],
|
|
497
|
+
out[OUT_COOKIES_JSON_LEN + 2],
|
|
498
|
+
out[OUT_COOKIES_JSON_LEN + 3],
|
|
499
|
+
]) as usize;
|
|
500
|
+
let query_len = u32::from_le_bytes([
|
|
501
|
+
out[OUT_QUERY_JSON_LEN],
|
|
502
|
+
out[OUT_QUERY_JSON_LEN + 1],
|
|
503
|
+
out[OUT_QUERY_JSON_LEN + 2],
|
|
504
|
+
out[OUT_QUERY_JSON_LEN + 3],
|
|
505
|
+
]) as usize;
|
|
506
|
+
assert!(cookies_len > 2);
|
|
507
|
+
assert!(query_len > 2);
|
|
508
|
+
|
|
509
|
+
let cookies_json = &out[OUT_DATA_START..OUT_DATA_START + cookies_len];
|
|
510
|
+
let query_json = &out[OUT_DATA_START + cookies_len..OUT_DATA_START + cookies_len + query_len];
|
|
511
|
+
assert_eq!(cookies_json, br#"{"sid":"abc123","theme":"dark"}"#);
|
|
512
|
+
// Query keys in order; `%20` decoded to a space and JSON-escaped.
|
|
513
|
+
assert_eq!(query_json, br#"{"page":"2","q":"hello world"}"#);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
#[test]
|
|
517
|
+
fn unicode_query_json_written() {
|
|
518
|
+
let inner = IngressInner {
|
|
519
|
+
parse_query: true,
|
|
520
|
+
..base_inner()
|
|
521
|
+
};
|
|
522
|
+
let input = packed_input(
|
|
523
|
+
0,
|
|
524
|
+
"/api?q=%E2%9C%93&e=%F0%9F%98%80".as_bytes(),
|
|
525
|
+
b"127.0.0.1",
|
|
526
|
+
b"rid",
|
|
527
|
+
);
|
|
528
|
+
let mut out = vec![0u8; 4096];
|
|
529
|
+
inner.handle_packed(&input, b"", &mut out).unwrap();
|
|
530
|
+
assert_eq!(status_at(&out), 200);
|
|
531
|
+
|
|
532
|
+
let query_len = u32::from_le_bytes([
|
|
533
|
+
out[OUT_QUERY_JSON_LEN],
|
|
534
|
+
out[OUT_QUERY_JSON_LEN + 1],
|
|
535
|
+
out[OUT_QUERY_JSON_LEN + 2],
|
|
536
|
+
out[OUT_QUERY_JSON_LEN + 3],
|
|
537
|
+
]) as usize;
|
|
538
|
+
let query_json = &out[OUT_DATA_START..OUT_DATA_START + query_len];
|
|
539
|
+
// Percent-decoded UTF-8 (`✓`, `😀`) is preserved verbatim in the JSON.
|
|
540
|
+
let expected = "{\"q\":\"✓\",\"e\":\"😀\"}".as_bytes();
|
|
541
|
+
assert_eq!(query_json, expected);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
#[test]
|
|
545
|
+
fn truncated_multibyte_query_escaped() {
|
|
546
|
+
let inner = IngressInner {
|
|
547
|
+
parse_query: true,
|
|
548
|
+
..base_inner()
|
|
549
|
+
};
|
|
550
|
+
// `%E2%9C` is a truncated 3-byte sequence: `decodeURIComponent` throws, so
|
|
551
|
+
// the component is returned RAW ("%E2%9C") — the same thing the JS
|
|
552
|
+
// fallback does — and the request is NOT rejected. The JSON writer then
|
|
553
|
+
// sees plain ASCII and the length accounting is the raw length.
|
|
554
|
+
let input = packed_input(0, "/api?q=%E2%9C".as_bytes(), b"127.0.0.1", b"rid");
|
|
555
|
+
let mut out = vec![0u8; 4096];
|
|
556
|
+
inner.handle_packed(&input, b"", &mut out).unwrap();
|
|
557
|
+
assert_eq!(status_at(&out), 200);
|
|
558
|
+
|
|
559
|
+
let query_len = u32::from_le_bytes([
|
|
560
|
+
out[OUT_QUERY_JSON_LEN],
|
|
561
|
+
out[OUT_QUERY_JSON_LEN + 1],
|
|
562
|
+
out[OUT_QUERY_JSON_LEN + 2],
|
|
563
|
+
out[OUT_QUERY_JSON_LEN + 3],
|
|
564
|
+
]) as usize;
|
|
565
|
+
let query_json = &out[OUT_DATA_START..OUT_DATA_START + query_len];
|
|
566
|
+
assert_eq!(query_json, br#"{"q":"%E2%9C"}"#);
|
|
567
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// rust/ingress/time.rs — Monotonic + wall-clock helpers.
|
|
2
|
+
//
|
|
3
|
+
// The rate limiter needs a stable monotonic clock plus a one-time wall offset
|
|
4
|
+
// so `rate_now_ms` returns a timestamp consistent across the process. The
|
|
5
|
+
// `OnceLock` statics are initialized lazily on first use.
|
|
6
|
+
|
|
7
|
+
use std::sync::OnceLock;
|
|
8
|
+
use std::time::{Instant, SystemTime, UNIX_EPOCH};
|
|
9
|
+
|
|
10
|
+
static START: OnceLock<Instant> = OnceLock::new();
|
|
11
|
+
static WALL_OFFSET_MS: OnceLock<i128> = OnceLock::new();
|
|
12
|
+
|
|
13
|
+
#[inline(always)]
|
|
14
|
+
pub(crate) fn monotonic_ms() -> u128 {
|
|
15
|
+
START.get_or_init(Instant::now).elapsed().as_millis()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[inline(always)]
|
|
19
|
+
fn wall_now_ms() -> u64 {
|
|
20
|
+
SystemTime::now()
|
|
21
|
+
.duration_since(UNIX_EPOCH)
|
|
22
|
+
.map(|d| d.as_millis() as u64)
|
|
23
|
+
.unwrap_or(0)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[inline(always)]
|
|
27
|
+
fn wall_offset_ms() -> i128 {
|
|
28
|
+
*WALL_OFFSET_MS.get_or_init(|| wall_now_ms() as i128 - monotonic_ms() as i128)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Current time in ms since the unix epoch, derived from the monotonic clock.
|
|
32
|
+
#[inline(always)]
|
|
33
|
+
pub(crate) fn rate_now_ms() -> u64 {
|
|
34
|
+
let v = monotonic_ms() as i128 + wall_offset_ms();
|
|
35
|
+
if v < 0 {
|
|
36
|
+
0
|
|
37
|
+
} else {
|
|
38
|
+
v as u64
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[cfg(test)]
|
|
43
|
+
mod tests {
|
|
44
|
+
use super::*;
|
|
45
|
+
|
|
46
|
+
#[test]
|
|
47
|
+
fn rate_now_ms_is_sane_unix_time() {
|
|
48
|
+
let now = rate_now_ms();
|
|
49
|
+
// A plausible unix-epoch millis value: after ~2001, before ~year 3000.
|
|
50
|
+
assert!(now > 978_307_200_000, "rate_now_ms too small: {now}");
|
|
51
|
+
assert!(now < 32_000_000_000_000, "rate_now_ms too large: {now}");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#[test]
|
|
55
|
+
fn monotonic_ms_is_non_decreasing() {
|
|
56
|
+
let a = monotonic_ms();
|
|
57
|
+
let b = monotonic_ms();
|
|
58
|
+
assert!(b >= a, "monotonic clock went backwards");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#[test]
|
|
62
|
+
fn rate_now_ms_is_stable_across_calls() {
|
|
63
|
+
// Two calls back-to-back must agree within a wide window (both derive
|
|
64
|
+
// from the same monotonic clock + wall offset).
|
|
65
|
+
let a = rate_now_ms();
|
|
66
|
+
let b = rate_now_ms();
|
|
67
|
+
assert!(b >= a);
|
|
68
|
+
assert!(b - a < 5_000);
|
|
69
|
+
}
|
|
70
|
+
}
|