linzer 0.7.0.beta2 → 0.7.0.beta4
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 +10 -0
- data/README.md +186 -36
- data/lib/linzer/http/bootstrap.rb +21 -0
- data/lib/linzer/http/signature_feature.rb +53 -0
- data/lib/linzer/http.rb +100 -0
- data/lib/linzer/message/adapter/abstract.rb +152 -0
- data/lib/linzer/message/adapter/http_gem/request.rb +22 -0
- data/lib/linzer/message/adapter/http_gem/response.rb +35 -0
- data/lib/linzer/message/adapter/net_http/request.rb +57 -0
- data/lib/linzer/message/adapter/net_http/response.rb +34 -0
- data/lib/linzer/message/adapter/rack/common.rb +106 -0
- data/lib/linzer/message/adapter/rack/request.rb +30 -0
- data/lib/linzer/message/adapter/rack/response.rb +31 -0
- data/lib/linzer/message/adapter.rb +8 -0
- data/lib/linzer/message/wrapper.rb +52 -0
- data/lib/linzer/message.rb +13 -192
- data/lib/linzer/options.rb +9 -0
- data/lib/linzer/signature.rb +7 -1
- data/lib/linzer/version.rb +1 -1
- data/lib/linzer.rb +27 -7
- data/lib/rack/auth/signature/helpers.rb +2 -0
- data/lib/rack/auth/signature.rb +2 -0
- metadata +64 -4
- data/lib/linzer/request.rb +0 -95
- data/lib/linzer/response.rb +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5cfa8303792377ef25d8a81300b1e22c2e6593237436e152ec04e99176eea1f7
|
4
|
+
data.tar.gz: e2f89b7b4d771a9912000961862bf77cda5ccf2caf40a3906394afa1b1595adc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3881ae41ea932485f39838bba647f60fce089f740dabe7db7e01d34c368ebaf85ec71788d997d64d9cc662811dd703babd62300f7d02c6a21072c1dafbb45ee5
|
7
|
+
data.tar.gz: 7a9589f81440612f7e3bc959973aa947614c132f67db247e043f911d1653c7621c376ade8005181fa6fc4814e542ec82556bf79ec5f2b0d3ede739d48d4ffeb0
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,15 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.7.0.beta4] - 2025-05-17
|
4
|
+
|
5
|
+
- Provide integration with http.rb gem to allow signing outgoing HTTP requests.
|
6
|
+
- Add simple HTTP client module.
|
7
|
+
|
8
|
+
## [0.7.0.beta3] - 2025-05-06 (aka the ["MiniDebConf Hamburg 2025"](https://wiki.debian.org/DebianEvents/de/2025/MiniDebConfHamburg) release)
|
9
|
+
|
10
|
+
- Refactor to improve Linzer APIs and streamline its usage along with different
|
11
|
+
HTTP libraries.
|
12
|
+
|
3
13
|
## [0.7.0.beta2] - 2025-04-13
|
4
14
|
|
5
15
|
- Refactor and improve Rack::Auth::Signature code organization.
|
data/README.md
CHANGED
@@ -23,7 +23,7 @@ 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
|
26
|
+
Add the following middleware to your Rack application and configure it
|
27
27
|
as needed, e.g.:
|
28
28
|
|
29
29
|
```ruby
|
@@ -58,46 +58,65 @@ look at
|
|
58
58
|
|
59
59
|
To learn about more specific scenarios or use cases, keep reading on below.
|
60
60
|
|
61
|
-
### To sign a HTTP
|
61
|
+
### To sign a HTTP request:
|
62
62
|
|
63
|
-
|
64
|
-
key = Linzer.generate_ed25519_key
|
65
|
-
# => #<Linzer::Ed25519::Key:0x00000fe13e9bd208
|
63
|
+
There are several options:
|
66
64
|
|
67
|
-
|
68
|
-
"date" => "Fri, 23 Feb 2024 17:57:23 GMT",
|
69
|
-
"x-custom-header" => "foo"
|
70
|
-
}
|
65
|
+
#### If you are using http gem:
|
71
66
|
|
72
|
-
|
73
|
-
#
|
74
|
-
|
75
|
-
# @params=nil>
|
67
|
+
```ruby
|
68
|
+
# first require http signatures feature class ready to be used with http gem:
|
69
|
+
require "linzer/http/signature_feature"
|
76
70
|
|
77
|
-
|
78
|
-
# => #<Linzer::
|
79
|
-
#
|
80
|
-
#
|
81
|
-
|
71
|
+
key = Linzer.generate_ed25519_key # generate a new key pair
|
72
|
+
# => #<Linzer::Ed25519::Key:0x00000fe13e9bd208
|
73
|
+
# or load an existing key with:
|
74
|
+
# key = Linzer.new_ed25519_key(IO.read("key"), "mykeyid")
|
75
|
+
|
76
|
+
# then send the request:
|
77
|
+
url = "https://example.org/api"
|
78
|
+
response = HTTP.headers(date: Time.now.to_s, foo: "bar")
|
79
|
+
.use(http_signature: {key: key} # <--- covered components
|
80
|
+
.get(url) # and signature params can also be customized on the client
|
81
|
+
=> #<HTTP::Response/1.1 200 OK {"Content-Type" => ...
|
82
|
+
response.body.to_s
|
83
|
+
=> "protected content..."
|
84
|
+
```
|
82
85
|
|
83
|
-
|
86
|
+
#### If you are using plain old Net::HTTP:
|
84
87
|
|
85
|
-
|
86
|
-
|
88
|
+
```ruby
|
89
|
+
key = Linzer.generate_ed25519_key
|
90
|
+
# => #<Linzer::Ed25519::Key:0x00000fe13e9bd208
|
87
91
|
|
88
|
-
|
89
|
-
|
90
|
-
|
92
|
+
uri = URI("https://example.org/api/task")
|
93
|
+
request = Net::HTTP::Get.new(uri)
|
94
|
+
request["date"] = Time.now.to_s
|
95
|
+
|
96
|
+
Linzer.sign!(
|
97
|
+
request,
|
98
|
+
key: key,
|
99
|
+
components: %w[@method @request-target date],
|
100
|
+
label: "sig1",
|
101
|
+
params: {
|
102
|
+
created: Time.now.to_i
|
103
|
+
}
|
104
|
+
)
|
105
|
+
|
106
|
+
request["signature"]
|
107
|
+
# => "sig1=:Cv1TUCxUpX+5SVa7pH0Xh..."
|
108
|
+
request["signature-input"]
|
109
|
+
# => "sig1=(\"@method\" \"@request-target\" \"date\" ..."}
|
91
110
|
```
|
92
111
|
|
93
|
-
|
112
|
+
Then you can submit the signed request with Net::HTTP client:
|
94
113
|
|
95
114
|
```ruby
|
96
115
|
require "net/http"
|
97
116
|
|
98
|
-
http = Net::HTTP.new(
|
117
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
99
118
|
http.set_debug_output($stderr)
|
100
|
-
response = http.
|
119
|
+
response = http.request(request)
|
101
120
|
# opening connection to localhost:9292...
|
102
121
|
# opened
|
103
122
|
# <- "POST /some_uri HTTP/1.1\r\n
|
@@ -131,7 +150,144 @@ response = http.post("/some_uri", "data", headers.merge(signature.to_h))
|
|
131
150
|
# => #<Net::HTTPOK 200 OK readbody=true>
|
132
151
|
```
|
133
152
|
|
134
|
-
|
153
|
+
#### Or you can also use the simple HTTP client bundled with this library:
|
154
|
+
|
155
|
+
(This client is probably not suitable for production use but could be useful
|
156
|
+
enough to get started. It's build on top of Net::HTTP.)
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
key = Linzer.generate_rsa_pss_sha512_key(4096)
|
160
|
+
uri = URI("https://example.org/api/task")
|
161
|
+
headers = {"date" => Time.now.to_s}
|
162
|
+
response =
|
163
|
+
Linzer::HTTP
|
164
|
+
.post("http://httpbin.org/headers",
|
165
|
+
data: "foo",
|
166
|
+
debug: true,
|
167
|
+
key: key,
|
168
|
+
headers: headers)
|
169
|
+
...
|
170
|
+
=> #<Net::HTTPOK 200 OK readbody=true>
|
171
|
+
```
|
172
|
+
|
173
|
+
### To verify an incoming request on the server side:
|
174
|
+
|
175
|
+
The middleware `Rack::Auth::Signature` can be used for this scenario
|
176
|
+
[as shown above](#tldr-i-just-want-to-protect-my-application).
|
177
|
+
|
178
|
+
Or directly in the application controller (or routes), the incoming request can
|
179
|
+
be verified with the following approach:
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
post "/foo" do
|
183
|
+
request
|
184
|
+
# =>
|
185
|
+
# #<Sinatra::Request:0x000000011e5a5d60
|
186
|
+
# @env=
|
187
|
+
# {"GATEWAY_INTERFACE" => "CGI/1.1",
|
188
|
+
# "PATH_INFO" => "/api",
|
189
|
+
# ...
|
190
|
+
|
191
|
+
result = Linzer.verify!(request, key: some_client_key)
|
192
|
+
# => true
|
193
|
+
...
|
194
|
+
end
|
195
|
+
```
|
196
|
+
|
197
|
+
If the signature is missing or invalid, the verification method will raise an
|
198
|
+
exception with a message clarifying why the request signature failed verification.
|
199
|
+
|
200
|
+
Also, for additional flexibility on the server side, the method above can take
|
201
|
+
a block with the `keyid` parameter extracted from the signature (if any) as argument.
|
202
|
+
This can be useful to retrieve key data from databases/caches on the server side, e.g.:
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
get "/bar" do
|
206
|
+
...
|
207
|
+
result = Linzer.verify!(request) do |keyid|
|
208
|
+
retrieve_pubkey_from_db(db_client, keyid)
|
209
|
+
end
|
210
|
+
# => true
|
211
|
+
...
|
212
|
+
end
|
213
|
+
```
|
214
|
+
|
215
|
+
### To verify a received response on the client side:
|
216
|
+
|
217
|
+
It's similar to verifying requests, the same method is used, see example below:
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
response
|
221
|
+
# => #<Net::HTTPOK 200 OK readbody=true>
|
222
|
+
response.body
|
223
|
+
# => "protected"
|
224
|
+
pubkey = Linzer.new_ed25519_key(IO.read("pubkey.pem"))
|
225
|
+
result = Linzer.verify!(response, key: pubkey, no_older_than: 600)
|
226
|
+
# => true
|
227
|
+
```
|
228
|
+
|
229
|
+
### To sign an outgoing response on the server side:
|
230
|
+
|
231
|
+
Again, the same principle used to sign outgoing requests, the same method is used,
|
232
|
+
see example below:
|
233
|
+
|
234
|
+
```ruby
|
235
|
+
put "/baz" do
|
236
|
+
...
|
237
|
+
response
|
238
|
+
# => #<Sinatra::Response:0x0000000109ac40b8 ...
|
239
|
+
response.headers["x-custom-app-header"] = "..."
|
240
|
+
Linzer.sign!(response,
|
241
|
+
key: my_key,
|
242
|
+
components: %w[@status content-type content-digest x-custom-app-header],
|
243
|
+
label: "sig1",
|
244
|
+
params: {
|
245
|
+
created: Time.now.to_i
|
246
|
+
}
|
247
|
+
)
|
248
|
+
response["signature"]
|
249
|
+
# => "sig1=:2TPCzD4l48bg6LMcVXdV9u..."
|
250
|
+
response["signature-input"]
|
251
|
+
# => "sig1=(\"@status\" \"content-type\" \"content-digest\"..."
|
252
|
+
...
|
253
|
+
end
|
254
|
+
```
|
255
|
+
|
256
|
+
### 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)?
|
257
|
+
|
258
|
+
You can provide an adapter class and then register it with this library.
|
259
|
+
For guidance on how to implement such adapters, you can consult an
|
260
|
+
[example adapter for http gem response](https://github.com/nomadium/linzer/blob/master/lib/linzer/message/adapter/http_gem/response.rb)
|
261
|
+
included with this gem or the ones
|
262
|
+
[provided out of the box](https://github.com/nomadium/linzer/blob/master/lib/linzer/message/adapter).
|
263
|
+
|
264
|
+
For how to register a custom adapter and how to verify signatures in a response,
|
265
|
+
see this example:
|
266
|
+
|
267
|
+
```ruby
|
268
|
+
Linzer::Message.register_adapter(HTTP::Response, Linzer::Message::Adapter::HTTPGem::Response)
|
269
|
+
# Linzer::Message.register_adapter(HTTP::Response, MyOwnResponseAdapter) # or use your own adapter
|
270
|
+
response = HTTP.get("http://www.example.com/api/service/task")
|
271
|
+
# => #<HTTP::Response/1.1 200 OK ...
|
272
|
+
response["signature"]
|
273
|
+
=> "sig1=:oqzDlQmfejfT..."
|
274
|
+
response["signature-input"]
|
275
|
+
=> "sig1=(\"@status\" \"foo\");created=1746480237"
|
276
|
+
result = Linzer.verify!(response, key: my_key)
|
277
|
+
# => true
|
278
|
+
```
|
279
|
+
---
|
280
|
+
|
281
|
+
Furthermore, on some low-level scenarios where a user wants or needs additional
|
282
|
+
control on how the signing and verification routines are performed, Linzer allows
|
283
|
+
to manipulate instances of internal HTTP messages (requests & responses, see
|
284
|
+
`Linzer::Message` class and available adapters), signature objects
|
285
|
+
(`Linzer::Signature`) and how to register additional message adapters for any
|
286
|
+
HTTP ruby library not supported out of the box by this gem.
|
287
|
+
|
288
|
+
See below for a few examples of these scenarios.
|
289
|
+
|
290
|
+
#### To verify a valid signature:
|
135
291
|
|
136
292
|
```ruby
|
137
293
|
test_ed25519_key_pub = key.material.public_to_pem
|
@@ -140,12 +296,6 @@ test_ed25519_key_pub = key.material.public_to_pem
|
|
140
296
|
pubkey = Linzer.new_ed25519_public_key(test_ed25519_key_pub, "some-key-ed25519")
|
141
297
|
# => #<Linzer::Ed25519::Key:0x00000fe19b9384b0
|
142
298
|
|
143
|
-
# if you have to, there is a helper method to build a request object on the server side
|
144
|
-
# although any standard Ruby web server or framework (Sinatra, Rails, etc) should expose
|
145
|
-
# a request object and this should not be required for most cases.
|
146
|
-
#
|
147
|
-
# request = Linzer.new_request(:post, "/some_uri", {}, headers)
|
148
|
-
|
149
299
|
message = Linzer::Message.new(request)
|
150
300
|
|
151
301
|
signature = Linzer::Signature.build(message.headers)
|
@@ -163,14 +313,14 @@ Linzer.verify(pubkey, message, signature, no_older_than: 500)
|
|
163
313
|
`no_older_than` expects a number of seconds, but you can pass anything that to responds to `#to_i`, including an `ActiveSupport::Duration`.
|
164
314
|
`::verify` will raise if the `created` parameter of the signature is older than the given number of seconds.
|
165
315
|
|
166
|
-
|
316
|
+
#### What if an invalid signature if verified?
|
167
317
|
|
168
318
|
```ruby
|
169
319
|
result = Linzer.verify(pubkey, message, signature)
|
170
320
|
lib/linzer/verifier.rb:38:in `verify_or_fail': Failed to verify message: Invalid signature. (Linzer::Error)
|
171
321
|
```
|
172
322
|
|
173
|
-
|
323
|
+
#### HTTP responses are also supported
|
174
324
|
|
175
325
|
HTTP responses can also be signed and verified in the same way as requests.
|
176
326
|
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Linzer
|
4
|
+
module HTTP
|
5
|
+
module Bootstrap
|
6
|
+
class << self
|
7
|
+
def require_dependencies
|
8
|
+
require "http"
|
9
|
+
require_relative "../message/adapter/http_gem/request"
|
10
|
+
end
|
11
|
+
|
12
|
+
def load_dependencies
|
13
|
+
require_dependencies
|
14
|
+
rescue LoadError
|
15
|
+
msg = "http gem is required to be installed to use this feature."
|
16
|
+
raise Linzer::Error, msg
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "bootstrap"
|
4
|
+
|
5
|
+
module Linzer
|
6
|
+
module HTTP
|
7
|
+
class << self
|
8
|
+
def register_adapter
|
9
|
+
request_class = ::HTTP::Request
|
10
|
+
adapter_class = Linzer::Message::Adapter::HTTPGem::Request
|
11
|
+
Linzer::Message.register_adapter(request_class, adapter_class)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
Bootstrap.load_dependencies
|
16
|
+
register_adapter
|
17
|
+
|
18
|
+
class SignatureFeature < ::HTTP::Feature
|
19
|
+
def initialize(key:, params: {}, covered_components: default_components)
|
20
|
+
@fields = Array(covered_components)
|
21
|
+
@key = validate_key(key)
|
22
|
+
@params = Hash(params)
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :fields, :params
|
26
|
+
|
27
|
+
def wrap_request(request)
|
28
|
+
message = Linzer::Message.new(request)
|
29
|
+
signature = Linzer.sign(key, message, fields, **params)
|
30
|
+
request.headers.merge!(signature.to_h)
|
31
|
+
request
|
32
|
+
end
|
33
|
+
|
34
|
+
def default_covered_components
|
35
|
+
Linzer::Options::DEFAULT[:covered_components]
|
36
|
+
end
|
37
|
+
|
38
|
+
alias_method :default_components, :default_covered_components
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :key
|
43
|
+
|
44
|
+
def validate_key(key)
|
45
|
+
raise ::HTTP::Error, "Key can not be nil!" if !key
|
46
|
+
raise ::HTTP::Error, "Key object is invalid!" if !key.respond_to?(:sign)
|
47
|
+
key
|
48
|
+
end
|
49
|
+
|
50
|
+
::HTTP::Options.register_feature(:http_signature, self)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/linzer/http.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
|
5
|
+
module Linzer
|
6
|
+
module HTTP
|
7
|
+
extend self
|
8
|
+
|
9
|
+
def self.known_http_methods
|
10
|
+
Net::HTTP
|
11
|
+
.constants
|
12
|
+
.map { |const| Net::HTTP.const_get(const) }
|
13
|
+
.select { |klass| klass.is_a?(Class) && klass.const_defined?(:METHOD) }
|
14
|
+
.map { |klass| klass::METHOD }
|
15
|
+
.freeze
|
16
|
+
end
|
17
|
+
|
18
|
+
known_http_methods.each do |http_method| # e.g.:
|
19
|
+
method = http_method.downcase.to_sym #
|
20
|
+
define_method(method) do |uri, options| # def post(uri, **options)
|
21
|
+
options ||= {} # request :post, uri, options
|
22
|
+
request method, uri, options # end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def request(verb, uri, options = {})
|
29
|
+
validate_verb(verb)
|
30
|
+
|
31
|
+
key = options[:key]
|
32
|
+
validate_key(key)
|
33
|
+
|
34
|
+
req_uri = URI(uri)
|
35
|
+
http = Net::HTTP.new(req_uri.host, req_uri.port)
|
36
|
+
http.set_debug_output($stderr) if options[:debug]
|
37
|
+
|
38
|
+
headers = build_headers(options[:headers] || {})
|
39
|
+
request = build_request(verb, uri, headers)
|
40
|
+
message = Linzer::Message.new(request)
|
41
|
+
components = options[:covered_components] || default_components
|
42
|
+
params = options[:params] || {}
|
43
|
+
signature = Linzer.sign(key, message, components, **params)
|
44
|
+
|
45
|
+
do_request(http, uri, verb, options[:data], signature, headers)
|
46
|
+
end
|
47
|
+
|
48
|
+
def default_components
|
49
|
+
Linzer::Options::DEFAULT[:covered_components]
|
50
|
+
end
|
51
|
+
|
52
|
+
def validate_verb(verb)
|
53
|
+
method_name = verb.to_s.upcase
|
54
|
+
if !known_http_methods.include?(method_name)
|
55
|
+
raise Linzer::Error, "Unknown/unsupported HTTP method: '#{method_name}'"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def validate_key(key)
|
60
|
+
raise Linzer::Error, "Key can not be nil!" if !key
|
61
|
+
raise Linzer::Error, "Key object is invalid!" if !key.respond_to?(:sign)
|
62
|
+
key
|
63
|
+
end
|
64
|
+
|
65
|
+
def build_headers(headers)
|
66
|
+
headers.merge({"user-agent" => "Linzer/#{Linzer::VERSION}"})
|
67
|
+
end
|
68
|
+
|
69
|
+
def build_request(method, uri, headers)
|
70
|
+
request_class = Net::HTTP.const_get(method.to_s.capitalize)
|
71
|
+
request = request_class.new(URI(uri))
|
72
|
+
headers.map { |k, v| request[k] = v }
|
73
|
+
request
|
74
|
+
end
|
75
|
+
|
76
|
+
def with_body?(verb)
|
77
|
+
# common HTTP
|
78
|
+
return false if %i[get head options trace delete].include?(verb)
|
79
|
+
# WebDAV
|
80
|
+
return false if %i[copy move].include?(verb)
|
81
|
+
|
82
|
+
# everything else that could have a body:
|
83
|
+
# common HTTP: post, put, patch
|
84
|
+
# WebDAV: lock, unlock, mkcol, propfind, proppatch
|
85
|
+
true
|
86
|
+
end
|
87
|
+
|
88
|
+
def do_request(http, uri, verb, data, signature, headers)
|
89
|
+
if with_body?(verb)
|
90
|
+
if !data
|
91
|
+
missed_body = "Missing request body on HTTP request: '#{verb.upcase}'"
|
92
|
+
raise Linzer::Error, missed_body
|
93
|
+
end
|
94
|
+
http.public_send(verb, uri, data, headers.merge(signature.to_h))
|
95
|
+
else
|
96
|
+
http.public_send(verb, uri, headers.merge(signature.to_h))
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
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,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Linzer
|
4
|
+
class Message
|
5
|
+
module Adapter
|
6
|
+
module HTTPGem
|
7
|
+
class Request < Linzer::Message::Adapter::NetHTTP::Request
|
8
|
+
def headers
|
9
|
+
@operation.headers.to_h
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def derived(name)
|
15
|
+
return @operation.verb.to_s.upcase if name.value == :method
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
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
|