url_canonicalize 1.0.0 → 1.0.2

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.
@@ -4,7 +4,11 @@ module URLCanonicalize
4
4
  # Persistent connection for possible repeated requests to the same host
5
5
  class HTTP
6
6
  def fetch
7
- loop { break last_known_good if handle_response }
7
+ ::Timeout.timeout(
8
+ options[:total_timeout],
9
+ URLCanonicalize::Exception::Timeout,
10
+ "Canonicalization exceeded #{options[:total_timeout]} seconds"
11
+ ) { fetch_within_deadline }
8
12
  end
9
13
 
10
14
  def uri
@@ -17,15 +21,24 @@ module URLCanonicalize
17
21
  end
18
22
 
19
23
  def do_request(http_request)
20
- http.request http_request
24
+ http.request(http_request) { |response| read_response_body(http_request, response) }
21
25
  end
22
26
 
23
27
  private
24
28
 
25
29
  attr_accessor :last_known_good
30
+ attr_reader :options
26
31
 
27
- def initialize(raw_url)
32
+ def fetch_within_deadline
33
+ loop do
34
+ result = handle_response
35
+ break result if result
36
+ end
37
+ end
38
+
39
+ def initialize(raw_url, **options)
28
40
  @raw_url = raw_url
41
+ @options = URLCanonicalize::Options.new(**options)
29
42
  end
30
43
 
31
44
  # Fetch the response
@@ -45,7 +58,8 @@ module URLCanonicalize
45
58
  request.with_uri(uri).fetch
46
59
  end
47
60
 
48
- # Parse the response, and clear the response ready to follow the next redirect
61
+ # Parse the response, and clear the response ready to follow the next redirect.
62
+ # Returns the final response when canonicalization is complete, or nil to continue
49
63
  def handle_response
50
64
  result = parse_response
51
65
  @response = nil
@@ -59,7 +73,7 @@ module URLCanonicalize
59
73
  when URLCanonicalize::Response::Success
60
74
  handle_success
61
75
  when URLCanonicalize::Response::Redirect
62
- redirect_loop_detected? || max_redirects_reached?
76
+ handle_redirect
63
77
  when URLCanonicalize::Response::CanonicalFound
64
78
  handle_canonical_found
65
79
  when URLCanonicalize::Response::Failure
@@ -69,54 +83,49 @@ module URLCanonicalize
69
83
  end
70
84
  end
71
85
 
72
- def redirect_loop_detected?
73
- if redirect_list.include?(response_url)
74
- return true if last_known_good
86
+ def handle_redirect
87
+ reason = hop_refusal_reason
88
+ return follow_hop unless reason
75
89
 
76
- raise URLCanonicalize::Exception::Redirect, 'Redirect loop detected'
77
- end
78
-
79
- redirect_list << response_url
80
- increment_redirects
81
- set_url_from_response
82
- false
90
+ last_known_good || raise(URLCanonicalize::Exception::Redirect, reason)
83
91
  end
84
92
 
85
- def max_redirects_reached?
86
- return false unless @redirects > options[:max_redirects]
87
- return true if last_known_good
93
+ def handle_canonical_found
94
+ self.last_known_good = response.response
95
+ return last_known_good if hop_refusal_reason
88
96
 
89
- raise URLCanonicalize::Exception::Redirect, "#{@redirects} redirects is too many"
97
+ follow_hop
90
98
  end
91
99
 
92
- def redirect_list
93
- @redirect_list ||= []
94
- end
100
+ # Redirects and followed canonical links share one visited-URL set and one
101
+ # hop budget, so any cycle or over-long chain terminates deterministically
102
+ def hop_refusal_reason
103
+ return 'Redirect loop detected' if visited.include?(response_url)
104
+ return "#{hops + 1} redirects is too many" if hops >= options[:max_redirects]
95
105
 
96
- def increment_redirects
97
- @redirects = redirects + 1
106
+ nil
98
107
  end
