philiprehberger-header_kit 0.3.1 → 0.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +28 -0
- data/lib/philiprehberger/header_kit/etag.rb +95 -0
- data/lib/philiprehberger/header_kit/retry_after.rb +21 -0
- data/lib/philiprehberger/header_kit/version.rb +1 -1
- data/lib/philiprehberger/header_kit.rb +26 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d06951b5686f64b328c573fc16904e84eddfc61424ab8491fc2da817c7241fca
|
|
4
|
+
data.tar.gz: 9bdc24a4fc3dabc8d34ccdc105564e5669e9024ada60869c09317fdc11f1c1d6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 82e06b0e3e146393f17782091fd76c5813be7c46df09c6366d9e892d40a4d05e652655b0017c2cd8242a8812961b428f853cb771e74721e08c46b778911dd124
|
|
7
|
+
data.tar.gz: f9970a177aa61fa120ea97eceeeda50f19d9517c143b2d91c4bce924a26466c13de2ca88f2a5e00375cadebae0cf2f804436ace824ac0fa3a53d00dbf8f4efec
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-04-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `HeaderKit.etag_match?(header_value, resource_etag)` for If-None-Match / If-Match evaluation with list, weak-prefix, and `*` wildcard support
|
|
14
|
+
|
|
15
|
+
## [0.4.0] - 2026-04-04
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `parse_retry_after` for parsing Retry-After headers (numeric seconds or HTTP date)
|
|
20
|
+
|
|
10
21
|
## [0.3.1] - 2026-03-31
|
|
11
22
|
|
|
12
23
|
### Changed
|
data/README.md
CHANGED
|
@@ -181,6 +181,32 @@ Philiprehberger::HeaderKit.parse_via('1.1 vegur, 1.0 fred')
|
|
|
181
181
|
# => [{protocol: "1.1", host: "vegur"}, {protocol: "1.0", host: "fred"}]
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
+
### Conditional-request matching
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
Philiprehberger::HeaderKit.etag_match?('"abc"', 'abc')
|
|
188
|
+
# => true
|
|
189
|
+
|
|
190
|
+
Philiprehberger::HeaderKit.etag_match?('W/"abc", W/"xyz"', 'xyz')
|
|
191
|
+
# => true
|
|
192
|
+
|
|
193
|
+
Philiprehberger::HeaderKit.etag_match?('*', 'anything')
|
|
194
|
+
# => true
|
|
195
|
+
|
|
196
|
+
Philiprehberger::HeaderKit.etag_match?('"abc"', 'nope')
|
|
197
|
+
# => false
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Parse Retry-After
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
Philiprehberger::HeaderKit.parse_retry_after("120")
|
|
204
|
+
# => {seconds: 120}
|
|
205
|
+
|
|
206
|
+
Philiprehberger::HeaderKit.parse_retry_after("Fri, 04 Apr 2026 12:00:00 GMT")
|
|
207
|
+
# => {date: 2026-04-04 12:00:00 UTC}
|
|
208
|
+
```
|
|
209
|
+
|
|
184
210
|
## API
|
|
185
211
|
|
|
186
212
|
| Method | Description |
|
|
@@ -204,6 +230,8 @@ Philiprehberger::HeaderKit.parse_via('1.1 vegur, 1.0 fred')
|
|
|
204
230
|
| `HeaderKit.security_headers(**options)` | Generate recommended security headers |
|
|
205
231
|
| `HeaderKit.parse_forwarded(header)` | Parse RFC 7239 Forwarded header |
|
|
206
232
|
| `HeaderKit.parse_via(header)` | Parse Via header into structured entries |
|
|
233
|
+
| `HeaderKit.parse_retry_after(header)` | Parse Retry-After header (seconds or HTTP date) |
|
|
234
|
+
| `HeaderKit.etag_match?(header_value, resource_etag)` | Check If-None-Match / If-Match against a resource ETag (list, `W/` weak prefix, `*` wildcard) |
|
|
207
235
|
|
|
208
236
|
## Development
|
|
209
237
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module HeaderKit
|
|
5
|
+
# Evaluates If-None-Match / If-Match style header values against a
|
|
6
|
+
# resource ETag.
|
|
7
|
+
#
|
|
8
|
+
# Accepts single ETags, comma-separated lists, weak-prefixed values
|
|
9
|
+
# (e.g. `W/"abc"`), and the `*` wildcard. Strong and weak semantics
|
|
10
|
+
# collapse to equality on the inner token — callers that need strict
|
|
11
|
+
# strong/weak differentiation should implement that separately.
|
|
12
|
+
module Etag
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# Check whether a header value matches a resource ETag.
|
|
16
|
+
#
|
|
17
|
+
# @param header_value [String, nil] the raw If-None-Match / If-Match header
|
|
18
|
+
# @param resource_etag [String, nil] the resource ETag (unquoted, no `W/` prefix)
|
|
19
|
+
# @return [Boolean] true if any value in the header matches the resource
|
|
20
|
+
def match?(header_value, resource_etag)
|
|
21
|
+
return false if header_value.nil?
|
|
22
|
+
|
|
23
|
+
stripped = header_value.strip
|
|
24
|
+
return false if stripped.empty?
|
|
25
|
+
|
|
26
|
+
tokens = split_values(stripped)
|
|
27
|
+
return false if tokens.empty?
|
|
28
|
+
|
|
29
|
+
tokens.any? { |token| token_matches?(token, resource_etag) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Split a header value into individual ETag tokens.
|
|
33
|
+
#
|
|
34
|
+
# Quoted sections (including escaped characters) are preserved so commas
|
|
35
|
+
# inside a quoted ETag do not split the value.
|
|
36
|
+
#
|
|
37
|
+
# @param value [String] the header value to split
|
|
38
|
+
# @return [Array<String>] trimmed, non-empty tokens
|
|
39
|
+
def split_values(value)
|
|
40
|
+
tokens = []
|
|
41
|
+
buffer = +''
|
|
42
|
+
in_quotes = false
|
|
43
|
+
escaped = false
|
|
44
|
+
|
|
45
|
+
value.each_char do |char|
|
|
46
|
+
if escaped
|
|
47
|
+
buffer << char
|
|
48
|
+
escaped = false
|
|
49
|
+
elsif in_quotes && char == '\\'
|
|
50
|
+
buffer << char
|
|
51
|
+
escaped = true
|
|
52
|
+
elsif char == '"'
|
|
53
|
+
buffer << char
|
|
54
|
+
in_quotes = !in_quotes
|
|
55
|
+
elsif char == ',' && !in_quotes
|
|
56
|
+
tokens << buffer.strip unless buffer.strip.empty?
|
|
57
|
+
buffer = +''
|
|
58
|
+
else
|
|
59
|
+
buffer << char
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
tokens << buffer.strip unless buffer.strip.empty?
|
|
64
|
+
tokens
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Determine whether a single token matches the resource ETag.
|
|
68
|
+
#
|
|
69
|
+
# @param token [String] a single header token
|
|
70
|
+
# @param resource_etag [String, nil] the resource ETag
|
|
71
|
+
# @return [Boolean] true if the token matches
|
|
72
|
+
def token_matches?(token, resource_etag)
|
|
73
|
+
return !resource_etag.nil? if token == '*'
|
|
74
|
+
return false if resource_etag.nil?
|
|
75
|
+
|
|
76
|
+
unwrap(token) == resource_etag
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Strip weak prefix and surrounding quotes from an ETag token.
|
|
80
|
+
#
|
|
81
|
+
# Backslash-escaped characters inside the quoted value are unescaped.
|
|
82
|
+
#
|
|
83
|
+
# @param token [String] a single ETag token
|
|
84
|
+
# @return [String] the inner ETag value
|
|
85
|
+
def unwrap(token)
|
|
86
|
+
value = token.sub(%r{\AW/}i, '')
|
|
87
|
+
|
|
88
|
+
return value unless value.start_with?('"') && value.end_with?('"') && value.length >= 2
|
|
89
|
+
|
|
90
|
+
inner = value[1..-2]
|
|
91
|
+
inner.gsub(/\\(.)/, '\1')
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
module Philiprehberger
|
|
6
|
+
module HeaderKit
|
|
7
|
+
module RetryAfter
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def parse(header)
|
|
11
|
+
return nil if header.nil? || header.empty?
|
|
12
|
+
|
|
13
|
+
if header.match?(/\A\d+\z/)
|
|
14
|
+
{ seconds: header.to_i }
|
|
15
|
+
else
|
|
16
|
+
{ date: Time.httpdate(header) }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -9,11 +9,13 @@ require_relative 'header_kit/authorization'
|
|
|
9
9
|
require_relative 'header_kit/cache_control'
|
|
10
10
|
require_relative 'header_kit/content_type'
|
|
11
11
|
require_relative 'header_kit/cookie'
|
|
12
|
+
require_relative 'header_kit/etag'
|
|
12
13
|
require_relative 'header_kit/link'
|
|
13
14
|
require_relative 'header_kit/negotiation'
|
|
14
15
|
require_relative 'header_kit/cors'
|
|
15
16
|
require_relative 'header_kit/security'
|
|
16
17
|
require_relative 'header_kit/forwarded'
|
|
18
|
+
require_relative 'header_kit/retry_after'
|
|
17
19
|
|
|
18
20
|
module Philiprehberger
|
|
19
21
|
module HeaderKit
|
|
@@ -174,5 +176,29 @@ module Philiprehberger
|
|
|
174
176
|
def self.parse_via(header)
|
|
175
177
|
Forwarded.parse_via(header)
|
|
176
178
|
end
|
|
179
|
+
|
|
180
|
+
# Check whether an If-None-Match / If-Match header matches a resource ETag.
|
|
181
|
+
#
|
|
182
|
+
# Accepts a single ETag, comma-separated list, weak-prefixed values
|
|
183
|
+
# (e.g. `W/"abc"`), or the `*` wildcard. Weak and strong ETags collapse
|
|
184
|
+
# to equality on the inner token.
|
|
185
|
+
#
|
|
186
|
+
# @param header_value [String, nil] the raw header value
|
|
187
|
+
# @param resource_etag [String, nil] the resource ETag (unquoted, no `W/` prefix)
|
|
188
|
+
# @return [Boolean] true if any value in the header matches the resource
|
|
189
|
+
def self.etag_match?(header_value, resource_etag)
|
|
190
|
+
Etag.match?(header_value, resource_etag)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Parse a Retry-After header.
|
|
194
|
+
#
|
|
195
|
+
# Returns a hash with :seconds (Integer) for numeric values,
|
|
196
|
+
# or :date (Time) for HTTP date values. Returns nil for nil/empty input.
|
|
197
|
+
#
|
|
198
|
+
# @param header [String] the Retry-After header value
|
|
199
|
+
# @return [Hash, nil] hash with :seconds or :date key, or nil
|
|
200
|
+
def self.parse_retry_after(header)
|
|
201
|
+
RetryAfter.parse(header)
|
|
202
|
+
end
|
|
177
203
|
end
|
|
178
204
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-header_kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Rehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-04-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Parse and build Accept, Accept-Language, Accept-Encoding, Authorization,
|
|
14
14
|
Cache-Control, Content-Type, Cookie, Link, CORS, Forwarded, and Via HTTP headers.
|
|
@@ -32,9 +32,11 @@ files:
|
|
|
32
32
|
- lib/philiprehberger/header_kit/content_type.rb
|
|
33
33
|
- lib/philiprehberger/header_kit/cookie.rb
|
|
34
34
|
- lib/philiprehberger/header_kit/cors.rb
|
|
35
|
+
- lib/philiprehberger/header_kit/etag.rb
|
|
35
36
|
- lib/philiprehberger/header_kit/forwarded.rb
|
|
36
37
|
- lib/philiprehberger/header_kit/link.rb
|
|
37
38
|
- lib/philiprehberger/header_kit/negotiation.rb
|
|
39
|
+
- lib/philiprehberger/header_kit/retry_after.rb
|
|
38
40
|
- lib/philiprehberger/header_kit/security.rb
|
|
39
41
|
- lib/philiprehberger/header_kit/version.rb
|
|
40
42
|
homepage: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-header_kit
|