Merge branch 'h2c' into 'master'

H2C Upgrade

See merge request honeyryderchuck/httpx!10
This commit is contained in:
HoneyryderChuck 2018-01-13 11:07:55 +00:00
commit 539bb3c7d0
24 changed files with 429 additions and 260 deletions

View File

@ -115,6 +115,11 @@ module HTTPX
nil
end
def upgrade_parser(protocol)
@parser.close if @parser
@parser = build_parser(protocol)
end
private
def connect
@ -151,12 +156,14 @@ module HTTPX
end
def parser
@parser || begin
@parser = registry(@io.protocol).new(@write_buffer, @options)
@parser.on(:response, &@on_response)
@parser.on(:close) { throw(:close, self) }
@parser
end
@parser ||= build_parser
end
def build_parser(protocol=@io.protocol)
parser = registry(protocol).new(@write_buffer, @options)
parser.on(:response, &@on_response)
parser.on(:close) { throw(:close, self) }
parser
end
end
end

View File

@ -12,16 +12,19 @@ module HTTPX
def initialize(buffer, options)
@options = Options.new(options)
@max_concurrent_requests = @options.max_concurrent_requests
@retries = options.max_retries
@parser = HTTP::Parser.new(self)
@parser.header_value_type = :arrays
@buffer = buffer
@version = [1,1]
@pending = []
@requests = []
@has_response = false
end
def close
@parser.reset!
@parser.reset!
@has_response = false
end
def empty?
@ -32,9 +35,10 @@ module HTTPX
def <<(data)
@parser << data
dispatch if @has_response
end
def send(request, **)
def send(request, retries: @retries, **)
if @requests.size >= @max_concurrent_requests
@pending << request
return
@ -73,15 +77,18 @@ module HTTPX
log(2) { "headers received" }
headers = @options.headers_class.new(h)
response = @options.response_class.new(@requests.last, @parser.status_code, headers, @options)
response = @options.response_class.new(@requests.last,
@parser.status_code,
@parser.http_version.join("."),
headers, @options)
log { "-> HEADLINE: #{response.status} HTTP/#{@parser.http_version.join(".")}" }
log { response.headers.each.map { |f, v| "-> HEADER: #{f}: #{v}" }.join("\n") }
request.response = response
# parser can't say if it's parsing GET or HEAD,
# call the completeness callback manually
on_message_complete if request.verb == :head ||
request.verb == :connect
dispatch if request.verb == :head ||
request.verb == :connect
end
def on_body(chunk)
@ -92,7 +99,10 @@ module HTTPX
def on_message_complete
log(2) { "parsing complete" }
@parser.reset!
@has_response = true
end
def dispatch
request = @requests.first
return handle(request) if request.expects?
@ -100,6 +110,11 @@ module HTTPX
response = request.response
emit(:response, request, response)
if @parser.upgrade?
response << @parser.upgrade_data
throw(:called)
end
close
send(@pending.shift) unless @pending.empty?
if response.headers["connection"] == "close"
unless @requests.empty?
@ -126,6 +141,7 @@ module HTTPX
end
def handle(request)
@has_response = false
set_request_headers(request)
catch(:buffer_full) do
request.transition(:headers)
@ -144,7 +160,7 @@ module HTTPX
buffer.clear
request.headers.each do |field, value|
buffer << "#{capitalized(field)}: #{value}" << CRLF
log { "<- HEADER: #{buffer.chomp.inspect}" }
log { "<- HEADER: #{buffer.chomp}" }
@buffer << buffer
buffer.clear
end
@ -162,8 +178,13 @@ module HTTPX
end
end
UPCASED = {
"www-authenticate" => "WWW-Authenticate",
"http2-settings" => "HTTP2-Settings"
}
def capitalized(field)
field.to_s.split("-").map(&:capitalize).join("-")
UPCASED[field] || field.to_s.split("-").map(&:capitalize).join("-")
end
end
Channel.register "http/1.1", Channel::HTTP1

