linzer 0.8.0.beta1 → 0.8.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/.standard.yml +1 -0
- data/CHANGELOG.md +44 -0
- data/README.md +55 -0
- data/lib/faraday/http_signature/middleware.rb +25 -4
- data/lib/linzer/common.rb +2 -2
- data/lib/linzer/faraday/utils.rb +8 -1
- data/lib/linzer/helper.rb +34 -15
- data/lib/linzer/http/signature_feature.rb +15 -4
- data/lib/linzer/http/structured_field.rb +123 -26
- data/lib/linzer/http.rb +13 -7
- data/lib/linzer/message/adapter/abstract.rb +30 -17
- data/lib/linzer/message/adapter/faraday/request.rb +15 -1
- data/lib/linzer/message/adapter/generic/request.rb +1 -0
- data/lib/linzer/message/field/parser.rb +5 -5
- data/lib/linzer/message/field.rb +4 -4
- data/lib/linzer/message/overlay.rb +143 -0
- data/lib/linzer/message.rb +18 -0
- data/lib/linzer/signature/context.rb +80 -0
- data/lib/linzer/signature/profile/base.rb +43 -0
- data/lib/linzer/signature/profile/example.rb +39 -0
- data/lib/linzer/signature/profile/web_bot_auth.rb +201 -0
- data/lib/linzer/signature/profile.rb +70 -0
- data/lib/linzer/signature.rb +29 -39
- data/lib/linzer/version.rb +1 -1
- data/lib/linzer.rb +5 -2
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 47932906cedb5dd66d7d2bbc5e8eaac9f01b4e17dd7089bc87b4a06b9505b366
|
|
4
|
+
data.tar.gz: abe9bf21ac6d66a08f229de3fb10e77bc911c112f812cd0cf620d9f3947d580e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b4352492dedad2bd9881b9bc6ac5888cdf7855a798f2b650040a9a64c4f7ea81c34178a4bfadb77f5953d82498f1eea69c90529d8b3888d9d182d3896e8415ed
|
|
7
|
+
data.tar.gz: b2540b62b29a07666c66969f5d6843f674994747fe80e47e71c0ba7f03ad554ccfbd1fcfefca796b7115f6fa2643f2b57b4d058de755c499a87ac87f883a4282
|
data/.standard.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.8.0] - 2026-08-08
|
|
4
|
+
|
|
5
|
+
(Only one change since the last beta: the Faraday bug fix below. Everything
|
|
6
|
+
else shipped in earlier 0.8.0 betas.)
|
|
7
|
+
|
|
8
|
+
- Optimize signature parsing, serialization, and validation performance
|
|
9
|
+
across signing and verifying flows, improving `sign!` throughput by
|
|
10
|
+
473% and `verify!` by 136% by minimizing Starry usage in hot paths
|
|
11
|
+
where possible.
|
|
12
|
+
|
|
13
|
+
- Add comprehensive benchmarking and profiling infrastructure across all
|
|
14
|
+
supported algorithms.
|
|
15
|
+
|
|
16
|
+
- Make Rack an optional dependency.
|
|
17
|
+
|
|
18
|
+
- Add Web Bot Auth support, implementing the current IETF draft
|
|
19
|
+
(draft-meunier-web-bot-auth-architecture-05).
|
|
20
|
+
Includes recommended signature parameter defaults, nonce generation,
|
|
21
|
+
and optional Signature-Agent header handling.
|
|
22
|
+
|
|
23
|
+
- Fix Faraday adapter deriving URI components from a re-encoded query
|
|
24
|
+
string, which could differ from the URI faraday actually sent and fail
|
|
25
|
+
verification at the origin. Affected `@target-uri`, `@request-target`
|
|
26
|
+
and `@query`. The query is now encoded with the request's own
|
|
27
|
+
`params_encoder`, so it tracks the URL faraday puts on the wire:
|
|
28
|
+
|
|
29
|
+
- Spaces no longer always encode as `+`. Faraday honours the
|
|
30
|
+
process-wide `Faraday::Utils.default_space_encoding`, which other
|
|
31
|
+
gems may set (the `oauth2` gem sets it to `"%20"` on load), so a
|
|
32
|
+
query value containing a space signed a URI the verifier could not
|
|
33
|
+
reproduce.
|
|
34
|
+
|
|
35
|
+
- A request with no query parameters no longer appends a stray `?`.
|
|
36
|
+
|
|
37
|
+
- A `params_encoder` configured on the connection or request is now
|
|
38
|
+
preserved, instead of being replaced with the default encoder.
|
|
39
|
+
|
|
40
|
+
## [0.8.0.beta2] - 2026-05-20
|
|
41
|
+
|
|
42
|
+
- Add Web Bot Auth support, implementing the current IETF draft
|
|
43
|
+
(draft-meunier-web-bot-auth-architecture-05).
|
|
44
|
+
Includes recommended signature parameter defaults, nonce generation,
|
|
45
|
+
and optional Signature-Agent header handling.
|
|
46
|
+
|
|
3
47
|
## [0.8.0.beta1] - 2026-05-07
|
|
4
48
|
|
|
5
49
|
- Optimize signature parsing, serialization, and validation performance
|
data/README.md
CHANGED
|
@@ -490,6 +490,61 @@ anything that to responds to `#to_i`, including an `ActiveSupport::Duration`.
|
|
|
490
490
|
If the signature is older than the allowed window, verification
|
|
491
491
|
fails with an error.
|
|
492
492
|
|
|
493
|
+
## Web Bot Auth
|
|
494
|
+
|
|
495
|
+
Linzer supports the Web Bot Auth authentication mechanism, which allows
|
|
496
|
+
automated clients to identify themselves using HTTP Message Signatures
|
|
497
|
+
(as defined in RFC 9421).
|
|
498
|
+
|
|
499
|
+
This is useful for distinguishing legitimate automated traffic from
|
|
500
|
+
anonymous or potentially abusive requests.
|
|
501
|
+
|
|
502
|
+
For more details on Web Bot Auth, refer to the
|
|
503
|
+
[relevant IETF drafts](https://datatracker.ietf.org/wg/webbotauth/documents/)
|
|
504
|
+
or to additional resources such as
|
|
505
|
+
[this Cloudflare article](https://blog.cloudflare.com/web-bot-auth/).
|
|
506
|
+
|
|
507
|
+
When enabled, as shown in the examples below, Linzer adds the required
|
|
508
|
+
signature headers to identify the client as an automated agent:
|
|
509
|
+
|
|
510
|
+
- Plain Linzer:
|
|
511
|
+
|
|
512
|
+
```ruby
|
|
513
|
+
|
|
514
|
+
Linzer.sign!(
|
|
515
|
+
request,
|
|
516
|
+
key: key,
|
|
517
|
+
label: "sig1",
|
|
518
|
+
profile: :web_bot_auth
|
|
519
|
+
# or override/set specific parameters like the following:
|
|
520
|
+
# profile: Linzer::Signing::Profile.web_bot_auth(agent: "https://...")
|
|
521
|
+
)
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
- http.rb gem:
|
|
525
|
+
|
|
526
|
+
```ruby
|
|
527
|
+
require "linzer/http/signature_feature"
|
|
528
|
+
|
|
529
|
+
response = HTTP.headers(date: Time.now.utc.httpdate, foo: "bar")
|
|
530
|
+
.use(http_signature: {key: key, profile: :web_bot_auth}
|
|
531
|
+
.get(url)
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
- Faraday:
|
|
535
|
+
|
|
536
|
+
```ruby
|
|
537
|
+
require "linzer/faraday"
|
|
538
|
+
|
|
539
|
+
conn = Faraday.new(url: api_url) do |builder|
|
|
540
|
+
builder.request :http_signature, key: signing_key,
|
|
541
|
+
components: components,
|
|
542
|
+
profile: :web_bot_auth,
|
|
543
|
+
params: signature_params
|
|
544
|
+
end
|
|
545
|
+
response = conn.post("/task")
|
|
546
|
+
```
|
|
547
|
+
|
|
493
548
|
## Supported algorithms
|
|
494
549
|
|
|
495
550
|
Linzer currently supports the following signature algorithms:
|
|
@@ -107,7 +107,16 @@ module Faraday
|
|
|
107
107
|
# @return [Boolean] when +true+ (default), raises
|
|
108
108
|
# {VerifyError} on verification failure; when +false+,
|
|
109
109
|
# sets +env[:http_signature_verified]+ to +false+ and continues
|
|
110
|
-
|
|
110
|
+
# @!attribute [rw] profile
|
|
111
|
+
# Optional HTTP Message Signatures signing profile.
|
|
112
|
+
#
|
|
113
|
+
# When set, the profile is passed to {Linzer.sign!} and may provide
|
|
114
|
+
# default covered components and signature parameters.
|
|
115
|
+
#
|
|
116
|
+
# @return [Symbol, Linzer::Signature::Profile::Base, nil]
|
|
117
|
+
# a registered profile name, a profile instance, or +nil+ to use
|
|
118
|
+
# the default signing behavior
|
|
119
|
+
class Options < Faraday::Options.new(:key, :sign_request, :sign_key, :components, :verify_response, :verify_key, :params, :strict, :profile)
|
|
111
120
|
# Returns the generic key.
|
|
112
121
|
# @return [Linzer::Key, nil]
|
|
113
122
|
def key
|
|
@@ -146,6 +155,13 @@ module Faraday
|
|
|
146
155
|
def params
|
|
147
156
|
Hash(self[:params])
|
|
148
157
|
end
|
|
158
|
+
|
|
159
|
+
# Returns the signing profile configuration.
|
|
160
|
+
#
|
|
161
|
+
# @return [Symbol, Linzer::Signature::Profile::Base, nil]
|
|
162
|
+
def profile
|
|
163
|
+
self[:profile]
|
|
164
|
+
end
|
|
149
165
|
end
|
|
150
166
|
|
|
151
167
|
# Creates a new middleware instance.
|
|
@@ -186,10 +202,15 @@ module Faraday
|
|
|
186
202
|
|
|
187
203
|
key = resolve_signing_key
|
|
188
204
|
request = Linzer::Faraday::Utils.create_request(env)
|
|
189
|
-
message = Linzer::Message.new(request)
|
|
190
205
|
|
|
191
|
-
|
|
192
|
-
|
|
206
|
+
Linzer.sign! request,
|
|
207
|
+
key: key,
|
|
208
|
+
components: options.components,
|
|
209
|
+
params: options.params,
|
|
210
|
+
profile: options.profile
|
|
211
|
+
|
|
212
|
+
signature_headers = request.headers.slice("signature", "signature-input")
|
|
213
|
+
env.request_headers.merge!(signature_headers)
|
|
193
214
|
env
|
|
194
215
|
rescue Linzer::Error => e
|
|
195
216
|
raise SigningError, e if options.strict?
|
data/lib/linzer/common.rb
CHANGED
|
@@ -67,7 +67,7 @@ module Linzer
|
|
|
67
67
|
# @param serialized_components [Array<String>] The covered components
|
|
68
68
|
# @param parameters [Hash] Signature parameters
|
|
69
69
|
# @return [String] The formatted @signature-params line
|
|
70
|
-
SERIALIZED_SIGNATURE_PARAMS =
|
|
70
|
+
SERIALIZED_SIGNATURE_PARAMS = HTTP::StructuredField.serialize("@signature-params").freeze
|
|
71
71
|
private_constant :SERIALIZED_SIGNATURE_PARAMS
|
|
72
72
|
|
|
73
73
|
def signature_params_line(serialized_components, parameters)
|
|
@@ -136,7 +136,7 @@ module Linzer
|
|
|
136
136
|
.flat_map
|
|
137
137
|
.with_index do |group, idx|
|
|
138
138
|
group
|
|
139
|
-
.map { |comp|
|
|
139
|
+
.map { |comp| HTTP::StructuredField.parse_item(idx.zero? ? comp[1..] : comp) }
|
|
140
140
|
.uniq { |comp| [comp.value, comp.parameters] }
|
|
141
141
|
end
|
|
142
142
|
|
data/lib/linzer/faraday/utils.rb
CHANGED
|
@@ -14,13 +14,20 @@ module Linzer
|
|
|
14
14
|
# {Linzer::Message::Adapter::Faraday::Request}, preserving the
|
|
15
15
|
# original HTTP method, URL, and headers from the environment.
|
|
16
16
|
#
|
|
17
|
+
# Preserves the environment's request options when present, so that
|
|
18
|
+
# a +params_encoder+ configured on the connection or request is
|
|
19
|
+
# carried over. The adapter re-encodes query parameters when
|
|
20
|
+
# deriving +@target-uri+ and friends, and must use the same encoder
|
|
21
|
+
# Faraday used to write the URL, or the signed value will not match
|
|
22
|
+
# the URI actually sent.
|
|
23
|
+
#
|
|
17
24
|
# @param env [::Faraday::Env] the middleware environment
|
|
18
25
|
# @return [::Faraday::Request] a new request object
|
|
19
26
|
def self.create_request(env)
|
|
20
27
|
::Faraday::Request.create(env.method) do |req|
|
|
21
28
|
req.params = ::Faraday::Utils::ParamsHash.new
|
|
22
29
|
req.headers = ::Faraday::Utils::Headers.new(env.request_headers.dup)
|
|
23
|
-
req.options = ::Faraday::ConnectionOptions.from(nil).request
|
|
30
|
+
req.options = env.request || ::Faraday::ConnectionOptions.from(nil).request
|
|
24
31
|
req.url env.url
|
|
25
32
|
end
|
|
26
33
|
end
|
data/lib/linzer/helper.rb
CHANGED
|
@@ -16,12 +16,22 @@ module Linzer
|
|
|
16
16
|
#
|
|
17
17
|
# @param request_or_response [Net::HTTPRequest, Net::HTTPResponse, Rack::Request,
|
|
18
18
|
# Rack::Response, HTTP::Request] The HTTP message to sign
|
|
19
|
-
#
|
|
20
|
-
# @
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# @
|
|
24
|
-
#
|
|
19
|
+
#
|
|
20
|
+
# @param key [Linzer::Key]
|
|
21
|
+
# The private key to sign with (required)
|
|
22
|
+
#
|
|
23
|
+
# @param components [Array<String>]
|
|
24
|
+
# The components to include in the signature (required).
|
|
25
|
+
# Example: `%w[@method @path content-type]`
|
|
26
|
+
#
|
|
27
|
+
# @param label [String, nil]
|
|
28
|
+
# Optional signature label (defaults to "sig1")
|
|
29
|
+
#
|
|
30
|
+
# @param params [Hash]
|
|
31
|
+
# Additional signature parameters (created, nonce, etc.)
|
|
32
|
+
#
|
|
33
|
+
# @param profile [Symbol, Linzer::Signature::Profile::Base, nil]
|
|
34
|
+
# Optional signing profile
|
|
25
35
|
#
|
|
26
36
|
# @return [Object] The original HTTP message with signature headers attached
|
|
27
37
|
#
|
|
@@ -46,17 +56,26 @@ module Linzer
|
|
|
46
56
|
# label: "my-sig",
|
|
47
57
|
# params: { nonce: SecureRandom.hex(16), tag: "my-app" }
|
|
48
58
|
# )
|
|
49
|
-
def sign!(request_or_response,
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
def sign!(request_or_response, key:, components: nil, label: nil, params: {}, profile: nil)
|
|
60
|
+
ctx = Signature::Context.new(
|
|
61
|
+
message: Message.new(request_or_response),
|
|
62
|
+
key: key,
|
|
63
|
+
label: label,
|
|
64
|
+
components: Array(components),
|
|
65
|
+
params: Hash(params)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
resolved_profile = Signature::Profile.resolve(profile)
|
|
69
|
+
resolved_profile&.apply(ctx)
|
|
52
70
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
71
|
+
signature = Linzer::Signer.sign(
|
|
72
|
+
ctx.key,
|
|
73
|
+
ctx.message,
|
|
74
|
+
ctx.components,
|
|
75
|
+
ctx.params
|
|
76
|
+
)
|
|
56
77
|
|
|
57
|
-
|
|
58
|
-
signature = Linzer::Signer.sign(key, message, args.fetch(:components), options)
|
|
59
|
-
message.attach!(signature)
|
|
78
|
+
ctx.message.attach!(signature)
|
|
60
79
|
end
|
|
61
80
|
|
|
62
81
|
# Verifies a signed HTTP request or response.
|
|
@@ -53,12 +53,17 @@ module Linzer
|
|
|
53
53
|
# @param covered_components [Array<String>] Components to include
|
|
54
54
|
# in the signature. Defaults to `@method`, `@request-target`,
|
|
55
55
|
# `@authority`, and `date`.
|
|
56
|
+
# @param profile [Symbol, Linzer::Signature::Profile::Base, nil]
|
|
57
|
+
# Optional signing profile used when generating signatures.
|
|
58
|
+
# When provided, the profile may supply default covered components
|
|
59
|
+
# and signature parameters.
|
|
56
60
|
#
|
|
57
61
|
# @raise [HTTP::Error] If key is nil or invalid
|
|
58
|
-
def initialize(key:, params: {}, covered_components: default_components)
|
|
62
|
+
def initialize(key:, params: {}, covered_components: default_components, profile: nil)
|
|
59
63
|
@fields = Array(covered_components)
|
|
60
64
|
@key = validate_key(key)
|
|
61
65
|
@params = Hash(params)
|
|
66
|
+
@profile = profile
|
|
62
67
|
end
|
|
63
68
|
|
|
64
69
|
# @return [Array<String>] The components to include in signatures
|
|
@@ -67,6 +72,10 @@ module Linzer
|
|
|
67
72
|
# @return [Hash] Additional signature parameters
|
|
68
73
|
attr_reader :params
|
|
69
74
|
|
|
75
|
+
# @return [Linzer::Signature::Profile::Base, Symbol, nil]
|
|
76
|
+
# Optional signing profile used during signature generation
|
|
77
|
+
attr_reader :profile
|
|
78
|
+
|
|
70
79
|
# Wraps an outgoing request to add signature headers.
|
|
71
80
|
#
|
|
72
81
|
# Called automatically by http.rb for each request.
|
|
@@ -74,9 +83,11 @@ module Linzer
|
|
|
74
83
|
# @param request [HTTP::Request] The outgoing request
|
|
75
84
|
# @return [HTTP::Request] The request with signature headers added
|
|
76
85
|
def wrap_request(request)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
Linzer.sign! request,
|
|
87
|
+
key: key,
|
|
88
|
+
components: fields,
|
|
89
|
+
params: params,
|
|
90
|
+
profile: profile
|
|
80
91
|
request
|
|
81
92
|
end
|
|
82
93
|
|
|
@@ -10,38 +10,135 @@ module Linzer
|
|
|
10
10
|
# @see https://www.rfc-editor.org/rfc/rfc8941 RFC 8941
|
|
11
11
|
# @see https://www.rfc-editor.org/rfc/rfc9421 RFC 9421
|
|
12
12
|
module StructuredField
|
|
13
|
-
|
|
13
|
+
InnerList = Starry::InnerList
|
|
14
|
+
Item = Starry::Item
|
|
15
|
+
|
|
16
|
+
# Parses an RFC 8941 Structured Field Dictionary.
|
|
17
|
+
#
|
|
18
|
+
# @param str [String] the serialized dictionary value
|
|
19
|
+
# @param field_name [String, nil] optional field name for contextual errors
|
|
20
|
+
# @return [Hash<String, Object>] parsed structured field dictionary
|
|
21
|
+
# @raise [Linzer::Error] if the field cannot be parsed
|
|
22
|
+
#
|
|
23
|
+
# @see https://www.rfc-editor.org/rfc/rfc8941 RFC 8941
|
|
24
|
+
def self.parse_dictionary(str, field_name: nil)
|
|
25
|
+
Starry.parse_dictionary(str)
|
|
26
|
+
# rescue Starry::ParseError => ex
|
|
27
|
+
# https://github.com/takemar/starry/pull/4
|
|
28
|
+
rescue => ex
|
|
29
|
+
cannot_parse = "Cannot parse %sfield!"
|
|
30
|
+
raise Error,
|
|
31
|
+
cannot_parse % [field_name ? "\"#{field_name}\" " : nil],
|
|
32
|
+
cause: ex
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Parses an RFC 8941 Structured Field Item.
|
|
36
|
+
#
|
|
37
|
+
# @param item [String] serialized structured field item
|
|
38
|
+
# @return [Starry::Item] parsed structured field item
|
|
39
|
+
# @raise [Linzer::Error] if the item is invalid or unparseable
|
|
40
|
+
#
|
|
41
|
+
# @see https://www.rfc-editor.org/rfc/rfc8941 RFC 8941
|
|
42
|
+
def self.parse_item(item)
|
|
43
|
+
Starry.parse_item(item)
|
|
44
|
+
# rescue Starry::ParseError => ex
|
|
45
|
+
# https://github.com/takemar/starry/pull/4
|
|
46
|
+
rescue => ex
|
|
47
|
+
raise Error, "Invalid/unparseable HTTP field item", cause: ex
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Parses an RFC 8941 Structured Field List.
|
|
51
|
+
#
|
|
52
|
+
# @param list [String] serialized structured field list
|
|
53
|
+
# @return [Array<Object>] parsed structured field list members
|
|
54
|
+
# @raise [Linzer::Error] if the list is invalid or unparseable
|
|
55
|
+
#
|
|
56
|
+
# @see https://www.rfc-editor.org/rfc/rfc8941 RFC 8941
|
|
57
|
+
def self.parse_list(list)
|
|
58
|
+
Starry.parse_list(list)
|
|
59
|
+
# rescue Starry::ParseError => ex
|
|
60
|
+
# https://github.com/takemar/starry/pull/4
|
|
61
|
+
rescue => ex
|
|
62
|
+
raise Error, "Invalid/unparseable HTTP field list", cause: ex
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Serializes a dictionary into RFC 8941 Structured Field format.
|
|
66
|
+
#
|
|
67
|
+
# @param hsh [Hash] structured field dictionary
|
|
68
|
+
# @return [String] serialized structured field dictionary
|
|
69
|
+
# @raise [Linzer::Error] if the dictionary cannot be serialized
|
|
14
70
|
#
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
71
|
+
# @see https://www.rfc-editor.org/rfc/rfc8941 RFC 8941
|
|
72
|
+
def self.serialize_dictionary(hsh)
|
|
73
|
+
Starry.serialize_dictionary(hsh)
|
|
74
|
+
# rescue Starry::SerializeError => ex
|
|
75
|
+
# https://github.com/takemar/starry/pull/4
|
|
76
|
+
rescue => ex
|
|
77
|
+
raise Error, ex.message, cause: ex
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Serializes a list into RFC 8941 Structured Field format.
|
|
81
|
+
#
|
|
82
|
+
# @param arr [Array] structured field list members
|
|
83
|
+
# @return [String] serialized structured field list
|
|
84
|
+
# @raise [Linzer::Error] if the list cannot be serialized
|
|
85
|
+
#
|
|
86
|
+
# @see https://www.rfc-editor.org/rfc/rfc8941 RFC 8941
|
|
87
|
+
def self.serialize_list(arr)
|
|
88
|
+
Starry.serialize_list(arr)
|
|
89
|
+
# rescue Starry::SerializeError => ex
|
|
90
|
+
# https://github.com/takemar/starry/pull/4
|
|
91
|
+
rescue => ex
|
|
92
|
+
raise Error, ex.message, cause: ex
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Serializes an object into RFC 8941 Structured Field format.
|
|
96
|
+
#
|
|
97
|
+
# @param obj [Object] structured field value
|
|
98
|
+
# @return [String] serialized structured field value
|
|
99
|
+
# @raise [Linzer::Error] if the object cannot be serialized
|
|
18
100
|
#
|
|
19
|
-
# @
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
101
|
+
# @see https://www.rfc-editor.org/rfc/rfc8941 RFC 8941
|
|
102
|
+
def self.serialize(obj)
|
|
103
|
+
Starry.serialize(obj)
|
|
104
|
+
rescue Starry::SerializeError => ex
|
|
105
|
+
raise Error, ex.message, cause: ex
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Serializes a Structured Field Item into RFC 8941 format.
|
|
109
|
+
#
|
|
110
|
+
# This helper serializes a single Structured Field Item, such as
|
|
111
|
+
# a string, integer, token, boolean, date, byte sequence, or
|
|
112
|
+
# an already constructed {Starry::Item}.
|
|
113
|
+
#
|
|
114
|
+
# @param item [Object] structured field item value
|
|
115
|
+
# @return [String] serialized structured field item
|
|
116
|
+
# @raise [Linzer::Error] if the item cannot be serialized
|
|
117
|
+
#
|
|
118
|
+
# @see https://www.rfc-editor.org/rfc/rfc8941 RFC 8941
|
|
119
|
+
def self.serialize_item(item)
|
|
120
|
+
Starry.serialize_item(item)
|
|
121
|
+
rescue Starry::SerializeError => ex
|
|
122
|
+
raise Error, ex.message, cause: ex
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Serializes Structured Field Parameters into RFC 8941 format.
|
|
25
126
|
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
127
|
+
# Parameters are serialized according to the Structured Fields
|
|
128
|
+
# parameter syntax defined in RFC 8941 Section 3.1.2.
|
|
28
129
|
#
|
|
29
|
-
# @
|
|
30
|
-
#
|
|
130
|
+
# @param parameters [Hash{String,Symbol => Object}] parameter names
|
|
131
|
+
# and values
|
|
132
|
+
# @return [String] serialized structured field parameters
|
|
133
|
+
# @raise [Linzer::Error] if the parameters cannot be serialized
|
|
31
134
|
#
|
|
135
|
+
# @see https://www.rfc-editor.org/rfc/rfc8941 RFC 8941
|
|
32
136
|
def self.serialize_parameters(parameters)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
when String
|
|
39
|
-
";#{key}=\"#{value}\""
|
|
40
|
-
else
|
|
41
|
-
";#{key}=#{value}"
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
params_str
|
|
137
|
+
Starry.serialize_parameters(parameters)
|
|
138
|
+
# rescue Starry::SerializeError => ex
|
|
139
|
+
# https://github.com/takemar/starry/pull/4
|
|
140
|
+
rescue => ex
|
|
141
|
+
raise Error, ex.message, cause: ex
|
|
45
142
|
end
|
|
46
143
|
end
|
|
47
144
|
end
|
data/lib/linzer/http.rb
CHANGED
|
@@ -100,12 +100,18 @@ module Linzer
|
|
|
100
100
|
|
|
101
101
|
headers = build_headers(options[:headers] || {})
|
|
102
102
|
request = build_request(verb, uri, headers)
|
|
103
|
-
message = Linzer::Message.new(request)
|
|
104
103
|
components = options[:covered_components] || default_components
|
|
105
104
|
params = options[:params] || {}
|
|
106
|
-
signature = Linzer.sign(key, message, components, **params)
|
|
107
105
|
|
|
108
|
-
|
|
106
|
+
Linzer.sign! request,
|
|
107
|
+
key: key,
|
|
108
|
+
components: components,
|
|
109
|
+
params: params,
|
|
110
|
+
profile: options[:profile]
|
|
111
|
+
|
|
112
|
+
signature_headers = request.each_header.to_h.slice("signature-input", "signature")
|
|
113
|
+
|
|
114
|
+
do_request(http, uri, verb, options[:data], signature_headers, headers)
|
|
109
115
|
end
|
|
110
116
|
|
|
111
117
|
# Returns the default covered components for signing.
|
|
@@ -180,19 +186,19 @@ module Linzer
|
|
|
180
186
|
# @param uri [String] the request URI
|
|
181
187
|
# @param verb [Symbol] the HTTP method
|
|
182
188
|
# @param data [String, nil] the request body
|
|
183
|
-
# @param
|
|
189
|
+
# @param signature_headers [Hash] the generated signature headers
|
|
184
190
|
# @param headers [Hash] request headers
|
|
185
191
|
# @return [Net::HTTPResponse] the response
|
|
186
192
|
# @raise [Linzer::Error] if a body is required but not provided
|
|
187
|
-
def do_request(http, uri, verb, data,
|
|
193
|
+
def do_request(http, uri, verb, data, signature_headers, headers)
|
|
188
194
|
if with_body?(verb)
|
|
189
195
|
if !data
|
|
190
196
|
missed_body = "Missing request body on HTTP request: '#{verb.upcase}'"
|
|
191
197
|
raise Linzer::Error, missed_body
|
|
192
198
|
end
|
|
193
|
-
http.public_send(verb, uri, data, headers.merge(
|
|
199
|
+
http.public_send(verb, uri, data, headers.merge(signature_headers))
|
|
194
200
|
else
|
|
195
|
-
http.public_send(verb, uri, headers.merge(
|
|
201
|
+
http.public_send(verb, uri, headers.merge(signature_headers))
|
|
196
202
|
end
|
|
197
203
|
end
|
|
198
204
|
end
|
|
@@ -97,26 +97,35 @@ module Linzer
|
|
|
97
97
|
# Attaches a signature to the underlying HTTP message.
|
|
98
98
|
#
|
|
99
99
|
# @param signature [Signature] The signature to attach
|
|
100
|
+
# @param additional_headers [#each]
|
|
101
|
+
# Additional headers to attach after signature processing.
|
|
102
|
+
# Header values overwrite existing values with the same field name.
|
|
103
|
+
#
|
|
100
104
|
# @return [Object] The underlying HTTP message
|
|
101
|
-
def attach!(signature)
|
|
105
|
+
def attach!(signature, additional_headers: {})
|
|
102
106
|
signature_headers = signature.to_h
|
|
103
107
|
|
|
104
|
-
|
|
108
|
+
if !has_signature?
|
|
105
109
|
signature_headers.each { |h, v| set_header!(h, v) }
|
|
106
|
-
|
|
110
|
+
else
|
|
111
|
+
begin
|
|
112
|
+
signature_headers.each do |hdr, value|
|
|
113
|
+
merged = HTTP::StructuredField.parse_dictionary(String(header(hdr)))
|
|
114
|
+
merged.merge!(HTTP::StructuredField.parse_dictionary(value))
|
|
115
|
+
set_header!(hdr, HTTP::StructuredField.serialize_dictionary(merged))
|
|
116
|
+
end
|
|
117
|
+
rescue Error => ex
|
|
118
|
+
raise Error,
|
|
119
|
+
"Cannot attach signature, invalid signature header(s)!",
|
|
120
|
+
cause: ex
|
|
121
|
+
end
|
|
107
122
|
end
|
|
108
123
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
merged.merge!(Starry.parse_dictionary(value))
|
|
112
|
-
set_header!(hdr, Starry.serialize_dictionary(merged))
|
|
124
|
+
if !additional_headers.empty?
|
|
125
|
+
additional_headers.each { |h, v| set_header!(h, v) }
|
|
113
126
|
end
|
|
114
127
|
|
|
115
128
|
@operation
|
|
116
|
-
rescue Starry::ParseError => e
|
|
117
|
-
raise Error,
|
|
118
|
-
"Cannot attach signature, invalid signature header(s)!",
|
|
119
|
-
cause: e
|
|
120
129
|
end
|
|
121
130
|
|
|
122
131
|
private
|
|
@@ -201,8 +210,10 @@ module Linzer
|
|
|
201
210
|
has_bs = name.parameters["bs"]
|
|
202
211
|
|
|
203
212
|
if has_req
|
|
204
|
-
|
|
205
|
-
|
|
213
|
+
request_field =
|
|
214
|
+
HTTP::StructuredField::Item.new(name.value,
|
|
215
|
+
name.parameters.except("req"))
|
|
216
|
+
return req(request_field, method)
|
|
206
217
|
end
|
|
207
218
|
|
|
208
219
|
value = send(method, name)
|
|
@@ -223,14 +234,16 @@ module Linzer
|
|
|
223
234
|
# @return [String] the serialized structured field value
|
|
224
235
|
# @see https://www.rfc-editor.org/rfc/rfc9421.html#section-2.1.1
|
|
225
236
|
def sf(value, key = nil)
|
|
226
|
-
dict =
|
|
237
|
+
dict = HTTP::StructuredField.parse_dictionary(value)
|
|
227
238
|
|
|
228
239
|
if key
|
|
229
240
|
obj = dict[key]
|
|
230
|
-
|
|
241
|
+
HTTP::StructuredField.serialize(obj.is_a?(HTTP::StructuredField::InnerList) ? [obj] : obj)
|
|
231
242
|
else
|
|
232
|
-
|
|
243
|
+
HTTP::StructuredField.serialize(dict)
|
|
233
244
|
end
|
|
245
|
+
rescue Error => _ex
|
|
246
|
+
nil
|
|
234
247
|
end
|
|
235
248
|
|
|
236
249
|
# Binary-wraps a field value as a byte sequence.
|
|
@@ -239,7 +252,7 @@ module Linzer
|
|
|
239
252
|
# @return [String] the serialized byte sequence
|
|
240
253
|
# @see https://www.rfc-editor.org/rfc/rfc9421.html#section-2.1.3
|
|
241
254
|
def bs(value)
|
|
242
|
-
|
|
255
|
+
HTTP::StructuredField.serialize(value.encode(Encoding::ASCII_8BIT))
|
|
243
256
|
end
|
|
244
257
|
|
|
245
258
|
# Retrieves a trailer field value.
|
|
@@ -37,10 +37,24 @@ module Linzer
|
|
|
37
37
|
|
|
38
38
|
# Builds the full URI including query parameters.
|
|
39
39
|
#
|
|
40
|
+
# Encodes the query with the request's own +params_encoder+,
|
|
41
|
+
# mirroring how faraday builds the URL it puts on the wire
|
|
42
|
+
# (+Faraday::Connection#build_env+). Encoding it independently
|
|
43
|
+
# here would diverge from the URI actually sent: notably
|
|
44
|
+
# +URI.encode_www_form+ always encodes a space as +++, while
|
|
45
|
+
# faraday honours the process-wide
|
|
46
|
+
# +Faraday::Utils.default_space_encoding+, which other gems may
|
|
47
|
+
# set to +%20+. The signer would then cover a +@target-uri+ the
|
|
48
|
+
# verifier cannot reproduce from the request it received.
|
|
49
|
+
#
|
|
50
|
+
# An empty query is left as +nil+ rather than +""+, as assigning
|
|
51
|
+
# +""+ would append a stray +?+ that faraday does not send.
|
|
52
|
+
#
|
|
40
53
|
# @return [URI] the complete request URI with encoded query string
|
|
41
54
|
def uri
|
|
42
55
|
uri = @operation.path.dup
|
|
43
|
-
|
|
56
|
+
query = @operation.params.to_query(@operation.options.params_encoder)
|
|
57
|
+
uri.query = query unless query.empty?
|
|
44
58
|
uri
|
|
45
59
|
end
|
|
46
60
|
|