linzer 0.7.7.beta1 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/LICENSE.txt +1 -1
- data/README.md +3 -1
- data/flake.lock +109 -0
- data/flake.nix +73 -0
- data/lib/linzer/common.rb +51 -0
- data/lib/linzer/ecdsa.rb +51 -0
- data/lib/linzer/ed25519.rb +35 -0
- data/lib/linzer/helper.rb +79 -0
- data/lib/linzer/hmac.rb +47 -1
- data/lib/linzer/http/bootstrap.rb +11 -0
- data/lib/linzer/http/signature_feature.rb +53 -1
- data/lib/linzer/http.rb +54 -0
- data/lib/linzer/jws.rb +74 -0
- data/lib/linzer/key/helper.rb +186 -10
- data/lib/linzer/key.rb +73 -0
- data/lib/linzer/message/adapter/abstract.rb +75 -10
- data/lib/linzer/message/adapter/generic/request.rb +27 -0
- data/lib/linzer/message/adapter/generic/response.rb +17 -0
- data/lib/linzer/message/adapter/http_gem/request.rb +11 -0
- data/lib/linzer/message/adapter/http_gem/response.rb +8 -5
- data/lib/linzer/message/adapter/net_http/request.rb +7 -0
- data/lib/linzer/message/adapter/net_http/response.rb +4 -0
- data/lib/linzer/message/adapter/rack/common.rb +14 -6
- data/lib/linzer/message/adapter/rack/request.rb +13 -0
- data/lib/linzer/message/adapter/rack/response.rb +11 -0
- data/lib/linzer/message/adapter.rb +17 -0
- data/lib/linzer/message/field/parser.rb +14 -0
- data/lib/linzer/message/field.rb +32 -2
- data/lib/linzer/message/wrapper.rb +20 -0
- data/lib/linzer/message.rb +113 -3
- data/lib/linzer/options.rb +13 -0
- data/lib/linzer/rsa.rb +34 -0
- data/lib/linzer/rsa_pss.rb +44 -0
- data/lib/linzer/signature.rb +113 -1
- data/lib/linzer/signer.rb +69 -0
- data/lib/linzer/verifier.rb +52 -0
- data/lib/linzer/version.rb +3 -1
- data/lib/linzer.rb +104 -0
- data/lib/rack/auth/signature.rb +90 -6
- metadata +30 -16
data/lib/linzer/key.rb
CHANGED
|
@@ -1,7 +1,39 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Linzer
|
|
4
|
+
# Abstract base class for cryptographic keys used in HTTP message signatures.
|
|
5
|
+
#
|
|
6
|
+
# This class provides the common interface for all key types supported by Linzer.
|
|
7
|
+
# Do not instantiate this class directly; use one of the concrete subclasses
|
|
8
|
+
# or the key generation/loading helper methods.
|
|
9
|
+
#
|
|
10
|
+
# @abstract Subclass and override {#sign} and {#verify} to implement
|
|
11
|
+
# a specific cryptographic algorithm.
|
|
12
|
+
#
|
|
13
|
+
# @example Using a concrete key class via helper methods
|
|
14
|
+
# # Generate a new Ed25519 key pair
|
|
15
|
+
# key = Linzer.generate_ed25519_key("my-key-id")
|
|
16
|
+
#
|
|
17
|
+
# # Load an existing RSA-PSS key from PEM
|
|
18
|
+
# key = Linzer.new_rsa_pss_sha512_key(File.read("private.pem"), "rsa-key")
|
|
19
|
+
#
|
|
20
|
+
# @see Ed25519::Key
|
|
21
|
+
# @see ECDSA::Key
|
|
22
|
+
# @see HMAC::Key
|
|
23
|
+
# @see RSA::Key
|
|
24
|
+
# @see RSAPSS::Key
|
|
25
|
+
# @see JWS::Key
|
|
4
26
|
class Key
|
|
27
|
+
# Creates a new Key instance.
|
|
28
|
+
#
|
|
29
|
+
# @param material [OpenSSL::PKey::PKey, String] The key material.
|
|
30
|
+
# For asymmetric keys, this is typically an OpenSSL key object.
|
|
31
|
+
# For HMAC, this is the raw secret bytes.
|
|
32
|
+
# @param params [Hash] Additional key parameters
|
|
33
|
+
# @option params [String] :id The key identifier (keyid) for this key
|
|
34
|
+
# @option params [String] :digest The digest algorithm (e.g., "SHA256", "SHA512")
|
|
35
|
+
#
|
|
36
|
+
# @raise [Error] If key material is nil or invalid
|
|
5
37
|
def initialize(material, params = {})
|
|
6
38
|
@material = material
|
|
7
39
|
@params = Hash(params).clone.freeze
|
|
@@ -9,54 +41,95 @@ module Linzer
|
|
|
9
41
|
freeze
|
|
10
42
|
end
|
|
11
43
|
|
|
44
|
+
# @return [Object] The underlying key material
|
|
12
45
|
attr_reader :material
|
|
13
46
|
|
|
47
|
+
# Returns the key identifier.
|
|
48
|
+
#
|
|
49
|
+
# The key ID is used in the `keyid` parameter of HTTP signatures to
|
|
50
|
+
# identify which key was used for signing.
|
|
51
|
+
#
|
|
52
|
+
# @return [String, nil] The key identifier, or nil if not set
|
|
14
53
|
def key_id
|
|
15
54
|
@params[:id]
|
|
16
55
|
end
|
|
17
56
|
|
|
57
|
+
# Signs data using this key.
|
|
58
|
+
#
|
|
59
|
+
# @abstract Subclasses must override this method.
|
|
60
|
+
#
|
|
61
|
+
# @param args [Array] Implementation-specific arguments (typically data to sign)
|
|
62
|
+
# @return [String] The signature bytes
|
|
63
|
+
# @raise [Error] If called on the abstract base class
|
|
64
|
+
# @raise [SigningError] If the key cannot be used for signing
|
|
18
65
|
def sign(*args)
|
|
19
66
|
abstract_error = "Cannot sign data, \"#{self.class}\" is an abstract class."
|
|
20
67
|
raise Error, abstract_error
|
|
21
68
|
end
|
|
22
69
|
|
|
70
|
+
# Verifies a signature against data using this key.
|
|
71
|
+
#
|
|
72
|
+
# @abstract Subclasses must override this method.
|
|
73
|
+
#
|
|
74
|
+
# @param args [Array] Implementation-specific arguments (typically signature and data)
|
|
75
|
+
# @return [Boolean] true if the signature is valid, false otherwise
|
|
76
|
+
# @raise [Error] If called on the abstract base class
|
|
77
|
+
# @raise [VerifyError] If the key cannot be used for verification
|
|
23
78
|
def verify(*args)
|
|
24
79
|
abstract_error = "Cannot verify signature, \"#{self.class}\" is an abstract class."
|
|
25
80
|
raise Error, abstract_error
|
|
26
81
|
end
|
|
27
82
|
|
|
83
|
+
# Checks if this key can be used for signature verification.
|
|
84
|
+
#
|
|
85
|
+
# @return [Boolean] true if the key contains public key material
|
|
28
86
|
def public?
|
|
29
87
|
material.public?
|
|
30
88
|
end
|
|
31
89
|
|
|
90
|
+
# Checks if this key can be used for signing.
|
|
91
|
+
#
|
|
92
|
+
# @return [Boolean] true if the key contains private key material
|
|
32
93
|
def private?
|
|
33
94
|
material.private?
|
|
34
95
|
end
|
|
35
96
|
|
|
36
97
|
private
|
|
37
98
|
|
|
99
|
+
# Validates that the key has material.
|
|
100
|
+
# @raise [Error] If key material is nil
|
|
38
101
|
def validate
|
|
39
102
|
!material.nil? or raise Error.new "Invalid key. No key material provided."
|
|
40
103
|
end
|
|
41
104
|
|
|
105
|
+
# Validates that a digest algorithm is configured.
|
|
106
|
+
# @raise [Error] If no digest algorithm is set
|
|
42
107
|
def validate_digest
|
|
43
108
|
no_digest = !@params.key?(:digest) || @params[:digest].nil? || String(@params[:digest]).empty?
|
|
44
109
|
no_digest_error = "Invalid key definition, no digest algorithm was selected."
|
|
45
110
|
raise Error, no_digest_error if no_digest
|
|
46
111
|
end
|
|
47
112
|
|
|
113
|
+
# Validates that this key can be used for signing.
|
|
114
|
+
# @raise [SigningError] If the key does not contain private material
|
|
48
115
|
def validate_signing_key
|
|
49
116
|
raise SigningError, "Private key is needed!" unless private?
|
|
50
117
|
end
|
|
51
118
|
|
|
119
|
+
# Validates that this key can be used for verification.
|
|
120
|
+
# @raise [VerifyError] If the key does not contain public material
|
|
52
121
|
def validate_verify_key
|
|
53
122
|
raise VerifyError, "Public key is needed!" unless public?
|
|
54
123
|
end
|
|
55
124
|
|
|
125
|
+
# Checks if the key material has a PEM-encoded public key.
|
|
126
|
+
# @return [Boolean]
|
|
56
127
|
def has_pem_public?
|
|
57
128
|
material.public_to_pem.match?(/^-----BEGIN PUBLIC KEY-----/)
|
|
58
129
|
end
|
|
59
130
|
|
|
131
|
+
# Checks if the key material has a PEM-encoded private key.
|
|
132
|
+
# @return [Boolean]
|
|
60
133
|
def has_pem_private?
|
|
61
134
|
material.private_to_pem.match?(/^-----BEGIN PRIVATE KEY-----/)
|
|
62
135
|
rescue
|
|
@@ -3,61 +3,122 @@
|
|
|
3
3
|
module Linzer
|
|
4
4
|
class Message
|
|
5
5
|
module Adapter
|
|
6
|
+
# Abstract base class for HTTP message adapters.
|
|
7
|
+
#
|
|
8
|
+
# Adapters provide a unified interface for accessing HTTP message
|
|
9
|
+
# components regardless of the underlying HTTP library. Each adapter
|
|
10
|
+
# implements field retrieval, header access, and signature attachment
|
|
11
|
+
# for a specific HTTP message type.
|
|
12
|
+
#
|
|
13
|
+
# @abstract Subclass and implement {#header}, {#attach!}, {#derived},
|
|
14
|
+
# and {#field} to create a new adapter.
|
|
15
|
+
#
|
|
16
|
+
# @see Rack::Request Rack request adapter
|
|
17
|
+
# @see Rack::Response Rack response adapter
|
|
18
|
+
# @see NetHTTP::Request Net::HTTP request adapter
|
|
19
|
+
# @see NetHTTP::Response Net::HTTP response adapter
|
|
6
20
|
class Abstract
|
|
21
|
+
# @raise [Error] This class cannot be instantiated directly
|
|
7
22
|
def initialize(operation, **options)
|
|
8
23
|
raise Linzer::Error, "Cannot instantiate an abstract class!"
|
|
9
24
|
end
|
|
10
25
|
|
|
26
|
+
# Checks if this adapter wraps an HTTP request.
|
|
27
|
+
# @return [Boolean] true if the wrapped message is a request
|
|
11
28
|
def request?
|
|
12
29
|
self.class.to_s.include?("Request")
|
|
13
30
|
end
|
|
14
31
|
|
|
32
|
+
# Checks if this adapter wraps an HTTP response.
|
|
33
|
+
# @return [Boolean] true if the wrapped message is a response
|
|
15
34
|
def response?
|
|
16
35
|
self.class.to_s.include?("Response")
|
|
17
36
|
end
|
|
18
37
|
|
|
38
|
+
# Checks if this response has an attached request.
|
|
39
|
+
#
|
|
40
|
+
# Attached requests enable the `;req` parameter for accessing
|
|
41
|
+
# request fields from a response signature.
|
|
42
|
+
#
|
|
43
|
+
# @return [Boolean] true if an attached request is present
|
|
19
44
|
def attached_request?
|
|
20
45
|
response? && !!@attached_request
|
|
21
46
|
end
|
|
22
47
|
|
|
48
|
+
# Checks if a component exists in the message.
|
|
49
|
+
#
|
|
50
|
+
# @param f [String] The component identifier
|
|
51
|
+
# @return [Boolean] true if the component can be retrieved
|
|
23
52
|
def field?(f)
|
|
24
53
|
!!self[f]
|
|
25
54
|
end
|
|
26
55
|
|
|
56
|
+
# Retrieves a component value from the message.
|
|
57
|
+
#
|
|
58
|
+
# Handles both regular header fields and derived components,
|
|
59
|
+
# including parameter processing (`;sf`, `;bs`, `;req`, `;key`).
|
|
60
|
+
#
|
|
61
|
+
# @param field [String, FieldId] The component identifier
|
|
62
|
+
# @return [String, Integer, nil] The component value, or nil if not found
|
|
63
|
+
#
|
|
64
|
+
# @example Header field
|
|
65
|
+
# adapter["content-type"] # => "application/json"
|
|
66
|
+
#
|
|
67
|
+
# @example Derived component
|
|
68
|
+
# adapter["@method"] # => "POST"
|
|
69
|
+
#
|
|
70
|
+
# @example With structured field parameter
|
|
71
|
+
# adapter['"example-dict";key="a"'] # => "1"
|
|
27
72
|
def [](field)
|
|
28
73
|
field_id = field.is_a?(FieldId) ? field : parse_field_name(field)
|
|
29
74
|
return nil if field_id.nil? || field_id.item.nil?
|
|
30
75
|
retrieve(field_id.item, field_id.derived? ? :derived : :field)
|
|
31
76
|
end
|
|
32
77
|
|
|
78
|
+
# Retrieves a raw header value by name.
|
|
79
|
+
#
|
|
80
|
+
# @abstract Subclasses must implement this method.
|
|
81
|
+
# @param name [String] The header name
|
|
82
|
+
# @return [String, nil] The header value
|
|
33
83
|
def header(name)
|
|
34
84
|
raise Linzer::Error, "Sub-classes are required to implement this method!"
|
|
35
85
|
end
|
|
36
86
|
|
|
87
|
+
# Attaches a signature to the underlying HTTP message.
|
|
88
|
+
#
|
|
89
|
+
# @abstract Subclasses must implement this method.
|
|
90
|
+
# @param signature [Signature] The signature to attach
|
|
91
|
+
# @return [Object] The underlying HTTP message
|
|
37
92
|
def attach!(signature)
|
|
38
93
|
raise Linzer::Error, "Sub-classes are required to implement this method!"
|
|
39
94
|
end
|
|
40
95
|
|
|
41
96
|
private
|
|
42
97
|
|
|
98
|
+
# Parses a field name string into a FieldId.
|
|
99
|
+
# @return [FieldId, nil] The parsed identifier, or nil if invalid
|
|
43
100
|
def parse_field_name(field_name)
|
|
44
101
|
field_id = FieldId.new(field_name: field_name)
|
|
45
102
|
component = field_id.item
|
|
46
103
|
|
|
47
104
|
return nil if component.nil?
|
|
48
105
|
|
|
49
|
-
# 2.2.9
|
|
106
|
+
# RFC 9421 Section 2.2.9: @status is only valid for responses
|
|
50
107
|
invalid = "@status component identifier is invalid in a request message"
|
|
51
108
|
raise Error, invalid if request? && component.value == "@status"
|
|
52
109
|
|
|
53
110
|
field_id
|
|
54
111
|
end
|
|
55
112
|
|
|
113
|
+
# Validates that an attached message is a request.
|
|
114
|
+
# @raise [Error] If the message is not a request
|
|
56
115
|
def validate_attached_request(message)
|
|
57
116
|
msg = "The attached message is not a valid HTTP request!"
|
|
58
117
|
raise Linzer::Error, msg unless message.request?
|
|
59
118
|
end
|
|
60
119
|
|
|
120
|
+
# Validates component identifier parameters.
|
|
121
|
+
# @return [Object, nil] The validated name, or nil if invalid
|
|
61
122
|
def validate_parameters(name, method)
|
|
62
123
|
has_unknown = name.parameters.any? { |p, _| !KNOWN_PARAMETERS.include?(p) }
|
|
63
124
|
return nil if has_unknown
|
|
@@ -68,29 +129,26 @@ module Linzer
|
|
|
68
129
|
has_bs = name.parameters["bs"]
|
|
69
130
|
value = name.value
|
|
70
131
|
|
|
71
|
-
# Section 2.2.8 of RFC 9421
|
|
132
|
+
# Section 2.2.8 of RFC 9421: name param only for @query-param
|
|
72
133
|
return nil if has_name && value != "@query-param"
|
|
73
134
|
|
|
74
135
|
# No derived values come from trailers section
|
|
75
136
|
return nil if method == :derived && name.parameters["tr"]
|
|
76
137
|
|
|
77
|
-
#
|
|
78
|
-
# The bs parameter, which requires the raw bytes of the field values
|
|
79
|
-
# from the message, is not compatible with the use of the sf or key
|
|
80
|
-
# parameters, which require the parsed data structures of the field
|
|
81
|
-
# values after combination
|
|
138
|
+
# RFC 9421 Section 2.1: bs incompatible with sf/key
|
|
82
139
|
return nil if has_sf && has_bs
|
|
83
140
|
|
|
84
|
-
# req param only makes sense on responses
|
|
85
|
-
# return nil if has_req && (!response? || !attached_request?)
|
|
141
|
+
# req param only makes sense on responses
|
|
86
142
|
return nil if has_req && !response?
|
|
87
143
|
|
|
88
144
|
name
|
|
89
145
|
end
|
|
90
146
|
|
|
147
|
+
# Known component identifier parameters from RFC 9421.
|
|
91
148
|
KNOWN_PARAMETERS = %w[sf key bs req tr name]
|
|
92
149
|
private_constant :KNOWN_PARAMETERS
|
|
93
150
|
|
|
151
|
+
# Retrieves a component value with parameter processing.
|
|
94
152
|
def retrieve(name, method)
|
|
95
153
|
if !name.parameters.empty?
|
|
96
154
|
valid_params = validate_parameters(name, method)
|
|
@@ -117,6 +175,8 @@ module Linzer
|
|
|
117
175
|
end
|
|
118
176
|
end
|
|
119
177
|
|
|
178
|
+
# Processes a structured field value with optional key extraction.
|
|
179
|
+
# @see https://www.rfc-editor.org/rfc/rfc9421.html#section-2.1.1
|
|
120
180
|
def sf(value, key = nil)
|
|
121
181
|
dict = Starry.parse_dictionary(value)
|
|
122
182
|
|
|
@@ -128,14 +188,19 @@ module Linzer
|
|
|
128
188
|
end
|
|
129
189
|
end
|
|
130
190
|
|
|
191
|
+
# Binary-wraps a field value.
|
|
192
|
+
# @see https://www.rfc-editor.org/rfc/rfc9421.html#section-2.1.3
|
|
131
193
|
def bs(value)
|
|
132
194
|
Starry.serialize(value.encode(Encoding::ASCII_8BIT))
|
|
133
195
|
end
|
|
134
196
|
|
|
197
|
+
# Retrieves a trailer field value.
|
|
198
|
+
# @abstract Subclasses should implement if trailer support is needed.
|
|
135
199
|
def tr(trailer)
|
|
136
|
-
|
|
200
|
+
raise Error, "Sub-classes are required to implement this method!"
|
|
137
201
|
end
|
|
138
202
|
|
|
203
|
+
# Retrieves a field from the attached request.
|
|
139
204
|
def req(field, method)
|
|
140
205
|
attached_request? ? @attached_request[String(field)] : nil
|
|
141
206
|
end
|
|
@@ -5,17 +5,44 @@ require "cgi"
|
|
|
5
5
|
module Linzer
|
|
6
6
|
class Message
|
|
7
7
|
module Adapter
|
|
8
|
+
# Generic adapters for HTTP messages.
|
|
9
|
+
#
|
|
10
|
+
# These adapters provide a base implementation that can be extended
|
|
11
|
+
# for HTTP libraries not directly supported by Linzer.
|
|
8
12
|
module Generic
|
|
13
|
+
# Generic HTTP request adapter.
|
|
14
|
+
#
|
|
15
|
+
# Provides a base implementation for request message access.
|
|
16
|
+
# Assumes the operation responds to `[]` for header access and
|
|
17
|
+
# has a `uri` attribute.
|
|
18
|
+
#
|
|
19
|
+
# @example Creating a custom adapter
|
|
20
|
+
# class MyRequestAdapter < Linzer::Message::Adapter::Generic::Request
|
|
21
|
+
# private
|
|
22
|
+
# def derived(name)
|
|
23
|
+
# return @operation.http_method if name.value == "@method"
|
|
24
|
+
# super
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
9
27
|
class Request < Abstract
|
|
28
|
+
# Creates a new request adapter.
|
|
29
|
+
# @param operation [Object] The HTTP request object
|
|
30
|
+
# @param options [Hash] Additional options (unused in base class)
|
|
10
31
|
def initialize(operation, **options)
|
|
11
32
|
@operation = operation
|
|
12
33
|
freeze
|
|
13
34
|
end
|
|
14
35
|
|
|
36
|
+
# Retrieves a header value by name.
|
|
37
|
+
# @param name [String] The header name
|
|
38
|
+
# @return [String, nil] The header value
|
|
15
39
|
def header(name)
|
|
16
40
|
@operation[name]
|
|
17
41
|
end
|
|
18
42
|
|
|
43
|
+
# Attaches a signature to the request.
|
|
44
|
+
# @param signature [Signature] The signature to attach
|
|
45
|
+
# @return [Object] The underlying request object
|
|
19
46
|
def attach!(signature)
|
|
20
47
|
signature.to_h.each { |h, v| @operation[h] = v }
|
|
21
48
|
@operation
|
|
@@ -4,7 +4,18 @@ module Linzer
|
|
|
4
4
|
class Message
|
|
5
5
|
module Adapter
|
|
6
6
|
module Generic
|
|
7
|
+
# Generic HTTP response adapter.
|
|
8
|
+
#
|
|
9
|
+
# Provides a base implementation for response message access.
|
|
10
|
+
# Assumes the operation responds to `[]` for header access.
|
|
11
|
+
#
|
|
12
|
+
# @abstract Subclass must implement {#derived} method.
|
|
7
13
|
class Response < Abstract
|
|
14
|
+
# Creates a new response adapter.
|
|
15
|
+
# @param operation [Object] The HTTP response object
|
|
16
|
+
# @param options [Hash] Additional options
|
|
17
|
+
# @option options [Object] :attached_request An associated request
|
|
18
|
+
# for `;req` parameter support
|
|
8
19
|
def initialize(operation, **options)
|
|
9
20
|
@operation = operation
|
|
10
21
|
attached_request = options[:attached_request]
|
|
@@ -13,10 +24,16 @@ module Linzer
|
|
|
13
24
|
freeze
|
|
14
25
|
end
|
|
15
26
|
|
|
27
|
+
# Retrieves a header value by name.
|
|
28
|
+
# @param name [String] The header name
|
|
29
|
+
# @return [String, nil] The header value
|
|
16
30
|
def header(name)
|
|
17
31
|
@operation[name]
|
|
18
32
|
end
|
|
19
33
|
|
|
34
|
+
# Attaches a signature to the response.
|
|
35
|
+
# @param signature [Signature] The signature to attach
|
|
36
|
+
# @return [Object] The underlying response object
|
|
20
37
|
def attach!(signature)
|
|
21
38
|
signature.to_h.each { |h, v| @operation[h] = v }
|
|
22
39
|
@operation
|
|
@@ -3,7 +3,18 @@
|
|
|
3
3
|
module Linzer
|
|
4
4
|
class Message
|
|
5
5
|
module Adapter
|
|
6
|
+
# http.rb gem message adapters.
|
|
7
|
+
#
|
|
8
|
+
# Provides adapters for {HTTP::Request} and {HTTP::Response} objects
|
|
9
|
+
# from the http.rb gem.
|
|
10
|
+
#
|
|
11
|
+
# @note These adapters are loaded on-demand when using the
|
|
12
|
+
# {Linzer::HTTP::SignatureFeature}.
|
|
6
13
|
module HTTPGem
|
|
14
|
+
# Adapter for {HTTP::Request} objects from http.rb gem.
|
|
15
|
+
#
|
|
16
|
+
# Extends the generic request adapter with http.rb-specific
|
|
17
|
+
# method name retrieval.
|
|
7
18
|
class Request < Generic::Request
|
|
8
19
|
private
|
|
9
20
|
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# HTTP message adapter for HTTP::Response class from http ruby gem.
|
|
4
|
-
# https://github.com/httprb/http
|
|
5
|
-
#
|
|
6
|
-
# It's not loaded automatically to avoid making http gem a dependency.
|
|
7
|
-
#
|
|
8
3
|
module Linzer
|
|
9
4
|
class Message
|
|
10
5
|
module Adapter
|
|
11
6
|
module HTTPGem
|
|
7
|
+
# Adapter for {HTTP::Response} objects from http.rb gem.
|
|
8
|
+
#
|
|
9
|
+
# Extends the generic response adapter with http.rb-specific
|
|
10
|
+
# status code retrieval.
|
|
11
|
+
#
|
|
12
|
+
# @note Not loaded automatically to avoid making http gem a dependency.
|
|
13
|
+
#
|
|
14
|
+
# @see https://github.com/httprb/http http.rb gem
|
|
12
15
|
class Response < Generic::Response
|
|
13
16
|
private
|
|
14
17
|
|
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
module Linzer
|
|
4
4
|
class Message
|
|
5
5
|
module Adapter
|
|
6
|
+
# Net::HTTP message adapters.
|
|
7
|
+
#
|
|
8
|
+
# Provides adapters for {Net::HTTPRequest} and {Net::HTTPResponse} objects.
|
|
6
9
|
module NetHTTP
|
|
10
|
+
# Adapter for {Net::HTTPRequest} objects.
|
|
11
|
+
#
|
|
12
|
+
# Extends the generic request adapter with Net::HTTP-specific
|
|
13
|
+
# method name retrieval.
|
|
7
14
|
class Request < Generic::Request
|
|
8
15
|
private
|
|
9
16
|
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
module Linzer
|
|
4
4
|
class Message
|
|
5
5
|
module Adapter
|
|
6
|
+
# Rack HTTP message adapters.
|
|
7
|
+
#
|
|
8
|
+
# Provides adapters for {::Rack::Request} and {::Rack::Response} objects.
|
|
6
9
|
module Rack
|
|
10
|
+
# Shared functionality for Rack request and response adapters.
|
|
11
|
+
# @api private
|
|
7
12
|
module Common
|
|
8
13
|
DERIVED_COMPONENT = {
|
|
9
14
|
"@method" => :request_method,
|
|
@@ -61,14 +66,17 @@ module Linzer
|
|
|
61
66
|
|
|
62
67
|
def field(name)
|
|
63
68
|
has_tr = name.parameters["tr"]
|
|
64
|
-
if has_tr
|
|
65
|
-
|
|
69
|
+
return nil if has_tr
|
|
70
|
+
|
|
71
|
+
item_value = String(name.value)
|
|
72
|
+
field_value = if request?
|
|
73
|
+
rack_header_name = rack_header_name(item_value)
|
|
74
|
+
@operation.env[rack_header_name]
|
|
66
75
|
else
|
|
67
|
-
|
|
68
|
-
value = @operation.env[rack_header_name] if request?
|
|
69
|
-
value = @operation.get_header(name.value.to_s) if response?
|
|
76
|
+
@operation.get_header(item_value)
|
|
70
77
|
end
|
|
71
|
-
|
|
78
|
+
|
|
79
|
+
field_value.dup&.strip
|
|
72
80
|
end
|
|
73
81
|
|
|
74
82
|
def derive(operation, method)
|
|
@@ -4,19 +4,32 @@ module Linzer
|
|
|
4
4
|
class Message
|
|
5
5
|
module Adapter
|
|
6
6
|
module Rack
|
|
7
|
+
# Adapter for {::Rack::Request} objects.
|
|
8
|
+
#
|
|
9
|
+
# Handles the Rack-specific header naming conventions (HTTP_* prefix,
|
|
10
|
+
# uppercase, underscores).
|
|
7
11
|
class Request < Abstract
|
|
8
12
|
include Common
|
|
9
13
|
|
|
14
|
+
# Creates a new Rack request adapter.
|
|
15
|
+
# @param operation [::Rack::Request] The Rack request
|
|
16
|
+
# @param options [Hash] Additional options (unused)
|
|
10
17
|
def initialize(operation, **options)
|
|
11
18
|
@operation = operation
|
|
12
19
|
validate
|
|
13
20
|
freeze
|
|
14
21
|
end
|
|
15
22
|
|
|
23
|
+
# Retrieves a header value by name.
|
|
24
|
+
# @param name [String] The header name (e.g., "content-type")
|
|
25
|
+
# @return [String, nil] The header value
|
|
16
26
|
def header(name)
|
|
17
27
|
@operation.get_header(rack_header_name(name))
|
|
18
28
|
end
|
|
19
29
|
|
|
30
|
+
# Attaches a signature to the request.
|
|
31
|
+
# @param signature [Signature] The signature to attach
|
|
32
|
+
# @return [::Rack::Request] The request with signature headers
|
|
20
33
|
def attach!(signature)
|
|
21
34
|
signature.to_h.each do |h, v|
|
|
22
35
|
@operation.set_header(rack_header_name(h), v)
|
|
@@ -4,9 +4,14 @@ module Linzer
|
|
|
4
4
|
class Message
|
|
5
5
|
module Adapter
|
|
6
6
|
module Rack
|
|
7
|
+
# Adapter for {::Rack::Response} objects.
|
|
7
8
|
class Response < Abstract
|
|
8
9
|
include Common
|
|
9
10
|
|
|
11
|
+
# Creates a new Rack response adapter.
|
|
12
|
+
# @param operation [::Rack::Response] The Rack response
|
|
13
|
+
# @param options [Hash] Additional options
|
|
14
|
+
# @option options [Object] :attached_request Request for `;req` support
|
|
10
15
|
def initialize(operation, **options)
|
|
11
16
|
@operation = operation
|
|
12
17
|
validate
|
|
@@ -16,10 +21,16 @@ module Linzer
|
|
|
16
21
|
freeze
|
|
17
22
|
end
|
|
18
23
|
|
|
24
|
+
# Retrieves a header value by name.
|
|
25
|
+
# @param name [String] The header name
|
|
26
|
+
# @return [String, nil] The header value
|
|
19
27
|
def header(name)
|
|
20
28
|
@operation.get_header(name)
|
|
21
29
|
end
|
|
22
30
|
|
|
31
|
+
# Attaches a signature to the response.
|
|
32
|
+
# @param signature [Signature] The signature to attach
|
|
33
|
+
# @return [::Rack::Response] The response with signature headers
|
|
23
34
|
def attach!(signature)
|
|
24
35
|
signature.to_h.each do |h, v|
|
|
25
36
|
@operation.set_header(h, v)
|
|
@@ -8,3 +8,20 @@ require_relative "adapter/rack/request"
|
|
|
8
8
|
require_relative "adapter/rack/response"
|
|
9
9
|
require_relative "adapter/net_http/request"
|
|
10
10
|
require_relative "adapter/net_http/response"
|
|
11
|
+
|
|
12
|
+
module Linzer
|
|
13
|
+
class Message
|
|
14
|
+
# Namespace for HTTP message adapters.
|
|
15
|
+
#
|
|
16
|
+
# Adapters provide a unified interface for accessing HTTP message
|
|
17
|
+
# components across different HTTP libraries. Each supported library
|
|
18
|
+
# has its own adapter implementation.
|
|
19
|
+
#
|
|
20
|
+
# @see Abstract Base adapter class
|
|
21
|
+
# @see Rack Rack request/response adapters
|
|
22
|
+
# @see NetHTTP Net::HTTP request/response adapters
|
|
23
|
+
# @see Generic Generic adapters for extension
|
|
24
|
+
module Adapter
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -4,9 +4,23 @@ module Linzer
|
|
|
4
4
|
class Message
|
|
5
5
|
class Field
|
|
6
6
|
class Identifier
|
|
7
|
+
# Parses component identifier strings into structured items.
|
|
8
|
+
#
|
|
9
|
+
# Handles various formats:
|
|
10
|
+
# - Simple names: `"content-type"`
|
|
11
|
+
# - Derived components: `"@method"`
|
|
12
|
+
# - With parameters: `"content-type";bs`, `"example-dict";key="a"`
|
|
13
|
+
# - Already serialized: `'"content-type"'`
|
|
14
|
+
#
|
|
15
|
+
# @api private
|
|
7
16
|
module Parser
|
|
8
17
|
extend self
|
|
9
18
|
|
|
19
|
+
# Parses a field name into a structured item.
|
|
20
|
+
#
|
|
21
|
+
# @param field_name [String] The component identifier string
|
|
22
|
+
# @return [Starry::Item] The parsed structured field item
|
|
23
|
+
# @raise [Error] If the field name cannot be parsed
|
|
10
24
|
def parse(field_name)
|
|
11
25
|
case
|
|
12
26
|
when field_name.match?(/";/), field_name.start_with?('"')
|