Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use stateful CookieJar in Redirector #613

Merged
merged 1 commit into from
Sep 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions lib/http/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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

Expand Down
10 changes: 8 additions & 2 deletions lib/http/redirector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Author

@Kache Kache Jun 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To use an existing cookiejar from disk:

persisted_jar = HTTP::CookieJar.new.load(from_disk_path, format: :cookiestxt)
response = HTTP.follow(jar: persisted_jar).get(endpoint)
persisted_jar.save(to_disk_path, format: :cookiestxt) # if you'd like

I suppose putting the persistent cookie jar option in HTTP.follow might not be considered intuitive. Also, Redirector is somewhat similar to Session, as they are both external to Client and hold longer-lived state.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, in fact I was thinking that we should completely refactor redirector. And provide some sort of Session object that will "mimic" the browser behaviour. So I think it's OK-ish for now.

end

# Follows redirects until non-redirect response found
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
3 changes: 2 additions & 1 deletion lib/http/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Author

@Kache Kache Jun 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would've liked to inline redirect -- I think it's more fitting to have the higher level controller, Redirector, do it rather than asking Request to redirect itself.

It'd also avoid having to cookies_raw: nil just to protect against api change.

thoughts?

headers = self.headers.dup
headers.delete(Headers::HOST)
headers[Headers::COOKIE] = cookies_raw if cookies_raw

self.class.new(
:verb => verb,
Expand Down