request exposes options: this allows responses to be created with the proper set of options, instead of the connection options, which is wrong

This commit is contained in:
HoneyryderChuck 2019-05-06 11:56:11 +00:00
parent d46a9aaab2
commit ae859f743f
29 changed files with 229 additions and 150 deletions

View File

@ -10,7 +10,8 @@ services:
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- vendor/ruby
- .bundle/ruby
- .bundle/jruby
before_script:
- docker info

View File

@ -20,7 +20,7 @@ AllCops:
Metrics/LineLength:
Exclude:
- 'test/resolver/native_test.rb'
Max: 120
Max: 140
Metrics/MethodLength:
Max: 200

View File

@ -0,0 +1,50 @@
# 0.4.0
* Feature: SSH proxy plugin -> send requests over ssh gateway;
```ruby
HTTPX.plugin(:"proxy/ssh").
with_proxy(uri: "ssh://localhost:2222",
username: "root",
auth_methods: %w[publickey],
host_key: "ssh-rsa",
keys: %w[test/support/ssh/ssh_host_ed25519_key]).get(URI)
```
* Feature: Faraday Adapter
* refactoring: cookies plugin API simplification (this is a breaking change!):
```ruby
session = HTTPX.plugin(:cookies)
session.with_cookies("a" => "b").get(...
session.cookies #=> session current cookie store, persists/updates session cookies as requests are processed
session.wrap do |session|
session.get(..) #=> "Set-Cookie"
...
end #=> after this, cookie store resets to the state previous to wrap
```
Removed `Session#cookie_store`
```ruby
client = HTTPX.plugin(:cookies)
redirect_response = client.get(URI) #=> ... 302 ... Set-Cookie: "blablalba" ...
# this sets the cookies
# GET .... Cookie: "blablabla" ....
response = client.get(URI) #=> ... 200 ...
# also, setting cookies:
client.cookies("a" => "b").get(URI) # ... Cookie: "a=b" ...
#also seamlessly integrates with redirect follows
client = HTTPX.plugins(:follow_redirects, :cookies)
response = client.get(URI) #=> ... 200 ...
* refactoring: connection pool now thread-local, improves thread-safety;
* bugfix: leaking dns query when passing IO object as option;
* bugfix: now multiple different resolvers are supported;
* support: JRuby is again supported (as usual, only latest stable is guaranteed)

View File

@ -142,7 +142,7 @@ module HTTPX
end
def inflight?
@parser && !@parser.empty?
@parser && !@parser.empty? && !@write_buffer.empty?
end
def interests

View File

@ -79,11 +79,11 @@ module HTTPX
return if @request.response
log(level: 2) { "headers received" }
headers = @options.headers_class.new(h)
response = @options.response_class.new(@request,
@parser.status_code,
@parser.http_version.join("."),
headers, @options)
headers = @request.options.headers_class.new(h)
response = @request.options.response_class.new(@request,
@parser.status_code,
@parser.http_version.join("."),
headers)
log(color: :yellow) { "-> HEADLINE: #{response.status} HTTP/#{@parser.http_version.join(".")}" }
log(color: :yellow) { response.headers.each.map { |f, v| "-> HEADER: #{f}: #{v}" }.join("\n") }

View File

@ -183,8 +183,8 @@ module HTTPX
h.map { |k, v| "<- HEADER: #{k}: #{v}" }.join("\n")
end
_, status = h.shift
headers = @options.headers_class.new(h)
response = @options.response_class.new(request, status, "2.0", headers, @options)
headers = request.options.headers_class.new(h)
response = request.options.response_class.new(request, status, "2.0", headers)
request.response = response
@streams[request] = stream
end

View File

@ -28,6 +28,9 @@ module HTTPX
module ResponseBodyMethods
def initialize(*)
super
return unless @headers.key?("content-encoding")
@_decoders = @headers.get("content-encoding").map do |encoding|
Compression.registry(encoding).decoder
end
@ -39,6 +42,8 @@ module HTTPX
end
def write(chunk)
return super unless defined?(@_compressed_length)
@_compressed_length -= chunk.bytesize
chunk = decompress(chunk)
super(chunk)
@ -46,6 +51,9 @@ module HTTPX
def close
super
return unless defined?(@_decoders)
@_decoders.each(&:close)
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
require "forwardable"
module HTTPX
module Plugins
module Cookies
@ -53,11 +55,12 @@ module HTTPX
end
module InstanceMethods
attr_reader :cookies_store
extend Forwardable
def initialize(*)
super
@cookies_store = @options.cookies || Store.new
def_delegator :@options, :cookies
def initialize(options = {}, &blk)
super({ cookies: Store.new }.merge(options), &blk)
end
def with_cookies(cookies)
@ -65,15 +68,14 @@ module HTTPX
end
def wrap
return unless block_given?
return super unless block_given?
super do |session|
old_cookies_store = @cookies_store
@cookies_store = old_cookies_store.dup
old_cookies_store = @options.cookies.dup
begin
yield session
ensure
@cookies_store = old_cookies_store
@options = @options.with_cookies(old_cookies_store)
end
end
end
@ -81,22 +83,22 @@ module HTTPX
private
def on_response(request, response)
@cookies_store.set(request.origin, response.headers["set-cookie"])
@options.cookies.set(request.origin, response.headers["set-cookie"])
super
end
def __build_req(*, _)
request = super
request.headers.cookies(@cookies_store[request.uri], request)
request.headers.set_cookie(@options.cookies[request.uri])
request
end
end
module HeadersMethods
def cookies(jar, request)
def set_cookie(jar)
return unless jar
cookie_value = HTTP::Cookie.cookie_value(jar.cookies(request.uri))
cookie_value = HTTP::Cookie.cookie_value(jar.cookies)
return if cookie_value.empty?
add("cookie", cookie_value)

View File

@ -5,6 +5,16 @@ module HTTPX
module DigestAuthentication
DigestError = Class.new(Error)
def self.extra_options(options)
Class.new(options.class) do
def_option(:digest) do |digest|
raise Error, ":digest must be a Digest" unless digest.is_a?(Digest)
digest
end
end.new(options)
end
def self.load_dependencies(*)
require "securerandom"
require "digest"
@ -12,28 +22,28 @@ module HTTPX
module InstanceMethods
def digest_authentication(user, password)
@_digest = Digest.new(user, password)
self
branch(default_options.with_digest(Digest.new(user, password)))
end
alias_method :digest_auth, :digest_authentication
def request(*args, **options)
return super unless @_digest
requests = __build_reqs(*args, options)
probe_request = requests.first
digest = probe_request.options.digest
return super unless digest
prev_response = wrap { __send_reqs(*probe_request, options).first }
unless prev_response.status == 401
raise Error, "request doesn't require authentication (status: #{prev_response})"
end
raise Error, "request doesn't require authentication (status: #{prev_response.status})" unless prev_response.status == 401
probe_request.transition(:idle)
responses = []
while (request = requests.shift)
token = @_digest.generate_header(request, prev_response)
token = digest.generate_header(request, prev_response)
request.headers["authorization"] = "Digest #{token}"
response = if requests.empty?
__send_reqs(*request, options).first
@ -58,7 +68,7 @@ module HTTPX
end
def generate_header(request, response, _iis = false)
method = request.verb.to_s.upcase
meth = request.verb.to_s.upcase
www = response.headers["www-authenticate"]
# discard first token, it's Digest
@ -104,7 +114,7 @@ module HTTPX
end
ha1 = algorithm.hexdigest(a1)
ha2 = algorithm.hexdigest("#{method}:#{uri}")
ha2 = algorithm.hexdigest("#{meth}:#{uri}")
request_digest = [ha1, nonce]
request_digest.push(nc, cnonce, qop) if qop
request_digest << ha2
@ -140,6 +150,7 @@ module HTTPX
end
end
end
register_plugin :digest_authentication, DigestAuthentication
end
end

View File

@ -19,7 +19,7 @@ module HTTPX
upgrade_request.headers["upgrade"] = "h2c"
upgrade_request.headers.add("connection", "upgrade")
upgrade_request.headers.add("connection", "http2-settings")
upgrade_request.headers["http2-settings"] = HTTP2::Client.settings_header(upgrade_request.http2_settings)
upgrade_request.headers["http2-settings"] = HTTP2::Client.settings_header(upgrade_request.options.http2_settings)
upgrade_response = wrap { __send_reqs(*upgrade_request, h2c_options).first }
if upgrade_response.status == 101
@ -54,13 +54,6 @@ module HTTPX
end
end
module RequestMethods
def self.included(klass)
klass.__send__(:attr_reader, :options)
klass.def_delegator :@options, :http2_settings
end
end
class H2CParser < Connection::HTTP2
def upgrade(request, response)
@connection.send_connection_preface
@ -78,10 +71,20 @@ module HTTPX
end
module ConnectionMethods
using URIExtensions
def match?(uri, options)
return super unless uri.scheme == "http" && @options.fallback_protocol == "h2c"
super && options.fallback_protocol == "h2c"
end
def coalescable?(connection)
return super unless @options.fallback_protocol == "h2c" && @uri.scheme == "http"
@uri.origin == connection.uri.origin && connection.options.fallback_protocol == "h2c"
end
def upgrade(request, response)
@parser.reset if @parser
@parser = H2CParser.new(@write_buffer, @options)

View File

@ -37,18 +37,20 @@ module HTTPX
end
end
def self.configure(klass, *)
klass.plugin(:"proxy/http")
klass.plugin(:"proxy/socks4")
klass.plugin(:"proxy/socks5")
end
class << self
def configure(klass, *)
klass.plugin(:"proxy/http")
klass.plugin(:"proxy/socks4")
klass.plugin(:"proxy/socks5")
end
def self.extra_options(options)
Class.new(options.class) do
def_option(:proxy) do |pr|
Hash[pr]
end
end.new(options)
def extra_options(options)
Class.new(options.class) do
def_option(:proxy) do |pr|
Hash[pr]
end
end.new(options)
end
end
module InstanceMethods
@ -71,19 +73,21 @@ module HTTPX
end
def find_connection(request, options)
return super unless options.respond_to?(:proxy)
uri = URI(request.uri)
next_proxy = proxy_uris(uri, options)
raise Error, "Failed to connect to proxy" unless next_proxy
proxy_options = options.merge(proxy: Parameters.new(**next_proxy))
@pool.find_connection(uri, proxy_options) || build_connection(uri, proxy_options)
pool.find_connection(uri, proxy_options) || build_connection(uri, proxy_options)
end
def __build_connection(uri, options)
proxy = options.proxy
return super unless proxy
options.connection_class.new(uri, "tcp", proxy.uri, options)
options.connection_class.new("tcp", uri, options)
end
def fetch_response(request, connections, options)
@ -107,9 +111,13 @@ module HTTPX
module ConnectionMethods
using URIExtensions
def initialize(request_uri, *args)
super(*args)
@origins = [request_uri.origin]
def initialize(*)
super
return unless @options.proxy
# redefining the connection uri as the proxy's URI,
# as this will be used as the tcp peer ip.
@uri = @options.proxy.uri
end
def match?(uri, options)
@ -118,6 +126,13 @@ module HTTPX
super && @options.proxy == options.proxy
end
# should not coalesce connections here, as the IP is the IP of the proxy
def coalescable?(*)
return super unless @options.proxy
false
end
def send(request, **args)
return super unless @options.proxy
return super unless connecting?

View File

@ -47,12 +47,8 @@ module HTTPX
# and therefore, will share the connection.
#
if req.uri.scheme == "https"
connect_request = ConnectRequest.new(req.uri)
connect_request = ConnectRequest.new(req.uri, @options)
proxy_params = @options.proxy
if proxy_params.authenticated?
connect_request.headers["proxy-authentication"] = "Basic #{proxy_params.token_authentication}"
end
parser.send(connect_request)
else
transition(:connected)
@ -106,8 +102,10 @@ module HTTPX
end
class ConnectRequest < Request
def initialize(uri, options = {})
super(:connect, uri, options)
def initialize(uri, options)
super(:connect, uri, {})
proxy_params = options.proxy
@headers["proxy-authentication"] = "Basic #{proxy_params.token_authentication}" if proxy_params.authenticated?
@headers.delete("accept")
end

View File

@ -10,6 +10,7 @@ module HTTPX
VERSION = 4
CONNECT = 1
GRANTED = 90
PROTOCOLS = %w[socks4 socks4a].freeze
Error = Class.new(Error)
@ -17,8 +18,7 @@ module HTTPX
private
def transition(nextstate)
return super unless @options.proxy &&
(@options.proxy.uri.scheme == "socks4" || @options.proxy.uri.scheme == "socks4a")
return super unless @options.proxy && PROTOCOLS.include?(@options.proxy.uri.scheme)
case nextstate
when :connecting

View File

@ -79,7 +79,7 @@ module HTTPX
transition(:authenticating)
return
when NONE
on_socks5_error("no supported authorization methods")
__on_socks5_error("no supported authorization methods")
else
transition(:negotiating)
end
@ -88,11 +88,11 @@ module HTTPX
__socks5_check_version(version)
return transition(:negotiating) if status == SUCCESS
on_socks5_error("socks authentication error: #{status}")
__on_socks5_error("socks authentication error: #{status}")
when :negotiating
version, reply, = packet.unpack("CC")
__socks5_check_version(version)
on_socks5_error("socks5 negotiation error: #{reply}") unless reply == SUCCESS
__on_socks5_error("socks5 negotiation error: #{reply}") unless reply == SUCCESS
req, _ = @pending.first
request_uri = req.uri
@io = ProxySSL.new(@io, request_uri, @options) if request_uri.scheme == "https"
@ -102,10 +102,10 @@ module HTTPX
end
def __socks5_check_version(version)
on_socks5_error("invalid SOCKS version (#{version})") if version != 5
__on_socks5_error("invalid SOCKS version (#{version})") if version != 5
end
def on_socks5_error(message)
def __on_socks5_error(message)
ex = Error.new(message)
ex.set_backtrace(caller)
on_error(ex)

View File

@ -29,10 +29,15 @@ module HTTPX
def __send_reqs(*requests, options)
request_options = @options.merge(options)
return super unless request_options.proxy
ssh_options = request_options.proxy
ssh_uris = ssh_options.delete(:uri)
ssh_username = ssh_options.delete(:username)
ssh_uri = URI.parse(ssh_uris.shift)
return super unless ssh_uri.scheme == "ssh"
ssh_username = ssh_options.delete(:username)
ssh_options[:port] ||= ssh_uri.port || 22
if request_options.debug
ssh_options[:verbose] = request_options.debug_level == 2 ? :debug : :info

View File

@ -161,7 +161,7 @@ module HTTPX
resolver_type = Resolver.registry(resolver_type) if resolver_type.is_a?(Symbol)
@resolvers[resolver_type] ||= begin
resolver = resolver_type.new(self, connection_options)
resolver = resolver_type.new(connection_options)
resolver.on(:resolve, &method(:on_resolver_connection))
resolver.on(:error, &method(:on_resolver_error))
resolver.on(:close) { on_resolver_close(resolver) }

View File

@ -34,6 +34,8 @@ module HTTPX
attr_reader :verb, :uri, :headers, :body, :state
attr_reader :options
attr_accessor :response
def_delegator :@body, :<<

View File

@ -26,8 +26,7 @@ module HTTPX
def_delegators :@resolver_connection, :to_io, :call, :interests, :close
def initialize(pool, options)
@pool = pool
def initialize(options)
@options = Options.new(options)
@resolver_options = Resolver::Options.new(DEFAULTS.merge(@options.resolver_options || {}))
@_record_types = Hash.new { |types, host| types[host] = RECORD_TYPES.keys.dup }
@ -61,11 +60,15 @@ module HTTPX
private
def pool
Thread.current[:httpx_connection_pool] ||= Pool.new
end
def resolver_connection
@resolver_connection ||= @pool.find_connection(@uri, @options) || begin
@resolver_connection ||= pool.find_connection(@uri, @options) || begin
@building_connection = true
connection = @options.connection_class.new("ssl", @uri, @options.merge(ssl: { alpn_protocols: %w[h2] }))
@pool.init_connection(connection, @options)
pool.init_connection(connection, @options)
emit_addresses(connection, @uri_addresses)
set_connection_callbacks(connection)
@building_connection = false

View File

@ -35,7 +35,7 @@ module HTTPX
def_delegator :@connections, :empty?
def initialize(_, options)
def initialize(options)
@options = Options.new(options)
@ns_index = 0
@resolver_options = Resolver::Options.new(DEFAULTS.merge(@options.resolver_options || {}))

View File

@ -12,7 +12,7 @@ module HTTPX
Resolv::DNS::EncodeError,
Resolv::DNS::DecodeError].freeze
def initialize(_, options)
def initialize(options)
@options = Options.new(options)
roptions = @options.resolver_options
@state = :idle

View File

@ -21,10 +21,10 @@ module HTTPX
def_delegator :@request, :uri
def initialize(request, status, version, headers, options = {})
@options = Options.new(options)
@version = version
def initialize(request, status, version, headers)
@request = request
@options = request.options
@version = version
@status = Integer(status)
@headers = @options.headers_class.new(headers)
@body = @options.response_body_class.new(self, threshold_size: @options.body_threshold_size,
@ -256,9 +256,7 @@ module HTTPX
# rubocop:disable Style/MissingRespondToMissing
def method_missing(meth, *, &block)
if Response.public_method_defined?(meth)
raise NoMethodError, "undefined response method `#{meth}' for error response"
end
raise NoMethodError, "undefined response method `#{meth}' for error response" if Response.public_method_defined?(meth)
super
end