View File

@ -37,37 +37,7 @@ module HTTPX
end
unless stream = @streams[request]
stream = @connection.new_stream
stream.on(:close) do |error|
if request.expects?
return handle(request, stream)
end
response = request.response || ErrorResponse.new(error, retries)
emit(:response, request, response)
log(2, "#{stream.id}: ") { "closing stream" }
@streams.delete(request)
send(@pending.shift) unless @pending.empty?
end
stream.on(:half_close) do
log(2, "#{stream.id}: ") { "waiting for response..." }
end
# stream.on(:altsvc)
stream.on(:headers) do |h|
log(stream.id) do
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, headers, @options)
request.response = response
@streams[request] = stream
end
stream.on(:data) do |data|
log(1, "#{stream.id}: ") { "<- DATA: #{data.bytesize} bytes..." }
log(2, "#{stream.id}: ") { "<- #{data.inspect}" }
request.response << data
end
handle_stream(stream, request)
@streams[request] = stream
end
handle(request, stream)
@ -109,7 +79,7 @@ module HTTPX
end
def init_connection
@connection = HTTP2::Client.new(settings_enable_push: 0)
@connection = HTTP2::Client.new(@options.http2_settings)
@connection.on(:frame, &method(:on_frame))
@connection.on(:frame_sent, &method(:on_frame_sent))
@connection.on(:frame_received, &method(:on_frame_received))
@ -119,6 +89,40 @@ module HTTPX
@connection.on(:goaway, &method(:on_close))
end
def handle_stream(stream, request)
stream.on(:close) do |error|
if request.expects?
return handle(request, stream)
end
response = request.response || ErrorResponse.new(error, retries)
emit(:response, request, response)
log(2, "#{stream.id}: ") { "closing stream" }
@streams.delete(request)
send(@pending.shift) unless @pending.empty?
end
stream.on(:half_close) do
log(2, "#{stream.id}: ") { "waiting for response..." }
end
# stream.on(:altsvc)
stream.on(:headers) do |h|
log(stream.id) do
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)
request.response = response
@streams[request] = stream
end
stream.on(:data) do |data|
log(1, "#{stream.id}: ") { "<- DATA: #{data.bytesize} bytes..." }
log(2, "#{stream.id}: ") { "<- #{data.inspect}" }
request.response << data
end
end
def join_headers(stream, request)
set_request_headers(request)
headers = {}
@ -202,6 +206,14 @@ module HTTPX
stream.refuse
# TODO: policy for handling promises
end
def method_missing(meth, *args, &blk)
if @connection.respond_to?(meth)
@connection.__send__(meth, *args, &blk)
else
super
end
end
end
Channel.register "h2", Channel::HTTP2
end

View File

@ -7,6 +7,7 @@ module HTTPX
def initialize(options = {})
@default_options = self.class.default_options.merge(options)
@connection = Connection.new(@default_options)
@responses = {}
if block_given?
begin
@keep_open = true
@ -33,10 +34,24 @@ module HTTPX
private
def on_response(request, response)
@responses[request] = response
end
def fetch_response(request)
response = @responses.delete(request)
if response.is_a?(ErrorResponse) && response.retryable?
channel = find_channel(request)
channel.send(request, retries: response.retries - 1)
return
end
response
end
def find_channel(request)
uri = URI(request.uri)
@connection.find_channel(uri) ||
@connection.build_channel(uri)
@connection.build_channel(uri, &method(:on_response))
end
def __build_reqs(*args, **options)
@ -71,7 +86,7 @@ module HTTPX
# guarantee ordered responses
loop do
request = requests.shift
@connection.next_tick until response = @connection.response(request)
@connection.next_tick until response = fetch_response(request)
responses << response

View File