99
108
 
100
- def redirects
101
- @redirects ||= 0
109
+ def follow_hop
110
+ visited << response_url
111
+ @hops = hops + 1
112
+ self.url = response_url
113
+ nil
102
114
  end
103
115
 
104
- def handle_canonical_found
105
- self.last_known_good = response.response
106
- return true if response_url == url || redirect_list.include?(response_url)
107
-
108
- set_url_from_response
109
- false
116
+ def visited
117
+ @visited ||= [url]
110
118
  end
111
119
 
112
- def set_url_from_response
113
- self.url = response_url
120
+ def hops
121
+ @hops ||= 0
114
122
  end
115
123
 
116
124
  def handle_failure
117
- return true if last_known_good
125
+ return last_known_good if last_known_good
118
126
 
119
- raise URLCanonicalize::Exception::Failure, "#{response.failure_class}: #{response.message}"
127
+ raise URLCanonicalize::Exception::Failure, "#{response.failure_class}: #{response.message}",
128
+ cause: response.error
120
129
  end
121
130
 
122
131
  def handle_unhandled_response
@@ -125,7 +134,6 @@ module URLCanonicalize
125
134
 
126
135
  def handle_success
127
136
  self.last_known_good = response
128
- true
129
137
  end
130
138
 
131
139
  def url
@@ -133,46 +141,65 @@ module URLCanonicalize
133
141
  end
134
142
 
135
143
  def http
136
- return @http if same_host_and_port # reuse connection
144
+ URLCanonicalize::Destination.validate!(uri, options)
145
+ return @http if same_origin? # reuse connection
137
146
 
138
147
  @previous = uri
139
148
  @http = new_http
140
149
  end
141
150
 
142
- def same_host_and_port
143
- uri.host == previous.host && uri.port == previous.port
151
+ def same_origin?
152
+ uri.scheme == previous.scheme && uri.host == previous.host && uri.port == previous.port
144
153
  end
145
154
 
146
155
  def previous
147
- @previous ||= Struct.new(:host, :port).new
156
+ @previous ||= Struct.new(:scheme, :host, :port).new
148
157
  end
149
158
 
150
159
  def new_http
151
- h = Net::HTTP.new uri.host, uri.port
152
-
153
- h.open_timeout = options[:open_timeout]
154
- h.read_timeout = options[:read_timeout]
160
+ h = Net::HTTP.new uri.host, uri.port, nil
155
161
 
162
+ h.ipaddr = URLCanonicalize::Destination.resolve(uri, options)
163
+ configure_timeouts(h)
156
164
  ssl!(h)
157
165
 
158
166
  h
159
167
  end
160
168
 
169
+ def read_response_body(http_request, response)
170
+ if buffer_response_body?(http_request, response)
171
+ enforce_content_length!(response)
172
+ response.read_body(URLCanonicalize::BoundedBody.new(options[:max_body_bytes]))
173
+ else
174
+ response.read_body { |_chunk| nil }
175
+ end
176
+ end
177
+
178
+ def buffer_response_body?(http_request, response)
179
+ http_request.method == 'GET' && response.is_a?(Net::HTTPSuccess) && URLCanonicalize::MediaType.buffered?(response)
180
+ end
181
+
182
+ def enforce_content_length!(response)
183
+ return unless response.content_length && response.content_length > options[:max_body_bytes]
184
+
185
+ raise URLCanonicalize::Exception::ResponseTooLarge,
186
+ "Response body exceeds #{options[:max_body_bytes]} bytes"
187
+ end
188
+
189
+ def configure_timeouts(http)
190
+ http.open_timeout = options[:open_timeout]
191
+ http.read_timeout = options[:read_timeout]
192
+ http.write_timeout = options[:write_timeout]
193
+ end
194
+
161
195
  def ssl!(http)
162
196
  if uri.scheme == 'https'
163
197
  http.use_ssl = true # Can generate exception
164
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
198
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
199
+ http.verify_hostname = true
165
200
  else
