linzer 0.8.0.beta1 → 0.8.0.beta2
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 +7 -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/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/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: 7a917fda2b4d4646e642ba3a52d11bd41a500b5bfed044142a6e0434e0ef1c94
|
|
4
|
+
data.tar.gz: 8d9296ba21595cba696532a41b20f123569d3b31de0730ba6e7e6f693e87e009
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2f6b61e4a340594891976d77b651527b8e0d49ff9ab11d73c56f01b5baecc1ad3fdf8ad23d987652496929ef30495cb116ba04a53f1b7ef2d881e8c1c1cfa1c9
|
|
7
|
+
data.tar.gz: fb4800f527d14823e9ed76b776056fe20a60f35fc3e08c30ce15c05758648476f8489c2d8bb89851fe2c77eeff56891b96ec955663bf08b7b6326cdf4425c881
|
data/.standard.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.8.0.beta2] - 2026-05-20
|
|
4
|
+
|
|
5
|
+
- Add Web Bot Auth support, implementing the current IETF draft
|
|
6
|
+
(draft-meunier-web-bot-auth-architecture-05).
|
|
7
|
+
Includes recommended signature parameter defaults, nonce generation,
|
|
8
|
+
and optional Signature-Agent header handling.
|
|
9
|
+
|
|
3
10
|
## [0.8.0.beta1] - 2026-05-07
|
|
4
11
|
|
|
5
12
|
- 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/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.
|
|
@@ -24,15 +24,15 @@ module Linzer
|
|
|
24
24
|
def parse(field_name)
|
|
25
25
|
case
|
|
26
26
|
when field_name.match?(/";/), field_name.start_with?('"')
|
|
27
|
-
|
|
27
|
+
HTTP::StructuredField.parse_item(field_name)
|
|
28
28
|
when field_name.match?(/;/)
|
|
29
29
|
parse_unserialized_input(field_name)
|
|
30
30
|
when field_name.start_with?("@"), field_name.match?(/^[a-z]/)
|
|
31
|
-
|
|
31
|
+
HTTP::StructuredField.parse_item(HTTP::StructuredField.serialize(field_name))
|
|
32
32
|
else
|
|
33
33
|
raise Error, "Invalid component identifier: '#{field_name}'!"
|
|
34
34
|
end
|
|
35
|
-
rescue
|
|
35
|
+
rescue Error => ex
|
|
36
36
|
parse_error = "Failed to parse component identifier: '#{field_name}'!"
|
|
37
37
|
raise Error, parse_error, cause: ex
|
|
38
38
|
end
|
|
@@ -49,7 +49,7 @@ module Linzer
|
|
|
49
49
|
# @return [Starry::Item] the parsed item with parameters
|
|
50
50
|
def parse_unserialized_input(field_name)
|
|
51
51
|
field, *raw_params = field_name.split(";")
|
|
52
|
-
item =
|
|
52
|
+
item = HTTP::StructuredField.parse_item(HTTP::StructuredField.serialize(field))
|
|
53
53
|
item.parameters = collect_parameters(raw_params)
|
|
54
54
|
item
|
|
55
55
|
end
|
|
@@ -67,7 +67,7 @@ module Linzer
|
|
|
67
67
|
{param => true}
|
|
68
68
|
else
|
|
69
69
|
Hash[*tokens.first(2)] # e.g.: ";key=\"foo\""
|
|
70
|
-
.transform_values! { |v|
|
|
70
|
+
.transform_values! { |v| HTTP::StructuredField.parse_item(v).value }
|
|
71
71
|
end
|
|
72
72
|
end
|
|
73
73
|
params.reduce({}, :merge)
|
data/lib/linzer/message/field.rb
CHANGED
|
@@ -34,7 +34,7 @@ module Linzer
|
|
|
34
34
|
# @raise [Error] If the component identifier is invalid
|
|
35
35
|
def serialize
|
|
36
36
|
raise Error, "Invalid component identifier: '#{field_name}'!" unless item
|
|
37
|
-
@serialized ||
|
|
37
|
+
@serialized || HTTP::StructuredField.serialize(@item)
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -113,7 +113,7 @@ module Linzer
|
|
|
113
113
|
# build the Item and serialized string directly,
|
|
114
114
|
# bypassing Starry.parse_item + Starry.serialize
|
|
115
115
|
quoted = "\"#{c}\""
|
|
116
|
-
item =
|
|
116
|
+
item = HTTP::StructuredField::Item.new(c, {})
|
|
117
117
|
field_ids[i] = FastIdentifier.new(quoted, item)
|
|
118
118
|
serialized[i] = quoted
|
|
119
119
|
end
|
|
@@ -127,8 +127,8 @@ module Linzer
|
|
|
127
127
|
# @return [Array<String>] Component names
|
|
128
128
|
def deserialize_components(components)
|
|
129
129
|
components.map do |c|
|
|
130
|
-
item =
|
|
131
|
-
item.parameters.empty? ? item.value :
|
|
130
|
+
item = HTTP::StructuredField.parse_item(c)
|
|
131
|
+
item.parameters.empty? ? item.value : HTTP::StructuredField.serialize(item)
|
|
132
132
|
end
|
|
133
133
|
end
|
|
134
134
|
end
|