@ -10,7 +10,6 @@ module HTTPX
@timeout = options.timeout
@selector = Selector.new
@channels = []
@responses = {}
end
def running?
@ -39,17 +38,8 @@ module HTTPX
end
end
def response(request)
response = @responses.delete(request)
if response.is_a?(ErrorResponse) && response.retryable?
send(request, retries: response.retries - 1)
return
end
response
end
def build_channel(uri)
channel = Channel.by(uri, @options, &method(:on_response))
def build_channel(uri, &on_response)
channel = Channel.by(uri, @options, &on_response)
register_channel(channel)
channel
end
@ -66,10 +56,6 @@ module HTTPX
private
def on_response(request, response)
@responses[request] = response
end
def register_channel(channel)
monitor = @selector.register(channel, :rw)
monitor.value = channel

View File

@ -95,7 +95,7 @@ module HTTPX
def each
return enum_for(__method__) { @headers.size } unless block_given?
@headers.each do |field, value|
yield(field, value.join(",")) unless value.empty?
yield(field, value.join(", ")) unless value.empty?
end
end

View File

@ -42,6 +42,7 @@ module HTTPX
:debug => ENV.key?("HTTPX_DEBUG") ? $stderr : nil,
:debug_level => (ENV["HTTPX_DEBUG"] || 1).to_i,
:ssl => { alpn_protocols: %w[h2 http/1.1] },
:http2_settings => { settings_enable_push: 0 },
:fallback_protocol => "http/1.1",
:timeout => Timeout.by(:per_operation),
:headers => {},
@ -84,7 +85,7 @@ module HTTPX
%w[
params form json body
follow ssl max_retries
follow ssl http2_settings max_retries
request_class response_class headers_class response_body_class
io fallback_protocol debug debug_level
].each do |method_name|
@ -97,9 +98,7 @@ module HTTPX
merged = h1.merge(h2) do |k, v1, v2|
case k
when :headers
v1.merge(v2)
when :ssl
when :headers, :ssl, :http2_settings
v1.merge(v2)
else
v2

View File

@ -5,8 +5,9 @@ module HTTPX
module Compression
ACCEPT_ENCODING = %w[gzip deflate].freeze
def self.load_dependencies(*)
require "zlib"
def self.configure(klass, *)
klass.plugin(:"compression/gzip")
klass.plugin(:"compression/deflate")
end
module RequestMethods
@ -78,116 +79,6 @@ module HTTPX
end
end
module GZIPTranscoder
class Encoder < CompressEncoder
def write(chunk)
@compressed_chunk = chunk
end
private
def compressed_chunk
compressed = @compressed_chunk
compressed
ensure
@compressed_chunk = nil
end
def compress
return unless @buffer.size.zero?
@raw.rewind
begin
gzip = Zlib::GzipWriter.new(self)
while chunk = @raw.read(16_384)
gzip.write(chunk)
gzip.flush
compressed = compressed_chunk
@buffer << compressed
yield compressed if block_given?
end
ensure
gzip.close
end
end
end
module_function
def encode(payload)
Encoder.new(payload)
end
def decode(io)
Zlib::GzipReader.new(io, window_size: 32 + Zlib::MAX_WBITS)
end
end
module DeflateTranscoder
class Encoder < CompressEncoder
private
def compress
return unless @buffer.size.zero?
@raw.rewind
begin
deflater = Zlib::Deflate.new(Zlib::BEST_COMPRESSION,
Zlib::MAX_WBITS,
Zlib::MAX_MEM_LEVEL,
Zlib::HUFFMAN_ONLY)
while chunk = @raw.read(16_384)
compressed = deflater.deflate(chunk)
@buffer << compressed
yield compressed if block_given?
end
last = deflater.finish
@buffer << last
yield last if block_given?
ensure
deflater.close
end
end
end
module_function
class Decoder
def initialize(io)
@io = io
@inflater = Zlib::Inflate.new(32 + Zlib::MAX_WBITS)
@buffer = StringIO.new
end
def rewind
@buffer.rewind
end
def read(*args)
return @buffer.read(*args) if @io.eof?
chunk = @io.read(*args)
inflated_chunk = @inflater.inflate(chunk)
inflated_chunk << @inflater.finish if @io.eof?
@buffer << chunk
inflated_chunk
end
def close
@io.close
@io.unlink if @io.respond_to?(:unlink)
@inflater.close
end
end
def encode(payload)
Encoder.new(payload)
end
def decode(io)
Decoder.new(io)
end
end
Transcoder.register "gzip", GZIPTranscoder
Transcoder.register "deflate", DeflateTranscoder
end
register_plugin :compression, Compression
end

