From fef377b6fb295acfeb2ec86c9689e2cdad6bdadf Mon Sep 17 00:00:00 2001 From: Kevin Cheng Date: Sun, 14 Jun 2020 16:26:51 -0700 Subject: [PATCH] Use stateful CookieJar in Redirector As [mentioned](../issues/264#issuecomment-157070867) in #264, CookieJar implements cookie domain scoping rules, which is useful when issuing redirects across domains. Resolves: #264 --- lib/http/client.rb | 20 ++++++++++++++------ lib/http/redirector.rb | 10 ++++++++-- lib/http/request.rb | 3 ++- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/lib/http/client.rb b/lib/http/client.rb index fdaaa44c..be38d836 100644 --- a/lib/http/client.rb +++ b/lib/http/client.rb @@ -40,7 +40,7 @@ def request(verb, uri, opts = {}) # rubocop:disable Style/OptionHash def build_request(verb, uri, opts = {}) # rubocop:disable Style/OptionHash opts = @default_options.merge(opts) uri = make_request_uri(uri, opts) - headers = make_request_headers(opts) + headers = make_request_headers(uri, opts) body = make_request_body(opts, headers) req = HTTP::Request.new( @@ -147,19 +147,27 @@ def make_request_uri(uri, opts) end # Creates request headers with cookies (if any) merged in - def make_request_headers(opts) + def make_request_headers(uri, opts) headers = opts.headers # Tell the server to keep the conn open headers[Headers::CONNECTION] = default_options.persistent? ? Connection::KEEP_ALIVE : Connection::CLOSE - cookies = opts.cookies.values + followjar = opts.follow && opts.follow[:jar] - unless cookies.empty? - cookies = opts.headers.get(Headers::COOKIE).concat(cookies).join("; ") - headers[Headers::COOKIE] = cookies + hash_cookies = opts.cookies.values + header_cookies = opts.headers.get(Headers::COOKIE) + followjar_cookies = followjar ? followjar.each(uri).map(&:cookie_value) : [] + + sync_to_followjar = followjar ? hash_cookies + header_cookies.join.split("; ") : [] + sync_to_followjar.each do |name_val| + name, value = name_val.split("=", 2) + followjar << Cookie.new(name, value, :domain => uri, :secure => uri.https?, :path => "/") end + cookies = header_cookies + hash_cookies + followjar_cookies + headers[Headers::COOKIE] = cookies.join("; ") unless cookies.empty? + headers end diff --git a/lib/http/redirector.rb b/lib/http/redirector.rb index 7487e7e3..9a0262d9 100644 --- a/lib/http/redirector.rb +++ b/lib/http/redirector.rb @@ -39,9 +39,11 @@ class EndlessRedirectError < TooManyRedirectsError; end # @param [Hash] opts # @option opts [Boolean] :strict (true) redirector hops policy # @option opts [#to_i] :max_hops (5) maximum allowed amount of hops + # @option opts [HTTP::CookieJar] :jar (HTTP::CookieJar.new) stateful CookieJar used across hops def initialize(opts = {}) # rubocop:disable Style/OptionHash @strict = opts.fetch(:strict, true) @max_hops = opts.fetch(:max_hops, 5).to_i + @jar = opts.fetch(:jar, CookieJar.new) end # Follows redirects until non-redirect response found @@ -61,6 +63,7 @@ def perform(request, response) # XXX(ixti): using `Array#inject` to return `nil` if no Location header. @request = redirect_to(@response.headers.get(Headers::LOCATION).inject(:+)) @response = yield @request + @response.cookies.inject(@jar, :<<) end @response @@ -77,7 +80,8 @@ def too_many_hops? # Check if we got into an endless loop # @return [Boolean] def endless_loop? - 2 <= @visited.count(@visited.last) + visits = @visited.count(@visited.last) + @visited.last == @visited.first ? visits > 2 : visits > 1 # allow retrying first uri once end # Redirect policy for follow @@ -94,8 +98,10 @@ def redirect_to(uri) end verb = :get if !SEE_OTHER_ALLOWED_VERBS.include?(verb) && 303 == code + redirect_uri = @request.uri.join(uri) + cookies_raw = @jar.each(redirect_uri).map(&:cookie_value).join("; ") - @request.redirect(uri, verb) + @request.redirect(uri, verb, :cookies_raw => cookies_raw) end end end diff --git a/lib/http/request.rb b/lib/http/request.rb index 5a2bf991..eb3509fe 100644 --- a/lib/http/request.rb +++ b/lib/http/request.rb @@ -97,9 +97,10 @@ def initialize(opts) end # Returns new Request with updated uri - def redirect(uri, verb = @verb) + def redirect(uri, verb = @verb, cookies_raw: nil) headers = self.headers.dup headers.delete(Headers::HOST) + headers[Headers::COOKIE] = cookies_raw if cookies_raw self.class.new( :verb => verb,