166
201
  http.use_ssl = false
167
202
  end
168
203
  end
169
-
170
- def options
171
- @options ||= {
172
- open_timeout: 8, # Twitter responds in >5s
173
- read_timeout: 15,
174
- max_redirects: 10
175
- }
176
- end
177
204
  end
178
205
  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,79 @@
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
+ max_body_bytes: 1_048_576,
10
+ total_timeout: 30,
11
+ open_timeout: 8,
12
+ read_timeout: 15,
13
+ write_timeout: 8,
14
+ max_redirects: 10
15
+ }.freeze
16
+
17
+ POSITIVE_NUMERIC_OPTIONS = %i[total_timeout open_timeout read_timeout write_timeout].freeze
18
+ POSITIVE_INTEGER_OPTIONS = %i[max_body_bytes max_redirects].freeze
19
+
20
+ def initialize(**values)
21
+ validate_known_options!(values)
22
+
23
+ @values = DEFAULTS.merge(values)
24
+ validate!
25
+ @values[:allowed_ports] = @values[:allowed_ports].dup.freeze
26
+ @values.freeze
27
+ freeze
28
+ end
29
+
30
+ def [](key)
31
+ @values.fetch(key)
32
+ end
33
+
34
+ def to_h
35
+ @values.merge(allowed_ports: @values[:allowed_ports].dup)
36
+ end
37
+
38
+ private
39
+
40
+ def validate!
41
+ validate_private_networks!
42
+ validate_ports!
43
+ validate_positive_values!
44
+ end
45
+
46
+ def validate_known_options!(values)
47
+ unknown = values.keys - DEFAULTS.keys
48
+ raise ArgumentError, "Unknown options: #{unknown.join(', ')}" unless unknown.empty?
49
+ end
50
+
51
+ def validate_private_networks!
52
+ return if [true, false].include?(@values[:allow_private_networks])
53
+
54
+ raise ArgumentError, 'allow_private_networks must be true or false'
55
+ end
56
+
57
+ def validate_ports!
58
+ ports = @values[:allowed_ports]
59
+ valid = ports.is_a?(Array) && !ports.empty? && ports.all? do |port|
60
+ port.is_a?(Integer) && port.between?(1, 65_535)
61
+ end
62
+ return if valid
63
+
64
+ raise ArgumentError, 'allowed_ports must contain only valid TCP ports'
65
+ end
66
+
67
+ def validate_positive_values!
68
+ POSITIVE_NUMERIC_OPTIONS.each do |name|
69
+ value = @values[name]
70
+ raise ArgumentError, "#{name} must be positive" unless value.is_a?(Numeric) && value.positive?
71
+ end
72
+
73
+ POSITIVE_INTEGER_OPTIONS.each do |name|
74
+ value = @values[name]
75
+ raise ArgumentError, "#{name} must be positive" unless value.is_a?(Integer) && value.positive?
76
+ end
77
+ end
78
+ end
79
+ end
@@ -3,6 +3,30 @@
3
3
  module URLCanonicalize
4
4
  # Make an HTTP request
5
5
  class Request
6
+ NETWORK_EXCEPTIONS = [
7
+ EOFError,
8
+ Errno::ECONNREFUSED,
9
+ Errno::ECONNRESET,
10
+ Errno::EHOSTUNREACH,
11
+ Errno::EINVAL,
12
+ Errno::ENETUNREACH,
13
+ Errno::ETIMEDOUT,
14
+ Net::OpenTimeout,
15
+ Net::ReadTimeout,
16
+ OpenSSL::SSL::SSLError,
17
+ SocketError,
18
+ Timeout::Error,
19
+ Zlib::BufError,
20
+ Zlib::DataError
21
+ ].freeze
22
+
23
+ # Unsuccessful responses to a HEAD request that trigger a retry with GET
24
+ HEAD_FALLBACK_RESPONSES = [
25
+ Net::HTTPForbidden,
26
+ Net::HTTPMethodNotAllowed,
27
+ Net::HTTPNotImplemented
28
+ ].freeze
29
+
6
30
  def fetch
