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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +153 -39
- data/examples/sinatra/Gemfile +1 -1
- data/examples/sinatra/http-signatures.yml +25 -18
- data/examples/sinatra/myapp.rb +11 -0
- data/lib/linzer/hmac.rb +13 -0
- data/lib/linzer/message/adapter/abstract.rb +152 -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/signature.rb +7 -1
- data/lib/linzer/version.rb +1 -1
- data/lib/linzer.rb +25 -7
- data/lib/rack/auth/signature/helpers.rb +132 -0
- data/lib/rack/auth/signature.rb +25 -116
- metadata +47 -5
- 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: 65ccb6f89aab47730e202a8ad376b2ceb310213b2a140d194b2b6fd285aa710f
|
4
|
+
data.tar.gz: 485efd259e353048041528c38f0072945592cfe6f8d4c4e6ea20af5546e6cb94
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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: {
|
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
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
request
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
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(
|
92
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
89
93
|
http.set_debug_output($stderr)
|
90
|
-
response = http.
|
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
|
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
|
-
|
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
|
-
|
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
|
|
data/examples/sinatra/Gemfile
CHANGED
@@ -1,26 +1,33 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
|
10
|
-
|
11
|
-
- "@
|
12
|
-
-
|
13
|
-
|
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
|
-
|
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:
|
30
|
+
path: pubkey_rsa.pem
|
31
|
+
baz:
|
32
|
+
alg: hmac-sha256
|
33
|
+
path: app.secret
|
data/examples/sinatra/myapp.rb
CHANGED
@@ -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
|