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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f13471dec9f1ca620ac5fa94faeb2173632bec2aede20fbb1e156988c48fd10d
4
- data.tar.gz: 4dffdf98551884ea0a1b11c892ef1cb2ee5cad1fb6234e1d54890feea210a898
3
+ metadata.gz: d39f9d5f600453197afb744fc030afc2325f40d580540f9fbbca2ba81e0275e2
4
+ data.tar.gz: 0ab05a81ade047eb116e0527c8647bce22008a04684b6dae5bdd37a9441b0fa8
5
5
  SHA512:
6
- metadata.gz: 1ca361d6e8dbc9d1960f37f33785a56cb7cd5a22a61d9fbe44228541c5cf54791acc21d9d19b4fb9a078322855776686d42aff86bbc05d4014cc017669375e9a
7
- data.tar.gz: 89979f2b5c0e2d2a527cb04e223879f543a26c0cc81ef702ce5c3858c8f241126a38d28aeb9070bacebfbfdf488eae75e5e75f9fd0a242b5e04dfa12c76f6d6d
6
+ metadata.gz: 52adbd0af86067b700fd60e2666675134bf58a8ed611b60087ecd3c74da64b50f83232271e4a8a4c2af6162558fd880598fb15af6cd201f9621ff4cd976bbdc5
7
+ data.tar.gz: b351287249fb050bb06bc08a5312b24bf9876dc555fd7c2ee69edbab15e0b43bec5d0cbc3ab852c1766c2eede159a1bd8c6c2ba963558e308b2ec7e73ad469d5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.7.9.beta3] - 2026-04-27
4
+
5
+ - Enforce `expires` signature parameter validation in Verifier.
6
+
7
+ - Add Faraday middleware and adapters for HTTP message signatures.
8
+
3
9
  ## [0.7.9.beta2] - 2026-04-19
4
10
 
5
11
  - Add support for http gem 6.x while maintaining compatibility with 5.x.
data/README.md CHANGED
@@ -30,6 +30,18 @@ Or just `gem install linzer`.
30
30
 
31
31
  ### Quick start
32
32
 