7
31
  handle_response
8
32
  end
@@ -11,15 +35,13 @@ module URLCanonicalize
11
35
  @location ||= relative_to_absolute(response['location'])
12
36
  end
13
37
 
38
+ # Point this request at a new URI, discarding all state from the previous
39
+ # response so nothing leaks between hops
14
40
  def with_uri(uri)
15
41
  @uri = uri
16
42
 
17
43
  @url = nil
18
- @host = nil
19
- @request = nil
20
- @response = nil
21
- @location = nil
22
- @html = nil
44
+ self.http_method = @default_http_method
23
45
 
24
46
  self
25
47
  end
@@ -31,6 +53,7 @@ module URLCanonicalize
31
53
  def initialize(http, http_method = :head)
32
54
  @http = http
33
55
  @http_method = http_method
56
+ @default_http_method = http_method
34
57
  end
35
58
 
36
59
  def response
@@ -55,41 +78,51 @@ module URLCanonicalize
55
78
  when Net::HTTPRedirection
56
79
  handle_redirection
57
80
  else
58
- handle_failure
81
+ handle_unsuccessful
59
82
  end
60
83
  rescue *NETWORK_EXCEPTIONS => e
61
- handle_failure(e.class, e.message)
84
+ handle_failure(e.class, e.message, e)
62
85
  end
63
86
 
64
87
  def handle_success
65
- @canonical_url = $LAST_MATCH_INFO['url'] if (response['link'] || '') =~ /<(?<url>.+)>\s*;\s*rel="canonical"/i
88
+ @canonical_url = relative_to_absolute(URLCanonicalize::LinkHeader.canonical(response))
66
89
 
67
90
  return enhanced_response if canonical_url || http_method == :get
68
91
 
92
+ fallback_to_get
93
+ end
94
+
95
+ # Some servers reject HEAD requests outright, so any HEAD request rejected
96
+ # with one of these statuses is retried as a GET before giving up
97
+ def handle_unsuccessful
98
+ return fallback_to_get if http_method == :head && HEAD_FALLBACK_RESPONSES.any? { |klass| response.is_a?(klass) }
99
+
100
+ handle_failure
101
+ end
102
+
103
+ def fallback_to_get
69
104
  self.http_method = :get
70
105
  fetch
71
106
  end
72
107
 
108
+ # Follow any redirection that carries a usable Location header, whether the
109
+ # redirect is temporary or permanent. A redirection without one cannot be
110
+ # followed, so it is reported as a failure.
73
111
  def handle_redirection
74
- case response
75
- when Net::HTTPFound, Net::HTTPMovedTemporarily, Net::HTTPTemporaryRedirect # Temporary redirection
76
- handle_success
77
- else # Permanent redirection
78
- if location
79
- URLCanonicalize::Response::Redirect.new(location)
80
- else
81
- URLCanonicalize::Response::Failure.new(::URI::InvalidURIError, response['location'])
82
- end
112
+ if location
113
+ URLCanonicalize::Response::Redirect.new(location)
114
+ else
115
+ URLCanonicalize::Response::Failure.new(::URI::InvalidURIError, response['location'])
83
116
  end
84
117
  end
85
118
 
86
- def handle_failure(klass = response.class, message = response.message)
87
- URLCanonicalize::Response::Failure.new(klass, message)
119
+ def handle_failure(klass = response.class, message = response.message, error = nil)
120
+ URLCanonicalize::Response::Failure.new(klass, message, error)
88
121
  end
89
122
 
90
123
  def enhanced_response
91
124
  if canonical_url
92
- puts " * canonical_url:\t#{canonical_url}" if ENV['DEBUG']
125
+ puts " * canonical_url:\t#{canonical_url}" if ENV.fetch('DEBUG', nil)
93
126
  response_plus = URLCanonicalize::Response::Success.new(canonical_url, response, html)
