From c862d3ef1cbca5e546664808b6a357fafcc250c4 Mon Sep 17 00:00:00 2001 From: Mamoon Raja Date: Thu, 21 Feb 2019 16:58:52 -0500 Subject: [PATCH] feat(uri_normalizer): add support for users to provide custom uri normalizer --- .rubocop.yml | 4 ++-- lib/http/client.rb | 4 +++- lib/http/feature.rb | 1 + lib/http/features/auto_deflate.rb | 3 ++- lib/http/features/uri_normalizer.rb | 32 ++++++++++++++++++++++++++++ lib/http/request.rb | 32 ++++++++++++++++------------ spec/lib/http_spec.rb | 32 ++++++++++++++++++++++++++++ spec/support/dummy_server/servlet.rb | 10 +++++++++ 8 files changed, 100 insertions(+), 18 deletions(-) create mode 100644 lib/http/features/uri_normalizer.rb diff --git a/.rubocop.yml b/.rubocop.yml index c0f965fe..fb6a20fb 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -31,10 +31,10 @@ Metrics/ClassLength: # TODO: Lower to 6 Metrics/CyclomaticComplexity: - Max: 8 + Max: 9 Metrics/PerceivedComplexity: - Max: 8 + Max: 9 # TODO: Lower to 80 Metrics/LineLength: diff --git a/lib/http/client.rb b/lib/http/client.rb index 5b9aa4fa..5f3f400f 100644 --- a/lib/http/client.rb +++ b/lib/http/client.rb @@ -43,13 +43,15 @@ def build_request(verb, uri, opts = {}) # rubocop:disable Style/OptionHash headers = make_request_headers(opts) body = make_request_body(opts, headers) proxy = opts.proxy + uri_normalizer = opts.features[:uri_normalizer]&.method(:normalize_uri) req = HTTP::Request.new( :verb => verb, :uri => uri, :headers => headers, :proxy => proxy, - :body => body + :body => body, + :uri_normalizer => uri_normalizer ) opts.features.inject(req) do |request, (_name, feature)| diff --git a/lib/http/feature.rb b/lib/http/feature.rb index 3c279e97..e2ee93a9 100644 --- a/lib/http/feature.rb +++ b/lib/http/feature.rb @@ -20,3 +20,4 @@ def wrap_response(response) require "http/features/auto_deflate" require "http/features/logging" require "http/features/instrumentation" +require "http/features/uri_normalizer" diff --git a/lib/http/features/auto_deflate.rb b/lib/http/features/auto_deflate.rb index 2a2b5300..98972927 100644 --- a/lib/http/features/auto_deflate.rb +++ b/lib/http/features/auto_deflate.rb @@ -32,7 +32,8 @@ def wrap_request(request) :uri => request.uri, :headers => request.headers, :proxy => request.proxy, - :body => deflated_body(request.body) + :body => deflated_body(request.body), + :uri_normalizer => request.uri_normalizer ) end diff --git a/lib/http/features/uri_normalizer.rb b/lib/http/features/uri_normalizer.rb new file mode 100644 index 00000000..30b8a15c --- /dev/null +++ b/lib/http/features/uri_normalizer.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module HTTP + module Features + class UriNormalizer < Feature + attr_reader :custom_uri_normalizer + + def initialize(custom_uri_normalizer: DefaultUriNormalizer.new) + @custom_uri_normalizer = custom_uri_normalizer + end + + def normalize_uri(uri) + custom_uri_normalizer.normalize_uri(uri) + end + + class DefaultUriNormalizer + def normalize_uri(uri) + uri = HTTP::URI.parse uri + HTTP::URI.new( + :scheme => uri.normalized_scheme, + :authority => uri.normalized_authority, + :path => uri.normalized_path, + :query => uri.query, + :fragment => uri.normalized_fragment + ) + end + end + + HTTP::Options.register_feature(:uri_normalizer, self) + end + end +end diff --git a/lib/http/request.rb b/lib/http/request.rb index e29c9bda..b851bd01 100644 --- a/lib/http/request.rb +++ b/lib/http/request.rb @@ -66,6 +66,8 @@ class UnsupportedSchemeError < RequestError; end # Scheme is normalized to be a lowercase symbol e.g. :http, :https attr_reader :scheme + attr_reader :uri_normalizer + # "Request URI" as per RFC 2616 # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html attr_reader :uri @@ -73,13 +75,15 @@ class UnsupportedSchemeError < RequestError; end # @option opts [String] :version # @option opts [#to_s] :verb HTTP request method + # @option opts [#to_s] :uri_normalizer uri normalizer # @option opts [HTTP::URI, #to_s] :uri # @option opts [Hash] :headers # @option opts [Hash] :proxy # @option opts [String, Enumerable, IO, nil] :body def initialize(opts) @verb = opts.fetch(:verb).to_s.downcase.to_sym - @uri = normalize_uri(opts.fetch(:uri)) + @uri_normalizer = opts[:uri_normalizer] ? opts.fetch(:uri_normalizer) : self.class.method(:normalize_uri) + @uri = @uri_normalizer.call(opts.fetch(:uri)) @scheme = @uri.scheme.to_s.downcase.to_sym if @uri.scheme raise(UnsupportedMethodError, "unknown method: #{verb}") unless METHODS.include?(@verb) @@ -196,6 +200,19 @@ def inspect "#<#{self.class}/#{@version} #{verb.to_s.upcase} #{uri}>" end + # @return [HTTP::URI] URI with all components but query being normalized. + def self.normalize_uri(uri) + uri = HTTP::URI.parse uri + + HTTP::URI.new( + :scheme => uri.normalized_scheme, + :authority => uri.normalized_authority, + :path => uri.normalized_path, + :query => uri.query, + :fragment => uri.normalized_fragment + ) + end + private # @!attribute [r] host @@ -212,18 +229,5 @@ def port def default_host_header_value PORTS[@scheme] != port ? "#{host}:#{port}" : host end - - # @return [HTTP::URI] URI with all componentes but query being normalized. - def normalize_uri(uri) - uri = HTTP::URI.parse uri - - HTTP::URI.new( - :scheme => uri.normalized_scheme, - :authority => uri.normalized_authority, - :path => uri.normalized_path, - :query => uri.query, - :fragment => uri.normalized_fragment - ) - end end end diff --git a/spec/lib/http_spec.rb b/spec/lib/http_spec.rb index 403c5da5..9c55e8c8 100644 --- a/spec/lib/http_spec.rb +++ b/spec/lib/http_spec.rb @@ -430,6 +430,38 @@ def setsockopt(*args) expect(response.to_s).to eq("#{body}-deflated") end end + + context "with :uri_normalizer" do + class CustomUriNormalizer + def normalize_uri(uri) + uri = HTTP::URI.parse uri + HTTP::URI.new( + :scheme => uri.normalized_scheme, + :authority => uri.normalized_authority, + :path => "uri_normalizer/custom", + :query => uri.query, + :fragment => uri.normalized_fragment + ) + end + end + + it "Use the defaul Uri normalizer when user does not use uri normalizer" do + response = HTTP.get HTTP::URI.parse "#{dummy.endpoint}/uri_normalizer/%EF%BC%A1%EF%BC%A2%EF%BC%A3" + expect(response.to_s).to eq("default normalizer") + end + + it "Use the custom Uri Normalizer method" do + client = HTTP.use(:uri_normalizer => {:custom_uri_normalizer => CustomUriNormalizer.new}) + response = client.get("#{dummy.endpoint}/uri_normalizer/%EF%BC%A1%EF%BC%A2%EF%BC%A3") + expect(response.to_s).to eq("custom normalizer") + end + + it "Use the default Uri normalizer when user does not specify custom uri normalizer" do + client = HTTP.use :uri_normalizer + response = client.get("#{dummy.endpoint}/uri_normalizer/%EF%BC%A1%EF%BC%A2%EF%BC%A3") + expect(response.to_s).to eq("default normalizer") + end + end end it "unifies socket errors into HTTP::ConnectionError" do diff --git a/spec/support/dummy_server/servlet.rb b/spec/support/dummy_server/servlet.rb index 1ca05653..8139547d 100644 --- a/spec/support/dummy_server/servlet.rb +++ b/spec/support/dummy_server/servlet.rb @@ -148,6 +148,16 @@ def do_#{method.upcase}(req, res) res.body = req.body end + get "/uri_normalizer/ABC" do |_req, res| + res.status = 200 + res.body = "default normalizer" + end + + get "/uri_normalizer/custom" do |_req, res| + res.status = 200 + res.body = "custom normalizer" + end + post "/encoded-body" do |req, res| res.status = 200