View File

@ -0,0 +1,85 @@
# frozen_string_literal: true
module HTTPX
module Plugins
module Compression
module Deflate
def self.load_dependencies(*)
require "stringio"
require "zlib"
end
def self.configure(*)
Transcoder.register "deflate", DeflateTranscoder
end
module DeflateTranscoder
class Encoder < CompressEncoder
private
def compress
return unless @buffer.size.zero?
@raw.rewind
begin
deflater = Zlib::Deflate.new(Zlib::BEST_COMPRESSION,
Zlib::MAX_WBITS,
Zlib::MAX_MEM_LEVEL,
Zlib::HUFFMAN_ONLY)
while chunk = @raw.read(16_384)
compressed = deflater.deflate(chunk)
@buffer << compressed
yield compressed if block_given?
end
last = deflater.finish
@buffer << last
yield last if block_given?
ensure
deflater.close
end
end
end
module_function
class Decoder
def initialize(io)
@io = io
@inflater = Zlib::Inflate.new(32 + Zlib::MAX_WBITS)
@buffer = StringIO.new
end
def rewind
@buffer.rewind
end
def read(*args)
return @buffer.read(*args) if @io.eof?
chunk = @io.read(*args)
inflated_chunk = @inflater.inflate(chunk)
inflated_chunk << @inflater.finish if @io.eof?
@buffer << chunk
inflated_chunk
end
def close
@io.close
@io.unlink if @io.respond_to?(:unlink)
@inflater.close
end
end
def encode(payload)
Encoder.new(payload)
end
def decode(io)
Decoder.new(io)
end
end
end
end
register_plugin :"compression/deflate", Compression::Deflate
end
end

View File

@ -0,0 +1,65 @@
# frozen_string_literal: true
module HTTPX
module Plugins
module Compression
module GZIP
def self_load_dependencies(*)
require "zlib"
end
def self.configure(*)
Transcoder.register "gzip", GZIPTranscoder
end
module GZIPTranscoder
class Encoder < CompressEncoder
def write(chunk)
@compressed_chunk = chunk
end
private
def compressed_chunk
compressed = @compressed_chunk
compressed
ensure
@compressed_chunk = nil
end
def compress
return unless @buffer.size.zero?
@raw.rewind
begin
gzip = Zlib::GzipWriter.new(self)
while chunk = @raw.read(16_384)
gzip.write(chunk)
gzip.flush
compressed = compressed_chunk
@buffer << compressed
yield compressed if block_given?
end
ensure
gzip.close
end
end
end
module_function
def encode(payload)
Encoder.new(payload)
end
def decode(io)
Zlib::GzipReader.new(io, window_size: 32 + Zlib::MAX_WBITS)
end
end
end
end
register_plugin :"compression/gzip", Compression::GZIP
end
end

View File

