linzer 0.7.0.beta2 → 0.7.0.beta4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -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
- @operation = operation
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
- def request?
13
- @operation.is_a?(Rack::Request) || @operation.respond_to?(:request_method)
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
- Request.headers(@operation)
28
- end
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
- def [](field_name)
46
- name = parse_field_name(field_name)
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 parse_structured_dictionary(str, field_name = nil)
58
- Starry.parse_dictionary(str)
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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ module Options
5
+ DEFAULT = {
6
+ covered_components: %w[@method @request-target @authority date]
7
+ }.freeze
8
+ end
9
+ end
@@ -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
- Message.parse_structured_dictionary(value, field_name)
99
+ parse_structured_dictionary(value, field_name)
94
100
  end
95
101
  end
96
102
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Linzer
4
- VERSION = "0.7.0.beta2"
4
+ VERSION = "0.7.0.beta4"
5
5
  end
data/lib/linzer.rb CHANGED
@@ -5,12 +5,14 @@ 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"
12
+ require_relative "linzer/options"
13
13
  require_relative "linzer/message"
14
+ require_relative "linzer/message/adapter"
15
+ require_relative "linzer/message/wrapper"
14
16
  require_relative "linzer/signature"
15
17
  require_relative "linzer/key"
16
18
  require_relative "linzer/rsa"
@@ -21,6 +23,7 @@ require_relative "linzer/ecdsa"
21
23
  require_relative "linzer/key/helper"
22
24
  require_relative "linzer/signer"
23
25
  require_relative "linzer/verifier"
26
+ require_relative "linzer/http"
24
27
  require_relative "rack/auth/signature"
25
28
 
26
29
  module Linzer
@@ -28,11 +31,6 @@ module Linzer
28
31
 
29
32
  class << self
30
33
  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
34
 
37
35
  def verify(pubkey, message, signature, no_older_than: nil)
38
36
  Linzer::Verifier.verify(pubkey, message, signature, no_older_than: no_older_than)
@@ -41,5 +39,27 @@ module Linzer
41
39
  def sign(key, message, components, options = {})
42
40
  Linzer::Signer.sign(key, message, components, options)
43
41
  end
42
+
43
+ def sign!(request_or_response, **args)
44
+ message = Message.new(request_or_response)
45
+ options = {}
46
+
47
+ label = args[:label]
48
+ options[:label] = label if label
49
+ options.merge!(args.fetch(:params, {}))
50
+
51
+ key = args.fetch(:key)
52
+ signature = Linzer::Signer.sign(key, message, args.fetch(:components), options)
53
+ message.attach!(signature)
54
+ end
55
+
56
+ def verify!(request_or_response, key: nil, no_older_than: 900)
57
+ message = Message.new(request_or_response)
58
+ signature = Signature.build(message.headers.slice("signature", "signature-input"))
59
+ keyid = signature.parameters["keyid"]
60
+ raise Linzer::Error, "key not found" if !key && !keyid
61
+ verify_key = block_given? ? (yield keyid) : key
62
+ Linzer.verify(verify_key, message, signature, no_older_than: no_older_than)
63
+ end
44
64
  end
45
65
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "yaml"
2
4
 
3
5
  module Rack
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "linzer"
2
4
  require "logger"
3
5
  require_relative "signature/helpers"