linzer 0.7.0.beta2 → 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 +5 -0
- data/README.md +142 -38
- 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
- metadata +46 -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,10 @@
|
|
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
|
+
|
3
8
|
## [0.7.0.beta2] - 2025-04-13
|
4
9
|
|
5
10
|
- 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,40 @@ 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
63
|
```ruby
|
64
64
|
key = Linzer.generate_ed25519_key
|
65
65
|
# => #<Linzer::Ed25519::Key:0x00000fe13e9bd208
|
66
66
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
request
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
signature = Linzer.sign(key, message, fields)
|
86
|
-
# => #<Linzer::Signature:0x0000000111f77ad0 ...
|
87
|
-
|
88
|
-
pp signature.to_h
|
89
|
-
# => {"signature"=>"sig1=:Cv1TUCxUpX+5SVa7pH0Xh...",
|
90
|
-
# "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\" ..."}
|
91
85
|
```
|
92
86
|
|
93
|
-
### Use the
|
87
|
+
### Use the signed request with an HTTP client:
|
94
88
|
|
95
89
|
```ruby
|
96
90
|
require "net/http"
|
97
91
|
|
98
|
-
http = Net::HTTP.new(
|
92
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
99
93
|
http.set_debug_output($stderr)
|
100
|
-
response = http.
|
94
|
+
response = http.request(request)
|
101
95
|
# opening connection to localhost:9292...
|
102
96
|
# opened
|
103
97
|
# <- "POST /some_uri HTTP/1.1\r\n
|
@@ -131,7 +125,123 @@ response = http.post("/some_uri", "data", headers.merge(signature.to_h))
|
|
131
125
|
# => #<Net::HTTPOK 200 OK readbody=true>
|
132
126
|
```
|
133
127
|
|
134
|
-
### 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:
|
135
245
|
|
136
246
|
```ruby
|
137
247
|
test_ed25519_key_pub = key.material.public_to_pem
|
@@ -140,12 +250,6 @@ test_ed25519_key_pub = key.material.public_to_pem
|
|
140
250
|
pubkey = Linzer.new_ed25519_public_key(test_ed25519_key_pub, "some-key-ed25519")
|
141
251
|
# => #<Linzer::Ed25519::Key:0x00000fe19b9384b0
|
142
252
|
|
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
253
|
message = Linzer::Message.new(request)
|
150
254
|
|
151
255
|
signature = Linzer::Signature.build(message.headers)
|
@@ -163,14 +267,14 @@ Linzer.verify(pubkey, message, signature, no_older_than: 500)
|
|
163
267
|
`no_older_than` expects a number of seconds, but you can pass anything that to responds to `#to_i`, including an `ActiveSupport::Duration`.
|
164
268
|
`::verify` will raise if the `created` parameter of the signature is older than the given number of seconds.
|
165
269
|
|
166
|
-
|
270
|
+
#### What if an invalid signature if verified?
|
167
271
|
|
168
272
|
```ruby
|
169
273
|
result = Linzer.verify(pubkey, message, signature)
|
170
274
|
lib/linzer/verifier.rb:38:in `verify_or_fail': Failed to verify message: Invalid signature. (Linzer::Error)
|
171
275
|
```
|
172
276
|
|
173
|
-
|
277
|
+
#### HTTP responses are also supported
|
174
278
|
|
175
279
|
HTTP responses can also be signed and verified in the same way as requests.
|
176
280
|
|
@@ -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
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Linzer
|
4
|
+
class Message
|
5
|
+
module Adapter
|
6
|
+
module Rack
|
7
|
+
module Common
|
8
|
+
DERIVED_COMPONENT = {
|
9
|
+
method: :request_method,
|
10
|
+
authority: :authority,
|
11
|
+
path: :path_info,
|
12
|
+
status: :status,
|
13
|
+
"target-uri": :url,
|
14
|
+
scheme: :scheme,
|
15
|
+
"request-target": :fullpath,
|
16
|
+
query: :query_string
|
17
|
+
}.freeze
|
18
|
+
private_constant :DERIVED_COMPONENT
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def validate
|
23
|
+
msg = "Message instance must be an HTTP request or response"
|
24
|
+
raise Error.new msg if response? == request?
|
25
|
+
end
|
26
|
+
|
27
|
+
def validate_header_name(name)
|
28
|
+
raise ArgumentError.new, "Blank header name." if name.empty?
|
29
|
+
name.to_str
|
30
|
+
rescue => ex
|
31
|
+
err_msg = "Invalid header name: '#{name}'"
|
32
|
+
raise Linzer::Error.new, err_msg, cause: ex
|
33
|
+
end
|
34
|
+
|
35
|
+
def rack_header_name(field_name)
|
36
|
+
validate_header_name field_name
|
37
|
+
|
38
|
+
rack_name = field_name.upcase.tr("-", "_")
|
39
|
+
case field_name.downcase
|
40
|
+
when "content-type", "content-length"
|
41
|
+
rack_name
|
42
|
+
else
|
43
|
+
"HTTP_#{rack_name}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def rack_request_headers(rack_request)
|
48
|
+
rack_request
|
49
|
+
.each_header
|
50
|
+
.to_h
|
51
|
+
.select do |k, _|
|
52
|
+
k.start_with?("HTTP_") || %w[CONTENT_TYPE CONTENT_LENGTH].include?(k)
|
53
|
+
end
|
54
|
+
.transform_keys { |k| k.downcase.tr("_", "-") }
|
55
|
+
.transform_keys do |k|
|
56
|
+
%w[content-type content-length].include?(k) ? k : k.gsub(/^http-/, "")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def derived(name)
|
61
|
+
method = DERIVED_COMPONENT[name.value]
|
62
|
+
|
63
|
+
value = case name.value
|
64
|
+
when :query then derive(@operation, method)
|
65
|
+
when :"query-param" then query_param(name)
|
66
|
+
end
|
67
|
+
|
68
|
+
return nil if !method && !value
|
69
|
+
value || derive(@operation, method)
|
70
|
+
end
|
71
|
+
|
72
|
+
def field(name)
|
73
|
+
has_tr = name.parameters["tr"]
|
74
|
+
if has_tr
|
75
|
+
value = tr(name)
|
76
|
+
else
|
77
|
+
if request?
|
78
|
+
rack_header_name = rack_header_name(name.value.to_s)
|
79
|
+
value = @operation.env[rack_header_name]
|
80
|
+
end
|
81
|
+
value = @operation.headers[name.value.to_s] if response?
|
82
|
+
end
|
83
|
+
value.dup&.strip
|
84
|
+
end
|
85
|
+
|
86
|
+
def derive(operation, method)
|
87
|
+
return nil unless operation.respond_to?(method)
|
88
|
+
value = operation.public_send(method)
|
89
|
+
return "?" + value if method == :query_string
|
90
|
+
return value.downcase if %i[authority scheme].include?(method)
|
91
|
+
value
|
92
|
+
end
|
93
|
+
|
94
|
+
def query_param(name)
|
95
|
+
param_name = name.parameters["name"]
|
96
|
+
return nil if !param_name
|
97
|
+
decoded_param_name = URI.decode_uri_component(param_name)
|
98
|
+
URI.encode_uri_component(@operation.params.fetch(decoded_param_name))
|
99
|
+
rescue => _
|
100
|
+
nil
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Linzer
|
4
|
+
class Message
|
5
|
+
module Adapter
|
6
|
+
module Rack
|
7
|
+
class Request < Abstract
|
8
|
+
include Common
|
9
|
+
|
10
|
+
def initialize(operation, **options)
|
11
|
+
@operation = operation
|
12
|
+
validate
|
13
|
+
freeze
|
14
|
+
end
|
15
|
+
|
16
|
+
def headers
|
17
|
+
rack_request_headers(@operation)
|
18
|
+
end
|
19
|
+
|
20
|
+
def attach!(signature)
|
21
|
+
signature.to_h.each do |h, v|
|
22
|
+
@operation.set_header(rack_header_name(h), v)
|
23
|
+
end
|
24
|
+
@operation
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Linzer
|
4
|
+
class Message
|
5
|
+
module Adapter
|
6
|
+
module Rack
|
7
|
+
class Response < Abstract
|
8
|
+
include Common
|
9
|
+
|
10
|
+
def initialize(operation, **options)
|
11
|
+
@operation = operation
|
12
|
+
validate
|
13
|
+
attached_request = options[:attached_request]
|
14
|
+
@attached_request = attached_request ? Message.new(attached_request) : nil
|
15
|
+
validate_attached_request @attached_request if @attached_request
|
16
|
+
freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
def headers
|
20
|
+
@operation.headers
|
21
|
+
end
|
22
|
+
|
23
|
+
def attach!(signature)
|
24
|
+
signature.to_h.each { |h, v| @operation[h] = v }
|
25
|
+
@operation
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "adapter/abstract"
|
4
|
+
require_relative "adapter/rack/common"
|
5
|
+
require_relative "adapter/rack/request"
|
6
|
+
require_relative "adapter/rack/response"
|
7
|
+
require_relative "adapter/net_http/request"
|
8
|
+
require_relative "adapter/net_http/response"
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Linzer
|
4
|
+
class Message
|
5
|
+
module Wrapper
|
6
|
+
@adapters = {
|
7
|
+
Rack::Request => Linzer::Message::Adapter::Rack::Request,
|
8
|
+
Rack::Response => Linzer::Message::Adapter::Rack::Response,
|
9
|
+
Net::HTTPRequest => Linzer::Message::Adapter::NetHTTP::Request,
|
10
|
+
Net::HTTPResponse => Linzer::Message::Adapter::NetHTTP::Response
|
11
|
+
}
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def wrap(operation, **options)
|
15
|
+
adapter_class = adapters[operation.class]
|
16
|
+
|
17
|
+
if !adapter_class
|
18
|
+
ancestor = find_ancestor(operation)
|
19
|
+
fail_with_unsupported(operation) unless ancestor
|
20
|
+
end
|
21
|
+
|
22
|
+
(adapter_class || ancestor).new(operation, **options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def register_adapter(operation_class, adapter_class)
|
26
|
+
adapters[operation_class] = adapter_class
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :adapters
|
32
|
+
|
33
|
+
def find_ancestor(operation)
|
34
|
+
adapters
|
35
|
+
.select { |klz, adpt| operation.is_a? klz }
|
36
|
+
.values
|
37
|
+
.first
|
38
|
+
end
|
39
|
+
|
40
|
+
def fail_with_unsupported(operation)
|
41
|
+
err_msg = <<~EOM
|
42
|
+
Unknown/unsupported HTTP message class: '#{operation.class}'!
|
43
|
+
|
44
|
+
Linzer supports custom HTTP messages implementation by register them first
|
45
|
+
with `Linzer::Message.register_adapter` method.
|
46
|
+
EOM
|
47
|
+
raise Linzer::Error, err_msg
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/linzer/message.rb
CHANGED
@@ -1,207 +1,28 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "forwardable"
|
4
|
+
|
3
5
|
module Linzer
|
4
6
|
class Message
|
7
|
+
extend Forwardable
|
8
|
+
|
5
9
|
def initialize(operation, attached_request: nil)
|
6
|
-
@
|
7
|
-
validate
|
8
|
-
@attached_request = attached_request ? Message.new(attached_request) : nil
|
10
|
+
@adapter = Wrapper.wrap(operation, attached_request: attached_request)
|
9
11
|
freeze
|
10
12
|
end
|
11
13
|
|
12
|
-
|
13
|
-
|
14
|
-
end
|
15
|
-
|
16
|
-
def response?
|
17
|
-
@operation.is_a?(Rack::Response) || @operation.respond_to?(:status)
|
18
|
-
end
|
19
|
-
|
20
|
-
def attached_request?
|
21
|
-
!!@attached_request
|
22
|
-
end
|
23
|
-
|
24
|
-
def headers
|
25
|
-
return @operation.headers if response? || @operation.respond_to?(:headers)
|
14
|
+
# common predicates
|
15
|
+
def_delegators :@adapter, :request?, :response?, :attached_request?
|
26
16
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
def field?(f)
|
31
|
-
!!self[f]
|
32
|
-
end
|
33
|
-
|
34
|
-
DERIVED_COMPONENT = {
|
35
|
-
method: :request_method,
|
36
|
-
authority: :authority,
|
37
|
-
path: :path_info,
|
38
|
-
status: :status,
|
39
|
-
"target-uri": :url,
|
40
|
-
scheme: :scheme,
|
41
|
-
"request-target": :fullpath,
|
42
|
-
query: :query_string
|
43
|
-
}.freeze
|
17
|
+
# fields look up
|
18
|
+
def_delegators :@adapter, :headers, :field?, :[]
|
44
19
|
|
45
|
-
|
46
|
-
|
47
|
-
return nil if name.nil?
|
48
|
-
|
49
|
-
if field_name.start_with?("@")
|
50
|
-
retrieve(name, :derived)
|
51
|
-
else
|
52
|
-
retrieve(name, :field)
|
53
|
-
end
|
54
|
-
end
|
20
|
+
# to attach a signature to the underlying HTTP message
|
21
|
+
def_delegators :@adapter, :attach!
|
55
22
|
|
56
23
|
class << self
|
57
|
-
def
|
58
|
-
|
59
|
-
rescue Starry::ParseError => _
|
60
|
-
raise Error.new "Cannot parse \"#{field_name}\" field. Bailing out!"
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
private
|
65
|
-
|
66
|
-
def validate
|
67
|
-
msg = "Message instance must be an HTTP request or response"
|
68
|
-
raise Error.new msg if response? == request?
|
69
|
-
end
|
70
|
-
|
71
|
-
def parse_field_name(field_name)
|
72
|
-
if field_name&.start_with?("@")
|
73
|
-
Starry.parse_item(field_name[1..])
|
74
|
-
else
|
75
|
-
Starry.parse_item(field_name)
|
76
|
-
end
|
77
|
-
rescue => _
|
78
|
-
nil
|
79
|
-
end
|
80
|
-
|
81
|
-
def validate_parameters(name, method)
|
82
|
-
has_unknown = name.parameters.any? { |p, _| !KNOWN_PARAMETERS.include?(p) }
|
83
|
-
return nil if has_unknown
|
84
|
-
|
85
|
-
has_name = name.parameters["name"]
|
86
|
-
has_req = name.parameters["req"]
|
87
|
-
has_sf = name.parameters["sf"] || name.parameters.key?("key")
|
88
|
-
has_bs = name.parameters["bs"]
|
89
|
-
value = name.value
|
90
|
-
|
91
|
-
# Section 2.2.8 of RFC 9421
|
92
|
-
return nil if has_name && value != :"query-param"
|
93
|
-
|
94
|
-
# No derived values come from trailers section
|
95
|
-
return nil if method == :derived && name.parameters["tr"]
|
96
|
-
|
97
|
-
# From: 2.1. HTTP Fields:
|
98
|
-
# The bs parameter, which requires the raw bytes of the field values
|
99
|
-
# from the message, is not compatible with the use of the sf or key
|
100
|
-
# parameters, which require the parsed data structures of the field
|
101
|
-
# values after combination
|
102
|
-
return nil if has_sf && has_bs
|
103
|
-
|
104
|
-
# req param only makes sense on responses with an associated request
|
105
|
-
return nil if has_req && (!response? || !attached_request?)
|
106
|
-
|
107
|
-
name
|
108
|
-
end
|
109
|
-
|
110
|
-
KNOWN_PARAMETERS = %w[sf key bs req tr name]
|
111
|
-
private_constant :KNOWN_PARAMETERS
|
112
|
-
|
113
|
-
def retrieve(name, method)
|
114
|
-
if !name.parameters.empty?
|
115
|
-
valid_params = validate_parameters(name, method)
|
116
|
-
return nil if !valid_params
|
117
|
-
end
|
118
|
-
|
119
|
-
has_req = name.parameters["req"]
|
120
|
-
has_sf = name.parameters["sf"] || name.parameters.key?("key")
|
121
|
-
has_bs = name.parameters["bs"]
|
122
|
-
|
123
|
-
if has_req
|
124
|
-
name.parameters.delete("req")
|
125
|
-
return req(name, method)
|
126
|
-
end
|
127
|
-
|
128
|
-
value = send(method, name)
|
129
|
-
|
130
|
-
case
|
131
|
-
when has_sf
|
132
|
-
key = name.parameters["key"]
|
133
|
-
sf(value, key)
|
134
|
-
when has_bs then bs(value)
|
135
|
-
else value
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
|
-
def derived(name)
|
140
|
-
method = DERIVED_COMPONENT[name.value]
|
141
|
-
|
142
|
-
value = case name.value
|
143
|
-
when :query then derive(@operation, method)
|
144
|
-
when :"query-param" then query_param(name)
|
145
|
-
end
|
146
|
-
|
147
|
-
return nil if !method && !value
|
148
|
-
value || derive(@operation, method)
|
149
|
-
end
|
150
|
-
|
151
|
-
def field(name)
|
152
|
-
has_tr = name.parameters["tr"]
|
153
|
-
if has_tr
|
154
|
-
value = tr(name)
|
155
|
-
else
|
156
|
-
if request?
|
157
|
-
rack_header_name = Request.rack_header_name(name.value.to_s)
|
158
|
-
value = @operation.env[rack_header_name]
|
159
|
-
end
|
160
|
-
value = @operation.headers[name.value.to_s] if response?
|
161
|
-
end
|
162
|
-
value.dup&.strip
|
163
|
-
end
|
164
|
-
|
165
|
-
def derive(operation, method)
|
166
|
-
return nil unless operation.respond_to?(method)
|
167
|
-
value = operation.public_send(method)
|
168
|
-
return "?" + value if method == :query_string
|
169
|
-
return value.downcase if %i[authority scheme].include?(method)
|
170
|
-
value
|
171
|
-
end
|
172
|
-
|
173
|
-
def query_param(name)
|
174
|
-
param_name = name.parameters["name"]
|
175
|
-
return nil if !param_name
|
176
|
-
decoded_param_name = URI.decode_uri_component(param_name)
|
177
|
-
URI.encode_uri_component(@operation.params.fetch(decoded_param_name))
|
178
|
-
rescue => _
|
179
|
-
nil
|
180
|
-
end
|
181
|
-
|
182
|
-
def sf(value, key = nil)
|
183
|
-
dict = Starry.parse_dictionary(value)
|
184
|
-
|
185
|
-
if key
|
186
|
-
obj = dict[key]
|
187
|
-
Starry.serialize(obj.is_a?(Starry::InnerList) ? [obj] : obj)
|
188
|
-
else
|
189
|
-
Starry.serialize(dict)
|
190
|
-
end
|
191
|
-
end
|
192
|
-
|
193
|
-
def bs(value)
|
194
|
-
Starry.serialize(value.encode(Encoding::ASCII_8BIT))
|
195
|
-
end
|
196
|
-
|
197
|
-
def tr(trailer)
|
198
|
-
@operation.body.trailers[trailer.value.to_s]
|
199
|
-
end
|
200
|
-
|
201
|
-
def req(field, method)
|
202
|
-
case method
|
203
|
-
when :derived then @attached_request["@#{field}"]
|
204
|
-
when :field then @attached_request[field.to_s]
|
24
|
+
def register_adapter(operation_class, adapter_class)
|
25
|
+
Wrapper.register_adapter(operation_class, adapter_class)
|
205
26
|
end
|
206
27
|
end
|
207
28
|
end
|
data/lib/linzer/signature.rb
CHANGED
@@ -86,11 +86,17 @@ module Linzer
|
|
86
86
|
raise Error.new "Unexpected value for covered components."
|
87
87
|
end
|
88
88
|
|
89
|
+
def parse_structured_dictionary(str, field_name = nil)
|
90
|
+
Starry.parse_dictionary(str)
|
91
|
+
rescue Starry::ParseError => _
|
92
|
+
raise Error.new "Cannot parse \"#{field_name}\" field. Bailing out!"
|
93
|
+
end
|
94
|
+
|
89
95
|
def parse_structured_field(hsh, field_name)
|
90
96
|
# Serialized Structured Field values for HTTP are ASCII strings.
|
91
97
|
# See: RFC 8941 (https://datatracker.ietf.org/doc/html/rfc8941)
|
92
98
|
value = hsh[field_name].encode(Encoding::US_ASCII)
|
93
|
-
|
99
|
+
parse_structured_dictionary(value, field_name)
|
94
100
|
end
|
95
101
|
end
|
96
102
|
end
|
data/lib/linzer/version.rb
CHANGED
data/lib/linzer.rb
CHANGED
@@ -5,12 +5,13 @@ require "openssl"
|
|
5
5
|
require "rack"
|
6
6
|
require "uri"
|
7
7
|
require "stringio"
|
8
|
+
require "net/http"
|
8
9
|
|
9
10
|
require_relative "linzer/version"
|
10
11
|
require_relative "linzer/common"
|
11
|
-
require_relative "linzer/request"
|
12
|
-
require_relative "linzer/response"
|
13
12
|
require_relative "linzer/message"
|
13
|
+
require_relative "linzer/message/adapter"
|
14
|
+
require_relative "linzer/message/wrapper"
|
14
15
|
require_relative "linzer/signature"
|
15
16
|
require_relative "linzer/key"
|
16
17
|
require_relative "linzer/rsa"
|
@@ -28,11 +29,6 @@ module Linzer
|
|
28
29
|
|
29
30
|
class << self
|
30
31
|
include Key::Helper
|
31
|
-
include Response
|
32
|
-
|
33
|
-
def new_request(verb, uri = "/", params = {}, headers = {})
|
34
|
-
Linzer::Request.build(verb, uri, params, headers)
|
35
|
-
end
|
36
32
|
|
37
33
|
def verify(pubkey, message, signature, no_older_than: nil)
|
38
34
|
Linzer::Verifier.verify(pubkey, message, signature, no_older_than: no_older_than)
|
@@ -41,5 +37,27 @@ module Linzer
|
|
41
37
|
def sign(key, message, components, options = {})
|
42
38
|
Linzer::Signer.sign(key, message, components, options)
|
43
39
|
end
|
40
|
+
|
41
|
+
def sign!(request_or_response, **args)
|
42
|
+
message = Message.new(request_or_response)
|
43
|
+
options = {}
|
44
|
+
|
45
|
+
label = args[:label]
|
46
|
+
options[:label] = label if label
|
47
|
+
options.merge!(args.fetch(:params, {}))
|
48
|
+
|
49
|
+
key = args.fetch(:key)
|
50
|
+
signature = Linzer::Signer.sign(key, message, args.fetch(:components), options)
|
51
|
+
message.attach!(signature)
|
52
|
+
end
|
53
|
+
|
54
|
+
def verify!(request_or_response, key: nil, no_older_than: 900)
|
55
|
+
message = Message.new(request_or_response)
|
56
|
+
signature = Signature.build(message.headers.slice("signature", "signature-input"))
|
57
|
+
keyid = signature.parameters["keyid"]
|
58
|
+
raise Linzer::Error, "key not found" if !key && !keyid
|
59
|
+
verify_key = block_given? ? (yield keyid) : key
|
60
|
+
Linzer.verify(verify_key, message, signature, no_older_than: no_older_than)
|
61
|
+
end
|
44
62
|
end
|
45
63
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: linzer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.7.0.
|
4
|
+
version: 0.7.0.beta3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Miguel Landaeta
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date:
|
10
|
+
date: 2025-05-06 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: openssl
|
@@ -123,6 +123,40 @@ dependencies:
|
|
123
123
|
- - ">="
|
124
124
|
- !ruby/object:Gem::Version
|
125
125
|
version: 1.7.0
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: forwardable
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - "~>"
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '1.3'
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: 1.3.3
|
136
|
+
type: :runtime
|
137
|
+
prerelease: false
|
138
|
+
version_requirements: !ruby/object:Gem::Requirement
|
139
|
+
requirements:
|
140
|
+
- - "~>"
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '1.3'
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 1.3.3
|
146
|
+
- !ruby/object:Gem::Dependency
|
147
|
+
name: net-http
|
148
|
+
requirement: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: 0.6.0
|
153
|
+
type: :runtime
|
154
|
+
prerelease: false
|
155
|
+
version_requirements: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: 0.6.0
|
126
160
|
email:
|
127
161
|
- miguel@miguel.cc
|
128
162
|
executables: []
|
@@ -148,8 +182,15 @@ files:
|
|
148
182
|
- lib/linzer/key.rb
|
149
183
|
- lib/linzer/key/helper.rb
|
150
184
|
- lib/linzer/message.rb
|
151
|
-
- lib/linzer/
|
152
|
-
- lib/linzer/
|
185
|
+
- lib/linzer/message/adapter.rb
|
186
|
+
- lib/linzer/message/adapter/abstract.rb
|
187
|
+
- lib/linzer/message/adapter/http_gem/response.rb
|
188
|
+
- lib/linzer/message/adapter/net_http/request.rb
|
189
|
+
- lib/linzer/message/adapter/net_http/response.rb
|
190
|
+
- lib/linzer/message/adapter/rack/common.rb
|
191
|
+
- lib/linzer/message/adapter/rack/request.rb
|
192
|
+
- lib/linzer/message/adapter/rack/response.rb
|
193
|
+
- lib/linzer/message/wrapper.rb
|
153
194
|
- lib/linzer/rsa.rb
|
154
195
|
- lib/linzer/rsa_pss.rb
|
155
196
|
- lib/linzer/signature.rb
|
@@ -179,7 +220,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
179
220
|
- !ruby/object:Gem::Version
|
180
221
|
version: '0'
|
181
222
|
requirements: []
|
182
|
-
rubygems_version: 3.6.
|
223
|
+
rubygems_version: 3.6.2
|
183
224
|
specification_version: 4
|
184
225
|
summary: An implementation of HTTP Messages Signatures (RFC9421)
|
185
226
|
test_files: []
|
data/lib/linzer/request.rb
DELETED
@@ -1,95 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Linzer
|
4
|
-
module Request
|
5
|
-
extend self
|
6
|
-
|
7
|
-
def build(verb, uri = "/", params = {}, headers = {})
|
8
|
-
validate verb, uri, params, headers
|
9
|
-
|
10
|
-
# XXX: to-do: handle rack request params?
|
11
|
-
request_method = Rack.const_get(verb.upcase)
|
12
|
-
args = {
|
13
|
-
"REQUEST_METHOD" => request_method,
|
14
|
-
"PATH_INFO" => uri.to_str,
|
15
|
-
"rack.input" => StringIO.new
|
16
|
-
}
|
17
|
-
|
18
|
-
Rack::Request.new(build_rack_env(headers).merge(args))
|
19
|
-
end
|
20
|
-
|
21
|
-
def rack_header_name(field_name)
|
22
|
-
validate_header_name field_name
|
23
|
-
|
24
|
-
rack_name = field_name.upcase.tr("-", "_")
|
25
|
-
case field_name.downcase
|
26
|
-
when "content-type", "content-length"
|
27
|
-
rack_name
|
28
|
-
else
|
29
|
-
"HTTP_#{rack_name}"
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
def headers(rack_request)
|
34
|
-
rack_request
|
35
|
-
.each_header
|
36
|
-
.to_h
|
37
|
-
.select do |k, _|
|
38
|
-
k.start_with?("HTTP_") || %w[CONTENT_TYPE CONTENT_LENGTH].include?(k)
|
39
|
-
end
|
40
|
-
.transform_keys { |k| k.downcase.tr("_", "-") }
|
41
|
-
.transform_keys do |k|
|
42
|
-
%w[content-type content-length].include?(k) ? k : k.gsub(/^http-/, "")
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
private
|
47
|
-
|
48
|
-
def validate(verb, uri, params, headers)
|
49
|
-
validate_verb verb
|
50
|
-
validate_uri uri
|
51
|
-
validate_arg_hash headers: headers
|
52
|
-
validate_arg_hash params: params
|
53
|
-
end
|
54
|
-
|
55
|
-
def validate_verb(verb)
|
56
|
-
Rack.const_get(verb.upcase)
|
57
|
-
rescue => ex
|
58
|
-
unknown_method = "Unknown/invalid HTTP request method"
|
59
|
-
raise Error.new, unknown_method, cause: ex
|
60
|
-
end
|
61
|
-
|
62
|
-
def validate_uri(uri)
|
63
|
-
uri.to_str
|
64
|
-
rescue => ex
|
65
|
-
invalid_uri = "Invalid URI"
|
66
|
-
raise Error.new, invalid_uri, cause: ex
|
67
|
-
end
|
68
|
-
|
69
|
-
def validate_arg_hash(hsh)
|
70
|
-
arg_name = hsh.keys.first
|
71
|
-
hsh[arg_name].to_hash
|
72
|
-
rescue => ex
|
73
|
-
err_msg = "invalid \"#{arg_name}\" parameter, cannot be converted to hash."
|
74
|
-
raise Error.new, "Cannot build request: #{err_msg}", cause: ex
|
75
|
-
end
|
76
|
-
|
77
|
-
def validate_header_name(name)
|
78
|
-
raise ArgumentError.new, "Blank header name." if name.empty?
|
79
|
-
name.to_str
|
80
|
-
rescue => ex
|
81
|
-
err_msg = "Invalid header name: '#{name}'"
|
82
|
-
raise Error.new, err_msg, cause: ex
|
83
|
-
end
|
84
|
-
|
85
|
-
def build_rack_env(headers)
|
86
|
-
headers
|
87
|
-
.to_hash
|
88
|
-
.transform_values(&:to_s)
|
89
|
-
.transform_keys { |k| k.upcase.tr("-", "_") }
|
90
|
-
.transform_keys do |k|
|
91
|
-
%w[CONTENT_TYPE CONTENT_LENGTH].include?(k) ? k : "HTTP_#{k}"
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end
|