@ -12,66 +12,55 @@ module HTTPX
module InstanceMethods
def digest_authentication(user, password)
@_digest_auth_user = user
@_digest_auth_pass = password
@_digest = Digest.new
@_digest = Digest.new(user, password)
self
end
alias :digest_auth :digest_authentication
def request(*args, **options)
def request(*args, keep_open: @keep_open, **options)
return super unless @_digest
begin
#keep_open = @keep_open
#@keep_open = true
requests = __build_reqs(*args, **options)
responses = __send_reqs(*requests)
probe_request = requests.first
prev_response = __send_reqs(*probe_request).first
failed_requests = []
failed_responses_ids = responses.each_with_index.map do |response, index|
next unless response.status == 401
request = requests[index]
unless prev_response.status == 401
raise Error, "request doesn't require authentication (status: #{prev_response})"
end
token = @_digest.generate_header(@_digest_auth_user,
@_digest_auth_pass,
request,
response)
probe_request.transition(:idle)
responses = []
requests.each do |request|
token = @_digest.generate_header(request, prev_response)
request.headers["authorization"] = "Digest #{token}"
request.transition(:idle)
failed_requests << request
index
end.compact
return responses if failed_requests.empty?
repeated_responses = __send_reqs(*failed_requests)
repeated_responses.each_with_index do |rep, index|
responses[index] = rep
response = __send_reqs(*request).first
responses << response
prev_response = response
end
return responses.first if responses.size == 1
responses
ensure
#@keep_open = keep_open
close unless keep_open
end
end
end
class Digest
def initialize
def initialize(user, password)
@user = user
@password = password
@nonce = 0
end
def generate_header(user, password, request, response, iis = false)
def generate_header(request, response, iis = false)
method = request.verb.to_s.upcase
www = response.headers["www-authenticate"]
# TODO: assert if auth-type is Digest
auth_info = www[/^(\w+) (.*)/, 2]
uri = request.path
params = Hash[ auth_info.scan(/(\w+)="(.*?)"/) ]
@ -103,52 +92,33 @@ module HTTPX
end
a1 = if sess then
[ algorithm.hexdigest("#{user}:#{params["realm"]}:#{password}"),
[ algorithm.hexdigest("#{@user}:#{params["realm"]}:#{@password}"),
nonce,
cnonce,
].join ":"
else
"#{user}:#{params["realm"]}:#{password}"
"#{@user}:#{params["realm"]}:#{@password}"
end
ha1 = algorithm.hexdigest(a1)
ha2 = algorithm.hexdigest("#{method}:#{request.path}")
ha2 = algorithm.hexdigest("#{method}:#{uri}")
request_digest = [ha1, nonce]
request_digest.push(nc, cnonce, qop) if qop
request_digest << ha2
request_digest = request_digest.join(":")
header = [
"username=\"#{user}\"",
"response=\"#{algorithm.hexdigest(request_digest)}\"",
"uri=\"#{request.path}\"",
"nonce=\"#{nonce}\""
%[username="#{@user}"],
%[nonce="#{nonce}"],
%[uri="#{uri}"],
%[response="#{algorithm.hexdigest(request_digest)}"]
]
header << "realm=\"#{params["realm"]}\"" if params.key?("realm")
header << "opaque=\"#{params["opaque"]}\"" if params.key?("opaque")
header << "algorithm=#{params["algorithm"]}" if params.key?("algorithm")
header << "cnonce=#{cnonce}" if cnonce
header << "nc=#{nc}"
header << "qop=#{qop}" if qop
#
# if qop.nil? then
# elsif iis then
# "qop=\"#{qop}\""
# else
# "qop=#{qop}"
# end,
# if qop then
# [
# "nc=#{"%08x" % nonce}",
# "cnonce=\"#{cnonce}\"",
# ]
# end,
# if params.key?("opaque") then
# "opaque=\"#{params["opaque"]}\""
# end
# ].compact
header << %[realm="#{params["realm"]}"] if params.key?("realm")
header << %[algorithm=#{params["algorithm"]}"] if params.key?("algorithm")
header << %[opaque="#{params["opaque"]}"] if params.key?("opaque")
header << %[cnonce="#{cnonce}"] if cnonce
header << %[nc=#{nc}]
header << %[qop=#{qop}] if qop
header.join ", "
end

79
lib/httpx/plugins/h2c.rb Normal file
View File

@ -0,0 +1,79 @@
# frozen_string_literal: true
module HTTPX
module Plugins
module H2C
def self.load_dependencies(*)
require "base64"
end
module InstanceMethods
def request(*args, keep_open: @keep_open, **options)
return super if @_h2c_probed
begin
requests = __build_reqs(*args, **options)
upgrade_request = requests.first
return super unless valid_h2c_upgrade_request?(upgrade_request)
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(@default_options.http2_settings)
# TODO: validate!
upgrade_response = __send_reqs(*upgrade_request).first
if upgrade_response.status == 101
channel = find_channel(upgrade_request)
parser = channel.upgrade_parser("h2")
parser.extend(UpgradeExtensions)
parser.upgrade(upgrade_request, upgrade_response, **options)
data = upgrade_response.to_s
parser << data
responses = __send_reqs(*requests)
else
# proceed as usual
responses = [upgrade_response] + __send_reqs(*requests[1..-1])
end
return responses.first if responses.size == 1
responses
ensure
@_h2c_probed = true
close unless keep_open
end
end
private
VALID_H2C_METHODS = %i[get options head].freeze
private_constant :VALID_H2C_METHODS
def valid_h2c_upgrade_request?(request)
VALID_H2C_METHODS.include?(request.verb) &&
request.scheme == "http"
end
end
module UpgradeExtensions
def upgrade(request, response, retries: @retries, **)
@connection.send_connection_preface
# skip checks, it is assumed that this is the first
# request in the connection
stream = @connection.upgrade
handle_stream(stream, request)
@streams[request] = stream
end
end
module FrameBuilder
include HTTP2
module_function
def settings_value(settings)
frame = Framer.new.generate(type: :settings, stream: 0, payload: settings)
Base64.urlsafe_encode64(frame[9..-1])
end
end
end
register_plugin(:h2c, H2C)
end
end

View File

@ -53,7 +53,7 @@ module HTTPX
uri = parameters.uri
io = TCP.new(uri.host, uri.port, @default_options)
proxy_type = Parameters.registry(parameters.uri.scheme)
channel = proxy_type.new(io, parameters, @default_options, &@connection.method(:on_response))
channel = proxy_type.new(io, parameters, @default_options, &method(:on_response))
@connection.__send__(:register_channel, channel)
channel
end
@ -78,8 +78,8 @@ module HTTPX
end
class ProxyChannel < Channel
def initialize(io, parameters, options)
super(io, options)
def initialize(io, parameters, options, &blk)
super(io, options, &blk)
@parameters = parameters
@state = :idle
end

View File

@ -67,6 +67,9 @@ module HTTPX
@options = Options.new(options)
end
def close
end
def consume(*)
end

View File

@ -95,6 +95,9 @@ module HTTPX
@options = Options.new(options)
end
def close
end
def consume(*)
end

View File

@ -90,6 +90,10 @@ module HTTPX
nil
end
def inspect
"#<Request #{@verb.to_s.upcase} #{path} @headers=#{@headers.to_hash} @body=#{@body}>"
end
class Body
class << self
def new(*, options)
@ -161,7 +165,7 @@ module HTTPX
def transition(nextstate)
case nextstate
when :idle
@response = nil
when :headers
return unless @state == :idle
when :body

View File

@ -9,7 +9,7 @@ module HTTPX
class Response
extend Forwardable
attr_reader :status, :headers, :body
attr_reader :status, :headers, :body, :version
def_delegator :@body, :to_s
@ -21,8 +21,9 @@ module HTTPX
def_delegator :@request, :uri
def initialize(request, status, headers, options = {})
@options = Options.new(options)
def initialize(request, status, version, headers, options = {})
@options = Options.new(options)
@version = version
@request = request
@status = Integer(status)
@headers = @options.headers_class.new(headers)
@ -47,6 +48,10 @@ module HTTPX
ContentType.parse(@headers["content-type"])
end
def inspect
"#<Response @status=#{@status} @headers=#{@headers} @body=#{@body}>"
end
class Body
def initialize(response, threshold_size: , window_size: 1 << 14)
@response = response

View File

@ -24,7 +24,7 @@ class ClientTest < Minitest::Test
assert request.headers.respond_to?(:foo), "headers methods haven't been added"
assert request.headers.foo == "headers-foo", "headers method is unexpected"
assert client.respond_to?(:response), "response constructor was added"
response = client.response(nil, 200, {})
response = client.response(nil, 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"

View File

@ -18,6 +18,7 @@ class HTTP1Test < HTTPTest
include Plugins::FollowRedirects
include Plugins::Cookies
include Plugins::Compression
include Plugins::H2C
private

View File

@ -67,6 +67,7 @@ class OptionsSpec < Minitest::Test
:form => {:bar => "bar"},
:timeout => Timeout::PerOperation.new,
:ssl => {:foo => "bar", :alpn_protocols => %w[h2 http/1.1] },
:http2_settings => { :settings_enable_push => 0 },
:fallback_protocol => "http/1.1",
:headers => {"Foo" => "foo", "Accept" => "xml", "Bar" => "bar"},
:max_concurrent_requests => 100,

View File

@ -6,9 +6,9 @@ class ResponseTest < Minitest::Test
include HTTPX
def test_response_status
r1 = Response.new(request, 200, {})
r1 = Response.new(request, 200, "1.1", {})
assert r1.status == 200, "unexpected status code (#{r1.status})"
r2 = Response.new(request, "200", {})
r2 = Response.new(request, "200", "1.1", {})
assert r2.status == 200, "unexpected status code (#{r2.status})"
end
@ -24,7 +24,7 @@ class ResponseTest < Minitest::Test
def test_response_body_to_s
opts = { threshold_size: 1024 }
body1 = Response::Body.new(Response.new(request, 200, {}), opts)
body1 = Response::Body.new(Response.new(request, 200, "2.0", {}), opts)
assert body1.empty?, "body must be empty after initialization"
body1.write("foo")
assert body1 == "foo", "body must be updated"
@ -32,7 +32,7 @@ class ResponseTest < Minitest::Test
body1.write("bar")
assert body1 == "foobar", "body must buffer subsequent chunks"
body3 = Response::Body.new(Response.new(request("head"), 200, {}), opts)
body3 = Response::Body.new(Response.new(request("head"), 200, "2.0", {}), opts)
assert body3.empty?, "body must be empty after initialization"
assert body3 == "", "HEAD requets body must be empty"
@ -40,7 +40,7 @@ class ResponseTest < Minitest::Test
def test_response_body_each
opts = { threshold_size: 1024 }
body1 = Response::Body.new(Response.new(request, 200, {}), opts)
body1 = Response::Body.new(Response.new(request, 200, "2.0", {}), opts)
body1.write("foo")
assert body1.each.to_a == %w(foo), "must yield buffer"
body1.write("foo")
@ -59,6 +59,6 @@ class ResponseTest < Minitest::Test
end
def resource
@resource ||= Response.new(request, 200, {})
@resource ||= Response.new(request, 200, "2.0", {})
end
end

View File

@ -20,7 +20,7 @@ module Requests
end
def test_plugin_digest_authentication
client = HTTPX.plugin(:digest_authentication)
client = HTTPX.plugin(:digest_authentication).headers("cookie" => "fake=fake_value")
response = client.digest_authentication(user, pass).get(digest_auth_uri)
verify_status(response.status, 200)
body = json_body(response)

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
module Requests
module Plugins
module H2C
def test_plugin_h2c_disabled
uri = build_uri("/get")
response = HTTPX.get(uri)
verify_status(response.status, 200)
assert response.version == "1.1", "http requests should be by default in HTTP/1.1"
end
def test_plugin_h2c
client = HTTPX.plugin(:h2c)
uri = build_uri("/get")
response = client.get(uri)
verify_status(response.status, 200)
assert response.version == "2.0", "http h2c requests should be in HTTP/2"
end
end
end
end

View File

@ -55,7 +55,7 @@ module Requests
end
def socks5_proxy_uri
"socks5://37.59.56.88:13372"
"socks5://118.201.230.192:58303"
end
end
end