33
+ - To sign/verify HTTP requests and responses, see the
34
+ [Signing Requests](#signing-http-requests-and-responses) and
35
+ [Verifying HTTP signatures](#verifying-http-signatures) or
36
+ [Verifying responses (client-side)](#verifying-responses-client-side)
37
+ sections.
38
+
39
+ - For a more hands-off approach to enforcing request authentication
40
+ with HTTP signatures in Rack applications (such as Rails),
41
+ see the examples in the next section.
42
+
43
+ ### Rack middleware
44
+
33
45
  Add the middleware to your Rack application:
34
46
 
35
47
  ```ruby
@@ -47,9 +59,6 @@ use Rack::Auth::Signature,
47
59
  In this example, the middleware requires a valid HTTP Message Signature
48
60
  for all endpoints except /login.
49
61
 
50
- To learn how to sign requests, see the
51
- [Signing Requests](#signing-http-requests-and-responses) section.
52
-
53
62
  #### Using a configuration file
54
63
 
55
64
  For more complex setups, you can load configuration from a file, e.g.:
@@ -80,10 +89,13 @@ without a valid signature will be rejected.
80
89
 
81
90
  #### Next steps
82
91
 
92
+ - See how to [sign HTTP messages](#signing-http-requests-and-responses)
93
+ or [verify HTTP requests and responses](#verifying-responses-client-side).
94
+
83
95
  - See a full configuration example:
84
96
  [examples/sinatra/http-signatures.yml](https://github.com/nomadium/linzer/tree/master/examples/sinatra/http-signatures.yml)
85
97
 
86
- - Browse the middleware implementation for all options:
98
+ - Browse the Rack middleware implementation for all options:
87
99
  [lib/rack/auth/signature.rb](https://github.com/nomadium/linzer/tree/master/lib/rack/auth/signature.rb)
88
100
 
89
101
  - For more specific scenarios and use cases, continue below.
@@ -97,6 +109,7 @@ method, path, headers, etc).
97
109
  Choose your client:
98
110
 
99
111
  - Use the [http gem](https://github.com/httprb/http) → recommended (simplest)
112
+ - Use Faraday and the provided middleware → also recommended (very simple)
100
113
  - Use `Net::HTTP` → lower-level control
101
114
  - Use `Linzer::HTTP` → quick experiments / debugging
102
115
 
@@ -121,6 +134,35 @@ response.body.to_s
121
134
  => "protected content..."
122
135
  ```
123
136
 
137
+ #### Using Faraday
138
+
139
+ Linzer ships Faraday middleware for signing outbound requests and verifying
140
+ signed responses.
141
+
142
+ To sign requests:
143
+
144
+ ```ruby
145
+ require "linzer/faraday"
146
+
147
+ api_url = "https://example.com/api/service"
148
+ components = %w[@target-uri @authority date cache-control]
149
+ signature_params = {alg: "rsa-pss-sha512", keyid: "test-key-rsa-pss",
150
+ expires: Time.now.to_i + 300}
151
+
152
+ conn = Faraday.new(url: api_url) do |builder|
153
+ builder.headers["Cache-control"] = "no-cache"
154
+ builder.headers["Date"] = Time.now.httpdate
155
+ builder.request :http_signature, key: signing_key,
156
+ components: components,
157
+ params: signature_params
158
+ end
159
+ response = conn.post("/task")
160
+ ```
161
+
162
+ This signs the request automatically before dispatch. In this example,
163
+ `Date` and `Cache-Control` are included in the signature to protect
164
+ freshness-related metadata from modification.
165
+
124
166
  #### Using `Net::HTTP` (manual control)
125
167
 
126
168
  ```ruby
@@ -293,8 +335,8 @@ end
293
335
 
294
336
  #### Dynamic key lookup
295
337
 
296
- In many cases, the verification key depends on the `keyid` parameter provided in
297
- the signature.
338
+ In many cases, the verification key depends on the `keyid` parameter
339
+ provided in the signature.
298
340
 
299
341
  You can supply a block to resolve keys dynamically:
300
342
 
@@ -331,14 +373,48 @@ result = Linzer.verify!(response, key: pubkey, no_older_than: 600)
331
373
  # => true
332
374
  ```
333
375
 
376
+ Or if you are using Faraday, response verification can be handled by
377
+ middleware as well:
378
+
379
+ ```ruby
380
+ require "linzer/faraday"
381
+
382
+ ...
383
+
384
+ conn = Faraday.new(url: api_url) do |builder|
385
+ builder.response :http_signature, key: verify_key
386
+ end
387
+ response = conn.post("/task")
388
+
389
+ response.env[:http_signature_verified]
390
+ # => true
391
+
392
+ response.env[:http_signature]
393
+ # => #<Linzer::Signature ...>
394
+ ```
395
+
396
+ After verification, the middleware stores:
397
+
398
+ - `env[:http_signature_verified]` — whether verification succeeded
399
+
400
+ - `env[:http_signature]` — the parsed `Linzer::Signature` object (when valid)
401
+
402
+ Verification failures can optionally raise instead of returning status;
403
+ see middleware options below.
404
+
334
405
  ### Using a custom HTTP library
335
406
 
336
- If you’re using an HTTP library or framework other than Rack, http gem or
337
- `Net::HTTP`, you can plug in your own adapter with very little effort.
407
+ If you are using another HTTP library, you may not need a custom Linzer
408
+ adapter at all. Because Linzer integrates with Faraday middleware, you can
409
+ often use Faraday together with the appropriate Faraday adapter for your
410
+ HTTP client of choice.
411
+
412
+ If you need tighter integration or are not using Faraday, you can also
413
+ implement a native Linzer adapter directly.
338
414
 
339
- In most cases, implementing an adapter just means mapping your librarys
340
- request/response objects to the small interface Linzer expects,
341
- then registering it.
415
+ In most cases, implementing an adapter just means mapping your library's
416
+ request/response objects to the small interface Linzer expects, then
417
+ registering it.
342
418
 
343
419
  To do this:
344
420
 
@@ -383,7 +459,7 @@ pubkey = Linzer.new_ed25519_public_key(test_ed25519_key_pub, "some-key-ed25519")
383
459
 
384
460
  message = Linzer::Message.new(request)
385
461
 
386
- signature = Linzer::Signature.build(message.headers)
462
+ signature = Linzer::Signature.build(request.headers)
387
463
 
388
464
  Linzer.verify(pubkey, message, signature)
389
465
  # => true
@@ -433,6 +509,14 @@ For deeper details or edge cases, the source code and unit tests are also a good
433
509
 
434
510
  linzer is built in [Continuous Integration](https://github.com/nomadium/linzer/actions/workflows/main.yml) on Ruby 3.0+.
435
511
 
512
+ > [!NOTE]
513
+ >
514
+ > Ruby 3.0 is supported and tested in CI, but RSA-based signature algorithms
515
+ > (RSA-PSS and RSA PKCS#1 v1.5) may not work correctly due to its older
516
+ > OpenSSL bindings. If you need RSA algorithms, use Ruby 3.1 or later.
517
+ > Ruby 3.0 has been EOL since March 2024 — users are advised to upgrade
518
+ > to a supported Ruby release.
519
+
436
520
  ## Security
437
521
 
438
522
  This gem is provided “as is” without any warranties. It has not
@@ -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
- # Determines if the HTTP method typically has a request body.
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