View File

@ -50,49 +50,44 @@ class HTTPX::Selector
def initialize
@readers = {}
@writers = {}
@lock = Mutex.new
@__r__, @__w__ = IO.pipe
@closed = false
end
# deregisters +io+ from selectables.
def deregister(io)
@lock.synchronize do
rmonitor = @readers.delete(io)
wmonitor = @writers.delete(io)
monitor = rmonitor || wmonitor
monitor.close(false) if monitor
end
rmonitor = @readers.delete(io)
wmonitor = @writers.delete(io)
monitor = rmonitor || wmonitor
monitor.close(false) if monitor
end
# register +io+ for +interests+ events.
def register(io, interests)
readable = READABLE.include?(interests)
writable = WRITABLE.include?(interests)
@lock.synchronize do
if readable
monitor = @readers[io]
if monitor
monitor.interests = interests
else
monitor = Monitor.new(io, interests, self)
end
@readers[io] = monitor
@writers.delete(io) unless writable
if readable
monitor = @readers[io]
if monitor
monitor.interests = interests
else
monitor = Monitor.new(io, interests, self)
end
if writable
monitor = @writers[io]
if monitor
monitor.interests = interests
else
# reuse object
monitor = readable ? @readers[io] : Monitor.new(io, interests, self)
end
@writers[io] = monitor
@readers.delete(io) unless readable
end
monitor
@readers[io] = monitor
@writers.delete(io) unless writable
end
if writable
monitor = @writers[io]
if monitor
monitor.interests = interests
else
# reuse object
monitor = readable ? @readers[io] : Monitor.new(io, interests, self)
end
@writers[io] = monitor
@readers.delete(io) unless readable
end
monitor
end
# waits for read/write events for +interval+. Yields for monitors of
@ -100,22 +95,16 @@ class HTTPX::Selector
#
def select(interval)
begin
r = nil
w = nil
@lock.synchronize do
r = @readers.keys
w = @writers.keys
end
r = @readers.keys
w = @writers.keys
r.unshift(@__r__)
readers, writers = IO.select(r, w, nil, interval)
raise HTTPX::TimeoutError.new(interval, "timed out while waiting on select") if readers.nil? && writers.nil?
rescue IOError, SystemCallError
@lock.synchronize do
@readers.reject! { |io, _| io.closed? }
@writers.reject! { |io, _| io.closed? }
end
@readers.reject! { |io, _| io.closed? }
@writers.reject! { |io, _| io.closed? }
retry
end

