linzer 0.7.9.beta1 → 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 +12 -0
- data/README.md +283 -114
- data/flake.lock +10 -34
- data/flake.nix +17 -5
- 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 +48 -0
- data/lib/linzer/message/adapter/http_gem/request.rb +13 -7
- data/lib/linzer/message/adapter/http_gem/response.rb +11 -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 +8 -1
data/flake.nix
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
inputs = {
|
|
3
3
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
|
|
5
|
+
systems = {
|
|
6
|
+
url = "github:nix-systems/default-linux";
|
|
7
|
+
# XXX: should work, not tested yet on MacOS
|
|
8
|
+
# url = "github:nix-systems/default";
|
|
9
|
+
inputs.nixpkgs.follows = "nixpkgs";
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
ruby-nix = {
|
|
13
|
+
url = "github:inscapist/ruby-nix";
|
|
14
|
+
inputs.nixpkgs.follows = "nixpkgs";
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
bundix = {
|
|
18
|
+
url = "github:inscapist/bundix";
|
|
19
|
+
inputs.nixpkgs.follows = "nixpkgs";
|
|
20
|
+
};
|
|
9
21
|
};
|
|
10
22
|
|
|
11
23
|
outputs = {
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Faraday
|
|
4
|
+
module HttpSignature
|
|
5
|
+
# Raised when HTTP response signature verification fails in strict mode.
|
|
6
|
+
#
|
|
7
|
+
# Inherits from {Faraday::Error} so that standard Faraday error handling
|
|
8
|
+
# (e.g. +rescue Faraday::Error+) catches verification failures.
|
|
9
|
+
# The original {Linzer::VerifyError} is preserved as {#wrapped_exception}
|
|
10
|
+
# and the {Faraday::Response} is available via {#response}.
|
|
11
|
+
#
|
|
12
|
+
# @example Catching a verification failure
|
|
13
|
+
# begin
|
|
14
|
+
# response = conn.get("/")
|
|
15
|
+
# rescue Faraday::HttpSignature::VerifyError => e
|
|
16
|
+
# e.message # => "Failed to verify message: Invalid signature."
|
|
17
|
+
# e.response # => the Faraday::Response object
|
|
18
|
+
# e.wrapped_exception # => the original Linzer::VerifyError
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @see Middleware
|
|
22
|
+
class VerifyError < Faraday::Error; end
|
|
23
|
+
|
|
24
|
+
# Raised when HTTP request signature creation fails.
|
|
25
|
+
#
|
|
26
|
+
# Inherits from {Faraday::Error} so that standard Faraday error handling
|
|
27
|
+
# (e.g. +rescue Faraday::Error+) catches verification failures.
|
|
28
|
+
# The original {Linzer::Error} is preserved as {#wrapped_exception}.
|
|
29
|
+
#
|
|
30
|
+
# @example Catching a signing failure
|
|
31
|
+
# begin
|
|
32
|
+
# response = conn.post("/")
|
|
33
|
+
# rescue Faraday::HttpSignature::SigningError => e
|
|
34
|
+
# e.message # => "Failed to sign message: Missing component."
|
|
35
|
+
# e.wrapped_exception # => the original Linzer::Error
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# @see Middleware
|
|
39
|
+
class SigningError < Faraday::Error; end
|
|
40
|
+
|
|
41
|
+
# Faraday middleware for HTTP message signing and verification (RFC 9421).
|
|
42
|
+
#
|
|
43
|
+
# When registered via +request+, signs outgoing requests (default).
|
|
44
|
+
# When registered via +response+, verifies incoming response signatures.
|
|
45
|
+
# When registered via +use+, signs requests by default; pass
|
|
46
|
+
# +verify_response: true+ to also verify responses.
|
|
47
|
+
#
|
|
48
|
+
# == Verification result metadata
|
|
49
|
+
#
|
|
50
|
+
# After response verification, the middleware stores results in
|
|
51
|
+
# +env[:http_signature_verified]+ (+true+ or +false+) and
|
|
52
|
+
# +env[:http_signature]+ (the {Linzer::Signature} on success).
|
|
53
|
+
# These are accessible via +response.env[:http_signature_verified]+.
|
|
54
|
+
#
|
|
55
|
+
# @example Sign requests
|
|
56
|
+
# conn = Faraday.new(url: "https://example.com") do |f|
|
|
57
|
+
# f.request :http_signature, key: my_key, components: %w[@method @path]
|
|
58
|
+
# end
|
|
59
|
+
#
|
|
60
|
+
# @example Verify responses
|
|
61
|
+
# conn = Faraday.new(url: "https://example.com") do |f|
|
|
62
|
+
# f.response :http_signature, verify_key: server_pubkey
|
|
63
|
+
# end
|
|
64
|
+
#
|
|
65
|
+
# @example Lenient verification (no exception on failure)
|
|
66
|
+
# conn = Faraday.new(url: "https://example.com") do |f|
|
|
67
|
+
# f.response :http_signature, verify_key: server_pubkey, strict: false
|
|
68
|
+
# end
|
|
69
|
+
# response = conn.get("/")
|
|
70
|
+
# response.env[:http_signature_verified] # => true or false
|
|
71
|
+
#
|
|
72
|
+
# @see https://datatracker.ietf.org/doc/html/rfc9421 RFC 9421
|
|
73
|
+
class Middleware < Faraday::Middleware
|
|
74
|
+
# Default options for the base middleware class (used by +use+
|
|
75
|
+
# and +request+ registrations). Signs requests, does not verify
|
|
76
|
+
# responses, strict mode enabled.
|
|
77
|
+
DEFAULT_OPTIONS = {
|
|
78
|
+
sign_request: true,
|
|
79
|
+
verify_response: false,
|
|
80
|
+
strict: true
|
|
81
|
+
}.freeze
|
|
82
|
+
|
|
83
|
+
# Configuration options for the HTTP signature middleware.
|
|
84
|
+
#
|
|
85
|
+
# @!attribute [rw] key
|
|
86
|
+
# @return [Linzer::Key, nil] generic key used for signing or
|
|
87
|
+
# verification when only one mode is active
|
|
88
|
+
# @!attribute [rw] sign_request
|
|
89
|
+
# @return [Boolean] whether to sign outgoing requests
|
|
90
|
+
# (defaults to +true+)
|
|
91
|
+
# @!attribute [rw] sign_key
|
|
92
|
+
# @return [Linzer::Key, nil] explicit key for signing; required
|
|
93
|
+
# when both signing and verification are enabled
|
|
94
|
+
# @!attribute [rw] components
|
|
95
|
+
# @return [Array<String>] HTTP message components to include in
|
|
96
|
+
# the signature (e.g. +["@method", "@path", "content-type"]+)
|
|
97
|
+
# @!attribute [rw] verify_response
|
|
98
|
+
# @return [Boolean] whether to verify incoming response signatures
|
|
99
|
+
# (defaults to +false+)
|
|
100
|
+
# @!attribute [rw] verify_key
|
|
101
|
+
# @return [Linzer::Key, nil] explicit key for verification; required
|
|
102
|
+
# when both signing and verification are enabled
|
|
103
|
+
# @!attribute [rw] params
|
|
104
|
+
# @return [Hash] additional signature parameters
|
|
105
|
+
# (e.g. +{ tag: "my_tag" }+)
|
|
106
|
+
# @!attribute [rw] strict
|
|
107
|
+
# @return [Boolean] when +true+ (default), raises
|
|
108
|
+
# {VerifyError} on verification failure; when +false+,
|
|
109
|
+
# sets +env[:http_signature_verified]+ to +false+ and continues
|
|
110
|
+
class Options < Faraday::Options.new(:key, :sign_request, :sign_key, :components, :verify_response, :verify_key, :params, :strict)
|
|
111
|
+
# Returns the generic key.
|
|
112
|
+
# @return [Linzer::Key, nil]
|
|
113
|
+
def key
|
|
114
|
+
self[:key]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Whether outgoing requests should be signed.
|
|
118
|
+
# Defaults to +true+ (returns +true+ when unset).
|
|
119
|
+
# @return [Boolean]
|
|
120
|
+
def sign_request?
|
|
121
|
+
self[:sign_request] != false
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Whether incoming responses should be verified.
|
|
125
|
+
# Defaults to +false+ (returns +false+ when unset).
|
|
126
|
+
# @return [Boolean]
|
|
127
|
+
def verify_response?
|
|
128
|
+
self[:verify_response]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Whether verification failures should raise an exception.
|
|
132
|
+
# Defaults to +true+ (returns +true+ when unset).
|
|
133
|
+
# @return [Boolean]
|
|
134
|
+
def strict?
|
|
135
|
+
self[:strict] != false
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Returns the list of HTTP message components to sign.
|
|
139
|
+
# @return [Array<String>]
|
|
140
|
+
def components
|
|
141
|
+
Array(self[:components])
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Returns additional signature parameters.
|
|
145
|
+
# @return [Hash]
|
|
146
|
+
def params
|
|
147
|
+
Hash(self[:params])
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Creates a new middleware instance.
|
|
152
|
+
#
|
|
153
|
+
# Merges class-level {DEFAULT_OPTIONS} with the user-provided options
|
|
154
|
+
# so that subclasses ({Request}, {Response}) can override defaults.
|
|
155
|
+
#
|
|
156
|
+
# @param app [#call] the next middleware or adapter in the stack
|
|
157
|
+
# @param options [Hash, nil] middleware options
|
|
158
|
+
# @option options [Linzer::Key] :key generic key for signing or verification
|
|
159
|
+
# @option options [Linzer::Key] :sign_key explicit signing key
|
|
160
|
+
# @option options [Linzer::Key] :verify_key explicit verification key
|
|
161
|
+
# @option options [Array<String>] :components components to sign
|
|
162
|
+
# @option options [Hash] :params additional signature parameters
|
|
163
|
+
# @option options [Boolean] :sign_request (+true+) whether to sign requests
|
|
164
|
+
# @option options [Boolean] :verify_response (+false+) whether to verify responses
|
|
165
|
+
# @option options [Boolean] :strict (+true+) raise on verification failure
|
|
166
|
+
def initialize(app, options = nil)
|
|
167
|
+
super(app)
|
|
168
|
+
defaults = self.class::DEFAULT_OPTIONS
|
|
169
|
+
merged = defaults.merge(Hash(options))
|
|
170
|
+
@options = Options.from(merged)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Signs the outgoing request when {Options#sign_request?} is +true+.
|
|
174
|
+
#
|
|
175
|
+
# Resolves the signing key, builds a {Linzer::Message} from the
|
|
176
|
+
# Faraday environment, generates a signature over the configured
|
|
177
|
+
# components, and merges the +signature+ and +signature-input+
|
|
178
|
+
# headers into the request.
|
|
179
|
+
#
|
|
180
|
+
# @param env [Faraday::Env] the middleware environment
|
|
181
|
+
# @return [Faraday::Env, nil] the modified env, or +nil+ if signing
|
|
182
|
+
# is disabled
|
|
183
|
+
# @raise [Linzer::Error] if no valid signing key is available
|
|
184
|
+
def on_request(env)
|
|
185
|
+
return unless options.sign_request?
|
|
186
|
+
|
|
187
|
+
key = resolve_signing_key
|
|
188
|
+
request = Linzer::Faraday::Utils.create_request(env)
|
|
189
|
+
message = Linzer::Message.new(request)
|
|
190
|
+
|
|
191
|
+
signature = Linzer.sign(key, message, options.components, options.params)
|
|
192
|
+
env.request_headers.merge!(signature.to_h)
|
|
193
|
+
env
|
|
194
|
+
rescue Linzer::Error => e
|
|
195
|
+
raise SigningError, e if options.strict?
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Verifies the response signature when {Options#verify_response?} is +true+.
|
|
199
|
+
#
|
|
200
|
+
# On success, sets +env[:http_signature_verified]+ to +true+ and
|
|
201
|
+
# +env[:http_signature]+ to the verified {Linzer::Signature}.
|
|
202
|
+
#
|
|
203
|
+
# On failure in strict mode (default), raises {VerifyError}.
|
|
204
|
+
# In lenient mode (+strict: false+), sets
|
|
205
|
+
# +env[:http_signature_verified]+ to +false+ and allows the response
|
|
206
|
+
# to continue through the middleware stack.
|
|
207
|
+
#
|
|
208
|
+
# @param env [Faraday::Env] the middleware environment
|
|
209
|
+
# @return [Faraday::Env, nil] the modified env, or +nil+ if verifying
|
|
210
|
+
# is disabled
|
|
211
|
+
# @raise [VerifyError] if verification fails and +strict+ is +true+
|
|
212
|
+
# @raise [Linzer::Error] if no valid verification key is available
|
|
213
|
+
def on_complete(env)
|
|
214
|
+
env[:http_signature_verified] = false
|
|
215
|
+
return unless options.verify_response?
|
|
216
|
+
|
|
217
|
+
key = resolve_verify_key
|
|
218
|
+
response = ::Faraday::Response.new(env)
|
|
219
|
+
message = Linzer::Message.new(response)
|
|
220
|
+
signature = Linzer::Signature.build(response.headers)
|
|
221
|
+
|
|
222
|
+
Linzer.verify(key, message, signature)
|
|
223
|
+
env[:http_signature_verified] = true
|
|
224
|
+
env[:http_signature] = signature
|
|
225
|
+
env
|
|
226
|
+
rescue Linzer::Error => e
|
|
227
|
+
raise VerifyError.new(e, response: response) if options.strict?
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
private
|
|
231
|
+
|
|
232
|
+
# Resolves the key to use for signing requests.
|
|
233
|
+
#
|
|
234
|
+
# Prefers {Options#sign_key}. Falls back to the generic {Options#key}
|
|
235
|
+
# when only one mode (sign or verify) is active. When both modes are
|
|
236
|
+
# active, the generic key is ambiguous and +sign_key+ must be set
|
|
237
|
+
# explicitly.
|
|
238
|
+
#
|
|
239
|
+
# @return [Linzer::Key] the resolved signing key
|
|
240
|
+
# @raise [Linzer::Error] if no key is available or the key is invalid
|
|
241
|
+
def resolve_signing_key
|
|
242
|
+
key = options.sign_key
|
|
243
|
+
key ||= options.key unless options.sign_request? && options.verify_response?
|
|
244
|
+
raise Linzer::Error, "No signing key provided!" if !key
|
|
245
|
+
raise Linzer::Error, "Invalid key!" if !key.is_a?(Linzer::Key)
|
|
246
|
+
|
|
247
|
+
key
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Resolves the key to use for verifying response signatures.
|
|
251
|
+
#
|
|
252
|
+
# Prefers {Options#verify_key}. Falls back to the generic {Options#key}
|
|
253
|
+
# when only one mode (sign or verify) is active. When both modes are
|
|
254
|
+
# active, the generic key is ambiguous and +verify_key+ must be set
|
|
255
|
+
# explicitly.
|
|
256
|
+
#
|
|
257
|
+
# @return [Linzer::Key] the resolved verification key
|
|
258
|
+
# @raise [Linzer::Error] if no key is available or the key is invalid
|
|
259
|
+
def resolve_verify_key
|
|
260
|
+
key = options.verify_key
|
|
261
|
+
key ||= options.key unless options.sign_request? && options.verify_response?
|
|
262
|
+
raise Linzer::Error, "No verification key provided!" if !key
|
|
263
|
+
raise Linzer::Error, "Invalid key!" if !key.is_a?(Linzer::Key)
|
|
264
|
+
|
|
265
|
+
key
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Subclass registered under {Faraday::Request}.
|
|
269
|
+
#
|
|
270
|
+
# Inherits the base {DEFAULT_OPTIONS} which sign requests by default
|
|
271
|
+
# and do not verify responses.
|
|
272
|
+
#
|
|
273
|
+
# @example
|
|
274
|
+
# f.request :http_signature, key: my_key, components: %w[@method @path]
|
|
275
|
+
class Request < self
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Subclass registered under {Faraday::Response}.
|
|
279
|
+
#
|
|
280
|
+
# Overrides {DEFAULT_OPTIONS} to verify responses by default and
|
|
281
|
+
# not sign requests.
|
|
282
|
+
#
|
|
283
|
+
# @example
|
|
284
|
+
# f.response :http_signature, verify_key: server_pubkey
|
|
285
|
+
class Response < self
|
|
286
|
+
# Default options for the response subclass. Verifies responses
|
|
287
|
+
# and does not sign requests.
|
|
288
|
+
DEFAULT_OPTIONS = {
|
|
289
|
+
sign_request: false,
|
|
290
|
+
verify_response: true,
|
|
291
|
+
strict: true
|
|
292
|
+
}.freeze
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require_relative "http_signature/middleware"
|
|
5
|
+
|
|
6
|
+
module Faraday
|
|
7
|
+
# Faraday middleware for signing and verifying HTTP messages
|
|
8
|
+
# as defined in RFC 9421.
|
|
9
|
+
#
|
|
10
|
+
# Three registration points are provided so the middleware can be added
|
|
11
|
+
# via +request+, +response+ or +use+, each with appropriate defaults:
|
|
12
|
+
#
|
|
13
|
+
# @example Sign outgoing requests
|
|
14
|
+
# conn = Faraday.new(url: "https://example.com") do |f|
|
|
15
|
+
# f.request :http_signature, key: my_key, components: %w[@method @path]
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# @example Verify incoming responses
|
|
19
|
+
# conn = Faraday.new(url: "https://example.com") do |f|
|
|
20
|
+
# f.response :http_signature, verify_key: server_pubkey
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# @example Sign requests and verify responses
|
|
24
|
+
# conn = Faraday.new(url: "https://example.com") do |f|
|
|
25
|
+
# f.use :http_signature, sign_key: my_key, verify_key: server_pubkey,
|
|
26
|
+
# verify_response: true, components: %w[@method @path]
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# @see Faraday::HttpSignature::Middleware
|
|
30
|
+
# @see https://datatracker.ietf.org/doc/html/rfc9421 RFC 9421 - HTTP Message Signatures
|
|
31
|
+
module HttpSignature
|
|
32
|
+
Faraday::Request.register_middleware(http_signature: Faraday::HttpSignature::Middleware::Request)
|
|
33
|
+
Faraday::Response.register_middleware(http_signature: Faraday::HttpSignature::Middleware::Response)
|
|
34
|
+
Faraday::Middleware.register_middleware(http_signature: Faraday::HttpSignature::Middleware)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Linzer
|
|
4
|
+
# Faraday integration for Linzer.
|
|
5
|
+
#
|
|
6
|
+
# @see file:lib/linzer/faraday.rb
|
|
7
|
+
module Faraday
|
|
8
|
+
# Utility methods for converting Faraday middleware objects into
|
|
9
|
+
# types compatible with Linzer adapters.
|
|
10
|
+
module Utils
|
|
11
|
+
# Creates a {::Faraday::Request} from a middleware environment.
|
|
12
|
+
#
|
|
13
|
+
# Builds a minimal request suitable for use with
|
|
14
|
+
# {Linzer::Message::Adapter::Faraday::Request}, preserving the
|
|
15
|
+
# original HTTP method, URL, and headers from the environment.
|
|
16
|
+
#
|
|
17
|
+
# @param env [::Faraday::Env] the middleware environment
|
|
18
|
+
# @return [::Faraday::Request] a new request object
|
|
19
|
+
def self.create_request(env)
|
|
20
|
+
::Faraday::Request.create(env.method) do |req|
|
|
21
|
+
req.params = ::Faraday::Utils::ParamsHash.new
|
|
22
|
+
req.headers = ::Faraday::Utils::Headers.new(env.request_headers.dup)
|
|
23
|
+
req.options = ::Faraday::ConnectionOptions.from(nil).request
|
|
24
|
+
req.url env.url
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Faraday integration for Linzer.
|
|
4
|
+
#
|
|
5
|
+
# Require this file to automatically register Faraday message adapters
|
|
6
|
+
# and the HTTP signature (RFC 9421) middleware.
|
|
7
|
+
#
|
|
8
|
+
# This sets up:
|
|
9
|
+
# - {Linzer::Message::Adapter::Faraday::Request} for {::Faraday::Request}
|
|
10
|
+
# - {Linzer::Message::Adapter::Faraday::Response} for {::Faraday::Response}
|
|
11
|
+
# - {Faraday::HttpSignature::Middleware} registered as +:http_signature+
|
|
12
|
+
# on +Faraday::Request+, +Faraday::Response+ and +Faraday::Middleware+
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# require "linzer/faraday"
|
|
16
|
+
#
|
|
17
|
+
# conn = Faraday.new(url: "https://example.com") do |f|
|
|
18
|
+
# f.request :http_signature, key: my_key, components: %w[@method @path]
|
|
19
|
+
# end
|
|
20
|
+
|
|
21
|
+
require "faraday"
|
|
22
|
+
require "linzer"
|
|
23
|
+
require "faraday/http_signature"
|
|
24
|
+
require "linzer/message/adapter/faraday/request"
|
|
25
|
+
require "linzer/message/adapter/faraday/response"
|
|
26
|
+
require "linzer/faraday/utils"
|
|
27
|
+
|
|
28
|
+
Linzer::Message.register_adapter(Faraday::Request, Linzer::Message::Adapter::Faraday::Request)
|
|
29
|
+
Linzer::Message.register_adapter(Faraday::Response, Linzer::Message::Adapter::Faraday::Response)
|
data/lib/linzer/http.rb
CHANGED
|
@@ -78,6 +78,14 @@ module Linzer
|
|
|
78
78
|
private
|
|
79
79
|
|
|
80
80
|
# Executes a signed HTTP request.
|
|
81
|
+
#
|
|
82
|
+
# Validates inputs, builds and signs the request, then sends it.
|
|
83
|
+
#
|
|
84
|
+
# @param verb [Symbol] the HTTP method (e.g. +:get+, +:post+)
|
|
85
|
+
# @param uri [String] the request URI
|
|
86
|
+
# @param options [Hash] request options
|
|
87
|
+
# @return [Net::HTTPResponse] the response
|
|
88
|
+
# @raise [Linzer::Error] if the verb or key is invalid
|
|
81
89
|
def request(verb, uri, options = {})
|
|
82
90
|
validate_verb(verb)
|
|
83
91
|
|
|
@@ -100,10 +108,16 @@ module Linzer
|
|
|
100
108
|
do_request(http, uri, verb, options[:data], signature, headers)
|
|
101
109
|
end
|
|
102
110
|
|
|
111
|
+
# Returns the default covered components for signing.
|
|
112
|
+
# @return [Array<String>]
|
|
103
113
|
def default_components
|
|
104
114
|
Linzer::Options::DEFAULT[:covered_components]
|
|
105
115
|
end
|
|
106
116
|
|
|
117
|
+
# Validates that the HTTP verb is recognized.
|
|
118
|
+
#
|
|
119
|
+
# @param verb [Symbol] the HTTP method
|
|
120
|
+
# @raise [Linzer::Error] if the verb is unknown
|
|
107
121
|
def validate_verb(verb)
|
|
108
122
|
method_name = verb.to_s.upcase
|
|
109
123
|
if !known_http_methods.include?(method_name)
|
|
@@ -111,17 +125,32 @@ module Linzer
|
|
|
111
125
|
end
|
|
112
126
|
end
|
|
113
127
|
|
|
128
|
+
# Validates that the signing key is present and usable.
|
|
129
|
+
#
|
|
130
|
+
# @param key [Linzer::Key] the signing key
|
|
131
|
+
# @return [Linzer::Key] the validated key
|
|
132
|
+
# @raise [Linzer::Error] if the key is nil or does not respond to +#sign+
|
|
114
133
|
def validate_key(key)
|
|
115
134
|
raise Linzer::Error, "Key can not be nil!" if !key
|
|
116
135
|
raise Linzer::Error, "Key object is invalid!" if !key.respond_to?(:sign)
|
|
117
136
|
key
|
|
118
137
|
end
|
|
119
138
|
|
|
139
|
+
# Builds request headers, adding a default User-Agent if not present.
|
|
140
|
+
#
|
|
141
|
+
# @param headers [Hash] user-provided headers
|
|
142
|
+
# @return [Hash] headers with User-Agent ensured
|
|
120
143
|
def build_headers(headers)
|
|
121
144
|
return headers if headers.transform_keys(&:downcase).key?("user-agent")
|
|
122
145
|
headers.merge({"user-agent" => "Linzer/#{Linzer::VERSION}"})
|
|
123
146
|
end
|
|
124
147
|
|
|
148
|
+
# Builds a Net::HTTP request object for the given method and URI.
|
|
149
|
+
#
|
|
150
|
+
# @param method [Symbol] the HTTP method
|
|
151
|
+
# @param uri [String] the request URI
|
|
152
|
+
# @param headers [Hash] request headers to set
|
|
153
|
+
# @return [Net::HTTPRequest] the constructed request
|
|
125
154
|
def build_request(method, uri, headers)
|
|
126
155
|
request_class = Net::HTTP.const_get(method.to_s.capitalize)
|
|
127
156
|
request = request_class.new(URI(uri))
|
|
@@ -129,7 +158,10 @@ module Linzer
|
|
|
129
158
|
request
|
|
130
159
|
end
|
|
131
160
|
|
|
132
|
-
#
|
|
161
|
+
# Checks if the HTTP method typically carries a request body.
|
|
162
|
+
#
|
|
163
|
+
# @param verb [Symbol] the HTTP method
|
|
164
|
+
# @return [Boolean] +true+ for POST, PUT, PATCH, and WebDAV write methods
|
|
133
165
|
def with_body?(verb)
|
|
134
166
|
# common HTTP
|
|
135
167
|
return false if %i[get head options trace delete].include?(verb)
|
|
@@ -142,6 +174,16 @@ module Linzer
|
|
|
142
174
|
true
|
|
143
175
|
end
|
|
144
176
|
|
|
177
|
+
# Sends the HTTP request with the signature headers attached.
|
|
178
|
+
#
|
|
179
|
+
# @param http [Net::HTTP] the HTTP connection
|
|
180
|
+
# @param uri [String] the request URI
|
|
181
|
+
# @param verb [Symbol] the HTTP method
|
|
182
|
+
# @param data [String, nil] the request body
|
|
183
|
+
# @param signature [Linzer::Signature] the generated signature
|
|
184
|
+
# @param headers [Hash] request headers
|
|
185
|
+
# @return [Net::HTTPResponse] the response
|
|
186
|
+
# @raise [Linzer::Error] if a body is required but not provided
|
|
145
187
|
def do_request(http, uri, verb, data, signature, headers)
|
|
146
188
|
if with_body?(verb)
|
|
147
189
|
if !data
|
|
@@ -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
|