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,185 @@
1
+ use crate::util::trim_ascii_whitespace;
2
+ use ipnet::{IpNet, Ipv4Net, Ipv6Net};
3
+ use napi::{Error, Result, Status};
4
+ use std::net::IpAddr;
5
+ use xxhash_rust::xxh3::Xxh3;
6
+
7
+ #[derive(Clone)]
8
+ pub enum ProxyTrustMode {
9
+ None,
10
+ All,
11
+ Networks(Vec<IpNet>),
12
+ }
13
+
14
+ impl ProxyTrustMode {
15
+ pub fn from_config(enabled: bool, networks: Option<Vec<String>>) -> Result<Self> {
16
+ if !enabled {
17
+ return Ok(Self::None);
18
+ }
19
+
20
+ // Safety: an enabled trust-proxy mode with NO explicit networks must
21
+ // never fall back to ProxyTrustMode::All (which trusts every hop and
22
+ // lets clients spoof X-Forwarded-For). Treat it as "trust nothing"
23
+ // unless the operator explicitly lists trusted networks.
24
+ let Some(networks) = networks else {
25
+ return Ok(Self::Networks(Vec::new()));
26
+ };
27
+
28
+ if networks.is_empty() {
29
+ return Ok(Self::Networks(Vec::new()));
30
+ }
31
+
32
+ let mut out = Vec::with_capacity(networks.len());
33
+
34
+ for raw in networks {
35
+ let raw = raw.trim();
36
+ if raw.is_empty() {
37
+ continue;
38
+ }
39
+
40
+ if let Ok(ip) = raw.parse::<IpAddr>() {
41
+ out.push(ip_to_net(ip));
42
+ } else {
43
+ let net: IpNet = raw.parse().map_err(|_| {
44
+ Error::new(
45
+ Status::InvalidArg,
46
+ format!("invalid trusted proxy network: {raw}"),
47
+ )
48
+ })?;
49
+ out.push(net);
50
+ }
51
+ }
52
+
53
+ // Networks that filtered down to nothing -> trust nothing (see above).
54
+ Ok(Self::Networks(out))
55
+ }
56
+
57
+ #[inline(always)]
58
+ pub fn is_none(&self) -> bool {
59
+ matches!(self, Self::None)
60
+ }
61
+
62
+ pub fn is_trusted(&self, ip: IpAddr) -> bool {
63
+ match self {
64
+ Self::None => false,
65
+ Self::All => true,
66
+ Self::Networks(nets) => nets.iter().any(|n| n.contains(&ip)),
67
+ }
68
+ }
69
+ }
70
+
71
+ pub enum ResolvedIp<'a> {
72
+ V4([u8; 4]),
73
+ V6([u8; 16]),
74
+ Raw(&'a [u8]),
75
+ }
76
+
77
+ impl<'a> From<IpAddr> for ResolvedIp<'a> {
78
+ fn from(ip: IpAddr) -> Self {
79
+ match ip {
80
+ IpAddr::V4(v4) => ResolvedIp::V4(v4.octets()),
81
+ IpAddr::V6(v6) => ResolvedIp::V6(v6.octets()),
82
+ }
83
+ }
84
+ }
85
+
86
+ impl ResolvedIp<'_> {
87
+ pub fn rate_key(&self, seed: u64) -> u64 {
88
+ let mut h = Xxh3::with_seed(seed);
89
+
90
+ match self {
91
+ ResolvedIp::V4(octets) => {
92
+ h.update(&[1]);
93
+ h.update(octets);
94
+ }
95
+ ResolvedIp::V6(octets) => {
96
+ h.update(&[2]);
97
+ h.update(octets);
98
+ }
99
+ ResolvedIp::Raw(bytes) => {
100
+ h.update(&[0]);
101
+ h.update(bytes);
102
+ }
103
+ }
104
+
105
+ h.digest()
106
+ }
107
+ }
108
+
109
+ fn ip_to_net(ip: IpAddr) -> IpNet {
110
+ match ip {
111
+ IpAddr::V4(v4) => IpNet::V4(Ipv4Net::new(v4, 32).expect("valid /32")),
112
+ IpAddr::V6(v6) => IpNet::V6(Ipv6Net::new(v6, 128).expect("valid /128")),
113
+ }
114
+ }
115
+
116
+ fn parse_ip_bytes(bytes: &[u8]) -> Option<IpAddr> {
117
+ let s = std::str::from_utf8(trim_ascii_whitespace(bytes)).ok()?;
118
+ s.parse::<IpAddr>().ok()
119
+ }
120
+
121
+ pub fn resolve_client_ip<'a>(
122
+ mode: &ProxyTrustMode,
123
+ socket_ip: &'a [u8],
124
+ xff: Option<&'a [u8]>,
125
+ x_real_ip: Option<&'a [u8]>,
126
+ ) -> (ResolvedIp<'a>, bool) {
127
+ let socket_trim = trim_ascii_whitespace(socket_ip);
128
+ let socket_parsed = parse_ip_bytes(socket_trim);
129
+
130
+ let peer_trusted = socket_parsed
131
+ .map(|ip| mode.is_trusted(ip))
132
+ .unwrap_or(false);
133
+
134
+ if mode.is_none() || !peer_trusted {
135
+ return (
136
+ socket_parsed
137
+ .map(Into::into)
138
+ .unwrap_or(ResolvedIp::Raw(socket_trim)),
139
+ peer_trusted,
140
+ );
141
+ }
142
+
143
+ if let Some(xff) = xff {
144
+ let mut last_valid: Option<ResolvedIp> = None;
145
+
146
+ for part in xff.split(|&b| b == b',').rev() {
147
+ let part = trim_ascii_whitespace(part);
148
+ if part.is_empty() {
149
+ continue;
150
+ }
151
+
152
+ match parse_ip_bytes(part) {
153
+ Some(ip) => {
154
+ let trusted = mode.is_trusted(ip);
155
+ last_valid = Some(ip.into());
156
+
157
+ if !trusted {
158
+ return (last_valid.take().unwrap(), peer_trusted);
159
+ }
160
+ }
161
+ None => break,
162
+ }
163
+ }
164
+
165
+ if let Some(ip) = last_valid {
166
+ return (ip, peer_trusted);
167
+ }
168
+ }
169
+
170
+ if let Some(xri) = x_real_ip {
171
+ let xri_trim = trim_ascii_whitespace(xri);
172
+ let first = xri_trim.split(|&b| b == b',').next().unwrap_or(xri_trim);
173
+
174
+ if let Some(ip) = parse_ip_bytes(first) {
175
+ return (ip.into(), peer_trusted);
176
+ }
177
+ }
178
+
179
+ (
180
+ socket_parsed
181
+ .map(Into::into)
182
+ .unwrap_or(ResolvedIp::Raw(socket_trim)),
183
+ peer_trusted,
184
+ )
185
+ }
@@ -0,0 +1,66 @@
1
+ // rust/json_ops.rs — v2: tighter inlining
2
+ use napi::bindgen_prelude::*;
3
+ use napi_derive::napi;
4
+ use serde::de::{Deserializer, IgnoredAny, SeqAccess, Visitor};
5
+ use serde::Deserialize;
6
+ use std::fmt;
7
+
8
+ #[derive(Deserialize)]
9
+ struct IdRow {
10
+ #[serde(default)]
11
+ id: Option<i64>,
12
+ }
13
+
14
+ struct IdSumVisitor;
15
+
16
+ impl<'de> Visitor<'de> for IdSumVisitor {
17
+ type Value = i64;
18
+
19
+ #[inline]
20
+ fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
21
+ f.write_str("an array of objects with numeric ids")
22
+ }
23
+
24
+ #[inline]
25
+ fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error> {
26
+ let mut sum = 0i64;
27
+ while let Some(row) = seq.next_element::<Option<IdRow>>()? {
28
+ if let Some(IdRow { id: Some(id) }) = row {
29
+ sum = sum.saturating_add(id);
30
+ }
31
+ }
32
+ Ok(sum)
33
+ }
34
+ }
35
+
36
+ struct IdSum(i64);
37
+
38
+ impl<'de> Deserialize<'de> for IdSum {
39
+ #[inline]
40
+ fn deserialize<D: Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
41
+ d.deserialize_seq(IdSumVisitor).map(IdSum)
42
+ }
43
+ }
44
+
45
+ #[inline(always)]
46
+ pub fn json_valid_bytes(input: &[u8]) -> bool {
47
+ // sonic-rs uses SIMD validation internally — already optimal.
48
+ sonic_rs::from_slice::<IgnoredAny>(input).is_ok()
49
+ }
50
+
51
+ #[inline]
52
+ pub fn json_sum_ids_bytes(input: &[u8]) -> Result<i64> {
53
+ sonic_rs::from_slice::<IdSum>(input)
54
+ .map(|s| s.0)
55
+ .map_err(|e| Error::from_reason(e.to_string()))
56
+ }
57
+
58
+ #[napi]
59
+ pub fn json_valid(input: Uint8Array) -> bool {
60
+ json_valid_bytes(input.as_ref())
61
+ }
62
+
63
+ #[napi]
64
+ pub fn json_sum_ids(input: Uint8Array) -> Result<i64> {
65
+ json_sum_ids_bytes(input.as_ref())
66
+ }
@@ -0,0 +1,25 @@
1
+ use napi::bindgen_prelude::*;
2
+ use napi_derive::napi;
3
+ use serde_json::Value;
4
+
5
+ #[napi]
6
+ pub fn json_patch(doc: Uint8Array, patch: Uint8Array) -> Result<Buffer> {
7
+ let mut doc_val: Value =
8
+ sonic_rs::from_slice(doc.as_ref()).map_err(|e| Error::from_reason(e.to_string()))?;
9
+
10
+ let patch_val: json_patch::Patch =
11
+ sonic_rs::from_slice(patch.as_ref()).map_err(|e| Error::from_reason(e.to_string()))?;
12
+
13
+ json_patch::patch(&mut doc_val, &patch_val).map_err(|e| Error::from_reason(e.to_string()))?;
14
+
15
+ let mut out = Vec::with_capacity(
16
+ doc.as_ref()
17
+ .len()
18
+ .saturating_add(patch.as_ref().len())
19
+ .saturating_add(32),
20
+ );
21
+
22
+ sonic_rs::to_writer(&mut out, &doc_val).map_err(|e| Error::from_reason(e.to_string()))?;
23
+
24
+ Ok(Buffer::from(out))
25
+ }
@@ -0,0 +1,151 @@
1
+ use napi::bindgen_prelude::*;
2
+ use napi_derive::napi;
3
+ use serde::de::{DeserializeSeed, Deserializer, SeqAccess, Visitor};
4
+ use serde_json::Value;
5
+ use std::fmt;
6
+ use std::sync::Arc;
7
+
8
+ use crate::util::{should_parallelize, total_bytes, unpack};
9
+ #[napi]
10
+ pub struct SchemaValidator {
11
+ schema: Arc<jsonschema::Validator>,
12
+ }
13
+
14
+ #[napi]
15
+ impl SchemaValidator {
16
+ #[napi(constructor)]
17
+ pub fn new(schema_bytes: Uint8Array) -> Result<Self> {
18
+ let schema_str = std::str::from_utf8(&schema_bytes)
19
+ .map_err(|_| Error::new(Status::InvalidArg, "Invalid UTF-8 in schema"))?;
20
+
21
+ let schema_value: Value = sonic_rs::from_str(schema_str)
22
+ .map_err(|e| Error::new(Status::InvalidArg, format!("Schema JSON error: {}", e)))?;
23
+
24
+ let compiled = jsonschema::validator_for(&schema_value).map_err(|e| {
25
+ Error::new(
26
+ Status::InvalidArg,
27
+ format!("Schema compilation error: {}", e),
28
+ )
29
+ })?;
30
+
31
+ Ok(Self {
32
+ schema: Arc::new(compiled),
33
+ })
34
+ }
35
+
36
+ #[inline]
37
+ fn validate_doc(&self, bytes: &[u8]) -> bool {
38
+ match sonic_rs::from_slice::<Value>(bytes) {
39
+ Ok(value) => self.schema.is_valid(&value),
40
+ Err(_) => false,
41
+ }
42
+ }
43
+
44
+ /// Validate a packed batch of individual JSON documents.
45
+ ///
46
+ /// Input format:
47
+ ///
48
+ /// [u32 count]
49
+ /// repeated:
50
+ /// [u32 doc_len]
51
+ /// [doc bytes]
52
+ ///
53
+ /// This avoids materializing the entire batch into one DOM array.
54
+ #[napi]
55
+ pub fn validate_batch_packed_count(&self, packed: Uint8Array) -> Result<u32> {
56
+ let items = unpack(packed.as_ref())?;
57
+
58
+ let count: u32 = if should_parallelize(items.len(), total_bytes(&items)) {
59
+ use rayon::prelude::*;
60
+
61
+ items
62
+ .par_chunks(256)
63
+ .map(|chunk| chunk.iter().filter(|item| self.validate_doc(item)).count() as u32)
64
+ .sum()
65
+ } else {
66
+ items.iter().filter(|item| self.validate_doc(item)).count() as u32
67
+ };
68
+
69
+ Ok(count)
70
+ }
71
+
72
+ /// Validate a packed batch and return a packed bitset.
73
+ ///
74
+ /// Output format:
75
+ ///
76
+ /// [u32 count]
77
+ /// [ceil(count / 8) bytes]
78
+ #[napi]
79
+ pub fn validate_batch_packed_bitset(&self, packed: Uint8Array) -> Result<Buffer> {
80
+ let items = unpack(packed.as_ref())?;
81
+
82
+ Ok(Buffer::from(crate::util::validation_bitset_chunked(
83
+ &items,
84
+ |item| self.validate_doc(item),
85
+ 256,
86
+ )))
87
+ }
88
+
89
+ /// Stream-validate a JSON array payload.
90
+ ///
91
+ /// This still materializes one array element at a time, because
92
+ /// `jsonschema` currently expects a DOM-like value, but it avoids
93
+ /// materializing the entire array at once.
94
+ #[napi]
95
+ pub fn validate_batch_streaming(&self, batch_bytes: Uint8Array) -> Result<u32> {
96
+ let mut de = sonic_rs::Deserializer::from_slice(batch_bytes.as_ref());
97
+ let seed = BatchSeed {
98
+ validator: &self.schema,
99
+ };
100
+
101
+ let count = seed
102
+ .deserialize(&mut de)
103
+ .map_err(|e| Error::from_reason(format!("Streaming batch JSON error: {}", e)))?;
104
+
105
+ Ok(count)
106
+ }
107
+ }
108
+
109
+ struct BatchSeed<'a> {
110
+ validator: &'a jsonschema::Validator,
111
+ }
112
+
113
+ impl<'de> DeserializeSeed<'de> for BatchSeed<'_> {
114
+ type Value = u32;
115
+
116
+ fn deserialize<D>(self, deserializer: D) -> std::result::Result<Self::Value, D::Error>
117
+ where
118
+ D: Deserializer<'de>,
119
+ {
120
+ deserializer.deserialize_seq(BatchVisitor {
121
+ validator: self.validator,
122
+ })
123
+ }
124
+ }
125
+
126
+ struct BatchVisitor<'a> {
127
+ validator: &'a jsonschema::Validator,
128
+ }
129
+
130
+ impl<'de> Visitor<'de> for BatchVisitor<'_> {
131
+ type Value = u32;
132
+
133
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
134
+ formatter.write_str("a JSON array of documents")
135
+ }
136
+
137
+ fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error>
138
+ where
139
+ A: SeqAccess<'de>,
140
+ {
141
+ let mut count = 0u32;
142
+
143
+ while let Some(value) = seq.next_element::<Value>()? {
144
+ if self.validator.is_valid(&value) {
145
+ count += 1;
146
+ }
147
+ }
148
+
149
+ Ok(count)
150
+ }
151
+ }