View File

@ -7,7 +7,6 @@ module HTTPX
def initialize(options = {}, &blk)
@options = self.class.default_options.merge(options)
@pool = Pool.new
@responses = {}
@keep_open = false
wrap(&blk) if block_given?
@ -26,7 +25,7 @@ module HTTPX
end
def close(*args)
@pool.close(*args)
pool.close(*args)
end
def request(*args, **options)
@ -39,6 +38,10 @@ module HTTPX
private
def pool
Thread.current[:httpx_connection_pool] ||= Pool.new
end
def on_response(request, response)
@responses[request] = response
end
@ -54,7 +57,7 @@ module HTTPX
def find_connection(request, options)
uri = URI(request.uri)
@pool.find_connection(uri, options) || build_connection(uri, options)
pool.find_connection(uri, options) || build_connection(uri, options)
end
def set_connection_callbacks(connection, options)
@ -71,7 +74,7 @@ module HTTPX
def build_connection(uri, options)
connection = __build_connection(uri, options)
@pool.init_connection(connection, options)
pool.init_connection(connection, options)
set_connection_callbacks(connection, options)
connection
end
@ -82,7 +85,7 @@ module HTTPX
# altsvc already exists, somehow it wasn't advertised, probably noop
return unless altsvc
connection = @pool.find_connection(alt_origin, options) || build_connection(alt_origin, options)
connection = pool.find_connection(alt_origin, options) || build_connection(alt_origin, options)
# advertised altsvc is the same origin being used, ignore
return if connection == existing_connection
@ -166,12 +169,12 @@ module HTTPX
loop do
begin
request = requests.first
@pool.next_tick(timeout) until (response = fetch_response(request, connections, request_options))
pool.next_tick(timeout) until (response = fetch_response(request, connections, request_options))
responses << response
requests.shift
break if requests.empty? || @pool.empty?
break if requests.empty? || pool.empty?
end
end
responses