94
127
  URLCanonicalize::Response::CanonicalFound.new(canonical_url, response_plus)
95
128
  else
@@ -98,19 +131,26 @@ module URLCanonicalize
98
131
  end
99
132
 
100
133
  def html
101
- @html ||= Nokogiri::HTML response.body
134
+ @html ||= Nokogiri::HTML(response.body) if URLCanonicalize::MediaType.html?(response)
102
135
  end
103
136
 
104
137
  def canonical_url
105
- @canonical_url ||= relative_to_absolute(canonical_url_raw)
138
+ @canonical_url ||= relative_to_absolute(canonical_url_element&.[]('href'), document_base_url)
106
139
  end
107
140
 
108
- def canonical_url_raw
109
- @canonical_url ||= canonical_url_element['href'] if canonical_url_element.is_a?(Nokogiri::XML::Element)
141
+ # The first HTML link element whose rel tokens include "canonical", however
142
+ # the tokens are cased, provided it has a usable href
143
+ def canonical_url_element
144
+ @canonical_url_element ||= html&.css('head link[rel]')&.find do |element|
145
+ element['rel'].split.any? { |token| token.casecmp?('canonical') } && !element['href'].to_s.strip.empty?
146
+ end
110
147
  end
111
148
 
112
- def canonical_url_element
113
- @canonical_url_element ||= html.xpath('//head/link[@rel="canonical"]').first
149
+ # HTML canonical links resolve against the document base URL, not the
150
+ # request URL, when the document declares one
151
+ def document_base_url
152
+ base_href = html&.at_css('head base[href]')&.[]('href')
153
+ relative_to_absolute(base_href) || url
114
154
  end
115
155
 
116
156
  def uri
@@ -121,10 +161,6 @@ module URLCanonicalize
121
161
  @url ||= uri.to_s
122
162
  end
123
163
 
124
- def host
125
- @host ||= uri.host
126
- end
127
-
128
164
  def request_for_method
129
165
  r = base_request
130
166
  headers.each { |header_key, header_value| r[header_key] = header_value }
@@ -132,8 +168,6 @@ module URLCanonicalize
132
168
  end
133
169
 
134
170
  def base_request
135
- check_http_method
136
-
137
171
  case http_method
138
172
  when :head
139
173
  Net::HTTP::Head.new uri
@@ -159,54 +193,31 @@ module URLCanonicalize
159
193
  @response = nil
160
194
  @location = nil
161
195
  @html = nil
196
+ @canonical_url = nil
197
+ @canonical_url_element = nil
162
198
  end
163
199
 
164
- # Some sites treat HEAD requests as suspicious activity and block the
165
- # requester after a few attempts. For these sites we'll use GET requests
166
- # only
167
- def check_http_method
168
- @http_method = :get if /(linkedin|crunchbase).com/ =~ host
169
- end
200
+ # Resolve absolute, protocol-relative, root-relative, path-relative and
201
+ # query-only references against the base URL per RFC 3986. Only http(s)
202
+ # results are usable
203
+ def relative_to_absolute(reference, base = url)
204
+ return unless reference
170
205
 
171
- def relative_to_absolute(partial_url)
172
- return unless partial_url
173
-
174
- partial_uri = ::URI.parse(partial_url)
175
-
176
- if partial_uri.host
177
- partial_url # It's already absolute
178
- else
179
- ::URI.join((uri || url), partial_url).to_s
180
- end
181
- rescue ::URI::InvalidURIError
206
+ absolute = ::URI.join(base, reference.strip)
207
+ absolute.to_s if absolute.is_a?(::URI::HTTP)
208
+ rescue ::URI::InvalidURIError, ArgumentError, Encoding::CompatibilityError
182
209
  nil
183
210
  end
184
211
 
185
212
  def log_response
