linzer 0.7.9 → 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 +19 -0
- data/README.md +63 -0
- data/Rakefile +6 -0
- data/benchmarks/profile_sign_ed25519.rb +102 -0
- data/benchmarks/profile_verify_ed25519.rb +107 -0
- data/benchmarks/sign.rb +55 -0
- data/benchmarks/verify.rb +68 -0
- data/lib/faraday/http_signature/middleware.rb +25 -4
- data/lib/linzer/common.rb +53 -23
- data/lib/linzer/ed25519.rb +4 -2
- data/lib/linzer/helper.rb +34 -15
- data/lib/linzer/hmac.rb +14 -12
- data/lib/linzer/http/signature_feature.rb +15 -4
- data/lib/linzer/http/structured_field.rb +145 -0
- data/lib/linzer/http.rb +13 -7
- data/lib/linzer/jws.rb +4 -4
- data/lib/linzer/key.rb +20 -2
- data/lib/linzer/message/adapter/abstract.rb +36 -22
- data/lib/linzer/message/adapter/generic/request.rb +1 -0
- data/lib/linzer/message/adapter.rb +0 -3
- data/lib/linzer/message/field/parser.rb +5 -5
- data/lib/linzer/message/field.rb +55 -3
- data/lib/linzer/message/overlay.rb +143 -0
- data/lib/linzer/message/wrapper.rb +0 -2
- data/lib/linzer/message.rb +18 -0
- data/lib/linzer/rack.rb +24 -0
- data/lib/linzer/rsa_pss.rb +4 -4
- 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 +147 -32
- data/lib/linzer/signer.rb +36 -15
- data/lib/linzer/verifier.rb +9 -3
- data/lib/linzer/version.rb +1 -1
- data/lib/linzer.rb +5 -4
- metadata +13 -41
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.
|
data/lib/linzer/hmac.rb
CHANGED
|
@@ -54,18 +54,6 @@ module Linzer
|
|
|
54
54
|
OpenSSL.secure_compare(signature, sign(data))
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
# HMAC keys can always sign (they contain the secret).
|
|
58
|
-
# @return [Boolean] true if key material is present
|
|
59
|
-
def private?
|
|
60
|
-
!material.nil?
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# HMAC keys are symmetric, not public/private.
|
|
64
|
-
# @return [Boolean] always false for HMAC keys
|
|
65
|
-
def public?
|
|
66
|
-
false
|
|
67
|
-
end
|
|
68
|
-
|
|
69
57
|
# Returns a safe string representation that doesn't leak the secret.
|
|
70
58
|
#
|
|
71
59
|
# The key material is intentionally excluded from the output to prevent
|
|
@@ -82,6 +70,20 @@ module Linzer
|
|
|
82
70
|
oid = Digest::SHA2.hexdigest(object_id.to_s)[48..63]
|
|
83
71
|
"#<%s:0x%s %s>" % [self.class, oid, vars.join(", ")]
|
|
84
72
|
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# HMAC keys can always sign (they contain the secret).
|
|
77
|
+
# @return [Boolean] true if key material is present
|
|
78
|
+
def compute_private?
|
|
79
|
+
!material.nil?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# HMAC keys are symmetric, not public/private.
|
|
83
|
+
# @return [Boolean] always false for HMAC keys
|
|
84
|
+
def compute_public?
|
|
85
|
+
false
|
|
86
|
+
end
|
|
85
87
|
end
|
|
86
88
|
end
|
|
87
89
|
end
|
|
@@ -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
|
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Linzer
|
|
4
|
+
module HTTP
|
|
5
|
+
# Utilities for serializing HTTP Structured Fields as defined in RFC 8941.
|
|
6
|
+
#
|
|
7
|
+
# This module currently provides helpers for serializing HTTP Message
|
|
8
|
+
# Signature parameters as used by RFC 9421.
|
|
9
|
+
#
|
|
10
|
+
# @see https://www.rfc-editor.org/rfc/rfc8941 RFC 8941
|
|
11
|
+
# @see https://www.rfc-editor.org/rfc/rfc9421 RFC 9421
|
|
12
|
+
module StructuredField
|
|
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
|
|
70
|
+
#
|
|
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
|
|
100
|
+
#
|
|
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.
|
|
126
|
+
#
|
|
127
|
+
# Parameters are serialized according to the Structured Fields
|
|
128
|
+
# parameter syntax defined in RFC 8941 Section 3.1.2.
|
|
129
|
+
#
|
|
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
|
|
134
|
+
#
|
|
135
|
+
# @see https://www.rfc-editor.org/rfc/rfc8941 RFC 8941
|
|
136
|
+
def self.serialize_parameters(parameters)
|
|
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
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
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
|
data/lib/linzer/jws.rb
CHANGED
|
@@ -101,18 +101,18 @@ module Linzer
|
|
|
101
101
|
algo.verify(data: data, signature: signature, verification_key: verify_key)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
private
|
|
105
|
+
|
|
104
106
|
# @return [Boolean] true if this key can verify signatures
|
|
105
|
-
def
|
|
107
|
+
def compute_public?
|
|
106
108
|
!!verify_key
|
|
107
109
|
end
|
|
108
110
|
|
|
109
111
|
# @return [Boolean] true if this key can create signatures
|
|
110
|
-
def
|
|
112
|
+
def compute_private?
|
|
111
113
|
!!signing_key
|
|
112
114
|
end
|
|
113
115
|
|
|
114
|
-
private
|
|
115
|
-
|
|
116
116
|
# Resolves the appropriate JWT algorithm implementation.
|
|
117
117
|
# @return [JWT::JWA::SigningAlgorithm] The algorithm implementation
|
|
118
118
|
# @raise [Error] If the algorithm cannot be determined
|
data/lib/linzer/key.rb
CHANGED
|
@@ -38,6 +38,8 @@ module Linzer
|
|
|
38
38
|
@material = material
|
|
39
39
|
@params = Hash(params).clone.freeze
|
|
40
40
|
validate
|
|
41
|
+
@is_private = compute_private?
|
|
42
|
+
@is_public = compute_public?
|
|
41
43
|
freeze
|
|
42
44
|
end
|
|
43
45
|
|
|
@@ -84,14 +86,14 @@ module Linzer
|
|
|
84
86
|
#
|
|
85
87
|
# @return [Boolean] true if the key contains public key material
|
|
86
88
|
def public?
|
|
87
|
-
|
|
89
|
+
@is_public
|
|
88
90
|
end
|
|
89
91
|
|
|
90
92
|
# Checks if this key can be used for signing.
|
|
91
93
|
#
|
|
92
94
|
# @return [Boolean] true if the key contains private key material
|
|
93
95
|
def private?
|
|
94
|
-
|
|
96
|
+
@is_private
|
|
95
97
|
end
|
|
96
98
|
|
|
97
99
|
private
|
|
@@ -102,6 +104,22 @@ module Linzer
|
|
|
102
104
|
!material.nil? or raise Error.new "Invalid key. No key material provided."
|
|
103
105
|
end
|
|
104
106
|
|
|
107
|
+
# Computes whether the key contains private key material.
|
|
108
|
+
# Override in subclasses where the OpenSSL key object does not
|
|
109
|
+
# respond to +private?+ (e.g. Ed25519, RSA-PSS).
|
|
110
|
+
# @return [Boolean]
|
|
111
|
+
def compute_private?
|
|
112
|
+
material.respond_to?(:private?) ? material.private? : false
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Computes whether the key contains public key material.
|
|
116
|
+
# Override in subclasses where the OpenSSL key object does not
|
|
117
|
+
# respond to +public?+ (e.g. Ed25519, RSA-PSS).
|
|
118
|
+
# @return [Boolean]
|
|
119
|
+
def compute_public?
|
|
120
|
+
material.respond_to?(:public?) ? material.public? : false
|
|
121
|
+
end
|
|
122
|
+
|
|
105
123
|
# Validates that a digest algorithm is configured.
|
|
106
124
|
# @raise [Error] If no digest algorithm is set
|
|
107
125
|
def validate_digest
|
|
@@ -70,7 +70,7 @@ module Linzer
|
|
|
70
70
|
# @example With structured field parameter
|
|
71
71
|
# adapter['"example-dict";key="a"'] # => "1"
|
|
72
72
|
def [](field)
|
|
73
|
-
field_id = field.is_a?(FieldId) ? field : parse_field_name(field)
|
|
73
|
+
field_id = (field.is_a?(FieldId) || field.is_a?(Field::FastIdentifier)) ? field : parse_field_name(field)
|
|
74
74
|
return nil if field_id.nil? || field_id.item.nil?
|
|
75
75
|
retrieve(field_id.item, field_id.derived? ? :derived : :field)
|
|
76
76
|
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
|
|
@@ -190,18 +199,21 @@ module Linzer
|
|
|
190
199
|
# @param method [Symbol] +:derived+ or +:field+
|
|
191
200
|
# @return [String, Integer, nil] the component value
|
|
192
201
|
def retrieve(name, method)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
202
|
+
# Fast path: no parameters means no special handling needed
|
|
203
|
+
return send(method, name) if name.parameters.empty?
|
|
204
|
+
|
|
205
|
+
valid_params = validate_parameters(name, method)
|
|
206
|
+
return nil if !valid_params
|
|
197
207
|
|
|
198
208
|
has_req = name.parameters["req"]
|
|
199
209
|
has_sf = name.parameters["sf"] || name.parameters.key?("key")
|
|
200
210
|
has_bs = name.parameters["bs"]
|
|
201
211
|
|
|
202
212
|
if has_req
|
|
203
|
-
|
|
204
|
-
|
|
213
|
+
request_field =
|
|
214
|
+
HTTP::StructuredField::Item.new(name.value,
|
|
215
|
+
name.parameters.except("req"))
|
|
216
|
+
return req(request_field, method)
|
|
205
217
|
end
|
|
206
218
|
|
|
207
219
|
value = send(method, name)
|
|
@@ -222,14 +234,16 @@ module Linzer
|
|
|
222
234
|
# @return [String] the serialized structured field value
|
|
223
235
|
# @see https://www.rfc-editor.org/rfc/rfc9421.html#section-2.1.1
|
|
224
236
|
def sf(value, key = nil)
|
|
225
|
-
dict =
|
|
237
|
+
dict = HTTP::StructuredField.parse_dictionary(value)
|
|
226
238
|
|
|
227
239
|
if key
|
|
228
240
|
obj = dict[key]
|
|
229
|
-
|
|
241
|
+
HTTP::StructuredField.serialize(obj.is_a?(HTTP::StructuredField::InnerList) ? [obj] : obj)
|
|
230
242
|
else
|
|
231
|
-
|
|
243
|
+
HTTP::StructuredField.serialize(dict)
|
|
232
244
|
end
|
|
245
|
+
rescue Error => _ex
|
|
246
|
+
nil
|
|
233
247
|
end
|
|
234
248
|
|
|
235
249
|
# Binary-wraps a field value as a byte sequence.
|
|
@@ -238,7 +252,7 @@ module Linzer
|
|
|
238
252
|
# @return [String] the serialized byte sequence
|
|
239
253
|
# @see https://www.rfc-editor.org/rfc/rfc9421.html#section-2.1.3
|
|
240
254
|
def bs(value)
|
|
241
|
-
|
|
255
|
+
HTTP::StructuredField.serialize(value.encode(Encoding::ASCII_8BIT))
|
|
242
256
|
end
|
|
243
257
|
|
|
244
258
|
# Retrieves a trailer field value.
|
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
require_relative "adapter/abstract"
|
|
4
4
|
require_relative "adapter/generic/request"
|
|
5
5
|
require_relative "adapter/generic/response"
|
|
6
|
-
require_relative "adapter/rack/common"
|
|
7
|
-
require_relative "adapter/rack/request"
|
|
8
|
-
require_relative "adapter/rack/response"
|
|
9
6
|
require_relative "adapter/net_http/request"
|
|
10
7
|
require_relative "adapter/net_http/response"
|
|
11
8
|
|
|
@@ -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
|
-
|
|
37
|
+
@serialized || HTTP::StructuredField.serialize(@item)
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -54,6 +54,29 @@ module Linzer
|
|
|
54
54
|
|
|
55
55
|
Identifier.include Message::Field::IdentifierMethods
|
|
56
56
|
|
|
57
|
+
# Lightweight FieldId for simple components (no parameters).
|
|
58
|
+
# Bypasses Starry parsing entirely. Duck-types with Identifier
|
|
59
|
+
# for use in the adapter's [] method.
|
|
60
|
+
# @api private
|
|
61
|
+
class FastIdentifier
|
|
62
|
+
def initialize(serialized, item)
|
|
63
|
+
@field_name = serialized
|
|
64
|
+
@item = item
|
|
65
|
+
@serialized = serialized
|
|
66
|
+
freeze
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
attr_reader :field_name, :item
|
|
70
|
+
|
|
71
|
+
def derived?
|
|
72
|
+
@item.value.start_with?("@")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def serialize
|
|
76
|
+
@serialized
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
57
80
|
class Identifier
|
|
58
81
|
class << self
|
|
59
82
|
# Serializes a single component identifier.
|
|
@@ -70,13 +93,42 @@ module Linzer
|
|
|
70
93
|
components.map(&method(:serialize))
|
|
71
94
|
end
|
|
72
95
|
|
|
96
|
+
# Serializes an array of component identifiers, returning both
|
|
97
|
+
# the serialized strings and the FieldId objects for reuse.
|
|
98
|
+
# @param components [Array<String>] Component names
|
|
99
|
+
# @return [Array(Array<String>, Array<Identifier>)] Serialized strings and FieldId objects
|
|
100
|
+
def serialize_components_with_field_ids(components)
|
|
101
|
+
serialized = Array.new(components.size)
|
|
102
|
+
field_ids = Array.new(components.size)
|
|
103
|
+
|
|
104
|
+
components.each_with_index do |c, i|
|
|
105
|
+
if c.include?(";") || c.include?('"')
|
|
106
|
+
# Complex component with parameters or already serialized:
|
|
107
|
+
# fall back to full Starry parsing
|
|
108
|
+
fid = new(field_name: c)
|
|
109
|
+
field_ids[i] = fid
|
|
110
|
+
serialized[i] = fid.serialize
|
|
111
|
+
else
|
|
112
|
+
# Simple component (e.g. "@method", "content-type"):
|
|
113
|
+
# build the Item and serialized string directly,
|
|
114
|
+
# bypassing Starry.parse_item + Starry.serialize
|
|
115
|
+
quoted = "\"#{c}\""
|
|
116
|
+
item = HTTP::StructuredField::Item.new(c, {})
|
|
117
|
+
field_ids[i] = FastIdentifier.new(quoted, item)
|
|
118
|
+
serialized[i] = quoted
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
[serialized, field_ids]
|
|
123
|
+
end
|
|
124
|
+
|
|
73
125
|
# Deserializes component identifiers back to names.
|
|
74
126
|
# @param components [Array<String>] Serialized identifiers
|
|
75
127
|
# @return [Array<String>] Component names
|
|
76
128
|
def deserialize_components(components)
|
|
77
129
|
components.map do |c|
|
|
78
|
-
item =
|
|
79
|
-
item.parameters.empty? ? item.value :
|
|
130
|
+
item = HTTP::StructuredField.parse_item(c)
|
|
131
|
+
item.parameters.empty? ? item.value : HTTP::StructuredField.serialize(item)
|
|
80
132
|
end
|
|
81
133
|
end
|
|
82
134
|
end
|