camada 0.1.4 → 0.1.5
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.
- checksums.yaml +4 -4
- data/lib/camada/snapshot/parse.rb +66 -9
- data/lib/camada/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 86d9659607a031e6d4802b62fa614a63c5cf682c8b25ad753e08dad5d5890f04
|
|
4
|
+
data.tar.gz: 3837b8f2e624efdca08154e3dabe053c4e924af155688be40a2980c59d9370c6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4b5eaee46ab80bf9d7b8af4fb2499461c4a2ad3a3ba604f691d77bef227eceb97d9698a1a37688bbc2e87949059f86569103cb43b83e96331012d22bae9bf0ee
|
|
7
|
+
data.tar.gz: 99709f2521ed91d8be8dd8714396e7184667814581c401031f33ba9d1b55ad5edda46a5d65139f5fdac44cc918974c768f544919e5ecb9f2fceeb6c6cfde8cf3
|
|
@@ -191,10 +191,54 @@ module Camada
|
|
|
191
191
|
|
|
192
192
|
# A regex test that never raises: a value in an encoding the pattern cannot read (a binary
|
|
193
193
|
# PATH_INFO with high bytes, say) reads as "no match" rather than an exception on the request path.
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
#
|
|
195
|
+
# `on_error` is what a value the engine cannot read answers. The legacy block-side path regexes
|
|
196
|
+
# keep false; a custom `matches` rule passes `deny` (regex-cap-sdk v1: an unevaluable value is a
|
|
197
|
+
# hit for block/challenge/warn and a miss for skip, the same as an over-cap one, camada-all-cwb4).
|
|
198
|
+
def self.regex_hit?(rx, value, on_error = false)
|
|
199
|
+
rx.match?(utf8_view(value))
|
|
196
200
|
rescue StandardError
|
|
197
|
-
|
|
201
|
+
on_error
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# A value tagged ASCII-8BIT (Rack) whose bytes are valid UTF-8 is read as UTF-8, so a non-ASCII
|
|
205
|
+
# pattern neither raises nor flips a deny/skip outcome. Genuinely invalid bytes stay as they are
|
|
206
|
+
# and take the on_error path.
|
|
207
|
+
def self.utf8_view(v)
|
|
208
|
+
return v if v.encoding == Encoding::UTF_8
|
|
209
|
+
|
|
210
|
+
s = v.dup.force_encoding(Encoding::UTF_8)
|
|
211
|
+
s.valid_encoding? ? s : v
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# regex-cap-sdk v1 S1: the longest value a `matches` regex runs on, in UTF-16 code units.
|
|
215
|
+
MAX_MATCH_INPUT = 2048
|
|
216
|
+
|
|
217
|
+
# Length in UTF-16 code units (what edge's `v.length` counts: an astral code point is 2). A String
|
|
218
|
+
# that is not valid UTF-8 (Rack hands over ASCII-8BIT) counts its bytes.
|
|
219
|
+
def self.utf16_len(v)
|
|
220
|
+
s = v.dup.force_encoding(Encoding::UTF_8)
|
|
221
|
+
return v.bytesize unless s.valid_encoding?
|
|
222
|
+
|
|
223
|
+
s.length + s.each_char.count { |c| c.ord > 0xFFFF }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def self.over_cap?(v)
|
|
227
|
+
v.bytesize > MAX_MATCH_INPUT && utf16_len(v) > MAX_MATCH_INPUT
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# regex-cap-sdk S2: delete [\x00-\x1f\x7f] from a ua/header value before a `matches` regex runs.
|
|
231
|
+
# Bytewise, so it stays safe on invalid UTF-8.
|
|
232
|
+
def self.strip_control(v)
|
|
233
|
+
v.b.delete("\x00-\x1f\x7f").force_encoding(v.encoding)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# regex-cap-sdk S3: a raw control character in the raw path form is spelled %xx, as canon_path does.
|
|
237
|
+
def self.encode_control(p)
|
|
238
|
+
b = p.b
|
|
239
|
+
return p unless b.match?(/[\x00-\x1f\x7f]/n)
|
|
240
|
+
|
|
241
|
+
b.gsub(/[\x00-\x1f\x7f]/n) { |c| format("%%%02x", c.ord) }.force_encoding(p.encoding)
|
|
198
242
|
end
|
|
199
243
|
|
|
200
244
|
# The same envelope for the plain string ops: Puma hands every env value to Rack as
|
|
@@ -275,7 +319,7 @@ module Camada
|
|
|
275
319
|
p = strip_query(raw)
|
|
276
320
|
return [p, p, p] if canonical?(p)
|
|
277
321
|
|
|
278
|
-
[p, canon_path(p, dots: false), canon_path(p)]
|
|
322
|
+
[encode_control(p), canon_path(p, dots: false), canon_path(p)]
|
|
279
323
|
end
|
|
280
324
|
|
|
281
325
|
def self.path_dir(p) = p.end_with?("/") ? p : "#{p}/"
|
|
@@ -308,12 +352,14 @@ module Camada
|
|
|
308
352
|
deny ? (pred.call(forms[0]) || pred.call(forms[1]) || pred.call(forms[2])) : (pred.call(forms[1]) && pred.call(forms[2]))
|
|
309
353
|
end
|
|
310
354
|
|
|
311
|
-
# One path condition -> a predicate over a single path form.
|
|
312
|
-
|
|
355
|
+
# One path condition -> a predicate over a single path form. A `matches` form over MAX_MATCH_INPUT
|
|
356
|
+
# never runs the regex: it answers `deny` (a hit for block/challenge/warn, a miss for skip), and so
|
|
357
|
+
# does a form the regex engine cannot read.
|
|
358
|
+
def self.path_pred(op, values, deny = true)
|
|
313
359
|
case op
|
|
314
360
|
when "matches"
|
|
315
361
|
rx = compile_regex(values[0], Regexp::IGNORECASE)
|
|
316
|
-
->(p) { !rx.nil? && regex_hit?(rx, p) }
|
|
362
|
+
->(p) { !rx.nil? && (over_cap?(p) ? deny : regex_hit?(rx, p, deny)) }
|
|
317
363
|
when "starts_with"
|
|
318
364
|
v = values[0]
|
|
319
365
|
key = v.end_with?("/") ? dir_key(v) : canon_path(v)
|
|
@@ -345,7 +391,7 @@ module Camada
|
|
|
345
391
|
op = c["op"].to_s
|
|
346
392
|
if f == "path" # every path op reads the canonical forms (see "paths" above)
|
|
347
393
|
raw = c["v"]
|
|
348
|
-
pred = path_pred(op, raw.is_a?(Array) ? raw.map(&:to_s) : [raw.to_s])
|
|
394
|
+
pred = path_pred(op, raw.is_a?(Array) ? raw.map(&:to_s) : [raw.to_s], deny)
|
|
349
395
|
return ->(r) { path_hit?(r.paths, deny, &pred) }
|
|
350
396
|
end
|
|
351
397
|
negate = op == "is_not" || op == "not_in"
|
|
@@ -386,9 +432,20 @@ module Camada
|
|
|
386
432
|
case op
|
|
387
433
|
when "matches"
|
|
388
434
|
rx = compile_regex(values[0])
|
|
435
|
+
strip = f == "ua" || f == "header"
|
|
389
436
|
lambda do |r|
|
|
390
437
|
v = read.call(r)
|
|
391
|
-
|
|
438
|
+
next false if v.nil? || rx.nil?
|
|
439
|
+
|
|
440
|
+
# The cap comes before regex_hit?, whose rescue would turn an encoding error into a miss.
|
|
441
|
+
hit = ->(s) { over_cap?(s) ? deny : regex_hit?(rx, s, deny) }
|
|
442
|
+
next hit.call(v) unless strip
|
|
443
|
+
|
|
444
|
+
stripped = strip_control(v) # one scan (S5)
|
|
445
|
+
next hit.call(v) if stripped.bytesize == v.bytesize
|
|
446
|
+
|
|
447
|
+
# S5: control chars never widen a skip and never neuter a deny regex
|
|
448
|
+
deny && (hit.call(v) || hit.call(stripped))
|
|
392
449
|
end
|
|
393
450
|
when "contains"
|
|
394
451
|
needle = values[0]
|
data/lib/camada/version.rb
CHANGED
|
@@ -5,6 +5,6 @@ module Camada
|
|
|
5
5
|
# source: camada.gemspec reads it at build, and the sibling drift guards (camada-backend,
|
|
6
6
|
# camada-web, camada-mkt) parse this file with /^\s*VERSION = "([^"]+)"/m the way they parse
|
|
7
7
|
# a package.json. Plain X.Y.Z only: the analyst's SDK_RE drops anything else.
|
|
8
|
-
VERSION = "0.1.
|
|
8
|
+
VERSION = "0.1.5"
|
|
9
9
|
SDK_ID = "@camada/ruby/#{VERSION}".freeze
|
|
10
10
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: camada
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- camada
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: 'camada SDK for Ruby: inline enforcement of your snapshot (ordered custom
|
|
14
14
|
rules, then allow, block, challenge), first-party beacon and proof-of-work challenge,
|