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.
Files changed (86) hide show
  1. package/Cargo.toml +73 -0
  2. package/README.md +432 -0
  3. package/bench.ts +3 -0
  4. package/castrum.linux-x64-gnu.node +0 -0
  5. package/index.ts +24 -0
  6. package/package.json +94 -0
  7. package/rust/batch.rs +195 -0
  8. package/rust/cookie_parser.rs +70 -0
  9. package/rust/cors.rs +263 -0
  10. package/rust/hashing.rs +164 -0
  11. package/rust/headers.rs +234 -0
  12. package/rust/hmac_sha256.rs +113 -0
  13. package/rust/http_parser.rs +132 -0
  14. package/rust/ingress.rs +616 -0
  15. package/rust/ingress_constants.rs +89 -0
  16. package/rust/ip_trust.rs +185 -0
  17. package/rust/json_ops.rs +66 -0
  18. package/rust/json_patch_ops.rs +25 -0
  19. package/rust/json_schema.rs +151 -0
  20. package/rust/json_ser.rs +429 -0
  21. package/rust/lib.rs +48 -0
  22. package/rust/method.rs +141 -0
  23. package/rust/mime_lookup.rs +76 -0
  24. package/rust/output.rs +238 -0
  25. package/rust/proxy.rs +90 -0
  26. package/rust/query_parser.rs +141 -0
  27. package/rust/random_token.rs +52 -0
  28. package/rust/rate_limit.rs +203 -0
  29. package/rust/terminal.rs +95 -0
  30. package/rust/unit_tests.rs +843 -0
  31. package/rust/url_codec.rs +133 -0
  32. package/rust/util.rs +537 -0
  33. package/rust/validation.rs +107 -0
  34. package/rust/websocket.rs +23 -0
  35. package/src/baseline/index.ts +12 -0
  36. package/src/baseline/tasks/cookie.ts +22 -0
  37. package/src/baseline/tasks/hashing.ts +18 -0
  38. package/src/baseline/tasks/hmac.ts +38 -0
  39. package/src/baseline/tasks/http.ts +62 -0
  40. package/src/baseline/tasks/json-patch.ts +12 -0
  41. package/src/baseline/tasks/json.ts +50 -0
  42. package/src/baseline/tasks/mime.ts +5 -0
  43. package/src/baseline/tasks/query.ts +14 -0
  44. package/src/baseline/tasks/token.ts +15 -0
  45. package/src/baseline/tasks/url.ts +11 -0
  46. package/src/baseline/tasks/validation.ts +19 -0
  47. package/src/baseline/tasks/websocket.ts +14 -0
  48. package/src/bench/assert.ts +70 -0
  49. package/src/bench/checks.ts +227 -0
  50. package/src/bench/checksum.ts +27 -0
  51. package/src/bench/comparisons.ts +70 -0
  52. package/src/bench/concurrent-worker.ts +135 -0
  53. package/src/bench/fixtures.ts +233 -0
  54. package/src/bench/index.ts +2 -0
  55. package/src/bench/measure.ts +177 -0
  56. package/src/bench/now.ts +3 -0
  57. package/src/bench/report.ts +79 -0
  58. package/src/bench/run.ts +57 -0
  59. package/src/bench/tasks/complex.ts +232 -0
  60. package/src/bench/tasks/concurrent.ts +213 -0
  61. package/src/bench/tasks/cookie.ts +44 -0
  62. package/src/bench/tasks/hashing.ts +33 -0
  63. package/src/bench/tasks/hmac.ts +36 -0
  64. package/src/bench/tasks/http.ts +50 -0
  65. package/src/bench/tasks/index.ts +52 -0
  66. package/src/bench/tasks/json-patch.ts +21 -0
  67. package/src/bench/tasks/json.ts +33 -0
  68. package/src/bench/tasks/mime.ts +21 -0
  69. package/src/bench/tasks/query.ts +44 -0
  70. package/src/bench/tasks/stress.ts +142 -0
  71. package/src/bench/tasks/token.ts +20 -0
  72. package/src/bench/tasks/url.ts +34 -0
  73. package/src/bench/tasks/validation.ts +57 -0
  74. package/src/bench/tasks/websocket.ts +21 -0
  75. package/src/bench/types.ts +60 -0
  76. package/src/data/json-rows.ts +33 -0
  77. package/src/ingress/constants.ts +56 -0
  78. package/src/ingress/fast.ts +1082 -0
  79. package/src/ingress/handlers.ts +1202 -0
  80. package/src/ingress/index.ts +457 -0
  81. package/src/ingress/packed-input.ts +59 -0
  82. package/src/native/index.ts +207 -0
  83. package/src/rust-ffi/index.ts +555 -0
  84. package/src/shared/bytes.ts +36 -0
  85. package/src/shared/json.ts +18 -0
  86. package/src/shared/packed.ts +316 -0