View File

@ -51,22 +51,20 @@ class HTTPSResolverTest < Minitest::Test
def build_connection(*)
connection = super
pool.expect(:find_connection, connection, [URI::HTTP, HTTPX::Options])
resolver.instance_variable_set(:@resolver_connection, connection)
connection
end
def resolver(options = Options.new)
@resolver ||= begin
resolver = Resolver::HTTPS.new(pool, options)
resolver = Resolver::HTTPS.new(options)
resolver.extend(ResolverHelpers::ResolverExtensions)
resolver.singleton_class.send(:attr_accessor, :pool)
resolver.pool = @pool
resolver
end
end
def pool
@pool ||= Minitest::Mock.new
end
def write_buffer
resolver.instance_variable_get(:@resolver_connection)
.instance_variable_get(:@pending)

View File

@ -52,16 +52,12 @@ class NativeResolverTest < Minitest::Test
def resolver(options = Options.new)
@resolver ||= begin
resolver = Resolver::Native.new(connection, options)
resolver = Resolver::Native.new(options)
resolver.extend(ResolverHelpers::ResolverExtensions)
resolver
end
end
def connection
@connection ||= Minitest::Mock.new
end
def write_buffer
resolver.instance_variable_get(:@write_buffer)
end

View File

@ -16,9 +16,6 @@ class SystemResolverTest < Minitest::Test
private
def resolver(options = Options.new)
@resolver ||= begin
connection = Minitest::Mock.new
Resolver::System.new(connection, options)
end
@resolver ||= Resolver::System.new(options)
end
end

