castrum 0.7.0
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.toml +73 -0
- package/README.md +432 -0
- package/bench.ts +3 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/index.ts +24 -0
- package/package.json +94 -0
- package/rust/batch.rs +195 -0
- package/rust/cookie_parser.rs +70 -0
- package/rust/cors.rs +263 -0
- package/rust/hashing.rs +164 -0
- package/rust/headers.rs +234 -0
- package/rust/hmac_sha256.rs +113 -0
- package/rust/http_parser.rs +132 -0
- package/rust/ingress.rs +616 -0
- package/rust/ingress_constants.rs +89 -0
- package/rust/ip_trust.rs +185 -0
- package/rust/json_ops.rs +66 -0
- package/rust/json_patch_ops.rs +25 -0
- package/rust/json_schema.rs +151 -0
- package/rust/json_ser.rs +429 -0
- package/rust/lib.rs +48 -0
- package/rust/method.rs +141 -0
- package/rust/mime_lookup.rs +76 -0
- package/rust/output.rs +238 -0
- package/rust/proxy.rs +90 -0
- package/rust/query_parser.rs +141 -0
- package/rust/random_token.rs +52 -0
- package/rust/rate_limit.rs +203 -0
- package/rust/terminal.rs +95 -0
- package/rust/unit_tests.rs +843 -0
- package/rust/url_codec.rs +133 -0
- package/rust/util.rs +537 -0
- package/rust/validation.rs +107 -0
- package/rust/websocket.rs +23 -0
- package/src/baseline/index.ts +12 -0
- package/src/baseline/tasks/cookie.ts +22 -0
- package/src/baseline/tasks/hashing.ts +18 -0
- package/src/baseline/tasks/hmac.ts +38 -0
- package/src/baseline/tasks/http.ts +62 -0
- package/src/baseline/tasks/json-patch.ts +12 -0
- package/src/baseline/tasks/json.ts +50 -0
- package/src/baseline/tasks/mime.ts +5 -0
- package/src/baseline/tasks/query.ts +14 -0
- package/src/baseline/tasks/token.ts +15 -0
- package/src/baseline/tasks/url.ts +11 -0
- package/src/baseline/tasks/validation.ts +19 -0
- package/src/baseline/tasks/websocket.ts +14 -0
- package/src/bench/assert.ts +70 -0
- package/src/bench/checks.ts +227 -0
- package/src/bench/checksum.ts +27 -0
- package/src/bench/comparisons.ts +70 -0
- package/src/bench/concurrent-worker.ts +135 -0
- package/src/bench/fixtures.ts +233 -0
- package/src/bench/index.ts +2 -0
- package/src/bench/measure.ts +177 -0
- package/src/bench/now.ts +3 -0
- package/src/bench/report.ts +79 -0
- package/src/bench/run.ts +57 -0
- package/src/bench/tasks/complex.ts +232 -0
- package/src/bench/tasks/concurrent.ts +213 -0
- package/src/bench/tasks/cookie.ts +44 -0
- package/src/bench/tasks/hashing.ts +33 -0
- package/src/bench/tasks/hmac.ts +36 -0
- package/src/bench/tasks/http.ts +50 -0
- package/src/bench/tasks/index.ts +52 -0
- package/src/bench/tasks/json-patch.ts +21 -0
- package/src/bench/tasks/json.ts +33 -0
- package/src/bench/tasks/mime.ts +21 -0
- package/src/bench/tasks/query.ts +44 -0
- package/src/bench/tasks/stress.ts +142 -0
- package/src/bench/tasks/token.ts +20 -0
- package/src/bench/tasks/url.ts +34 -0
- package/src/bench/tasks/validation.ts +57 -0
- package/src/bench/tasks/websocket.ts +21 -0
- package/src/bench/types.ts +60 -0
- package/src/data/json-rows.ts +33 -0
- package/src/ingress/constants.ts +56 -0
- package/src/ingress/fast.ts +1082 -0
- package/src/ingress/handlers.ts +1202 -0
- package/src/ingress/index.ts +457 -0
- package/src/ingress/packed-input.ts +59 -0
- package/src/native/index.ts +207 -0
- package/src/rust-ffi/index.ts +555 -0
- package/src/shared/bytes.ts +36 -0
- package/src/shared/json.ts +18 -0
- package/src/shared/packed.ts +316 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
use crate::hashing::fast_hash_bytes;
|
|
2
|
+
use lru::LruCache;
|
|
3
|
+
use parking_lot::Mutex;
|
|
4
|
+
use std::collections::HashMap;
|
|
5
|
+
use std::num::NonZeroUsize;
|
|
6
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
7
|
+
use std::sync::{Arc, OnceLock};
|
|
8
|
+
|
|
9
|
+
static LIMITER_ID: AtomicU64 = AtomicU64::new(0);
|
|
10
|
+
|
|
11
|
+
struct Counter {
|
|
12
|
+
window_start: u64,
|
|
13
|
+
prev: u32,
|
|
14
|
+
curr: u32,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#[repr(align(64))]
|
|
18
|
+
struct Shard {
|
|
19
|
+
map: Mutex<LruCache<u64, Counter>>,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
pub struct RateOutcome {
|
|
23
|
+
pub allowed: bool,
|
|
24
|
+
pub remaining: u32,
|
|
25
|
+
pub reset_ms: u64,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pub struct KeyedRateLimiter {
|
|
29
|
+
shards: Box<[Shard]>,
|
|
30
|
+
shard_mask: usize,
|
|
31
|
+
limit: u32,
|
|
32
|
+
window_ms: u64,
|
|
33
|
+
seed: u64,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
impl KeyedRateLimiter {
|
|
37
|
+
pub fn new(limit: u32, window_ms: u32, max_entries: Option<usize>) -> Self {
|
|
38
|
+
const SHARD_COUNT: usize = 256;
|
|
39
|
+
|
|
40
|
+
let id = LIMITER_ID.fetch_add(1, Ordering::Relaxed);
|
|
41
|
+
let seed = fast_hash_bytes(&id.to_le_bytes());
|
|
42
|
+
|
|
43
|
+
let max_entries = max_entries.unwrap_or(1_048_576);
|
|
44
|
+
let max_per_shard = (max_entries / SHARD_COUNT).max(64);
|
|
45
|
+
|
|
46
|
+
let cap = NonZeroUsize::new(max_per_shard)
|
|
47
|
+
.unwrap_or_else(|| NonZeroUsize::new(64).expect("64 is nonzero"));
|
|
48
|
+
|
|
49
|
+
let shards = (0..SHARD_COUNT)
|
|
50
|
+
.map(|_| Shard {
|
|
51
|
+
map: Mutex::new(LruCache::new(cap)),
|
|
52
|
+
})
|
|
53
|
+
.collect();
|
|
54
|
+
|
|
55
|
+
Self {
|
|
56
|
+
shards,
|
|
57
|
+
shard_mask: SHARD_COUNT - 1,
|
|
58
|
+
limit,
|
|
59
|
+
window_ms: window_ms.max(1) as u64,
|
|
60
|
+
seed,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#[inline(always)]
|
|
65
|
+
pub fn seed(&self) -> u64 {
|
|
66
|
+
self.seed
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[inline(always)]
|
|
70
|
+
pub fn limit(&self) -> u32 {
|
|
71
|
+
self.limit
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
pub fn check_key(&self, key: u64, now_ms: u64) -> RateOutcome {
|
|
75
|
+
if self.limit == 0 {
|
|
76
|
+
return RateOutcome {
|
|
77
|
+
allowed: false,
|
|
78
|
+
remaining: 0,
|
|
79
|
+
reset_ms: now_ms.saturating_add(self.window_ms),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if self.limit == u32::MAX {
|
|
84
|
+
return RateOutcome {
|
|
85
|
+
allowed: true,
|
|
86
|
+
remaining: u32::MAX,
|
|
87
|
+
reset_ms: now_ms.saturating_add(self.window_ms),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let shard = &self.shards[(key as usize) & self.shard_mask];
|
|
92
|
+
let mut map = shard.map.lock();
|
|
93
|
+
|
|
94
|
+
let window = self.window_ms.max(1);
|
|
95
|
+
|
|
96
|
+
let c = match map.get_mut(&key) {
|
|
97
|
+
Some(c) => c,
|
|
98
|
+
None => {
|
|
99
|
+
map.put(
|
|
100
|
+
key,
|
|
101
|
+
Counter {
|
|
102
|
+
window_start: now_ms,
|
|
103
|
+
prev: 0,
|
|
104
|
+
curr: 0,
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
map.get_mut(&key).expect("rate limiter counter must exist after insert")
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
advance_window(c, now_ms, window);
|
|
113
|
+
|
|
114
|
+
let elapsed = now_ms.saturating_sub(c.window_start);
|
|
115
|
+
let overlap = window.saturating_sub(elapsed.min(window));
|
|
116
|
+
|
|
117
|
+
let weighted =
|
|
118
|
+
((c.prev as u128) * (overlap as u128) / (window as u128)) + (c.curr as u128);
|
|
119
|
+
|
|
120
|
+
let reset = c.window_start.saturating_add(window);
|
|
121
|
+
|
|
122
|
+
if weighted < self.limit as u128 {
|
|
123
|
+
c.curr = c.curr.saturating_add(1);
|
|
124
|
+
|
|
125
|
+
let remaining = (self.limit as u128)
|
|
126
|
+
.saturating_sub(weighted.saturating_add(1)) as u32;
|
|
127
|
+
|
|
128
|
+
RateOutcome {
|
|
129
|
+
allowed: true,
|
|
130
|
+
remaining,
|
|
131
|
+
reset_ms: reset,
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
RateOutcome {
|
|
135
|
+
allowed: false,
|
|
136
|
+
remaining: 0,
|
|
137
|
+
reset_ms: reset,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fn advance_window(c: &mut Counter, now_ms: u64, window: u64) {
|
|
144
|
+
let elapsed = now_ms.saturating_sub(c.window_start);
|
|
145
|
+
|
|
146
|
+
if elapsed < window {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let double_window = window.saturating_mul(2);
|
|
151
|
+
|
|
152
|
+
if elapsed >= double_window {
|
|
153
|
+
c.window_start = now_ms;
|
|
154
|
+
c.prev = 0;
|
|
155
|
+
c.curr = 0;
|
|
156
|
+
} else {
|
|
157
|
+
c.window_start = c.window_start.saturating_add(window);
|
|
158
|
+
c.prev = c.curr;
|
|
159
|
+
c.curr = 0;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Shared limiter registry ───────────────────────────────────────
|
|
164
|
+
// Rate limiting must be shared across all ingress instances/routes in the
|
|
165
|
+
// process, otherwise a client can bypass a per-IP limit by spreading requests
|
|
166
|
+
// across endpoints (each route would get its own independent bucket). All
|
|
167
|
+
// instances configured with the same (limit, window_ms, max_entries) share one
|
|
168
|
+
// process-wide limiter.
|
|
169
|
+
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
|
170
|
+
struct LimiterKey {
|
|
171
|
+
limit: u32,
|
|
172
|
+
window_ms: u32,
|
|
173
|
+
max_entries: usize,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
static SHARED_LIMITERS: OnceLock<Mutex<HashMap<LimiterKey, Arc<KeyedRateLimiter>>>> =
|
|
177
|
+
OnceLock::new();
|
|
178
|
+
|
|
179
|
+
/// Return the process-wide shared limiter for the given configuration.
|
|
180
|
+
/// Instances with identical configuration share the same underlying limiter.
|
|
181
|
+
pub fn shared_limiter(
|
|
182
|
+
limit: u32,
|
|
183
|
+
window_ms: u32,
|
|
184
|
+
max_entries: Option<usize>,
|
|
185
|
+
) -> Arc<KeyedRateLimiter> {
|
|
186
|
+
let resolved = max_entries.unwrap_or(1_048_576);
|
|
187
|
+
let key = LimiterKey {
|
|
188
|
+
limit,
|
|
189
|
+
window_ms,
|
|
190
|
+
max_entries: resolved,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
let map = SHARED_LIMITERS.get_or_init(|| Mutex::new(HashMap::new()));
|
|
194
|
+
let mut guard = map.lock();
|
|
195
|
+
|
|
196
|
+
if let Some(existing) = guard.get(&key) {
|
|
197
|
+
return existing.clone();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let limiter = Arc::new(KeyedRateLimiter::new(limit, window_ms, Some(resolved)));
|
|
201
|
+
guard.insert(key, limiter.clone());
|
|
202
|
+
limiter
|
|
203
|
+
}
|
package/rust/terminal.rs
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// rust/terminal.rs — Cold-path terminal response writers
|
|
2
|
+
// Extracted from ingress.rs. These functions are #[cold] so they don't
|
|
3
|
+
// pollute the hot-path instruction cache.
|
|
4
|
+
|
|
5
|
+
use crate::output::{
|
|
6
|
+
write_output_header, ERR_CODE_BODY_TOO_LARGE, ERR_CODE_CORS_PREFLIGHT,
|
|
7
|
+
ERR_CODE_INVALID_JSON, ERR_CODE_NONE, ERR_CODE_RATE_LIMITED,
|
|
8
|
+
ERR_CODE_SCHEMA_VALIDATION, HV_JSON,
|
|
9
|
+
};
|
|
10
|
+
use napi::Result;
|
|
11
|
+
|
|
12
|
+
/// Write a simple terminal response (no CORS, no rate limiting).
|
|
13
|
+
#[cold]
|
|
14
|
+
pub fn terminal_simple(out: &mut [u8], verdict: u8, error_code: u8, status: u16) -> Result<usize> {
|
|
15
|
+
Ok(write_output_header(
|
|
16
|
+
out, verdict, error_code, status, 0, 0, 0, 0, 0, 0, 0, HV_JSON, 0,
|
|
17
|
+
))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Write a preflight OK response (204).
|
|
21
|
+
#[cold]
|
|
22
|
+
pub fn terminal_preflight_ok(flags: u32, hv: u8, out: &mut [u8]) -> Result<usize> {
|
|
23
|
+
Ok(write_output_header(
|
|
24
|
+
out, 2, ERR_CODE_NONE, 204, flags, 0, 0, 0, 0, 0, 0, hv, 0,
|
|
25
|
+
))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Write a preflight forbidden response (403).
|
|
29
|
+
#[cold]
|
|
30
|
+
pub fn terminal_preflight_forbidden(flags: u32, hv: u8, out: &mut [u8]) -> Result<usize> {
|
|
31
|
+
Ok(write_output_header(
|
|
32
|
+
out, 2, ERR_CODE_CORS_PREFLIGHT, 403, flags, 0, 0, 0, 0, 0, 0, hv, 0,
|
|
33
|
+
))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Write a rate-limited response (429).
|
|
37
|
+
#[cold]
|
|
38
|
+
pub fn terminal_rate_limited(
|
|
39
|
+
flags: u32,
|
|
40
|
+
hv: u8,
|
|
41
|
+
rl: u32,
|
|
42
|
+
rr: u32,
|
|
43
|
+
reset: u64,
|
|
44
|
+
retry: u64,
|
|
45
|
+
out: &mut [u8],
|
|
46
|
+
) -> Result<usize> {
|
|
47
|
+
Ok(write_output_header(
|
|
48
|
+
out, 1, ERR_CODE_RATE_LIMITED, 429, flags, rl, rr, reset, retry, 0, 0, hv, 0,
|
|
49
|
+
))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Write a body-too-large response (413).
|
|
53
|
+
#[cold]
|
|
54
|
+
pub fn terminal_body_too_large(
|
|
55
|
+
flags: u32,
|
|
56
|
+
hv: u8,
|
|
57
|
+
rl: u32,
|
|
58
|
+
rr: u32,
|
|
59
|
+
reset: u64,
|
|
60
|
+
out: &mut [u8],
|
|
61
|
+
) -> Result<usize> {
|
|
62
|
+
Ok(write_output_header(
|
|
63
|
+
out, 1, ERR_CODE_BODY_TOO_LARGE, 413, flags, rl, rr, reset, 0, 0, 0, hv, 0,
|
|
64
|
+
))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Write an invalid JSON response (400).
|
|
68
|
+
#[cold]
|
|
69
|
+
pub fn terminal_invalid_json(
|
|
70
|
+
flags: u32,
|
|
71
|
+
hv: u8,
|
|
72
|
+
rl: u32,
|
|
73
|
+
rr: u32,
|
|
74
|
+
reset: u64,
|
|
75
|
+
out: &mut [u8],
|
|
76
|
+
) -> Result<usize> {
|
|
77
|
+
Ok(write_output_header(
|
|
78
|
+
out, 1, ERR_CODE_INVALID_JSON, 400, flags, rl, rr, reset, 0, 0, 0, hv, 0,
|
|
79
|
+
))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Write a schema validation failure response (422).
|
|
83
|
+
#[cold]
|
|
84
|
+
pub fn terminal_schema_validation(
|
|
85
|
+
flags: u32,
|
|
86
|
+
hv: u8,
|
|
87
|
+
rl: u32,
|
|
88
|
+
rr: u32,
|
|
89
|
+
reset: u64,
|
|
90
|
+
out: &mut [u8],
|
|
91
|
+
) -> Result<usize> {
|
|
92
|
+
Ok(write_output_header(
|
|
93
|
+
out, 1, ERR_CODE_SCHEMA_VALIDATION, 422, flags, rl, rr, reset, 0, 0, 0, hv, 0,
|
|
94
|
+
))
|
|
95
|
+
}
|