linzer 0.7.0.beta1 → 0.7.0.beta3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a19b05f41aa933779f504056b56b00fc1a8a6cda19bb26065dce56177caf0c2
4
- data.tar.gz: 25d7d8b4afbd60a3e6e872064d92f9be22670466292e23a6ba8010ca546cdf23
3
+ metadata.gz: 65ccb6f89aab47730e202a8ad376b2ceb310213b2a140d194b2b6fd285aa710f
4
+ data.tar.gz: 485efd259e353048041528c38f0072945592cfe6f8d4c4e6ea20af5546e6cb94
5
5
  SHA512:
6
- metadata.gz: 527fd9a73a1605b706fc7ca597ce14f4f5c95be099907bf61832aa698b11fe51dbf25a5eaf70d48d7c6715c91c27bfa391482a88a67f0e932c5f60ff8f3f16df
7
- data.tar.gz: 2b30454801734973711afe81559ad18f09f6d08f473796241d56e1b13d5619971a697945703b85012e18ae969059ca5a3718e677fc71716151bd46e795eac8a7
6
+ metadata.gz: a01a27867e6dd628365268f1106880b7f61ad80b4d314ea336e21e4f1a4edbb17394d0c7da4f23256566e34a6acb2e6eec54680461141d917189a37219d3c84e
7
+ data.tar.gz: 2efef6b1fcad53964e1f54c8f1453b2ad3aa481ca98960ada2e735439d0736624118a99ab58d1f73fdef517f9802e1453215e17bfc660e56dae81cd5b044769d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.7.0.beta3] - 2025-05-06 (aka the ["MiniDebConf Hamburg 2025"](https://wiki.debian.org/DebianEvents/de/2025/MiniDebConfHamburg) release)
4
+
5
+ - Refactor to improve Linzer APIs and streamline its usage along with different
6
+ HTTP libraries.
7
+
8
+ ## [0.7.0.beta2] - 2025-04-13
9
+
10
+ - Refactor and improve Rack::Auth::Signature code organization.
11
+ - Do not expose secret material on HMAC SHA-256 key when #inspect method is used.
12
+ - Update Rack::Auth::Signature configuration file options.
13
+ - Validate and test Rack::Auth::Signature with example Rails and Sinatra apps.
14
+
3
15
  ## [0.7.0.beta1] - 2025-04-12
4
16
 
5
17
  - Introduce Rack::Auth::Signature middleware.
data/README.md CHANGED
@@ -23,12 +23,14 @@ Or just `gem install linzer`.
23
23
 
24
24
  ### TL;DR: I just want to protect my application!!
25
25
 
26
- Add the following middleware to you run Rack application, e.g.:
26
+ Add the following middleware to your Rack application and configure it
27
+ as needed, e.g.:
27
28
 
28
29
  ```ruby
29
30
  # config.ru
30
31
  use Rack::Auth::Signature, except: "/login",
31
- default_key: {"key" => IO.read("app/config/pubkey.pem"), "alg" => "ed25519"}
32
+ default_key: {material: Base64.strict_decode64(ENV["MYAPP_KEY"]), alg: "hmac-sha256"}
33
+ # or: default_key: {material: IO.read("app/config/pubkey.pem"), "ed25519"}
32
34
  ```
33
35
 
34
36
  or on more complex scenarios:
@@ -39,6 +41,14 @@ use Rack::Auth::Signature, except: "/login",
39
41
  config_path: "app/configuration/http-signatures.yml"
40
42
  ```
41
43
 
44
+ or with a typical Rails application:
45
+
46
+ ```ruby
47
+ # config/application.rb
48
+ config.middleware.use Rack::Auth::Signature, except: "/login",
49
+ config_path: "http-signatures.yml"
50
+ ```
51
+
42
52
  And that's it, all routes in the example app (except `/login`) above will
43
53
  require a valid signature created with the respective private key held by a
44
54
  client. For more details on what configuration options are available, take a
@@ -48,46 +58,40 @@ look at
48
58
 
49
59
  To learn about more specific scenarios or use cases, keep reading on below.
50
60
 
51
- ### To sign a HTTP message:
61
+ ### To sign a HTTP request:
52
62
 
53
63
  ```ruby
54
64
  key = Linzer.generate_ed25519_key
55
65
  # => #<Linzer::Ed25519::Key:0x00000fe13e9bd208
56
66
 
57
- headers = {
58
- "date" => "Fri, 23 Feb 2024 17:57:23 GMT",
59
- "x-custom-header" => "foo"
60
- }
61
-
62
- request = Linzer.new_request(:post, "/some_uri", {}, headers)
63
- # => #<Rack::Request:0x0000000104c1c8c0
64
- # @env={"HTTP_DATE"=>"Fri, 23 Feb 2024 17:57:23 GMT", "HTTP_X_CUSTOM..."
65
- # @params=nil>
66
-
67
- message = Linzer::Message.new(request)
68
- # => #<Linzer::Message:0x0000000104afa960
69
- # @operation=#<Rack::Request:0x00000001049754a0
70
- # @env={"HTTP_DATE"=>"Fri, 23 Feb 2024 17:57:23 GMT", "HTTP_X_CUSTOM..."
71
- # @params=nil>>
72
-
73
- fields = %w[date x-custom-header @method @path]
74
-
75
- signature = Linzer.sign(key, message, fields)
76
- # => #<Linzer::Signature:0x0000000111f77ad0 ...
77
-
78
- pp signature.to_h
79
- # => {"signature"=>"sig1=:Cv1TUCxUpX+5SVa7pH0Xh...",
80
- # "signature-input"=>"sig1=(\"date\" \"x-custom-header\" ..."}
67
+ uri = URI("https://example.org/api/task")
68
+ request = Net::HTTP::Get.new(uri)
69
+ request["date"] = Time.now.to_s
70
+
71
+ Linzer.sign!(
72
+ request,
73
+ key: key,
74
+ components: %w[@method @request-target date],
75
+ label: "sig1",
76
+ params: {
77
+ created: Time.now.to_i
78
+ }
79
+ )
80
+
81
+ request["signature"]
82
+ # => "sig1=:Cv1TUCxUpX+5SVa7pH0Xh..."
83
+ request["signature-input"]
84
+ # => "sig1=(\"@method\" \"@request-target\" \"date\" ..."}
81
85
  ```
82
86
 
83
- ### Use the message signature with any HTTP client:
87
+ ### Use the signed request with an HTTP client:
84
88
 
85
89
  ```ruby
86
90
  require "net/http"
87
91
 
88
- http = Net::HTTP.new("localhost", 9292)
92
+ http = Net::HTTP.new(uri.host, uri.port)
89
93
  http.set_debug_output($stderr)
90
- response = http.post("/some_uri", "data", headers.merge(signature.to_h))
94
+ response = http.request(request)
91
95
  # opening connection to localhost:9292...
92
96
  # opened
93
97
  # <- "POST /some_uri HTTP/1.1\r\n
@@ -121,7 +125,123 @@ response = http.post("/some_uri", "data", headers.merge(signature.to_h))
121
125
  # => #<Net::HTTPOK 200 OK readbody=true>
122
126
  ```
123
127
 
124
- ### To verify a valid signature:
128
+ ### To verify an incoming request on the server side:
129
+
130
+ The middleware `Rack::Auth::Signature` can be used for this scenario
131
+ [as shown above](#tldr-i-just-want-to-protect-my-application).
132
+
133
+ Or directly in the application controller (or routes), the incoming request can
134
+ be verified with the following approach:
135
+
136
+ ```ruby
137
+ post "/foo" do
138
+ request
139
+ # =>
140
+ # #<Sinatra::Request:0x000000011e5a5d60
141
+ # @env=
142
+ # {"GATEWAY_INTERFACE" => "CGI/1.1",
143
+ # "PATH_INFO" => "/api",
144
+ # ...
145
+
146
+ result = Linzer.verify!(request, key: some_client_key)
147
+ # => true
148
+ ...
149
+ end
150
+ ```
151
+
152
+ If the signature is missing or invalid, the verification method will raise an
153
+ exception with a message clarifying why the request signature failed verification.
154
+
155
+ Also, for additional flexibility on the server side, the method above can take
156
+ a block with the `keyid` parameter extracted from the signature (if any) as argument.
157
+ This can be useful to retrieve key data from databases/caches on the server side, e.g.:
158
+
159
+ ```ruby
160
+ get "/bar" do
161
+ ...
162
+ result = Linzer.verify!(request) do |keyid|
163
+ retrieve_pubkey_from_db(db_client, keyid)
164
+ end
165
+ # => true
166
+ ...
167
+ end
168
+ ```
169
+
170
+ ### To verify a received response on the client side:
171
+
172
+ It's similar to verifying requests, the same method is used, see example below:
173
+
174
+ ```ruby
175
+ response
176
+ # => #<Net::HTTPOK 200 OK readbody=true>
177
+ response.body
178
+ # => "protected"
179
+ pubkey = Linzer.new_ed25519_key(IO.read("pubkey.pem"))
180
+ result = Linzer.verify!(response, key: pubkey, no_older_than: 600)
181
+ # => true
182
+ ```
183
+
184
+ ### To sign an outgoing response on the server side:
185
+
186
+ Again, the same principle used to sign outgoing requests, the same method is used,
187
+ see example below:
188
+
189
+ ```ruby
190
+ put "/baz" do
191
+ ...
192
+ response
193
+ # => #<Sinatra::Response:0x0000000109ac40b8 ...
194
+ response.headers["x-custom-app-header"] = "..."
195
+ Linzer.sign!(response,
196
+ key: my_key,
197
+ components: %w[@status content-type content-digest x-custom-app-header],
198
+ label: "sig1",
199
+ params: {
200
+ created: Time.now.to_i
201
+ }
202
+ )
203
+ response["signature"]
204
+ # => "sig1=:2TPCzD4l48bg6LMcVXdV9u..."
205
+ response["signature-input"]
206
+ # => "sig1=(\"@status\" \"content-type\" \"content-digest\"..."
207
+ ...
208
+ end
209
+ ```
210
+
211
+ ### What do you do if you want to sign/verify requests and responses with your preferred HTTP ruby library/framework (not using Rack or `Net::HTTP`, for example)?
212
+
213
+ You can provide an adapter class and then register it with this library.
214
+ For guidance on how to implement such adapters, you can consult an
215
+ [example adapter for http gem response](https://github.com/nomadium/linzer/blob/master/lib/linzer/message/adapter/http_gem/response.rb)
216
+ included with this gem or the ones
217
+ [provided out of the box](https://github.com/nomadium/linzer/blob/master/lib/linzer/message/adapter).
218
+
219
+ For how to register a custom adapter and how to verify signatures in a response,
220
+ see this example:
221
+
222
+ ```ruby
223
+ Linzer::Message.register_adapter(HTTP::Response, MyOwnResponseAdapter)
224
+ response = HTTP.get("http://www.example.com/api/service/task")
225
+ # => #<HTTP::Response/1.1 200 OK ...
226
+ response["signature"]
227
+ => "sig1=:oqzDlQmfejfT..."
228
+ response["signature-input"]
229
+ => "sig1=(\"@status\" \"foo\");created=1746480237"
230
+ result = Linzer.verify!(response, key: my_key)
231
+ # => true
232
+ ```
233
+ ---
234
+
235
+ Furthermore, on some low-level scenarios where a user wants or needs additional
236
+ control on how the signing and verification routines are performed, Linzer allows
237
+ to manipulate instances of internal HTTP messages (requests & responses, see
238
+ `Linzer::Message` class and available adapters), signature objects
239
+ (`Linzer::Signature`) and how to register additional message adapters for any
240
+ HTTP ruby library not supported out of the box by this gem.
241
+
242
+ See below for a few examples of these scenarios.
243
+
244
+ #### To verify a valid signature:
125
245
 
126
246
  ```ruby
127
247
  test_ed25519_key_pub = key.material.public_to_pem
@@ -130,12 +250,6 @@ test_ed25519_key_pub = key.material.public_to_pem
130
250
  pubkey = Linzer.new_ed25519_public_key(test_ed25519_key_pub, "some-key-ed25519")
131
251
  # => #<Linzer::Ed25519::Key:0x00000fe19b9384b0
132
252
 
133
- # if you have to, there is a helper method to build a request object on the server side
134
- # although any standard Ruby web server or framework (Sinatra, Rails, etc) should expose
135
- # a request object and this should not be required for most cases.
136
- #
137
- # request = Linzer.new_request(:post, "/some_uri", {}, headers)
138
-
139
253
  message = Linzer::Message.new(request)
140
254
 
141
255
  signature = Linzer::Signature.build(message.headers)
@@ -153,14 +267,14 @@ Linzer.verify(pubkey, message, signature, no_older_than: 500)
153
267
  `no_older_than` expects a number of seconds, but you can pass anything that to responds to `#to_i`, including an `ActiveSupport::Duration`.
154
268
  `::verify` will raise if the `created` parameter of the signature is older than the given number of seconds.
155
269
 
156
- ### What if an invalid signature if verified?
270
+ #### What if an invalid signature if verified?
157
271
 
158
272
  ```ruby
159
273
  result = Linzer.verify(pubkey, message, signature)
160
274
  lib/linzer/verifier.rb:38:in `verify_or_fail': Failed to verify message: Invalid signature. (Linzer::Error)
161
275
  ```
162
276
 
163
- ### HTTP responses are also supported
277
+ #### HTTP responses are also supported
164
278
 
165
279
  HTTP responses can also be signed and verified in the same way as requests.
166
280
 
@@ -6,4 +6,4 @@ gem "sinatra", "~> 4.0"
6
6
  gem "rackup", "~> 2.1"
7
7
  gem "webrick", "~> 1.9"
8
8
  gem "rack-contrib", "~> 2.4"
9
- gem "linzer", "~> 0.7.0.beta1"
9
+ gem "linzer", "~> 0.7.0.beta2"
@@ -1,26 +1,33 @@
1
1
  ---
2
- no_older_than: 6000
3
- created_required: true
4
- keyid_required: true
5
- # nonce_required: false
6
- # alg_required: false
7
- # tag_required: false
8
- # expires_required: false
9
- covered_components:
10
- - "@method"
11
- - "@request-target"
12
- - date
13
- known_keys:
2
+ signatures:
3
+ reject_older_than: 6000 # seconds
4
+ created_required: true
5
+ keyid_required: true
6
+ # nonce_required: false
7
+ # alg_required: false
8
+ # tag_required: false
9
+ # expires_required: false
10
+ covered_components:
11
+ - "@method"
12
+ - "@request-target"
13
+ - date
14
+ # In most cases is not needed to configure a label but it
15
+ # could useful in the event of receiving a signature
16
+ # header with more than 1 signature. Currently, linzer signatures
17
+ # middleware will only validate 1 signature per request and multiple
18
+ # signatures validation at the same time are not supported.
19
+ # If you need this, feel free to open an issue and explain your use case.
20
+ # default_label: "mylabel"
21
+ keys:
14
22
  foo:
15
23
  alg: ed25519
16
- key: |
24
+ material: |
17
25
  -----BEGIN PUBLIC KEY-----
18
26
  MCowBQYDK2VwAyEAMEH9bSanwgAWE5qxUEaXjK6qei8z2hiHT0nlr7ljG0Y=
19
27
  -----END PUBLIC KEY-----
20
28
  bar:
21
- alg: hmac-sha256
22
- key: !binary |-
23
- KuOR/Q8U1Crgp0WsFqW+5ZuC+KbN41dIlXWp71UhPEeJcRfuxZEy6XAUE9nf0yl4jt55yRASBnD0kQKucO6SfA==
24
- baz:
25
29
  alg: rsa-pss-sha512
26
- path: rsa.pem
30
+ path: pubkey_rsa.pem
31
+ baz:
32
+ alg: hmac-sha256
33
+ path: app.secret
@@ -6,5 +6,16 @@ get "/" do
6
6
  end
7
7
 
8
8
  get "/protected" do
9
+ # if signature is valid, rack will expose it in the request object,
10
+ # so application specific checks can be performed, e.g.:
11
+ #
12
+ # halt if !request.env["rack.signature"].parameters["nonce"]
13
+ # halt if redis.exists?(request.env["rack.signature"].parameters["nonce"])
14
+ # halt unless request.env["rack.signature"].parameters["tag"] == "myapp"
15
+ #
16
+ # Note that these examples are deliberately simple for illustration
17
+ # purposes since such validations would make more sense to be
18
+ # encapsulated in helper methods called in a before { ... } block.
19
+ #
9
20
  "secure area"
10
21
  end
data/lib/linzer/hmac.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "digest"
4
+
3
5
  module Linzer
4
6
  module HMAC
5
7
  class Key < Linzer::Key
@@ -15,6 +17,17 @@ module Linzer
15
17
  def verify(signature, data)
16
18
  signature == sign(data)
17
19
  end
20
+
21
+ def inspect
22
+ vars =
23
+ instance_variables
24
+ .reject { |v| v == :@material } # don't leak secret unneccesarily
25
+ .map do |n|
26
+ "#{n}=#{instance_variable_get(n).inspect}"
27
+ end
28
+ oid = Digest::SHA2.hexdigest(object_id.to_s)[48..63]
29
+ "#<%s:0x%s %s>" % [self.class, oid, vars.join(", ")]
30
+ end
18
31
  end
19
32
  end
20
33
  end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ class Message
5
+ module Adapter
6
+ class Abstract
7
+ def initialize(operation, **options)
8
+ raise Linzer::Error, "Cannot instantiate an abstract class!"
9
+ end
10
+
11
+ def request?
12
+ self.class.to_s.include?("Request")
13
+ end
14
+
15
+ def response?
16
+ self.class.to_s.include?("Response")
17
+ end
18
+
19
+ # XXX: attached request as specified in RFC has to be tested for Net::HTTP classes
20
+ # and custom HTTP message classes
21
+ def attached_request?
22
+ response? && !!@attached_request
23
+ end
24
+
25
+ def field?(f)
26
+ !!self[f]
27
+ end
28
+
29
+ def [](field_name)
30
+ name = parse_field_name(field_name)
31
+ return nil if name.nil?
32
+
33
+ if field_name.start_with?("@")
34
+ retrieve(name, :derived)
35
+ else
36
+ retrieve(name, :field)
37
+ end
38
+ end
39
+
40
+ def headers
41
+ raise Linzer::Error, "Sub-classes are required to implement this method!"
42
+ end
43
+
44
+ def attach!(signature)
45
+ raise Linzer::Error, "Sub-classes are required to implement this method!"
46
+ end
47
+
48
+ private
49
+
50
+ def parse_field_name(field_name)
51
+ if field_name&.start_with?("@")
52
+ Starry.parse_item(field_name[1..])
53
+ else
54
+ Starry.parse_item(field_name)
55
+ end
56
+ rescue => _
57
+ nil
58
+ end
59
+
60
+ def validate_attached_request(message)
61
+ msg = "The attached message is not a valid HTTP request!"
62
+ raise Linzer::Error, msg unless message.request?
63
+ end
64
+
65
+ def validate_parameters(name, method)
66
+ has_unknown = name.parameters.any? { |p, _| !KNOWN_PARAMETERS.include?(p) }
67
+ return nil if has_unknown
68
+
69
+ has_name = name.parameters["name"]
70
+ has_req = name.parameters["req"]
71
+ has_sf = name.parameters["sf"] || name.parameters.key?("key")
72
+ has_bs = name.parameters["bs"]
73
+ value = name.value
74
+
75
+ # Section 2.2.8 of RFC 9421
76
+ return nil if has_name && value != :"query-param"
77
+
78
+ # No derived values come from trailers section
79
+ return nil if method == :derived && name.parameters["tr"]
80
+
81
+ # From: 2.1. HTTP Fields:
82
+ # The bs parameter, which requires the raw bytes of the field values
83
+ # from the message, is not compatible with the use of the sf or key
84
+ # parameters, which require the parsed data structures of the field
85
+ # values after combination
86
+ return nil if has_sf && has_bs
87
+
88
+ # req param only makes sense on responses with an associated request
89
+ # return nil if has_req && (!response? || !attached_request?)
90
+ return nil if has_req && !response?
91
+
92
+ name
93
+ end
94
+
95
+ KNOWN_PARAMETERS = %w[sf key bs req tr name]
96
+ private_constant :KNOWN_PARAMETERS
97
+
98
+ def retrieve(name, method)
99
+ if !name.parameters.empty?
100
+ valid_params = validate_parameters(name, method)
101
+ return nil if !valid_params
102
+ end
103
+
104
+ has_req = name.parameters["req"]
105
+ has_sf = name.parameters["sf"] || name.parameters.key?("key")
106
+ has_bs = name.parameters["bs"]
107
+
108
+ if has_req
109
+ name.parameters.delete("req")
110
+ return req(name, method)
111
+ end
112
+
113
+ value = send(method, name)
114
+
115
+ case
116
+ when has_sf
117
+ key = name.parameters["key"]
118
+ sf(value, key)
119
+ when has_bs then bs(value)
120
+ else value
121
+ end
122
+ end
123
+
124
+ def sf(value, key = nil)
125
+ dict = Starry.parse_dictionary(value)
126
+
127
+ if key
128
+ obj = dict[key]
129
+ Starry.serialize(obj.is_a?(Starry::InnerList) ? [obj] : obj)
130
+ else
131
+ Starry.serialize(dict)
132
+ end
133
+ end
134
+
135
+ def bs(value)
136
+ Starry.serialize(value.encode(Encoding::ASCII_8BIT))
137
+ end
138
+
139
+ def tr(trailer)
140
+ @operation.body.trailers[trailer.value.to_s]
141
+ end
142
+
143
+ def req(field, method)
144
+ case method
145
+ when :derived then @attached_request["@#{field}"]
146
+ when :field then @attached_request[field.to_s]
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Example HTTP message adapter for HTTP::Response class from http ruby gem.
4
+ # https://github.com/httprb/http
5
+ # It's not required automatically to avoid making http gem a dependency.
6
+ #
7
+ module Linzer
8
+ class Message
9
+ module Adapter
10
+ module HTTPGem
11
+ class Response < Abstract
12
+ def initialize(operation, **options)
13
+ @operation = operation
14
+ freeze
15
+ end
16
+
17
+ def headers
18
+ @operation.headers
19
+ end
20
+
21
+ # XXX: this implementation is incomplete, e.g.: ;tr parameter is not supported yet
22
+ def [](field_name)
23
+ return @operation.code if field_name == "@status"
24
+ @operation[field_name]
25
+ end
26
+
27
+ def attach!(signature)
28
+ signature.to_h.each { |h, v| @operation[h] = v }
29
+ @operation
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module Linzer
6
+ class Message
7
+ module Adapter
8
+ module NetHTTP
9
+ class Request < Abstract
10
+ def initialize(operation, **options)
11
+ @operation = operation
12
+ freeze
13
+ end
14
+
15
+ def headers
16
+ @operation.each_header.to_h
17
+ end
18
+
19
+ def attach!(signature)
20
+ signature.to_h.each { |h, v| @operation[h] = v }
21
+ @operation
22
+ end
23
+
24
+ private
25
+
26
+ def derived(name)
27
+ case name.value
28
+ when :method then @operation.method
29
+ when :"target-uri" then @operation.uri.to_s
30
+ when :authority then @operation.uri.authority.downcase
31
+ when :scheme then @operation.uri.scheme.downcase
32
+ when :"request-target" then @operation.uri.request_uri
33
+ when :path then @operation.uri.path
34
+ when :query then "?%s" % String(@operation.uri.query)
35
+ when :"query-param" then query_param(name)
36
+ end
37
+ end
38
+
39
+ def query_param(name)
40
+ param_name = name.parameters["name"]
41
+ return nil if !param_name
42
+ decoded_param_name = URI.decode_uri_component(param_name)
43
+ params = CGI.parse(@operation.uri.query)
44
+ URI.encode_uri_component(params[decoded_param_name]&.first)
45
+ end
46
+
47
+ def field(name)
48
+ has_tr = name.parameters["tr"]
49
+ return nil if has_tr # HTTP requests don't have trailer fields
50
+ value = @operation[name.value.to_s]
51
+ value.dup&.strip
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ class Message
5
+ module Adapter
6
+ module NetHTTP
7
+ class Response < Abstract
8
+ def initialize(operation, **options)
9
+ @operation = operation
10
+ attached_request = options[:attached_request]
11
+ @attached_request = attached_request ? Message.new(attached_request) : nil
12
+ validate_attached_request @attached_request if @attached_request
13
+ freeze
14
+ end
15
+
16
+ def headers
17
+ @operation.each_header.to_h
18
+ end
19
+
20
+ # XXX: this implementation is incomplete, e.g.: ;tr parameter is not supported yet
21
+ def [](field_name)
22
+ return @operation.code.to_i if field_name == "@status"
23
+ @operation[field_name]
24
+ end
25
+
26
+ def attach!(signature)
27
+ signature.to_h.each { |h, v| @operation[h] = v }
28
+ @operation
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end