186
- return unless ENV['DEBUG']
213
+ debug = ENV.fetch('DEBUG', nil)
214
+ return unless debug
187
215
 
188
216
  puts "#{http_method.upcase} #{url} #{response.code} #{response.message}"
189
217
 
190
- return unless ENV['DEBUG'].casecmp('headers')
218
+ return unless debug.casecmp?('headers')
191
219
 
192
220
  response.each { |k, v| puts " #{k}:\t#{v}" }
193
221
  end
194
-
195
- NETWORK_EXCEPTIONS = [
196
- EOFError,
197
- Errno::ECONNREFUSED,
198
- Errno::ECONNRESET,
199
- Errno::EHOSTUNREACH,
200
- Errno::EINVAL,
201
- Errno::ENETUNREACH,
202
- Errno::ETIMEDOUT,
203
- Net::OpenTimeout,
204
- Net::ReadTimeout,
205
- OpenSSL::SSL::SSLError,
206
- SocketError,
207
- Timeout::Error,
208
- Zlib::BufError,
209
- Zlib::DataError
210
- ].freeze
211
222
  end
212
223
  end
@@ -13,7 +13,8 @@ module URLCanonicalize
13
13
  end
14
14
  end
15
15
 
16
- Redirect = Class.new(Generic)
16
+ # A redirect to another URL
17
+ class Redirect < Generic; end
17
18
 
18
19
  # Add HTML to a successful response
19
20
  class Success < Generic
@@ -44,15 +45,17 @@ module URLCanonicalize
44
45
  end
45
46
  end
46
47
 
47
- # It barfed
48
+ # It barfed. When the failure came from a rescued exception, `error` holds
49
+ # the original exception so callers can see the real cause
48
50
  class Failure
49
- attr_reader :failure_class, :message
51
+ attr_reader :failure_class, :message, :error
50
52
 
51
53
  private
52
54
 
53
- def initialize(failure_class, message)
55
+ def initialize(failure_class, message, error = nil)
54
56
  @failure_class = failure_class
55
57
  @message = message
58
+ @error = error
56
59
  end
57
60
  end
58
61
  end
@@ -4,23 +4,23 @@ module URLCanonicalize
4
4
  # Manage the URL into a URI with local exception handling
5
5
  class URI
6
6
  class << self
7
+ VALID_CLASSES = [::URI::HTTP, ::URI::HTTPS].freeze
8
+ COLON = ':'
9
+
7
10
  def parse(url)
8
11
  uri = ::URI.parse decorate(url)
9
- uri if valid? uri
12
+ validate! uri
13
+ uri
10
14
  rescue ::URI::InvalidURIError => e
11
- new_exception = URLCanonicalize::Exception::URI.new("#{e.class}: #{e.message}")
12
- new_exception.set_backtrace e.backtrace
13
- raise new_exception
15
+ raise URLCanonicalize::Exception::URI, "#{e.class}: #{e.message}" # the original error becomes the cause
14
16
  end
15
17
 
16
18
  private
17
19
 
18
- def valid?(uri)
20
+ def validate!(uri)
19
21
  raise URLCanonicalize::Exception::URI, "#{uri} must be http or https" unless VALID_CLASSES.include?(uri.class)
20
22
  raise URLCanonicalize::Exception::URI, "Missing host name in #{uri}" unless uri.host
21
23
  raise URLCanonicalize::Exception::URI, "Empty host name in #{uri}" if uri.host.empty?
22
-
23
- true
24
24
  end
25
25
 
26
26
  def decorate(url)
@@ -28,9 +28,6 @@ module URLCanonicalize
28
28
 
29
29
  "http://#{url}" # Add protocol if we just receive a host name
30
30
  end
31
-
32
- VALID_CLASSES = [::URI::HTTP, ::URI::HTTPS].freeze
33
- COLON = ':'
34
31
  end
35
32
  end
36
33
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module URLCanonicalize
4
- VERSION = '1.0.0'
4
+ VERSION = '1.0.2'
5
5
  end