linzer 0.7.9.beta2 → 0.7.9
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 +25 -0
- data/README.md +96 -12
- data/lib/faraday/http_signature/middleware.rb +296 -0
- data/lib/faraday/http_signature.rb +36 -0
- data/lib/linzer/faraday/utils.rb +29 -0
- data/lib/linzer/faraday.rb +29 -0
- data/lib/linzer/http.rb +43 -1
- data/lib/linzer/message/adapter/abstract.rb +65 -9
- data/lib/linzer/message/adapter/faraday/request.rb +63 -0
- data/lib/linzer/message/adapter/faraday/response.rb +46 -0
- data/lib/linzer/message/adapter/generic/request.rb +27 -20
- data/lib/linzer/message/adapter/generic/response.rb +11 -8
- data/lib/linzer/message/adapter/http_gem/common.rb +16 -8
- data/lib/linzer/message/adapter/http_gem/request.rb +8 -0
- data/lib/linzer/message/adapter/http_gem/response.rb +7 -0
- data/lib/linzer/message/adapter/net_http/request.rb +8 -0
- data/lib/linzer/message/adapter/net_http/response.rb +7 -0
- data/lib/linzer/message/adapter/rack/common.rb +37 -0
- data/lib/linzer/message/adapter/rack/request.rb +11 -8
- data/lib/linzer/message/adapter/rack/response.rb +11 -8
- data/lib/linzer/message/field/parser.rb +15 -0
- data/lib/linzer/message/wrapper.rb +12 -2
- data/lib/linzer/signature.rb +20 -0
- data/lib/linzer/verifier.rb +8 -0
- data/lib/linzer/version.rb +1 -1
- data/lib/linzer.rb +0 -1
- data/lib/rack/auth/signature/helpers.rb +72 -0
- metadata +7 -21
data/lib/linzer/http.rb
CHANGED
|
@@ -78,6 +78,14 @@ module Linzer
|
|
|
78
78
|
private
|
|
79
79
|
|
|
80
80
|
# Executes a signed HTTP request.
|
|
81
|
+
#
|
|
82
|
+
# Validates inputs, builds and signs the request, then sends it.
|
|
83
|
+
#
|
|
84
|
+
# @param verb [Symbol] the HTTP method (e.g. +:get+, +:post+)
|
|
85
|
+
# @param uri [String] the request URI
|
|
86
|
+
# @param options [Hash] request options
|
|
87
|
+
# @return [Net::HTTPResponse] the response
|
|
88
|
+
# @raise [Linzer::Error] if the verb or key is invalid
|
|
81
89
|
def request(verb, uri, options = {})
|
|
82
90
|
validate_verb(verb)
|
|
83
91
|
|
|
@@ -100,10 +108,16 @@ module Linzer
|
|
|
100
108
|
do_request(http, uri, verb, options[:data], signature, headers)
|
|
101
109
|
end
|
|
102
110
|
|
|
111
|
+
# Returns the default covered components for signing.
|
|
112
|
+
# @return [Array<String>]
|
|
103
113
|
def default_components
|
|
104
114
|
Linzer::Options::DEFAULT[:covered_components]
|
|
105
115
|
end
|
|
106
116
|
|
|
117
|
+
# Validates that the HTTP verb is recognized.
|
|
118
|
+
#
|
|
119
|
+
# @param verb [Symbol] the HTTP method
|
|
120
|
+
# @raise [Linzer::Error] if the verb is unknown
|
|
107
121
|
def validate_verb(verb)
|
|
108
122
|
method_name = verb.to_s.upcase
|
|
109
123
|
if !known_http_methods.include?(method_name)
|
|
@@ -111,17 +125,32 @@ module Linzer
|
|
|
111
125
|
end
|
|
112
126
|
end
|
|
113
127
|
|
|
128
|
+
# Validates that the signing key is present and usable.
|
|
129
|
+
#
|
|
130
|
+
# @param key [Linzer::Key] the signing key
|
|
131
|
+
# @return [Linzer::Key] the validated key
|
|
132
|
+
# @raise [Linzer::Error] if the key is nil or does not respond to +#sign+
|
|
114
133
|
def validate_key(key)
|
|
115
134
|
raise Linzer::Error, "Key can not be nil!" if !key
|
|
116
135
|
raise Linzer::Error, "Key object is invalid!" if !key.respond_to?(:sign)
|
|
117
136
|
key
|
|
118
137
|
end
|
|
119
138
|
|
|
139
|
+
# Builds request headers, adding a default User-Agent if not present.
|
|
140
|
+
#
|
|
141
|
+
# @param headers [Hash] user-provided headers
|
|
142
|
+
# @return [Hash] headers with User-Agent ensured
|
|
120
143
|
def build_headers(headers)
|
|
121
144
|
return headers if headers.transform_keys(&:downcase).key?("user-agent")
|
|
122
145
|
headers.merge({"user-agent" => "Linzer/#{Linzer::VERSION}"})
|
|
123
146
|
end
|
|
124
147
|
|
|
148
|
+
# Builds a Net::HTTP request object for the given method and URI.
|
|
149
|
+
#
|
|
150
|
+
# @param method [Symbol] the HTTP method
|
|
151
|
+
# @param uri [String] the request URI
|
|
152
|
+
# @param headers [Hash] request headers to set
|
|
153
|
+
# @return [Net::HTTPRequest] the constructed request
|
|
125
154
|
def build_request(method, uri, headers)
|
|
126
155
|
request_class = Net::HTTP.const_get(method.to_s.capitalize)
|
|
127
156
|
request = request_class.new(URI(uri))
|
|
@@ -129,7 +158,10 @@ module Linzer
|
|
|
129
158
|
request
|
|
130
159
|
end
|
|
131
160
|
|
|
132
|
-
#
|
|
161
|
+
# Checks if the HTTP method typically carries a request body.
|
|
162
|
+
#
|
|
163
|
+
# @param verb [Symbol] the HTTP method
|
|
164
|
+
# @return [Boolean] +true+ for POST, PUT, PATCH, and WebDAV write methods
|
|
133
165
|
def with_body?(verb)
|
|
134
166
|
# common HTTP
|
|
135
167
|
return false if %i[get head options trace delete].include?(verb)
|
|
@@ -142,6 +174,16 @@ module Linzer
|
|
|
142
174
|
true
|
|
143
175
|
end
|
|
144
176
|
|
|
177
|
+
# Sends the HTTP request with the signature headers attached.
|
|
178
|
+
#
|
|
179
|
+
# @param http [Net::HTTP] the HTTP connection
|
|
180
|
+
# @param uri [String] the request URI
|
|
181
|
+
# @param verb [Symbol] the HTTP method
|
|
182
|
+
# @param data [String, nil] the request body
|
|
183
|
+
# @param signature [Linzer::Signature] the generated signature
|
|
184
|
+
# @param headers [Hash] request headers
|
|
185
|
+
# @return [Net::HTTPResponse] the response
|
|
186
|
+
# @raise [Linzer::Error] if a body is required but not provided
|
|
145
187
|
def do_request(http, uri, verb, data, signature, headers)
|
|
146
188
|
if with_body?(verb)
|
|
147
189
|
if !data
|
|
@@ -10,8 +10,8 @@ module Linzer
|
|
|
10
10
|
# implements field retrieval, header access, and signature attachment
|
|
11
11
|
# for a specific HTTP message type.
|
|
12
12
|
#
|
|
13
|
-
# @abstract Subclass and implement {#header}, {#
|
|
14
|
-
#
|
|
13
|
+
# @abstract Subclass and implement {#header}, {#derived}, and {#field}
|
|
14
|
+
# to create a new adapter.
|
|
15
15
|
#
|
|
16
16
|
# @see Rack::Request Rack request adapter
|
|
17
17
|
# @see Rack::Response Rack response adapter
|
|
@@ -84,19 +84,48 @@ module Linzer
|
|
|
84
84
|
raise Linzer::Error, "Sub-classes are required to implement this method!"
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
# Checks whether the request contains HTTP Message Signature headers.
|
|
88
|
+
#
|
|
89
|
+
# Returns true if either the "signature-input" or "signature" header
|
|
90
|
+
# is present.
|
|
91
|
+
#
|
|
92
|
+
# @return [Boolean] true if the request includes HTTP Message Signature headers
|
|
93
|
+
def has_signature?
|
|
94
|
+
!!header("signature-input") || !!header("signature")
|
|
95
|
+
end
|
|
96
|
+
|
|
87
97
|
# Attaches a signature to the underlying HTTP message.
|
|
88
98
|
#
|
|
89
|
-
# @abstract Subclasses must implement this method.
|
|
90
99
|
# @param signature [Signature] The signature to attach
|
|
91
100
|
# @return [Object] The underlying HTTP message
|
|
92
101
|
def attach!(signature)
|
|
93
|
-
|
|
102
|
+
signature_headers = signature.to_h
|
|
103
|
+
|
|
104
|
+
unless has_signature?
|
|
105
|
+
signature_headers.each { |h, v| set_header!(h, v) }
|
|
106
|
+
return @operation
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
signature_headers.each do |hdr, value|
|
|
110
|
+
merged = Starry.parse_dictionary(String(header(hdr)))
|
|
111
|
+
merged.merge!(Starry.parse_dictionary(value))
|
|
112
|
+
set_header!(hdr, Starry.serialize_dictionary(merged))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
@operation
|
|
116
|
+
rescue Starry::ParseError => e
|
|
117
|
+
raise Error,
|
|
118
|
+
"Cannot attach signature, invalid signature header(s)!",
|
|
119
|
+
cause: e
|
|
94
120
|
end
|
|
95
121
|
|
|
96
122
|
private
|
|
97
123
|
|
|
98
124
|
# Parses a field name string into a FieldId.
|
|
99
|
-
#
|
|
125
|
+
#
|
|
126
|
+
# @param field_name [String] the component identifier string
|
|
127
|
+
# @return [FieldId, nil] the parsed identifier, or +nil+ if invalid
|
|
128
|
+
# @raise [Error] if +@status+ is used in a request message
|
|
100
129
|
def parse_field_name(field_name)
|
|
101
130
|
field_id = FieldId.new(field_name: field_name)
|
|
102
131
|
component = field_id.item
|
|
@@ -117,8 +146,12 @@ module Linzer
|
|
|
117
146
|
raise Linzer::Error, msg unless message.request?
|
|
118
147
|
end
|
|
119
148
|
|
|
120
|
-
# Validates component identifier parameters.
|
|
121
|
-
#
|
|
149
|
+
# Validates component identifier parameters against RFC 9421 rules.
|
|
150
|
+
#
|
|
151
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
152
|
+
# @param method [Symbol] +:derived+ or +:field+
|
|
153
|
+
# @return [Starry::Item, nil] the validated name, or +nil+ if
|
|
154
|
+
# the parameter combination is invalid
|
|
122
155
|
def validate_parameters(name, method)
|
|
123
156
|
has_unknown = name.parameters.any? { |p, _| !KNOWN_PARAMETERS.include?(p) }
|
|
124
157
|
return nil if has_unknown
|
|
@@ -149,6 +182,13 @@ module Linzer
|
|
|
149
182
|
private_constant :KNOWN_PARAMETERS
|
|
150
183
|
|
|
151
184
|
# Retrieves a component value with parameter processing.
|
|
185
|
+
#
|
|
186
|
+
# Handles +;req+, +;sf+, +;key+, and +;bs+ parameters by
|
|
187
|
+
# delegating to the corresponding helper methods.
|
|
188
|
+
#
|
|
189
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
190
|
+
# @param method [Symbol] +:derived+ or +:field+
|
|
191
|
+
# @return [String, Integer, nil] the component value
|
|
152
192
|
def retrieve(name, method)
|
|
153
193
|
if !name.parameters.empty?
|
|
154
194
|
valid_params = validate_parameters(name, method)
|
|
@@ -176,6 +216,10 @@ module Linzer
|
|
|
176
216
|
end
|
|
177
217
|
|
|
178
218
|
# Processes a structured field value with optional key extraction.
|
|
219
|
+
#
|
|
220
|
+
# @param value [String] the raw header value to parse as a dictionary
|
|
221
|
+
# @param key [String, nil] if present, extracts a single dictionary member
|
|
222
|
+
# @return [String] the serialized structured field value
|
|
179
223
|
# @see https://www.rfc-editor.org/rfc/rfc9421.html#section-2.1.1
|
|
180
224
|
def sf(value, key = nil)
|
|
181
225
|
dict = Starry.parse_dictionary(value)
|
|
@@ -188,19 +232,31 @@ module Linzer
|
|
|
188
232
|
end
|
|
189
233
|
end
|
|
190
234
|
|
|
191
|
-
# Binary-wraps a field value.
|
|
235
|
+
# Binary-wraps a field value as a byte sequence.
|
|
236
|
+
#
|
|
237
|
+
# @param value [String] the header value to wrap
|
|
238
|
+
# @return [String] the serialized byte sequence
|
|
192
239
|
# @see https://www.rfc-editor.org/rfc/rfc9421.html#section-2.1.3
|
|
193
240
|
def bs(value)
|
|
194
241
|
Starry.serialize(value.encode(Encoding::ASCII_8BIT))
|
|
195
242
|
end
|
|
196
243
|
|
|
197
244
|
# Retrieves a trailer field value.
|
|
245
|
+
#
|
|
198
246
|
# @abstract Subclasses should implement if trailer support is needed.
|
|
247
|
+
# @param trailer [Object] the trailer field identifier
|
|
248
|
+
# @return [String, nil] the trailer value
|
|
249
|
+
# @raise [Error] always, since no built-in adapters support trailers
|
|
199
250
|
def tr(trailer)
|
|
200
251
|
raise Error, "Sub-classes are required to implement this method!"
|
|
201
252
|
end
|
|
202
253
|
|
|
203
|
-
# Retrieves a field from the attached request.
|
|
254
|
+
# Retrieves a field from the attached request (for +;req+ parameter).
|
|
255
|
+
#
|
|
256
|
+
# @param field [Starry::Item] the component identifier
|
|
257
|
+
# @param method [Symbol] +:derived+ or +:field+
|
|
258
|
+
# @return [String, nil] the value from the attached request, or
|
|
259
|
+
# +nil+ if no request is attached
|
|
204
260
|
def req(field, method)
|
|
205
261
|
attached_request? ? @attached_request[String(field)] : nil
|
|
206
262
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Linzer
|
|
4
|
+
class Message
|
|
5
|
+
module Adapter
|
|
6
|
+
module Faraday
|
|
7
|
+
# Adapter for {::Faraday::Request} objects from the faraday gem.
|
|
8
|
+
#
|
|
9
|
+
# Extends the generic request adapter with faraday-specific
|
|
10
|
+
# derived component retrieval, field lookup, and URI handling.
|
|
11
|
+
#
|
|
12
|
+
# @note Not loaded automatically to avoid making faraday a hard
|
|
13
|
+
# dependency. Require +"linzer/faraday"+ to register this adapter.
|
|
14
|
+
#
|
|
15
|
+
# @see Generic::Request
|
|
16
|
+
# @see https://github.com/lostisland/faraday faraday gem
|
|
17
|
+
class Request < Generic::Request
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# Resolves a derived component value from the request.
|
|
21
|
+
#
|
|
22
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
23
|
+
# @return [String, nil] the derived value, or +nil+ if unknown
|
|
24
|
+
def derived(name)
|
|
25
|
+
url = @operation.path
|
|
26
|
+
case name.value
|
|
27
|
+
when "@method" then @operation.http_method.to_s.upcase
|
|
28
|
+
when "@target-uri" then uri.to_s
|
|
29
|
+
when "@authority" then url.authority.downcase
|
|
30
|
+
when "@scheme" then url.scheme.downcase
|
|
31
|
+
when "@request-target" then uri.request_uri
|
|
32
|
+
when "@path" then url.path
|
|
33
|
+
when "@query" then "?%s" % String(uri_query)
|
|
34
|
+
when "@query-param" then query_param(uri_query, name)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Builds the full URI including query parameters.
|
|
39
|
+
#
|
|
40
|
+
# @return [URI] the complete request URI with encoded query string
|
|
41
|
+
def uri
|
|
42
|
+
uri = @operation.path.dup
|
|
43
|
+
uri.query = URI.encode_www_form(@operation.params)
|
|
44
|
+
uri
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns the raw query string from the request URI.
|
|
48
|
+
#
|
|
49
|
+
# Prefers the raw query string from the URI when available,
|
|
50
|
+
# as Faraday normalises percent-encoding when parsing params
|
|
51
|
+
# (e.g. +%2D+ becomes +-+), which would break signature
|
|
52
|
+
# verification.
|
|
53
|
+
#
|
|
54
|
+
# @return [String, nil] the raw query string
|
|
55
|
+
def uri_query
|
|
56
|
+
url = @operation.path
|
|
57
|
+
url.query || uri.query
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Linzer
|
|
4
|
+
class Message
|
|
5
|
+
module Adapter
|
|
6
|
+
module Faraday
|
|
7
|
+
# Adapter for {::Faraday::Response} objects from the faraday gem.
|
|
8
|
+
#
|
|
9
|
+
# Extends the generic response adapter with faraday-specific
|
|
10
|
+
# derived component retrieval (e.g. +@status+) and header
|
|
11
|
+
# attachment.
|
|
12
|
+
#
|
|
13
|
+
# @note Not loaded automatically to avoid making faraday a hard
|
|
14
|
+
# dependency. Require +"linzer/faraday"+ to register this adapter.
|
|
15
|
+
#
|
|
16
|
+
# @see Generic::Response
|
|
17
|
+
# @see https://github.com/lostisland/faraday faraday gem
|
|
18
|
+
class Response < Generic::Response
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# Resolves a derived component value from the response.
|
|
22
|
+
#
|
|
23
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
24
|
+
# @return [Integer, nil] the HTTP status code for +@status+,
|
|
25
|
+
# or +nil+ if the component is unknown
|
|
26
|
+
def derived(name)
|
|
27
|
+
case name.value
|
|
28
|
+
when "@status" then @operation.status.to_i
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Sets a header on the underlying HTTP message.
|
|
33
|
+
#
|
|
34
|
+
# If a header with the given name already exists, its value is overwritten.
|
|
35
|
+
#
|
|
36
|
+
# @param header [String] the header name
|
|
37
|
+
# @param value [String] the header value
|
|
38
|
+
# @return [String] the value assigned to the header
|
|
39
|
+
def set_header!(header, value)
|
|
40
|
+
@operation.headers[header] = value
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -40,42 +40,49 @@ module Linzer
|
|
|
40
40
|
@operation[name]
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
# Attaches a signature to the request.
|
|
44
|
-
# @param signature [Signature] The signature to attach
|
|
45
|
-
# @return [Object] The underlying request object
|
|
46
|
-
def attach!(signature)
|
|
47
|
-
signature.to_h.each { |h, v| @operation[h] = v }
|
|
48
|
-
@operation
|
|
49
|
-
end
|
|
50
|
-
|
|
51
43
|
private
|
|
52
44
|
|
|
53
45
|
def derived(name)
|
|
54
|
-
unimplemented_method = 'Derived field "
|
|
46
|
+
unimplemented_method = 'Derived field "%s" lookup is not implemented!'
|
|
47
|
+
|
|
48
|
+
uri = @operation.uri rescue nil
|
|
49
|
+
raise Error, unimplemented_method % name.value if uri.nil?
|
|
50
|
+
|
|
55
51
|
case name.value
|
|
56
|
-
when "@method" then raise Error, unimplemented_method
|
|
57
|
-
when "@target-uri" then
|
|
58
|
-
when "@authority" then
|
|
59
|
-
when "@scheme" then
|
|
60
|
-
when "@request-target" then
|
|
61
|
-
when "@path" then
|
|
62
|
-
when "@query" then "?%s" % String(
|
|
63
|
-
when "@query-param" then query_param(name)
|
|
52
|
+
when "@method" then raise Error, unimplemented_method % name.value
|
|
53
|
+
when "@target-uri" then uri.to_s
|
|
54
|
+
when "@authority" then uri.authority.downcase
|
|
55
|
+
when "@scheme" then uri.scheme.downcase
|
|
56
|
+
when "@request-target" then uri.request_uri
|
|
57
|
+
when "@path" then uri.path
|
|
58
|
+
when "@query" then "?%s" % String(uri.query)
|
|
59
|
+
when "@query-param" then query_param(uri.query, name)
|
|
64
60
|
end
|
|
65
61
|
end
|
|
66
62
|
|
|
67
|
-
|
|
63
|
+
# Sets a header on the underlying HTTP message.
|
|
64
|
+
#
|
|
65
|
+
# If a header with the given name already exists, its value is overwritten.
|
|
66
|
+
#
|
|
67
|
+
# @param header [String] the header name
|
|
68
|
+
# @param value [String] the header value
|
|
69
|
+
# @return [String] the value assigned to the header
|
|
70
|
+
def set_header!(header, value)
|
|
71
|
+
@operation[header] = value
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def query_param(uri_query, name)
|
|
68
75
|
param_name = name.parameters["name"]
|
|
69
76
|
return nil if !param_name
|
|
70
77
|
decoded_param_name = URI.decode_uri_component(param_name)
|
|
71
|
-
params = CGI.parse(
|
|
78
|
+
params = CGI.parse(uri_query)
|
|
72
79
|
URI.encode_uri_component(params[decoded_param_name]&.first)
|
|
73
80
|
end
|
|
74
81
|
|
|
75
82
|
def field(name)
|
|
76
83
|
has_tr = name.parameters["tr"]
|
|
77
84
|
return nil if has_tr # HTTP requests don't have trailer fields
|
|
78
|
-
value =
|
|
85
|
+
value = header(name.value.to_s)
|
|
79
86
|
value.dup&.strip
|
|
80
87
|
end
|
|
81
88
|
end
|
|
@@ -31,16 +31,19 @@ module Linzer
|
|
|
31
31
|
@operation[name]
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
# Attaches a signature to the response.
|
|
35
|
-
# @param signature [Signature] The signature to attach
|
|
36
|
-
# @return [Object] The underlying response object
|
|
37
|
-
def attach!(signature)
|
|
38
|
-
signature.to_h.each { |h, v| @operation[h] = v }
|
|
39
|
-
@operation
|
|
40
|
-
end
|
|
41
|
-
|
|
42
34
|
private
|
|
43
35
|
|
|
36
|
+
# Sets a header on the underlying HTTP message.
|
|
37
|
+
#
|
|
38
|
+
# If a header with the given name already exists, its value is overwritten.
|
|
39
|
+
#
|
|
40
|
+
# @param header [String] the header name
|
|
41
|
+
# @param value [String] the header value
|
|
42
|
+
# @return [String] the value assigned to the header
|
|
43
|
+
def set_header!(header, value)
|
|
44
|
+
@operation[header] = value
|
|
45
|
+
end
|
|
46
|
+
|
|
44
47
|
def derived(name)
|
|
45
48
|
raise Linzer::Error, "Sub-classes are required to implement this method!"
|
|
46
49
|
end
|
|
@@ -20,22 +20,30 @@ module Linzer
|
|
|
20
20
|
@operation.headers[name]
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
# Attaches a signature to the response.
|
|
24
|
-
# @param signature [Signature] The signature to attach
|
|
25
|
-
# @return [Object] The underlying response object
|
|
26
|
-
def attach!(signature)
|
|
27
|
-
signature.to_h.each { |h, v| @operation.headers[h] = v }
|
|
28
|
-
@operation
|
|
29
|
-
end
|
|
30
|
-
|
|
31
23
|
private
|
|
32
24
|
|
|
25
|
+
# Retrieves an HTTP field value from the request or response headers.
|
|
26
|
+
#
|
|
27
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
28
|
+
# @return [String, nil] the stripped header value, or +nil+ if the
|
|
29
|
+
# field has a +tr+ (trailer) parameter or is not present
|
|
33
30
|
def field(name)
|
|
34
31
|
has_tr = name.parameters["tr"]
|
|
35
32
|
return nil if has_tr # XXX: is there a library actually supporting trailers?
|
|
36
33
|
value = @operation.headers[name.value.to_s]
|
|
37
34
|
value.dup&.strip
|
|
38
35
|
end
|
|
36
|
+
|
|
37
|
+
# Sets a header on the underlying HTTP message.
|
|
38
|
+
#
|
|
39
|
+
# If a header with the given name already exists, its value is overwritten.
|
|
40
|
+
#
|
|
41
|
+
# @param header [String] the header name
|
|
42
|
+
# @param value [String] the header value
|
|
43
|
+
# @return [String] the value assigned to the header
|
|
44
|
+
def set_header!(header, value)
|
|
45
|
+
@operation.headers[header] = value
|
|
46
|
+
end
|
|
39
47
|
end
|
|
40
48
|
end
|
|
41
49
|
end
|
|
@@ -15,6 +15,14 @@ module Linzer
|
|
|
15
15
|
|
|
16
16
|
private
|
|
17
17
|
|
|
18
|
+
# Resolves a derived component value from the request.
|
|
19
|
+
#
|
|
20
|
+
# Overrides the generic implementation for http.rb-specific
|
|
21
|
+
# accessor methods: +uri.host+ for +@authority+ and
|
|
22
|
+
# +verb+ for +@method+.
|
|
23
|
+
#
|
|
24
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
25
|
+
# @return [String, nil] the derived value
|
|
18
26
|
def derived(name)
|
|
19
27
|
return @operation.uri.host if name.value == "@authority"
|
|
20
28
|
return @operation.verb.to_s.upcase if name.value == "@method"
|
|
@@ -19,6 +19,13 @@ module Linzer
|
|
|
19
19
|
|
|
20
20
|
private
|
|
21
21
|
|
|
22
|
+
# Resolves a derived component value from the response.
|
|
23
|
+
#
|
|
24
|
+
# Uses +HTTP::Response#status+ converted to Integer for
|
|
25
|
+
# the +@status+ component.
|
|
26
|
+
#
|
|
27
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
28
|
+
# @return [Integer, nil] the HTTP status code, or +nil+ if unknown
|
|
22
29
|
def derived(name)
|
|
23
30
|
case name.value
|
|
24
31
|
when "@status" then @operation.status.to_i
|
|
@@ -14,6 +14,14 @@ module Linzer
|
|
|
14
14
|
class Request < Generic::Request
|
|
15
15
|
private
|
|
16
16
|
|
|
17
|
+
# Resolves a derived component value from the request.
|
|
18
|
+
#
|
|
19
|
+
# Overrides the generic implementation to use
|
|
20
|
+
# +Net::HTTPRequest#method+ for the +@method+ component,
|
|
21
|
+
# which returns the HTTP verb as an uppercase string.
|
|
22
|
+
#
|
|
23
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
24
|
+
# @return [String, nil] the derived value
|
|
17
25
|
def derived(name)
|
|
18
26
|
return @operation.method if name.value == "@method"
|
|
19
27
|
super
|
|
@@ -11,6 +11,13 @@ module Linzer
|
|
|
11
11
|
class Response < Generic::Response
|
|
12
12
|
private
|
|
13
13
|
|
|
14
|
+
# Resolves a derived component value from the response.
|
|
15
|
+
#
|
|
16
|
+
# Uses +Net::HTTPResponse#code+ (a String) converted to Integer
|
|
17
|
+
# for the +@status+ component.
|
|
18
|
+
#
|
|
19
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
20
|
+
# @return [Integer, nil] the HTTP status code, or +nil+ if unknown
|
|
14
21
|
def derived(name)
|
|
15
22
|
case name.value
|
|
16
23
|
when "@status" then @operation.code.to_i
|
|
@@ -24,11 +24,19 @@ module Linzer
|
|
|
24
24
|
|
|
25
25
|
private
|
|
26
26
|
|
|
27
|
+
# Validates that the operation is exclusively a request or response.
|
|
28
|
+
# @raise [Error] if the operation is both or neither
|
|
27
29
|
def validate
|
|
28
30
|
msg = "Message instance must be an HTTP request or response"
|
|
29
31
|
raise Error.new msg if response? == request?
|
|
30
32
|
end
|
|
31
33
|
|
|
34
|
+
# Validates that a header name is non-empty.
|
|
35
|
+
#
|
|
36
|
+
# @param name [String] the header name
|
|
37
|
+
# @return [String] the validated header name
|
|
38
|
+
# @raise [ArgumentError] if the name is blank
|
|
39
|
+
# @raise [Linzer::Error] if the name is otherwise invalid
|
|
32
40
|
def validate_header_name(name)
|
|
33
41
|
raise ArgumentError.new, "Blank header name." if name.empty?
|
|
34
42
|
name.to_str
|
|
@@ -40,6 +48,13 @@ module Linzer
|
|
|
40
48
|
# :nocov:
|
|
41
49
|
end
|
|
42
50
|
|
|
51
|
+
# Converts an HTTP header name to Rack's environment key format.
|
|
52
|
+
#
|
|
53
|
+
# Rack stores headers as uppercase with underscores and an +HTTP_+
|
|
54
|
+
# prefix, except for +Content-Type+ and +Content-Length+.
|
|
55
|
+
#
|
|
56
|
+
# @param field_name [String] the HTTP header name (e.g. +"content-type"+)
|
|
57
|
+
# @return [String] the Rack env key (e.g. +"CONTENT_TYPE"+ or +"HTTP_ACCEPT"+)
|
|
43
58
|
def rack_header_name(field_name)
|
|
44
59
|
validate_header_name field_name
|
|
45
60
|
|
|
@@ -52,6 +67,10 @@ module Linzer
|
|
|
52
67
|
end
|
|
53
68
|
end
|
|
54
69
|
|
|
70
|
+
# Resolves a derived component value from the Rack request/response.
|
|
71
|
+
#
|
|
72
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
73
|
+
# @return [String, nil] the derived value, or +nil+ if unknown
|
|
55
74
|
def derived(name)
|
|
56
75
|
method = DERIVED_COMPONENT[name.value]
|
|
57
76
|
|
|
@@ -64,6 +83,11 @@ module Linzer
|
|
|
64
83
|
value || derive(@operation, method)
|
|
65
84
|
end
|
|
66
85
|
|
|
86
|
+
# Retrieves an HTTP field value from the Rack request or response.
|
|
87
|
+
#
|
|
88
|
+
# @param name [Starry::Item] the parsed component identifier
|
|
89
|
+
# @return [String, nil] the stripped header value, or +nil+ if the
|
|
90
|
+
# field has a +tr+ (trailer) parameter or is not present
|
|
67
91
|
def field(name)
|
|
68
92
|
has_tr = name.parameters["tr"]
|
|
69
93
|
return nil if has_tr
|
|
@@ -79,6 +103,14 @@ module Linzer
|
|
|
79
103
|
field_value.dup&.strip
|
|
80
104
|
end
|
|
81
105
|
|
|
106
|
+
# Invokes a method on the Rack operation to extract a derived value.
|
|
107
|
+
#
|
|
108
|
+
# Applies post-processing for +@query+ (prepends +?+) and
|
|
109
|
+
# +@authority+/+@scheme+ (downcases).
|
|
110
|
+
#
|
|
111
|
+
# @param operation [Rack::Request, Rack::Response] the Rack object
|
|
112
|
+
# @param method [Symbol] the method to call
|
|
113
|
+
# @return [String, nil] the derived value
|
|
82
114
|
def derive(operation, method)
|
|
83
115
|
return nil unless operation.respond_to?(method)
|
|
84
116
|
value = operation.public_send(method)
|
|
@@ -87,6 +119,11 @@ module Linzer
|
|
|
87
119
|
value
|
|
88
120
|
end
|
|
89
121
|
|
|
122
|
+
# Extracts a single query parameter value by name.
|
|
123
|
+
#
|
|
124
|
+
# @param name [Starry::Item] the component with a +name+ parameter
|
|
125
|
+
# @return [String, nil] the percent-encoded parameter value, or
|
|
126
|
+
# +nil+ if the parameter is missing or not found
|
|
90
127
|
def query_param(name)
|
|
91
128
|
param_name = name.parameters["name"]
|
|
92
129
|
return nil if !param_name
|
|
@@ -27,14 +27,17 @@ module Linzer
|
|
|
27
27
|
@operation.get_header(rack_header_name(name))
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Sets a header on the underlying HTTP message.
|
|
33
|
+
#
|
|
34
|
+
# If a header with the given name already exists, its value is overwritten.
|
|
35
|
+
#
|
|
36
|
+
# @param header [String] the header name
|
|
37
|
+
# @param value [String] the header value
|
|
38
|
+
# @return [String] the value assigned to the header
|
|
39
|
+
def set_header!(header, value)
|
|
40
|
+
@operation.set_header(rack_header_name(header), value)
|
|
38
41
|
end
|
|
39
42
|
end
|
|
40
43
|
end
|
|
@@ -28,14 +28,17 @@ module Linzer
|
|
|
28
28
|
@operation.get_header(name)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Sets a header on the underlying HTTP message.
|
|
34
|
+
#
|
|
35
|
+
# If a header with the given name already exists, its value is overwritten.
|
|
36
|
+
#
|
|
37
|
+
# @param header [String] the header name
|
|
38
|
+
# @param value [String] the header value
|
|
39
|
+
# @return [String] the value assigned to the header
|
|
40
|
+
def set_header!(header, value)
|
|
41
|
+
@operation.set_header(header, value)
|
|
39
42
|
end
|
|
40
43
|
end
|
|
41
44
|
end
|