@@ -0,0 +1,234 @@
1
+ // rust/headers.rs — HeaderRefs: zero-alloc packed header parser
2
+ // Extracted from ingress.rs for composability
3
+ // Uses u64 word-at-a-time comparison for header name matching
4
+
5
+ use napi::{Error, Result};
6
+
7
+ // ── Header presence flags ─────────────────────────────────────────
8
+ const HAS_ORIGIN: u8 = 1 << 0;
9
+ const HAS_COOKIE: u8 = 1 << 1;
10
+ const HAS_XFF: u8 = 1 << 2;
11
+ const HAS_X_REAL_IP: u8 = 1 << 3;
12
+ const HAS_XFP: u8 = 1 << 4;
13
+ const HAS_ACRM: u8 = 1 << 5;
14
+ const HAS_ACRH: u8 = 1 << 6;
15
+
16
+ /// Zero-alloc reference-based representation of interesting headers.
17
+ #[derive(Clone, Copy)]
18
+ pub struct HeaderRefs<'a> {
19
+ origin: Option<&'a [u8]>,
20
+ cookie: Option<&'a [u8]>,
21
+ xff: Option<&'a [u8]>,
22
+ x_real_ip: Option<&'a [u8]>,
23
+ x_forwarded_proto: Option<&'a [u8]>,
24
+ acrm: Option<&'a [u8]>,
25
+ acrh: Option<&'a [u8]>,
26
+ flags: u8,
27
+ }
28
+
29
+ /// Load a u64 from the first N bytes of a byte slice, zero-padded.
30
+ #[inline(always)]
31
+ fn load_u64_padded(bytes: &[u8]) -> u64 {
32
+ let mut buf = [0u8; 8];
33
+ let len = bytes.len().min(8);
34
+ buf[..len].copy_from_slice(&bytes[..len]);
35
+ u64::from_le_bytes(buf)
36
+ }
37
+
38
+ /// Compare two byte slices using u64 word comparison for case-insensitive matching.
39
+ /// Both inputs should be lowercase already (or we compute lowercase u64).
40
+ #[inline(always)]
41
+ fn byte_slice_eq_ignore_case(a: &[u8], b: &[u8]) -> bool {
42
+ if a.len() != b.len() {
43
+ return false;
44
+ }
45
+ // For short names (up to 8 bytes) use single u64 comparison
46
+ if a.len() <= 8 {
47
+ let wa = load_u64_padded(a) | 0x2020_2020_2020_2020;
48
+ let wb = load_u64_padded(b) | 0x2020_2020_2020_2020;
49
+ return wa == wb;
50
+ }
51
+ // For longer names: compare first 8 bytes, then remaining
52
+ if a.len() <= 16 {
53
+ let wa = load_u64_padded(a) | 0x2020_2020_2020_2020;
54
+ let wb = load_u64_padded(b) | 0x2020_2020_2020_2020;
55
+ if wa != wb {
56
+ return false;
57
+ }
58
+ let wa2 = load_u64_padded(&a[8..]) | 0x2020_2020_2020_2020;
59
+ let wb2 = load_u64_padded(&b[8..]) | 0x2020_2020_2020_2020;
60
+ return wa2 == wb2;
61
+ }
62
+ // Fallback
63
+ a.eq_ignore_ascii_case(b)
64
+ }
65
+
66
+ impl<'a> HeaderRefs<'a> {
67
+ #[inline(always)]
68
+ pub fn empty() -> Self {
69
+ Self {
70
+ origin: None,
71
+ cookie: None,
72
+ xff: None,
73
+ x_real_ip: None,
74
+ x_forwarded_proto: None,
75
+ acrm: None,
76
+ acrh: None,
77
+ flags: 0,
78
+ }
79
+ }
80
+
81
+ /// Parse packed headers from the input buffer.
82
+ /// Format: [u16 count] repeated { [u16 name_len] [name] [u32 value_len] [value] }
83
+ #[inline]
84
+ pub fn parse(packed: &'a [u8], is_options: bool, max_headers: usize) -> Result<Self> {
85
+ let mut h = Self::empty();
86
+
87
+ if packed.len() < 2 {
88
+ return Ok(h);
89
+ }
90
+
91
+ let count = u16::from_le_bytes([packed[0], packed[1]]) as usize;
92
+ if count > max_headers {
93
+ return Err(Error::from_reason("too many headers"));
94
+ }
95
+
96
+ let mut pos = 2usize;
97
+
98
+ for _ in 0..count {
99
+ if pos + 2 > packed.len() {
100
+ return Err(Error::from_reason("malformed packed headers"));
101
+ }
102
+
103
+ let name_len = u16::from_le_bytes([packed[pos], packed[pos + 1]]) as usize;
104
+ pos += 2;
105
+
106
+ if pos + name_len > packed.len() {
107
+ return Err(Error::from_reason("malformed packed header name"));
108
+ }
109
+
110
+ let name = &packed[pos..pos + name_len];
111
+ pos += name_len;
112
+
113
+ if pos + 4 > packed.len() {
114
+ return Err(Error::from_reason("malformed packed header value length"));
115
+ }
116
+
117
+ let value_len = u32::from_le_bytes([
118
+ packed[pos],
119
+ packed[pos + 1],
120
+ packed[pos + 2],
121
+ packed[pos + 3],
122
+ ]) as usize;
123
+ pos += 4;
124
+
125
+ if pos + value_len > packed.len() {
126
+ return Err(Error::from_reason("malformed packed header value"));
127
+ }
128
+
129
+ let value = &packed[pos..pos + value_len];
130
+ pos += value_len;
131
+
132
+ if name.is_empty() {
133
+ continue;
134
+ }
135
+
136
+ let first = name[0] | 0x20;
137
+
138
+ // Fast path: dispatch based on first character + name length
139
+ if first == b'o' && name.len() == 6 {
140
+ if byte_slice_eq_ignore_case(name, b"origin") {
141
+ h.flags |= HAS_ORIGIN;
142
+ h.origin = Some(value);
143
+ }
144
+ } else if first == b'c' && name.len() == 6 {
145
+ if byte_slice_eq_ignore_case(name, b"cookie") {
146
+ h.flags |= HAS_COOKIE;
147
+ h.cookie = Some(value);
148
+ }
149
+ } else if first == b'x' {
150
+ let name_len = name.len();
151
+ if name_len == 15 && byte_slice_eq_ignore_case(name, b"x-forwarded-for") {
152
+ h.flags |= HAS_XFF;
153
+ h.xff = Some(value);
154
+ } else if name_len == 9 && byte_slice_eq_ignore_case(name, b"x-real-ip") {
155
+ h.flags |= HAS_X_REAL_IP;
156
+ h.x_real_ip = Some(value);
157
+ } else if name_len == 17 && byte_slice_eq_ignore_case(name, b"x-forwarded-proto") {
158
+ h.flags |= HAS_XFP;
159
+ h.x_forwarded_proto = Some(value);
160
+ }
161
+ } else if first == b'a' && is_options {
162
+ if name.len() == 29 && byte_slice_eq_ignore_case(name, b"access-control-request-method") {
163
+ h.flags |= HAS_ACRM;
164
+ h.acrm = Some(value);
165
+ } else if name.len() == 30 && byte_slice_eq_ignore_case(name, b"access-control-request-headers") {
166
+ h.flags |= HAS_ACRH;
167
+ h.acrh = Some(value);
168
+ }
169
+ }
170
+ }
171
+
172
+ Ok(h)
173
+ }
174
+
175
+ #[inline(always)]
176
+ pub fn origin(&self) -> Option<&[u8]> {
177
+ self.origin
178
+ }
179
+
180
+ #[inline(always)]
181
+ pub fn cookie(&self) -> Option<&[u8]> {
182
+ self.cookie
183
+ }
184
+
185
+ #[inline(always)]
186
+ pub fn xff(&self) -> Option<&[u8]> {
187
+ self.xff
188
+ }
189
+
190
+ #[inline(always)]
191
+ pub fn x_real_ip(&self) -> Option<&[u8]> {
192
+ self.x_real_ip
193
+ }
194
+
195
+ #[inline(always)]
196
+ pub fn x_forwarded_proto(&self) -> Option<&[u8]> {
197
+ self.x_forwarded_proto
198
+ }
199
+
200
+ #[inline(always)]
201
+ pub fn acrm(&self) -> Option<&[u8]> {
202
+ self.acrm
203
+ }
204
+
205
+ #[inline(always)]
206
+ pub fn acrh(&self) -> Option<&[u8]> {
207
+ self.acrh
208
+ }
209
+
210
+ #[inline(always)]
211
+ pub fn has_origin(&self) -> bool {
212
+ (self.flags & HAS_ORIGIN) != 0
213
+ }
214
+
215
+ #[inline(always)]
216
+ pub fn has_cookie(&self) -> bool {
217
+ (self.flags & HAS_COOKIE) != 0
218
+ }
219
+
220
+ #[inline(always)]
221
+ pub fn has_xfp(&self) -> bool {
222
+ (self.flags & HAS_XFP) != 0
223
+ }
224
+
225
+ #[inline(always)]
226
+ pub fn has_acrm(&self) -> bool {
227
+ (self.flags & HAS_ACRM) != 0
228
+ }
229
+
230
+ #[inline(always)]
231
+ pub fn has_acrh(&self) -> bool {
232
+ (self.flags & HAS_ACRH) != 0
233
+ }
234
+ }
@@ -0,0 +1,113 @@
1
+ use napi::bindgen_prelude::*;
2
+ use napi_derive::napi;
3
+ use aws_lc_rs::hmac;
4
+ use crate::util::{hex_val, trim_ascii_whitespace};
5
+
6
+ const HEX_LOWER: &[u8; 16] = b"0123456789abcdef";
7
+
8
+
9
+
10
+ #[napi]
11
+ pub fn hmac_sha256(key: Uint8Array, data: Buffer) -> Buffer {
12
+ let key = hmac::Key::new(hmac::HMAC_SHA256, key.as_ref());
13
+ let tag = hmac::sign(&key, data.as_ref());
14
+
15
+ let mut out = [0u8; 64];
16
+ hex_encode_32(tag.as_ref(), &mut out);
17
+
18
+ Buffer::from(out.to_vec())
19
+ }
20
+
21
+ #[napi]
22
+ pub fn hmac_sha256_verify(key: Uint8Array, data: Buffer, sig: Buffer) -> bool {
23
+ let sig = trim_ascii_whitespace(sig.as_ref());
24
+
25
+ if sig.len() != 64 {
26
+ return false;
27
+ }
28
+
29
+ let mut sig_bytes = [0u8; 32];
30
+
31
+ let mut i = 0;
32
+ while i < 32 {
33
+ let hi = match hex_val(sig[2 * i]) {
34
+ Some(v) => v,
35
+ None => return false,
36
+ };
37
+
38
+ let lo = match hex_val(sig[2 * i + 1]) {
39
+ Some(v) => v,
40
+ None => return false,
41
+ };
42
+
43
+ sig_bytes[i] = (hi << 4) | lo;
44
+ i += 1;
45
+ }
46
+
47
+ let key = hmac::Key::new(hmac::HMAC_SHA256, key.as_ref());
48
+ hmac::verify(&key, data.as_ref(), &sig_bytes).is_ok()
49
+ }
50
+
51
+ #[napi]
52
+ pub struct HmacSigner {
53
+ key: hmac::Key,
54
+ }
55
+
56
+ #[napi]
57
+ impl HmacSigner {
58
+ #[napi(constructor)]
59
+ pub fn new(key: Uint8Array) -> Self {
60
+ Self {
61
+ key: hmac::Key::new(hmac::HMAC_SHA256, key.as_ref()),
62
+ }
63
+ }
64
+
65
+ #[napi]
66
+ pub fn sign(&self, data: Buffer) -> Buffer {
67
+ let tag = hmac::sign(&self.key, data.as_ref());
68
+
69
+ let mut out = [0u8; 64];
70
+ hex_encode_32(tag.as_ref(), &mut out);
71
+
72
+ Buffer::from(out.to_vec())
73
+ }
74
+
75
+ #[napi]
76
+ pub fn verify(&self, data: Buffer, sig: Buffer) -> bool {
77
+ let sig = trim_ascii_whitespace(sig.as_ref());
78
+
79
+ if sig.len() != 64 {
80
+ return false;
81
+ }
82
+
83
+ let mut sig_bytes = [0u8; 32];
84
+
85
+ let mut i = 0;
86
+
87
+ while i < 32 {
88
+ let hi = match hex_val(sig[2 * i]) {
89
+ Some(v) => v,
90
+ None => return false,
91
+ };
92
+
93
+ let lo = match hex_val(sig[2 * i + 1]) {
94
+ Some(v) => v,
95
+ None => return false,
96
+ };
97
+
98
+ sig_bytes[i] = (hi << 4) | lo;
99
+ i += 1;
100
+ }
101
+
102
+ hmac::verify(&self.key, data.as_ref(), &sig_bytes).is_ok()
103
+ }
104
+ }
105
+
106
+ fn hex_encode_32(bytes: &[u8], out: &mut [u8; 64]) {
107
+ debug_assert_eq!(bytes.len(), 32);
108
+
109
+ for (i, b) in bytes.iter().enumerate() {
110
+ out[2 * i] = HEX_LOWER[(b >> 4) as usize];
111
+ out[2 * i + 1] = HEX_LOWER[(b & 0x0f) as usize];
112
+ }
113
+ }
@@ -0,0 +1,132 @@
1
+ // rust/http_parser.rs
2
+ use crate::util::{write_bytes, write_u32_le, VecWriter};
3
+ use napi::bindgen_prelude::*;
4
+ use napi_derive::napi;
5
+
6
+ pub fn http_parse_request_packed_into_slice(input: &[u8], out: &mut [u8]) -> Result<usize> {
7
+ let mut headers = [httparse::EMPTY_HEADER; 64];
8
+ let mut req = httparse::Request::new(&mut headers);
9
+
10
+ match req.parse(input) {
11
+ Ok(httparse::Status::Complete(_)) => {}
12
+ Ok(_) => return Err(Error::new(Status::InvalidArg, "incomplete HTTP request")),
13
+ Err(e) => return Err(Error::new(Status::InvalidArg, e.to_string())),
14
+ }
15
+
16
+ let method = req
17
+ .method
18
+ .ok_or_else(|| Error::new(Status::InvalidArg, "missing HTTP method"))?
19
+ .as_bytes();
20
+
21
+ let path = req
22
+ .path
23
+ .ok_or_else(|| Error::new(Status::InvalidArg, "missing HTTP path"))?
24
+ .as_bytes();
25
+
26
+ let version: &[u8] = match req.version {
27
+ Some(0) => b"HTTP/1.0",
28
+ Some(1) => b"HTTP/1.1",
29
+ _ => b"HTTP/1.1",
30
+ };
31
+
32
+ let mut pos = 0usize;
33
+
34
+ write_u32_le(out, &mut pos, method.len() as u32)?;
35
+ write_bytes(out, &mut pos, method)?;
36
+
37
+ write_u32_le(out, &mut pos, path.len() as u32)?;
38
+ write_bytes(out, &mut pos, path)?;
39
+
40
+ write_u32_le(out, &mut pos, version.len() as u32)?;
41
+ write_bytes(out, &mut pos, version)?;
42
+
43
+ write_u32_le(out, &mut pos, req.headers.len() as u32)?;
44
+
45
+ for header in req.headers.iter() {
46
+ let name = header.name.as_bytes();
47
+ let value = header.value;
48
+
49
+ write_u32_le(out, &mut pos, name.len() as u32)?;
50
+ crate::util::write_bytes_lowercase(out, &mut pos, name)?;
51
+
52
+ write_u32_le(out, &mut pos, value.len() as u32)?;
53
+ crate::util::write_bytes(out, &mut pos, value)?;
54
+ }
55
+
56
+ Ok(pos)
57
+ }
58
+
59
+ /// Allocating, zero-copy-input, non-zeroing-output parser.
60
+ pub fn http_parse_request_packed_vec(input: &[u8]) -> Result<Vec<u8>> {
61
+ let mut headers = [httparse::EMPTY_HEADER; 64];
62
+ let mut req = httparse::Request::new(&mut headers);
63
+
64
+ match req.parse(input) {
65
+ Ok(httparse::Status::Complete(_)) => {}
66
+ Ok(_) => return Err(Error::new(Status::InvalidArg, "incomplete HTTP request")),
67
+ Err(e) => return Err(Error::new(Status::InvalidArg, e.to_string())),
68
+ }
69
+
70
+ let method = req
71
+ .method
72
+ .ok_or_else(|| Error::new(Status::InvalidArg, "missing HTTP method"))?
73
+ .as_bytes();
74
+
75
+ let path = req
76
+ .path
77
+ .ok_or_else(|| Error::new(Status::InvalidArg, "missing HTTP path"))?
78
+ .as_bytes();
79
+
80
+ let version: &[u8] = match req.version {
81
+ Some(0) => b"HTTP/1.0",
82
+ Some(1) => b"HTTP/1.1",
83
+ _ => b"HTTP/1.1",
84
+ };
85
+
86
+ // Max 64 headers.
87
+ // Metadata overhead:
88
+ // 4 bytes method len
89
+ // 4 bytes path len
90
+ // 4 bytes version len
91
+ // 4 bytes header count
92
+ // 8 bytes per header name/value length prefix
93
+ //
94
+ // 16 + 64 * 8 = 528
95
+ let mut w = VecWriter::with_capacity(input.len().saturating_add(528));
96
+
97
+ w.write_u32(method.len() as u32);
98
+ w.write_bytes(method);
99
+
100
+ w.write_u32(path.len() as u32);
101
+ w.write_bytes(path);
102
+
103
+ w.write_u32(version.len() as u32);
104
+ w.write_bytes(version);
105
+
106
+ w.write_u32(req.headers.len() as u32);
107
+
108
+ for header in req.headers.iter() {
109
+ let name = header.name.as_bytes();
110
+ let value = header.value;
111
+
112
+ w.write_u32(name.len() as u32);
113
+ w.write_bytes_ascii_lowercase(name);
114
+
115
+ w.write_u32(value.len() as u32);
116
+ w.write_bytes(value);
117
+ }
118
+
119
+ Ok(w.into_bytes())
120
+ }
121
+
122
+ #[napi]
123
+ pub fn http_parse_request_packed(input: Uint8Array) -> Result<Buffer> {
124
+ Ok(Buffer::from(http_parse_request_packed_vec(
125
+ input.as_ref(),
126
+ )?))
127
+ }
128
+
129
+ #[napi]
130
+ pub fn http_parse_request_packed_into(input: Uint8Array, mut output: Uint8Array) -> Result<u32> {
131
+ crate::util::run_packed_into(&input, &mut output, http_parse_request_packed_into_slice)
132
+ }