View File

@ -37,7 +37,7 @@ class SessionTest < Minitest::Test
assert req_body.respond_to?(:foo), "request body methods haven't been added"
assert req_body.foo == "request-body-foo", "request body method is unexpected"
response = session.response(nil, 200, "2.0", {})
response = session.response(request, 200, "2.0", {})
assert response.respond_to?(:foo), "response methods haven't been added"
assert response.foo == "response-foo", "response method is unexpected"
assert request.headers.respond_to?(:foo), "headers methods haven't been added"
@ -62,7 +62,7 @@ class SessionTest < Minitest::Test
attr_reader :options
def response(*args)
@options.response_class.new(*args, @options)
@options.response_class.new(*args)
end
end
self::RequestClassMethods = Module.new do

View File

@ -11,7 +11,7 @@ fi
export PATH=$GEM_HOME/bin:$BUNDLE_PATH/gems/bin:$PATH
mkdir -p "$GEM_HOME" && chmod 777 "$GEM_HOME"
gem install bundler -v="1.17.3" --no-doc --conservative
cd /home && bundle install --jobs 4 && \
cd /home && bundle install --jobs 4 --path .bundle && \
bundle exec rake test:ci
RET=$?
@ -24,7 +24,7 @@ fi
if [[ $RET = 0 ]] && [[ ${RUBY_VERSION:0:3} = "2.6" ]]; then
bundle exec rake website_rdoc && \
cd www && bundle install && \
cd www && bundle install --jobs 4 --path ../vendor && \
bundle exec jekyll build -d public
fi

View File

@ -22,7 +22,7 @@ module Requests
session_uri = cookies_set_uri(session_cookies)
session_response = session.get(session_uri)
verify_status(session_response, 302)
verify_cookies(session.cookies_store[URI(session_uri)], session_cookies)
verify_cookies(session.cookies[URI(session_uri)], session_cookies)
# first request sets the session
response = session.get(cookies_uri)