diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 81c4d362..47d5f9fe 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -74,7 +74,7 @@ Metrics/AbcSize: - 'lib/http/request.rb' - 'lib/http/response.rb' -# Offense count: 69 +# Offense count: 70 # Configuration parameters: CountComments, Max, CountAsOne, ExcludedMethods, IgnoredMethods. # IgnoredMethods: refine Metrics/BlockLength: @@ -98,6 +98,7 @@ Metrics/BlockLength: - 'spec/lib/http/response/parser_spec.rb' - 'spec/lib/http/response/status_spec.rb' - 'spec/lib/http/response_spec.rb' + - 'spec/lib/http/uri_spec.rb' - 'spec/lib/http_spec.rb' - 'spec/support/http_handling_shared.rb' diff --git a/lib/http/uri.rb b/lib/http/uri.rb index f694ed08..9c8a54c3 100644 --- a/lib/http/uri.rb +++ b/lib/http/uri.rb @@ -9,7 +9,6 @@ class URI def_delegators :@uri, :scheme, :normalized_scheme, :scheme= def_delegators :@uri, :user, :normalized_user, :user= def_delegators :@uri, :password, :normalized_password, :password= - def_delegators :@uri, :host, :normalized_host, :host= def_delegators :@uri, :authority, :normalized_authority, :authority= def_delegators :@uri, :origin, :origin= def_delegators :@uri, :normalized_port, :port= @@ -20,6 +19,18 @@ class URI def_delegators :@uri, :fragment, :normalized_fragment, :fragment= def_delegators :@uri, :omit, :join, :normalize + # Host, either a domain name or IP address. If the host is an IPv6 address, it will be returned + # without brackets surrounding it. + # + # @return [String] The host of the URI + attr_reader :host + + # Normalized host, either a domain name or IP address. If the host is an IPv6 address, it will + # be returned without brackets surrounding it. + # + # @return [String] The normalized host of the URI + attr_reader :normalized_host + # @private HTTP_SCHEME = "http" @@ -83,6 +94,9 @@ def initialize(options_or_uri = {}) else raise TypeError, "expected Hash for options, got #{options_or_uri.class}" end + + @host = process_ipv6_brackets(@uri.host) + @normalized_host = process_ipv6_brackets(@uri.normalized_host) end # Are these URI objects equal? Normalizes both URIs prior to comparison @@ -110,6 +124,17 @@ def hash @hash ||= to_s.hash * -1 end + # Sets the host component for the URI. + # + # @param [String, #to_str] new_host The new host component. + # @return [void] + def host=(new_host) + @uri.host = process_ipv6_brackets(new_host, :brackets => true) + + @host = process_ipv6_brackets(@uri.host) + @normalized_host = process_ipv6_brackets(@uri.normalized_host) + end + # Port number, either as specified or the default if unspecified # # @return [Integer] port number @@ -146,5 +171,25 @@ def to_s def inspect format("#<%s:0x%014x URI:%s>", self.class.name, object_id << 1, to_s) end + + private + + # Process a URI host, adding or removing surrounding brackets if the host is an IPv6 address. + # + # @param [Boolean] brackets When true, brackets will be added to IPv6 addresses if missing. When + # false, they will be removed if present. + # + # @return [String] Host with IPv6 address brackets added or removed + def process_ipv6_brackets(raw_host, brackets: false) + ip = IPAddr.new(raw_host) + + if ip.ipv6? + brackets ? "[#{ip}]" : ip.to_s + else + raw_host + end + rescue IPAddr::Error + raw_host + end end end diff --git a/spec/lib/http/uri_spec.rb b/spec/lib/http/uri_spec.rb index 7e91760e..4a330d61 100644 --- a/spec/lib/http/uri_spec.rb +++ b/spec/lib/http/uri_spec.rb @@ -1,11 +1,15 @@ # frozen_string_literal: true RSpec.describe HTTP::URI do + let(:example_ipv6_address) { "2606:2800:220:1:248:1893:25c8:1946" } + let(:example_http_uri_string) { "http://example.com" } let(:example_https_uri_string) { "https://example.com" } + let(:example_ipv6_uri_string) { "https://[#{example_ipv6_address}]" } subject(:http_uri) { described_class.parse(example_http_uri_string) } subject(:https_uri) { described_class.parse(example_https_uri_string) } + subject(:ipv6_uri) { described_class.parse(example_ipv6_uri_string) } it "knows URI schemes" do expect(http_uri.scheme).to eq "http" @@ -20,6 +24,41 @@ expect(https_uri.port).to eq 443 end + describe "#host" do + it "strips brackets from IPv6 addresses" do + expect(ipv6_uri.host).to eq("2606:2800:220:1:248:1893:25c8:1946") + end + end + + describe "#normalized_host" do + it "strips brackets from IPv6 addresses" do + expect(ipv6_uri.normalized_host).to eq("2606:2800:220:1:248:1893:25c8:1946") + end + end + + describe "#host=" do + it "updates cached values for #host and #normalized_host" do + expect(http_uri.host).to eq("example.com") + expect(http_uri.normalized_host).to eq("example.com") + + http_uri.host = "[#{example_ipv6_address}]" + + expect(http_uri.host).to eq(example_ipv6_address) + expect(http_uri.normalized_host).to eq(example_ipv6_address) + end + + it "ensures IPv6 addresses are bracketed in the inner Addressable::URI" do + expect(http_uri.host).to eq("example.com") + expect(http_uri.normalized_host).to eq("example.com") + + http_uri.host = example_ipv6_address + + expect(http_uri.host).to eq(example_ipv6_address) + expect(http_uri.normalized_host).to eq(example_ipv6_address) + expect(http_uri.instance_variable_get(:@uri).host).to eq("[#{example_ipv6_address}]") + end + end + describe "#dup" do it "doesn't share internal value between duplicates" do duplicated_uri = http_uri.dup