linzer 0.7.9.beta2 → 0.7.9.beta3
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 +6 -0
- data/README.md +96 -12
- data/lib/faraday/http_signature/middleware.rb +296 -0
- data/lib/faraday/http_signature.rb +36 -0
- data/lib/linzer/faraday/utils.rb +29 -0
- data/lib/linzer/faraday.rb +29 -0
- data/lib/linzer/http.rb +43 -1
- data/lib/linzer/message/adapter/abstract.rb +35 -5
- data/lib/linzer/message/adapter/faraday/request.rb +63 -0
- data/lib/linzer/message/adapter/faraday/response.rb +44 -0
- data/lib/linzer/message/adapter/generic/request.rb +16 -12
- data/lib/linzer/message/adapter/http_gem/common.rb +5 -0
- data/lib/linzer/message/adapter/http_gem/request.rb +8 -0
- data/lib/linzer/message/adapter/http_gem/response.rb +7 -0
- data/lib/linzer/message/adapter/net_http/request.rb +8 -0
- data/lib/linzer/message/adapter/net_http/response.rb +7 -0
- data/lib/linzer/message/adapter/rack/common.rb +37 -0
- data/lib/linzer/message/field/parser.rb +15 -0
- data/lib/linzer/message/wrapper.rb +12 -2
- data/lib/linzer/signature.rb +20 -0
- data/lib/linzer/verifier.rb +8 -0
- data/lib/linzer/version.rb +1 -1
- data/lib/rack/auth/signature/helpers.rb +72 -0
- metadata +7 -1
|
@@ -96,7 +96,10 @@ module Linzer
|
|
|
96
96
|
private
|
|
97
97
|
|
|
98
98
|
# Parses a field name string into a FieldId.
|
|
99
|
-
#
|
|
99
|
+
#
|
|
100
|
+
# @param field_name [String] the component identifier string
|
|
101
|
+
# @return [FieldId, nil] the parsed identifier, or +nil+ if invalid
|
|
102
|
+
# @raise [Error] if +@status+ is used in a request message
|
|
100
103
|
def parse_field_name(field_name)
|
|
101
104
|
field_id = FieldId.new(field_name: field_name)
|
|
102
105
|
component = field_id.item
|
|
@@ -117,8 +120,12 @@ module Linzer
|
|
|
117
120
|
raise Linzer::Error, msg unless message.request?
|
|
118
121
|
end
|
|
119
122
|
|
|
120
|
-
# Validates component identifier parameters.
|
|
121
|
-
#
|
|
123
|
+
# Validates component identifier parameters against RFC 9421 rules.
|
|
124
|
+
#
|
|
125
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
126
|
+
# @param method [Symbol] +:derived+ or +:field+
|
|
127
|
+
# @return [Starry::Item, nil] the validated name, or +nil+ if
|
|
128
|
+
# the parameter combination is invalid
|
|
122
129
|
def validate_parameters(name, method)
|
|
123
130
|
has_unknown = name.parameters.any? { |p, _| !KNOWN_PARAMETERS.include?(p) }
|
|
124
131
|
return nil if has_unknown
|
|
@@ -149,6 +156,13 @@ module Linzer
|
|
|
149
156
|
private_constant :KNOWN_PARAMETERS
|
|
150
157
|
|
|
151
158
|
# Retrieves a component value with parameter processing.
|
|
159
|
+
#
|
|
160
|
+
# Handles +;req+, +;sf+, +;key+, and +;bs+ parameters by
|
|
161
|
+
# delegating to the corresponding helper methods.
|
|
162
|
+
#
|
|
163
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
164
|
+
# @param method [Symbol] +:derived+ or +:field+
|
|
165
|
+
# @return [String, Integer, nil] the component value
|
|
152
166
|
def retrieve(name, method)
|
|
153
167
|
if !name.parameters.empty?
|
|
154
168
|
valid_params = validate_parameters(name, method)
|
|
@@ -176,6 +190,10 @@ module Linzer
|
|
|
176
190
|
end
|
|
177
191
|
|
|
178
192
|
# Processes a structured field value with optional key extraction.
|
|
193
|
+
#
|
|
194
|
+
# @param value [String] the raw header value to parse as a dictionary
|
|
195
|
+
# @param key [String, nil] if present, extracts a single dictionary member
|
|
196
|
+
# @return [String] the serialized structured field value
|
|
179
197
|
# @see https://www.rfc-editor.org/rfc/rfc9421.html#section-2.1.1
|
|
180
198
|
def sf(value, key = nil)
|
|
181
199
|
dict = Starry.parse_dictionary(value)
|
|
@@ -188,19 +206,31 @@ module Linzer
|
|
|
188
206
|
end
|
|
189
207
|
end
|
|
190
208
|
|
|
191
|
-
# Binary-wraps a field value.
|
|
209
|
+
# Binary-wraps a field value as a byte sequence.
|
|
210
|
+
#
|
|
211
|
+
# @param value [String] the header value to wrap
|
|
212
|
+
# @return [String] the serialized byte sequence
|
|
192
213
|
# @see https://www.rfc-editor.org/rfc/rfc9421.html#section-2.1.3
|
|
193
214
|
def bs(value)
|
|
194
215
|
Starry.serialize(value.encode(Encoding::ASCII_8BIT))
|
|
195
216
|
end
|
|
196
217
|
|
|
197
218
|
# Retrieves a trailer field value.
|
|
219
|
+
#
|
|
198
220
|
# @abstract Subclasses should implement if trailer support is needed.
|
|
221
|
+
# @param trailer [Object] the trailer field identifier
|
|
222
|
+
# @return [String, nil] the trailer value
|
|
223
|
+
# @raise [Error] always, since no built-in adapters support trailers
|
|
199
224
|
def tr(trailer)
|
|
200
225
|
raise Error, "Sub-classes are required to implement this method!"
|
|
201
226
|
end
|
|
202
227
|
|
|
203
|
-
# Retrieves a field from the attached request.
|
|
228
|
+
# Retrieves a field from the attached request (for +;req+ parameter).
|
|
229
|
+
#
|
|
230
|
+
# @param field [Starry::Item] the component identifier
|
|
231
|
+
# @param method [Symbol] +:derived+ or +:field+
|
|
232
|
+
# @return [String, nil] the value from the attached request, or
|
|
233
|
+
# +nil+ if no request is attached
|
|
204
234
|
def req(field, method)
|
|
205
235
|
attached_request? ? @attached_request[String(field)] : nil
|
|
206
236
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Linzer
|
|
4
|
+
class Message
|
|
5
|
+
module Adapter
|
|
6
|
+
module Faraday
|
|
7
|
+
# Adapter for {::Faraday::Request} objects from the faraday gem.
|
|
8
|
+
#
|
|
9
|
+
# Extends the generic request adapter with faraday-specific
|
|
10
|
+
# derived component retrieval, field lookup, and URI handling.
|
|
11
|
+
#
|
|
12
|
+
# @note Not loaded automatically to avoid making faraday a hard
|
|
13
|
+
# dependency. Require +"linzer/faraday"+ to register this adapter.
|
|
14
|
+
#
|
|
15
|
+
# @see Generic::Request
|
|
16
|
+
# @see https://github.com/lostisland/faraday faraday gem
|
|
17
|
+
class Request < Generic::Request
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# Resolves a derived component value from the request.
|
|
21
|
+
#
|
|
22
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
23
|
+
# @return [String, nil] the derived value, or +nil+ if unknown
|
|
24
|
+
def derived(name)
|
|
25
|
+
url = @operation.path
|
|
26
|
+
case name.value
|
|
27
|
+
when "@method" then @operation.http_method.to_s.upcase
|
|
28
|
+
when "@target-uri" then uri.to_s
|
|
29
|
+
when "@authority" then url.authority.downcase
|
|
30
|
+
when "@scheme" then url.scheme.downcase
|
|
31
|
+
when "@request-target" then uri.request_uri
|
|
32
|
+
when "@path" then url.path
|
|
33
|
+
when "@query" then "?%s" % String(uri_query)
|
|
34
|
+
when "@query-param" then query_param(uri_query, name)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Builds the full URI including query parameters.
|
|
39
|
+
#
|
|
40
|
+
# @return [URI] the complete request URI with encoded query string
|
|
41
|
+
def uri
|
|
42
|
+
uri = @operation.path.dup
|
|
43
|
+
uri.query = URI.encode_www_form(@operation.params)
|
|
44
|
+
uri
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns the raw query string from the request URI.
|
|
48
|
+
#
|
|
49
|
+
# Prefers the raw query string from the URI when available,
|
|
50
|
+
# as Faraday normalises percent-encoding when parsing params
|
|
51
|
+
# (e.g. +%2D+ becomes +-+), which would break signature
|
|
52
|
+
# verification.
|
|
53
|
+
#
|
|
54
|
+
# @return [String, nil] the raw query string
|
|
55
|
+
def uri_query
|
|
56
|
+
url = @operation.path
|
|
57
|
+
url.query || uri.query
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Linzer
|
|
4
|
+
class Message
|
|
5
|
+
module Adapter
|
|
6
|
+
module Faraday
|
|
7
|
+
# Adapter for {::Faraday::Response} objects from the faraday gem.
|
|
8
|
+
#
|
|
9
|
+
# Extends the generic response adapter with faraday-specific
|
|
10
|
+
# derived component retrieval (e.g. +@status+) and header
|
|
11
|
+
# attachment.
|
|
12
|
+
#
|
|
13
|
+
# @note Not loaded automatically to avoid making faraday a hard
|
|
14
|
+
# dependency. Require +"linzer/faraday"+ to register this adapter.
|
|
15
|
+
#
|
|
16
|
+
# @see Generic::Response
|
|
17
|
+
# @see https://github.com/lostisland/faraday faraday gem
|
|
18
|
+
class Response < Generic::Response
|
|
19
|
+
# Attaches a signature to the underlying response headers.
|
|
20
|
+
#
|
|
21
|
+
# @param signature [Linzer::Signature] the signature to attach
|
|
22
|
+
# @return [::Faraday::Response] the underlying response object
|
|
23
|
+
def attach!(signature)
|
|
24
|
+
signature.to_h.each { |h, v| @operation.headers[h] = v }
|
|
25
|
+
@operation
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# Resolves a derived component value from the response.
|
|
31
|
+
#
|
|
32
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
33
|
+
# @return [Integer, nil] the HTTP status code for +@status+,
|
|
34
|
+
# or +nil+ if the component is unknown
|
|
35
|
+
def derived(name)
|
|
36
|
+
case name.value
|
|
37
|
+
when "@status" then @operation.status.to_i
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -51,31 +51,35 @@ module Linzer
|
|
|
51
51
|
private
|
|
52
52
|
|
|
53
53
|
def derived(name)
|
|
54
|
-
unimplemented_method = 'Derived field "
|
|
54
|
+
unimplemented_method = 'Derived field "%s" lookup is not implemented!'
|
|
55
|
+
|
|
56
|
+
uri = @operation.uri rescue nil
|
|
57
|
+
raise Error, unimplemented_method % name.value if uri.nil?
|
|
58
|
+
|
|
55
59
|
case name.value
|
|
56
|
-
when "@method" then raise Error, unimplemented_method
|
|
57
|
-
when "@target-uri" then
|
|
58
|
-
when "@authority" then
|
|
59
|
-
when "@scheme" then
|
|
60
|
-
when "@request-target" then
|
|
61
|
-
when "@path" then
|
|
62
|
-
when "@query" then "?%s" % String(
|
|
63
|
-
when "@query-param" then query_param(name)
|
|
60
|
+
when "@method" then raise Error, unimplemented_method % name.value
|
|
61
|
+
when "@target-uri" then uri.to_s
|
|
62
|
+
when "@authority" then uri.authority.downcase
|
|
63
|
+
when "@scheme" then uri.scheme.downcase
|
|
64
|
+
when "@request-target" then uri.request_uri
|
|
65
|
+
when "@path" then uri.path
|
|
66
|
+
when "@query" then "?%s" % String(uri.query)
|
|
67
|
+
when "@query-param" then query_param(uri.query, name)
|
|
64
68
|
end
|
|
65
69
|
end
|
|
66
70
|
|
|
67
|
-
def query_param(name)
|
|
71
|
+
def query_param(uri_query, name)
|
|
68
72
|
param_name = name.parameters["name"]
|
|
69
73
|
return nil if !param_name
|
|
70
74
|
decoded_param_name = URI.decode_uri_component(param_name)
|
|
71
|
-
params = CGI.parse(
|
|
75
|
+
params = CGI.parse(uri_query)
|
|
72
76
|
URI.encode_uri_component(params[decoded_param_name]&.first)
|
|
73
77
|
end
|
|
74
78
|
|
|
75
79
|
def field(name)
|
|
76
80
|
has_tr = name.parameters["tr"]
|
|
77
81
|
return nil if has_tr # HTTP requests don't have trailer fields
|
|
78
|
-
value =
|
|
82
|
+
value = header(name.value.to_s)
|
|
79
83
|
value.dup&.strip
|
|
80
84
|
end
|
|
81
85
|
end
|
|
@@ -30,6 +30,11 @@ module Linzer
|
|
|
30
30
|
|
|
31
31
|
private
|
|
32
32
|
|
|
33
|
+
# Retrieves an HTTP field value from the request or response headers.
|
|
34
|
+
#
|
|
35
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
36
|
+
# @return [String, nil] the stripped header value, or +nil+ if the
|
|
37
|
+
# field has a +tr+ (trailer) parameter or is not present
|
|
33
38
|
def field(name)
|
|
34
39
|
has_tr = name.parameters["tr"]
|
|
35
40
|
return nil if has_tr # XXX: is there a library actually supporting trailers?
|
|
@@ -15,6 +15,14 @@ module Linzer
|
|
|
15
15
|
|
|
16
16
|
private
|
|
17
17
|
|
|
18
|
+
# Resolves a derived component value from the request.
|
|
19
|
+
#
|
|
20
|
+
# Overrides the generic implementation for http.rb-specific
|
|
21
|
+
# accessor methods: +uri.host+ for +@authority+ and
|
|
22
|
+
# +verb+ for +@method+.
|
|
23
|
+
#
|
|
24
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
25
|
+
# @return [String, nil] the derived value
|
|
18
26
|
def derived(name)
|
|
19
27
|
return @operation.uri.host if name.value == "@authority"
|
|
20
28
|
return @operation.verb.to_s.upcase if name.value == "@method"
|
|
@@ -19,6 +19,13 @@ module Linzer
|
|
|
19
19
|
|
|
20
20
|
private
|
|
21
21
|
|
|
22
|
+
# Resolves a derived component value from the response.
|
|
23
|
+
#
|
|
24
|
+
# Uses +HTTP::Response#status+ converted to Integer for
|
|
25
|
+
# the +@status+ component.
|
|
26
|
+
#
|
|
27
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
28
|
+
# @return [Integer, nil] the HTTP status code, or +nil+ if unknown
|
|
22
29
|
def derived(name)
|
|
23
30
|
case name.value
|
|
24
31
|
when "@status" then @operation.status.to_i
|
|
@@ -14,6 +14,14 @@ module Linzer
|
|
|
14
14
|
class Request < Generic::Request
|
|
15
15
|
private
|
|
16
16
|
|
|
17
|
+
# Resolves a derived component value from the request.
|
|
18
|
+
#
|
|
19
|
+
# Overrides the generic implementation to use
|
|
20
|
+
# +Net::HTTPRequest#method+ for the +@method+ component,
|
|
21
|
+
# which returns the HTTP verb as an uppercase string.
|
|
22
|
+
#
|
|
23
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
24
|
+
# @return [String, nil] the derived value
|
|
17
25
|
def derived(name)
|
|
18
26
|
return @operation.method if name.value == "@method"
|
|
19
27
|
super
|
|
@@ -11,6 +11,13 @@ module Linzer
|
|
|
11
11
|
class Response < Generic::Response
|
|
12
12
|
private
|
|
13
13
|
|
|
14
|
+
# Resolves a derived component value from the response.
|
|
15
|
+
#
|
|
16
|
+
# Uses +Net::HTTPResponse#code+ (a String) converted to Integer
|
|
17
|
+
# for the +@status+ component.
|
|
18
|
+
#
|
|
19
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
20
|
+
# @return [Integer, nil] the HTTP status code, or +nil+ if unknown
|
|
14
21
|
def derived(name)
|
|
15
22
|
case name.value
|
|
16
23
|
when "@status" then @operation.code.to_i
|
|
@@ -24,11 +24,19 @@ module Linzer
|
|
|
24
24
|
|
|
25
25
|
private
|
|
26
26
|
|
|
27
|
+
# Validates that the operation is exclusively a request or response.
|
|
28
|
+
# @raise [Error] if the operation is both or neither
|
|
27
29
|
def validate
|
|
28
30
|
msg = "Message instance must be an HTTP request or response"
|
|
29
31
|
raise Error.new msg if response? == request?
|
|
30
32
|
end
|
|
31
33
|
|
|
34
|
+
# Validates that a header name is non-empty.
|
|
35
|
+
#
|
|
36
|
+
# @param name [String] the header name
|
|
37
|
+
# @return [String] the validated header name
|
|
38
|
+
# @raise [ArgumentError] if the name is blank
|
|
39
|
+
# @raise [Linzer::Error] if the name is otherwise invalid
|
|
32
40
|
def validate_header_name(name)
|
|
33
41
|
raise ArgumentError.new, "Blank header name." if name.empty?
|
|
34
42
|
name.to_str
|
|
@@ -40,6 +48,13 @@ module Linzer
|
|
|
40
48
|
# :nocov:
|
|
41
49
|
end
|
|
42
50
|
|
|
51
|
+
# Converts an HTTP header name to Rack's environment key format.
|
|
52
|
+
#
|
|
53
|
+
# Rack stores headers as uppercase with underscores and an +HTTP_+
|
|
54
|
+
# prefix, except for +Content-Type+ and +Content-Length+.
|
|
55
|
+
#
|
|
56
|
+
# @param field_name [String] the HTTP header name (e.g. +"content-type"+)
|
|
57
|
+
# @return [String] the Rack env key (e.g. +"CONTENT_TYPE"+ or +"HTTP_ACCEPT"+)
|
|
43
58
|
def rack_header_name(field_name)
|
|
44
59
|
validate_header_name field_name
|
|
45
60
|
|
|
@@ -52,6 +67,10 @@ module Linzer
|
|
|
52
67
|
end
|
|
53
68
|
end
|
|
54
69
|
|
|
70
|
+
# Resolves a derived component value from the Rack request/response.
|
|
71
|
+
#
|
|
72
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
73
|
+
# @return [String, nil] the derived value, or +nil+ if unknown
|
|
55
74
|
def derived(name)
|
|
56
75
|
method = DERIVED_COMPONENT[name.value]
|
|
57
76
|
|
|
@@ -64,6 +83,11 @@ module Linzer
|
|
|
64
83
|
value || derive(@operation, method)
|
|
65
84
|
end
|
|
66
85
|
|
|
86
|
+
# Retrieves an HTTP field value from the Rack request or response.
|
|
87
|
+
#
|
|
88
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
89
|
+
# @return [String, nil] the stripped header value, or +nil+ if the
|
|
90
|
+
# field has a +tr+ (trailer) parameter or is not present
|
|
67
91
|
def field(name)
|
|
68
92
|
has_tr = name.parameters["tr"]
|
|
69
93
|
return nil if has_tr
|
|
@@ -79,6 +103,14 @@ module Linzer
|
|
|
79
103
|
field_value.dup&.strip
|
|
80
104
|
end
|
|
81
105
|
|
|
106
|
+
# Invokes a method on the Rack operation to extract a derived value.
|
|
107
|
+
#
|
|
108
|
+
# Applies post-processing for +@query+ (prepends +?+) and
|
|
109
|
+
# +@authority+/+@scheme+ (downcases).
|
|
110
|
+
#
|
|
111
|
+
# @param operation [Rack::Request, Rack::Response] the Rack object
|
|
112
|
+
# @param method [Symbol] the method to call
|
|
113
|
+
# @return [String, nil] the derived value
|
|
82
114
|
def derive(operation, method)
|
|
83
115
|
return nil unless operation.respond_to?(method)
|
|
84
116
|
value = operation.public_send(method)
|
|
@@ -87,6 +119,11 @@ module Linzer
|
|
|
87
119
|
value
|
|
88
120
|
end
|
|
89
121
|
|
|
122
|
+
# Extracts a single query parameter value by name.
|
|
123
|
+
#
|
|
124
|
+
# @param name [Starry::Item] the component with a +name+ parameter
|
|
125
|
+
# @return [String, nil] the percent-encoded parameter value, or
|
|
126
|
+
# +nil+ if the parameter is missing or not found
|
|
90
127
|
def query_param(name)
|
|
91
128
|
param_name = name.parameters["name"]
|
|
92
129
|
return nil if !param_name
|
|
@@ -39,6 +39,14 @@ module Linzer
|
|
|
39
39
|
|
|
40
40
|
private
|
|
41
41
|
|
|
42
|
+
# Parses an unserialized component identifier with parameters.
|
|
43
|
+
#
|
|
44
|
+
# Splits on +;+ to separate the field name from parameters,
|
|
45
|
+
# then serializes the field name and collects parameters.
|
|
46
|
+
#
|
|
47
|
+
# @param field_name [String] e.g. +"content-type;bs"+ or
|
|
48
|
+
# +"example-dict;key=\"a\""+
|
|
49
|
+
# @return [Starry::Item] the parsed item with parameters
|
|
42
50
|
def parse_unserialized_input(field_name)
|
|
43
51
|
field, *raw_params = field_name.split(";")
|
|
44
52
|
item = Starry.parse_item(Starry.serialize(field))
|
|
@@ -46,6 +54,13 @@ module Linzer
|
|
|
46
54
|
item
|
|
47
55
|
end
|
|
48
56
|
|
|
57
|
+
# Parses raw parameter strings into a merged Hash.
|
|
58
|
+
#
|
|
59
|
+
# Handles both boolean parameters (+";bs"+ → +{"bs" => true}+)
|
|
60
|
+
# and key-value parameters (+";key=\"a\""+ → +{"key" => "a"}+).
|
|
61
|
+
#
|
|
62
|
+
# @param str [Array<String>] raw parameter strings
|
|
63
|
+
# @return [Hash] merged parameter hash
|
|
49
64
|
def collect_parameters(str)
|
|
50
65
|
params = str.map do |param|
|
|
51
66
|
if (tokens = param.split("=")) == [param] # e.g.: ";bs"
|
|
@@ -48,8 +48,14 @@ module Linzer
|
|
|
48
48
|
|
|
49
49
|
attr_reader :adapters
|
|
50
50
|
|
|
51
|
-
# Finds an adapter by checking
|
|
52
|
-
#
|
|
51
|
+
# Finds an adapter by checking the operation's ancestry.
|
|
52
|
+
#
|
|
53
|
+
# This allows subclasses of registered classes (e.g.
|
|
54
|
+
# +Net::HTTP::Get < Net::HTTPRequest+) to use the parent's adapter
|
|
55
|
+
# without explicit registration.
|
|
56
|
+
#
|
|
57
|
+
# @param operation [Object] the HTTP message object
|
|
58
|
+
# @return [Class, nil] the adapter class, or +nil+ if no ancestor matches
|
|
53
59
|
def find_ancestor(operation)
|
|
54
60
|
adapters
|
|
55
61
|
.select { |klz, adpt| operation.is_a? klz }
|
|
@@ -57,6 +63,10 @@ module Linzer
|
|
|
57
63
|
.first
|
|
58
64
|
end
|
|
59
65
|
|
|
66
|
+
# Raises an error for unsupported HTTP message types.
|
|
67
|
+
#
|
|
68
|
+
# @param operation [Object] the unsupported HTTP message
|
|
69
|
+
# @raise [Linzer::Error] with a message suggesting +register_adapter+
|
|
60
70
|
def fail_with_unsupported(operation)
|
|
61
71
|
err_msg = <<~EOM
|
|
62
72
|
Unknown/unsupported HTTP message class: '#{operation.class}'!
|
data/lib/linzer/signature.rb
CHANGED
|
@@ -102,6 +102,26 @@ module Linzer
|
|
|
102
102
|
(Time.now.to_i - created) > seconds
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
+
# Checks if the signature has expired based on the `expires` parameter.
|
|
106
|
+
#
|
|
107
|
+
# If the `expires` parameter is not present, the signature is considered
|
|
108
|
+
# not expired (returns false). If the parameter is present but not a valid
|
|
109
|
+
# integer, an error is raised.
|
|
110
|
+
#
|
|
111
|
+
# @return [Boolean] true if the signature has expired
|
|
112
|
+
# @raise [Error] If the `expires` parameter is not a valid integer
|
|
113
|
+
#
|
|
114
|
+
# @example Check if a signature has expired
|
|
115
|
+
# signature.expired? # => true or false
|
|
116
|
+
#
|
|
117
|
+
# @see https://www.rfc-editor.org/rfc/rfc9421.html#section-2.3 RFC 9421 Section 2.3
|
|
118
|
+
def expired?
|
|
119
|
+
return false if !parameters.key?("expires")
|
|
120
|
+
Time.now.to_i >= Integer(parameters["expires"])
|
|
121
|
+
rescue ArgumentError, TypeError
|
|
122
|
+
raise Error.new "Signature has a non-integer `expires` parameter"
|
|
123
|
+
end
|
|
124
|
+
|
|
105
125
|
# Converts the signature to HTTP header format.
|
|
106
126
|
#
|
|
107
127
|
# Returns a hash suitable for setting as HTTP headers on a request or
|
data/lib/linzer/verifier.rb
CHANGED
|
@@ -23,6 +23,7 @@ module Linzer
|
|
|
23
23
|
# - All covered components exist in the message
|
|
24
24
|
# - The signature base matches what was signed
|
|
25
25
|
# - The cryptographic signature is valid for the public key
|
|
26
|
+
# - The signature has not expired (if `expires` parameter is present)
|
|
26
27
|
# - The signature is not older than `no_older_than` (if specified)
|
|
27
28
|
#
|
|
28
29
|
# @param key [Linzer::Key] The public key to verify with. Must respond to
|
|
@@ -85,6 +86,13 @@ module Linzer
|
|
|
85
86
|
raise VerifyError, ex.message, cause: ex
|
|
86
87
|
end
|
|
87
88
|
|
|
89
|
+
begin
|
|
90
|
+
exp_sig_msg = "Signature has expired or is invalid"
|
|
91
|
+
raise VerifyError, exp_sig_msg if signature.expired?
|
|
92
|
+
rescue Error => ex
|
|
93
|
+
raise VerifyError, ex.message, cause: ex
|
|
94
|
+
end
|
|
95
|
+
|
|
88
96
|
return unless no_older_than
|
|
89
97
|
old_sig_msg = "Signature created more than #{no_older_than} seconds ago"
|
|
90
98
|
begin
|
data/lib/linzer/version.rb
CHANGED
|
@@ -5,42 +5,82 @@ require "yaml"
|
|
|
5
5
|
module Rack
|
|
6
6
|
module Auth
|
|
7
7
|
class Signature
|
|
8
|
+
# Shared helpers for the Rack signature verification middleware.
|
|
9
|
+
#
|
|
10
|
+
# Organizes functionality into three sub-modules:
|
|
11
|
+
# - {Parameters} — validates required signature parameters
|
|
12
|
+
# - {Configuration} — loads and merges middleware options
|
|
13
|
+
# - {Key} — resolves verification keys by keyid
|
|
14
|
+
#
|
|
15
|
+
# @api private
|
|
8
16
|
module Helpers
|
|
17
|
+
# Validates the presence of required signature parameters.
|
|
18
|
+
#
|
|
19
|
+
# Each method checks whether a specific parameter is required
|
|
20
|
+
# (per configuration) and, if so, whether it is present and valid
|
|
21
|
+
# in the current signature.
|
|
22
|
+
#
|
|
23
|
+
# @api private
|
|
9
24
|
module Parameters
|
|
10
25
|
private
|
|
11
26
|
|
|
27
|
+
# Checks if the +created+ parameter requirement is satisfied.
|
|
28
|
+
# @return [Boolean] +true+ if not required or present and valid
|
|
12
29
|
def created?
|
|
13
30
|
!options[:signatures][:created_required] || !!Integer(params.fetch("created"))
|
|
14
31
|
end
|
|
15
32
|
|
|
33
|
+
# Checks if the +expires+ parameter requirement is satisfied.
|
|
34
|
+
# @return [Boolean] +true+ if not required or present and not yet expired
|
|
16
35
|
def expires?
|
|
17
36
|
return true if !options[:signatures][:expires_required]
|
|
18
37
|
Integer(params.fetch("expires")) > Time.now.to_i
|
|
19
38
|
end
|
|
20
39
|
|
|
40
|
+
# Checks if the +keyid+ parameter requirement is satisfied.
|
|
41
|
+
# @return [Boolean] +true+ if not required or present
|
|
21
42
|
def keyid?
|
|
22
43
|
!options[:signatures][:keyid_required] || String(params.fetch("keyid"))
|
|
23
44
|
end
|
|
24
45
|
|
|
46
|
+
# Checks if the +nonce+ parameter requirement is satisfied.
|
|
47
|
+
# @return [Boolean] +true+ if not required or present
|
|
25
48
|
def nonce?
|
|
26
49
|
!options[:signatures][:nonce_required] || String(params.fetch("nonce"))
|
|
27
50
|
end
|
|
28
51
|
|
|
52
|
+
# Checks if the +alg+ parameter requirement is satisfied.
|
|
53
|
+
# @return [Boolean] +true+ if not required or present
|
|
29
54
|
def alg?
|
|
30
55
|
!options[:signatures][:alg_required] || String(params.fetch("alg"))
|
|
31
56
|
end
|
|
32
57
|
|
|
58
|
+
# Checks if the +tag+ parameter requirement is satisfied.
|
|
59
|
+
# @return [Boolean] +true+ if not required or present
|
|
33
60
|
def tag?
|
|
34
61
|
!options[:signatures][:tag_required] || String(params.fetch("tag"))
|
|
35
62
|
end
|
|
36
63
|
end
|
|
37
64
|
|
|
65
|
+
# Handles loading and merging of middleware configuration.
|
|
66
|
+
#
|
|
67
|
+
# Configuration can come from three sources (in order of precedence):
|
|
68
|
+
# 1. Options passed directly to the middleware constructor
|
|
69
|
+
# 2. A YAML configuration file (via +:config_path+)
|
|
70
|
+
# 3. {DEFAULT_OPTIONS}
|
|
71
|
+
#
|
|
72
|
+
# @api private
|
|
38
73
|
module Configuration
|
|
74
|
+
# Returns the default covered components for signature verification.
|
|
75
|
+
# @return [Array<String>] the default components from {Linzer::Options::DEFAULT}
|
|
39
76
|
def default_covered_components
|
|
40
77
|
Linzer::Options::DEFAULT[:covered_components]
|
|
41
78
|
end
|
|
42
79
|
module_function :default_covered_components
|
|
43
80
|
|
|
81
|
+
# Default middleware configuration.
|
|
82
|
+
#
|
|
83
|
+
# @api private
|
|
44
84
|
DEFAULT_OPTIONS = {
|
|
45
85
|
signatures: {
|
|
46
86
|
reject_older_than: 900,
|
|
@@ -62,6 +102,10 @@ module Rack
|
|
|
62
102
|
|
|
63
103
|
private
|
|
64
104
|
|
|
105
|
+
# Loads and merges options from all sources.
|
|
106
|
+
#
|
|
107
|
+
# @param options [Hash] options passed to the middleware constructor
|
|
108
|
+
# @return [Hash] the merged configuration
|
|
65
109
|
def load_options(options)
|
|
66
110
|
options_from_file = load_options_from_config_file(options)
|
|
67
111
|
{
|
|
@@ -78,6 +122,10 @@ module Rack
|
|
|
78
122
|
}
|
|
79
123
|
end
|
|
80
124
|
|
|
125
|
+
# Loads configuration from a YAML file.
|
|
126
|
+
#
|
|
127
|
+
# @param options [Hash] options containing +:config_path+
|
|
128
|
+
# @return [Hash] parsed configuration, or empty hash if unavailable
|
|
81
129
|
def load_options_from_config_file(options)
|
|
82
130
|
config_path = options[:config_path]
|
|
83
131
|
YAML.safe_load_file(config_path, symbolize_names: true)
|
|
@@ -86,13 +134,29 @@ module Rack
|
|
|
86
134
|
end
|
|
87
135
|
end
|
|
88
136
|
|
|
137
|
+
# Resolves verification keys from the middleware configuration.
|
|
138
|
+
#
|
|
139
|
+
# Keys can be configured inline (with +:material+) or via file path
|
|
140
|
+
# (with +:path+). When a +keyid+ is present in the signature, the
|
|
141
|
+
# corresponding key is looked up in the +:keys+ hash. If not found,
|
|
142
|
+
# the +:default_key+ is used as fallback.
|
|
143
|
+
#
|
|
144
|
+
# @api private
|
|
89
145
|
module Key
|
|
90
146
|
private
|
|
91
147
|
|
|
148
|
+
# Returns the verification key for the current signature.
|
|
149
|
+
# @return [Linzer::Key] the resolved key
|
|
150
|
+
# @raise [Linzer::Error] if no key can be found
|
|
92
151
|
def key
|
|
93
152
|
build_key(params["keyid"])
|
|
94
153
|
end
|
|
95
154
|
|
|
155
|
+
# Builds a key instance from configuration.
|
|
156
|
+
#
|
|
157
|
+
# @param keyid [String, nil] the key identifier from the signature
|
|
158
|
+
# @return [Linzer::Key] the resolved key
|
|
159
|
+
# @raise [Linzer::Error] if no matching key configuration is found
|
|
96
160
|
def build_key(keyid)
|
|
97
161
|
key_data = if keyid.nil? ||
|
|
98
162
|
(!options[:keys].key?(keyid.to_sym) && options[:default_key])
|
|
@@ -114,6 +178,14 @@ module Rack
|
|
|
114
178
|
instantiate_key(keyid || :default, alg, key_data)
|
|
115
179
|
end
|
|
116
180
|
|
|
181
|
+
# Instantiates the appropriate key class for the given algorithm.
|
|
182
|
+
#
|
|
183
|
+
# @param keyid [String, Symbol] the key identifier
|
|
184
|
+
# @param alg [String, Symbol] the algorithm identifier
|
|
185
|
+
# (e.g. +"ed25519"+, +"rsa-pss-sha512"+)
|
|
186
|
+
# @param key_data [Hash] key configuration with +:material+
|
|
187
|
+
# @return [Linzer::Key] the instantiated key
|
|
188
|
+
# @raise [Linzer::Error] if the algorithm is unsupported
|
|
117
189
|
def instantiate_key(keyid, alg, key_data)
|
|
118
190
|
key_methods = {
|
|
119
191
|
"rsa-pss-sha512" => :new_rsa_pss_sha512_key,
|