url_canonicalize 1.0.0 → 2.0.0
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 +148 -113
- data/README.md +234 -9
- data/SECURITY.md +25 -0
- data/lib/url_canonicalize/bounded_body.rb +23 -0
- data/lib/url_canonicalize/client.rb +21 -0
- data/lib/{monkey_patches/addressable/uri.rb → url_canonicalize/core_ext/addressable.rb} +4 -2
- data/lib/{monkey_patches → url_canonicalize/core_ext}/string.rb +2 -2
- data/lib/{monkey_patches → url_canonicalize/core_ext}/uri.rb +4 -2
- data/lib/url_canonicalize/core_ext.rb +6 -0
- data/lib/url_canonicalize/destination.rb +120 -0
- data/lib/url_canonicalize/exception.rb +3 -0
- data/lib/url_canonicalize/http.rb +86 -63
- data/lib/url_canonicalize/link_header.rb +34 -0
- data/lib/url_canonicalize/media_type.rb +23 -0
- data/lib/url_canonicalize/options.rb +112 -0
- data/lib/url_canonicalize/request.rb +86 -77
- data/lib/url_canonicalize/response.rb +7 -4
- data/lib/url_canonicalize/result.rb +28 -0
- data/lib/url_canonicalize/transport.rb +47 -0
- data/lib/url_canonicalize/uri.rb +26 -13
- data/lib/url_canonicalize/version.rb +1 -1
- data/lib/url_canonicalize.rb +13 -9
- metadata +22 -25
- data/.codeclimate.yml +0 -25
- data/.github/workflows/codeql-analysis.yml +0 -70
- data/.gitignore +0 -53
- data/.hound.yml +0 -4
- data/.rspec +0 -5
- data/.rubocop.yml +0 -61
- data/.ruby-gemset +0 -1
- data/.travis.yml +0 -8
- data/Gemfile +0 -22
- data/Gemfile.lock +0 -110
- data/Guardfile +0 -18
- data/Rakefile +0 -3
- data/circle.yml +0 -29
- data/renovate.json +0 -6
- data/url_canonicalize.gemspec +0 -29
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ipaddr'
|
|
4
|
+
require 'socket'
|
|
5
|
+
|
|
6
|
+
module URLCanonicalize
|
|
7
|
+
# Resolves a URI to an address that is permitted by the fetch security policy
|
|
8
|
+
class Destination
|
|
9
|
+
FORBIDDEN_IPV4_RANGES = %w[
|
|
10
|
+
0.0.0.0/8
|
|
11
|
+
10.0.0.0/8
|
|
12
|
+
100.64.0.0/10
|
|
13
|
+
127.0.0.0/8
|
|
14
|
+
169.254.0.0/16
|
|
15
|
+
172.16.0.0/12
|
|
16
|
+
192.0.0.0/24
|
|
17
|
+
192.0.2.0/24
|
|
18
|
+
192.88.99.0/24
|
|
19
|
+
192.168.0.0/16
|
|
20
|
+
198.18.0.0/15
|
|
21
|
+
198.51.100.0/24
|
|
22
|
+
203.0.113.0/24
|
|
23
|
+
224.0.0.0/4
|
|
24
|
+
240.0.0.0/4
|
|
25
|
+
].map { |range| IPAddr.new(range) }.freeze
|
|
26
|
+
|
|
27
|
+
# Fail closed: only prefixes in IANA's IPv6 Global Unicast Address Space registry are accepted.
|
|
28
|
+
ALLOCATED_IPV6_RANGES = %w[
|
|
29
|
+
2001:200::/23
|
|
30
|
+
2001:400::/23
|
|
31
|
+
2001:600::/23
|
|
32
|
+
2001:800::/22
|
|
33
|
+
2001:c00::/23
|
|
34
|
+
2001:e00::/23
|
|
35
|
+
2001:1200::/23
|
|
36
|
+
2001:1400::/22
|
|
37
|
+
2001:1800::/23
|
|
38
|
+
2001:1a00::/23
|
|
39
|
+
2001:1c00::/22
|
|
40
|
+
2001:2000::/19
|
|
41
|
+
2001:4000::/23
|
|
42
|
+
2001:4200::/23
|
|
43
|
+
2001:4400::/23
|
|
44
|
+
2001:4600::/23
|
|
45
|
+
2001:4800::/23
|
|
46
|
+
2001:4a00::/23
|
|
47
|
+
2001:4c00::/23
|
|
48
|
+
2001:5000::/20
|
|
49
|
+
2001:8000::/19
|
|
50
|
+
2001:a000::/20
|
|
51
|
+
2001:b000::/20
|
|
52
|
+
2003::/18
|
|
53
|
+
2400::/12
|
|
54
|
+
2410::/12
|
|
55
|
+
2600::/12
|
|
56
|
+
2610::/23
|
|
57
|
+
2620::/23
|
|
58
|
+
2630::/12
|
|
59
|
+
2800::/12
|
|
60
|
+
2a00::/12
|
|
61
|
+
2a10::/12
|
|
62
|
+
2c00::/12
|
|
63
|
+
].map { |range| IPAddr.new(range) }.freeze
|
|
64
|
+
|
|
65
|
+
FORBIDDEN_IPV6_RANGES = %w[
|
|
66
|
+
2001::/23
|
|
67
|
+
2001:db8::/32
|
|
68
|
+
2002::/16
|
|
69
|
+
].map { |range| IPAddr.new(range) }.freeze
|
|
70
|
+
|
|
71
|
+
class << self
|
|
72
|
+
def resolve(uri, options)
|
|
73
|
+
validate!(uri, options)
|
|
74
|
+
addresses = resolve_addresses(uri)
|
|
75
|
+
validate_addresses!(uri, addresses, options)
|
|
76
|
+
addresses.first
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def validate!(uri, options)
|
|
80
|
+
raise URLCanonicalize::Exception::Security, 'URLs containing userinfo are not allowed' if uri.userinfo
|
|
81
|
+
return if options[:allowed_ports].include?(uri.port)
|
|
82
|
+
|
|
83
|
+
raise URLCanonicalize::Exception::Security, "Port #{uri.port} is not allowed"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def resolve_addresses(uri)
|
|
89
|
+
addresses = Addrinfo.getaddrinfo(uri.host, uri.port, nil, Socket::SOCK_STREAM)
|
|
90
|
+
addresses = addresses.map(&:ip_address).uniq
|
|
91
|
+
raise SocketError, "No addresses found for #{uri.host}" if addresses.empty?
|
|
92
|
+
|
|
93
|
+
addresses
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def validate_addresses!(uri, addresses, options)
|
|
97
|
+
return if options[:allow_private_networks]
|
|
98
|
+
|
|
99
|
+
addresses.each do |address|
|
|
100
|
+
next if public_address?(address)
|
|
101
|
+
|
|
102
|
+
raise URLCanonicalize::Exception::Security,
|
|
103
|
+
"Address #{address} for #{uri.host} is not publicly routable"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def public_address?(value)
|
|
108
|
+
address = IPAddr.new(value)
|
|
109
|
+
address = address.native if address.ipv4_mapped?
|
|
110
|
+
|
|
111
|
+
if address.ipv4?
|
|
112
|
+
FORBIDDEN_IPV4_RANGES.none? { |range| range.include?(address) }
|
|
113
|
+
else
|
|
114
|
+
ALLOCATED_IPV6_RANGES.any? { |range| range.include?(address) } &&
|
|
115
|
+
FORBIDDEN_IPV6_RANGES.none? { |range| range.include?(address) }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -4,7 +4,12 @@ module URLCanonicalize
|
|
|
4
4
|
# Persistent connection for possible repeated requests to the same host
|
|
5
5
|
class HTTP
|
|
6
6
|
def fetch
|
|
7
|
-
|
|
7
|
+
final = ::Timeout.timeout(
|
|
8
|
+
options[:total_timeout],
|
|
9
|
+
URLCanonicalize::Exception::Timeout,
|
|
10
|
+
"Canonicalization exceeded #{options[:total_timeout]} seconds"
|
|
11
|
+
) { fetch_within_deadline }
|
|
12
|
+
build_result(final)
|
|
8
13
|
end
|
|
9
14
|
|
|
10
15
|
def uri
|
|
@@ -17,15 +22,31 @@ module URLCanonicalize
|
|
|
17
22
|
end
|
|
18
23
|
|
|
19
24
|
def do_request(http_request)
|
|
20
|
-
http.request http_request
|
|
25
|
+
http.request(http_request) { |response| read_response_body(http_request, response) }
|
|
21
26
|
end
|
|
22
27
|
|
|
28
|
+
attr_reader :options
|
|
29
|
+
|
|
23
30
|
private
|
|
24
31
|
|
|
25
32
|
attr_accessor :last_known_good
|
|
26
33
|
|
|
27
|
-
def initialize(raw_url)
|
|
34
|
+
def initialize(raw_url, options: nil, **values)
|
|
35
|
+
raise ArgumentError, 'Pass either options: or keyword options, not both' if options && !values.empty?
|
|
36
|
+
|
|
28
37
|
@raw_url = raw_url
|
|
38
|
+
@options = options || URLCanonicalize::Options.new(**values)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def build_result(final)
|
|
42
|
+
Result.new(url: final.url, response: final.response, html: final.html, chain: chain, source: @final_source)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def fetch_within_deadline
|
|
46
|
+
loop do
|
|
47
|
+
result = handle_response
|
|
48
|
+
break result if result
|
|
49
|
+
end
|
|
29
50
|
end
|
|
30
51
|
|
|
31
52
|
# Fetch the response
|
|
@@ -45,7 +66,8 @@ module URLCanonicalize
|
|
|
45
66
|
request.with_uri(uri).fetch
|
|
46
67
|
end
|
|
47
68
|
|
|
48
|
-
# Parse the response, and clear the response ready to follow the next redirect
|
|
69
|
+
# Parse the response, and clear the response ready to follow the next redirect.
|
|
70
|
+
# Returns the final response when canonicalization is complete, or nil to continue
|
|
49
71
|
def handle_response
|
|
50
72
|
result = parse_response
|
|
51
73
|
@response = nil
|
|
@@ -59,7 +81,7 @@ module URLCanonicalize
|
|
|
59
81
|
when URLCanonicalize::Response::Success
|
|
60
82
|
handle_success
|
|
61
83
|
when URLCanonicalize::Response::Redirect
|
|
62
|
-
|
|
84
|
+
handle_redirect
|
|
63
85
|
when URLCanonicalize::Response::CanonicalFound
|
|
64
86
|
handle_canonical_found
|
|
65
87
|
when URLCanonicalize::Response::Failure
|
|
@@ -69,54 +91,62 @@ module URLCanonicalize
|
|
|
69
91
|
end
|
|
70
92
|
end
|
|
71
93
|
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
-
|
|
94
|
+
def handle_redirect
|
|
95
|
+
reason = hop_refusal_reason
|
|
96
|
+
return follow_hop(:redirect) unless reason
|
|
75
97
|
|
|
76
|
-
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
redirect_list << response_url
|
|
80
|
-
increment_redirects
|
|
81
|
-
set_url_from_response
|
|
82
|
-
false
|
|
98
|
+
finish_with_last_known_good || raise(URLCanonicalize::Exception::Redirect, reason)
|
|
83
99
|
end
|
|
84
100
|
|
|
85
|
-
def
|
|
86
|
-
|
|
87
|
-
return
|
|
101
|
+
def handle_canonical_found
|
|
102
|
+
self.last_known_good = response.response
|
|
103
|
+
return finish_with_last_known_good if hop_refusal_reason
|
|
88
104
|
|
|
89
|
-
|
|
105
|
+
follow_hop(:canonical_link)
|
|
90
106
|
end
|
|
91
107
|
|
|
92
|
-
|
|
93
|
-
|
|
108
|
+
# A terminal response whose URL came from a server-declared canonical link
|
|
109
|
+
def finish_with_last_known_good
|
|
110
|
+
return unless last_known_good
|
|
111
|
+
|
|
112
|
+
@final_source = :canonical_link
|
|
113
|
+
last_known_good
|
|
94
114
|
end
|
|
95
115
|
|
|
96
|
-
|
|
97
|
-
|
|
116
|
+
# Redirects and followed canonical links share one visited-URL set and one
|
|
117
|
+
# hop budget, so any cycle or over-long chain terminates deterministically
|
|
118
|
+
def hop_refusal_reason
|
|
119
|
+
return 'Redirect loop detected' if visited?(response_url)
|
|
120
|
+
return "#{hops + 1} redirects is too many" if hops >= options[:max_redirects]
|
|
121
|
+
|
|
122
|
+
nil
|
|
98
123
|
end
|
|
99
124
|
|
|
100
|
-
def
|
|
101
|
-
|
|
125
|
+
def follow_hop(via)
|
|
126
|
+
chain << Result::Hop.new(url: response_url, via: via)
|
|
127
|
+
self.url = response_url
|
|
128
|
+
nil
|
|
102
129
|
end
|
|
103
130
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
131
|
+
# The URLs requested so far and how each one was discovered
|
|
132
|
+
def chain
|
|
133
|
+
@chain ||= [Result::Hop.new(url: url, via: :initial)]
|
|
134
|
+
end
|
|
107
135
|
|
|
108
|
-
|
|
109
|
-
|
|
136
|
+
def visited?(candidate)
|
|
137
|
+
chain.any? { |hop| hop.url == candidate }
|
|
110
138
|
end
|
|
111
139
|
|
|
112
|
-
def
|
|
113
|
-
|
|
140
|
+
def hops
|
|
141
|
+
chain.length - 1
|
|
114
142
|
end
|
|
115
143
|
|
|
116
144
|
def handle_failure
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
145
|
+
finish_with_last_known_good || raise(
|
|
146
|
+
URLCanonicalize::Exception::Failure,
|
|
147
|
+
"#{response.failure_class}: #{response.message}",
|
|
148
|
+
cause: response.error
|
|
149
|
+
)
|
|
120
150
|
end
|
|
121
151
|
|
|
122
152
|
def handle_unhandled_response
|
|
@@ -124,8 +154,8 @@ module URLCanonicalize
|
|
|
124
154
|
end
|
|
125
155
|
|
|
126
156
|
def handle_success
|
|
157
|
+
@final_source = chain.last.via
|
|
127
158
|
self.last_known_good = response
|
|
128
|
-
true
|
|
129
159
|
end
|
|
130
160
|
|
|
131
161
|
def url
|
|
@@ -133,46 +163,39 @@ module URLCanonicalize
|
|
|
133
163
|
end
|
|
134
164
|
|
|
135
165
|
def http
|
|
136
|
-
|
|
166
|
+
URLCanonicalize::Destination.validate!(uri, options)
|
|
167
|
+
return @http if same_origin? # reuse connection
|
|
137
168
|
|
|
138
169
|
@previous = uri
|
|
139
|
-
@http =
|
|
170
|
+
@http = options[:transport].call(uri, options)
|
|
140
171
|
end
|
|
141
172
|
|
|
142
|
-
def
|
|
143
|
-
uri.host == previous.host && uri.port == previous.port
|
|
173
|
+
def same_origin?
|
|
174
|
+
uri.scheme == previous.scheme && uri.host == previous.host && uri.port == previous.port
|
|
144
175
|
end
|
|
145
176
|
|
|
146
177
|
def previous
|
|
147
|
-
@previous ||= Struct.new(:host, :port).new
|
|
178
|
+
@previous ||= Struct.new(:scheme, :host, :port).new
|
|
148
179
|
end
|
|
149
180
|
|
|
150
|
-
def
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
h.read_timeout = options[:read_timeout]
|
|
155
|
-
|
|
156
|
-
ssl!(h)
|
|
157
|
-
|
|
158
|
-
h
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def ssl!(http)
|
|
162
|
-
if uri.scheme == 'https'
|
|
163
|
-
http.use_ssl = true # Can generate exception
|
|
164
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
181
|
+
def read_response_body(http_request, response)
|
|
182
|
+
if buffer_response_body?(http_request, response)
|
|
183
|
+
enforce_content_length!(response)
|
|
184
|
+
response.read_body(URLCanonicalize::BoundedBody.new(options[:max_body_bytes]))
|
|
165
185
|
else
|
|
166
|
-
|
|
186
|
+
response.read_body { |_chunk| nil }
|
|
167
187
|
end
|
|
168
188
|
end
|
|
169
189
|
|
|
170
|
-
def
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
190
|
+
def buffer_response_body?(http_request, response)
|
|
191
|
+
http_request.method == 'GET' && response.is_a?(Net::HTTPSuccess) && URLCanonicalize::MediaType.buffered?(response)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def enforce_content_length!(response)
|
|
195
|
+
return unless response.content_length && response.content_length > options[:max_body_bytes]
|
|
196
|
+
|
|
197
|
+
raise URLCanonicalize::Exception::ResponseTooLarge,
|
|
198
|
+
"Response body exceeds #{options[:max_body_bytes]} bytes"
|
|
176
199
|
end
|
|
177
200
|
end
|
|
178
201
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module URLCanonicalize
|
|
4
|
+
# Parses HTTP Link header fields (RFC 8288) far enough to find the target of
|
|
5
|
+
# the first link whose rel tokens include "canonical"
|
|
6
|
+
module LinkHeader
|
|
7
|
+
LINK_VALUE = /<([^>]*)>((?:\s*;\s*[\w!#$%&'*+.^`|~-]+\s*=\s*(?:"[^"]*"|[^",;]+))*)/
|
|
8
|
+
REL_PARAM = /;\s*rel\s*=\s*(?:"([^"]*)"|([^",;\s]+))/i
|
|
9
|
+
CANONICAL = 'canonical'
|
|
10
|
+
|
|
11
|
+
extend self
|
|
12
|
+
|
|
13
|
+
def canonical(response)
|
|
14
|
+
fields = response.get_fields('link')
|
|
15
|
+
return unless fields
|
|
16
|
+
|
|
17
|
+
fields.each do |field|
|
|
18
|
+
field.scan(LINK_VALUE) do |target, params|
|
|
19
|
+
return target if canonical?(params)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def canonical?(params)
|
|
27
|
+
match = REL_PARAM.match(params)
|
|
28
|
+
return false unless match
|
|
29
|
+
|
|
30
|
+
rel = match[1] || match[2]
|
|
31
|
+
rel.split.any? { |token| token.casecmp?(CANONICAL) }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module URLCanonicalize
|
|
4
|
+
# Normalizes response media types for safe buffering and parsing decisions
|
|
5
|
+
module MediaType
|
|
6
|
+
HTML = %w[text/html application/xhtml+xml].freeze
|
|
7
|
+
BUFFERED = (HTML + %w[application/xml text/xml]).freeze
|
|
8
|
+
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
def buffered?(response)
|
|
12
|
+
BUFFERED.include?(normalized(response))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def html?(response)
|
|
16
|
+
HTML.include?(normalized(response))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def normalized(response)
|
|
20
|
+
response['content-type'].to_s.split(';', 2).first.to_s.strip.downcase
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module URLCanonicalize
|
|
4
|
+
# Validated configuration for one canonicalization operation
|
|
5
|
+
class Options
|
|
6
|
+
DEFAULTS = {
|
|
7
|
+
allow_private_networks: false,
|
|
8
|
+
allowed_ports: [80, 443].freeze,
|
|
9
|
+
headers: {
|
|
10
|
+
'Accept-Language' => 'en-US,en;q=0.8',
|
|
11
|
+
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; WOW64) ' \
|
|
12
|
+
'AppleWebKit/537.36 (KHTML, like Gecko) ' \
|
|
13
|
+
'Chrome/51.0.2704.103 Safari/537.36'
|
|
14
|
+
}.freeze,
|
|
15
|
+
logger: nil,
|
|
16
|
+
max_body_bytes: 1_048_576,
|
|
17
|
+
total_timeout: 30,
|
|
18
|
+
open_timeout: 8,
|
|
19
|
+
read_timeout: 15,
|
|
20
|
+
write_timeout: 8,
|
|
21
|
+
max_redirects: 10,
|
|
22
|
+
transport: Transport.new
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
POSITIVE_NUMERIC_OPTIONS = %i[total_timeout open_timeout read_timeout write_timeout].freeze
|
|
26
|
+
POSITIVE_INTEGER_OPTIONS = %i[max_body_bytes max_redirects].freeze
|
|
27
|
+
|
|
28
|
+
def initialize(**values)
|
|
29
|
+
validate_known_options!(values)
|
|
30
|
+
|
|
31
|
+
@values = DEFAULTS.merge(values)
|
|
32
|
+
validate!
|
|
33
|
+
@values[:allowed_ports] = @values[:allowed_ports].dup.freeze
|
|
34
|
+
@values[:headers] = @values[:headers].dup.freeze
|
|
35
|
+
@values.freeze
|
|
36
|
+
freeze
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def [](key)
|
|
40
|
+
@values.fetch(key)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_h
|
|
44
|
+
@values.merge(allowed_ports: @values[:allowed_ports].dup, headers: @values[:headers].dup)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def validate!
|
|
50
|
+
validate_private_networks!
|
|
51
|
+
validate_ports!
|
|
52
|
+
validate_positive_values!
|
|
53
|
+
validate_headers!
|
|
54
|
+
validate_logger!
|
|
55
|
+
validate_transport!
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def validate_known_options!(values)
|
|
59
|
+
unknown = values.keys - DEFAULTS.keys
|
|
60
|
+
raise ArgumentError, "Unknown options: #{unknown.join(', ')}" unless unknown.empty?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def validate_private_networks!
|
|
64
|
+
return if [true, false].include?(@values[:allow_private_networks])
|
|
65
|
+
|
|
66
|
+
raise ArgumentError, 'allow_private_networks must be true or false'
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def validate_ports!
|
|
70
|
+
ports = @values[:allowed_ports]
|
|
71
|
+
valid = ports.is_a?(Array) && !ports.empty? && ports.all? do |port|
|
|
72
|
+
port.is_a?(Integer) && port.between?(1, 65_535)
|
|
73
|
+
end
|
|
74
|
+
return if valid
|
|
75
|
+
|
|
76
|
+
raise ArgumentError, 'allowed_ports must contain only valid TCP ports'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def validate_positive_values!
|
|
80
|
+
POSITIVE_NUMERIC_OPTIONS.each do |name|
|
|
81
|
+
value = @values[name]
|
|
82
|
+
raise ArgumentError, "#{name} must be positive" unless value.is_a?(Numeric) && value.positive?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
POSITIVE_INTEGER_OPTIONS.each do |name|
|
|
86
|
+
value = @values[name]
|
|
87
|
+
raise ArgumentError, "#{name} must be positive" unless value.is_a?(Integer) && value.positive?
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def validate_headers!
|
|
92
|
+
headers = @values[:headers]
|
|
93
|
+
valid = headers.is_a?(Hash) && headers.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
|
|
94
|
+
return if valid
|
|
95
|
+
|
|
96
|
+
raise ArgumentError, 'headers must be a Hash of String keys and String values'
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def validate_logger!
|
|
100
|
+
logger = @values[:logger]
|
|
101
|
+
return if logger.nil? || logger.respond_to?(:debug)
|
|
102
|
+
|
|
103
|
+
raise ArgumentError, 'logger must respond to debug'
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def validate_transport!
|
|
107
|
+
return if @values[:transport].respond_to?(:call)
|
|
108
|
+
|
|
109
|
+
raise ArgumentError, 'transport must respond to call'
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|