mirror of
https://github.com/HoneyryderChuck/httpx.git
synced 2025-10-04 00:00:37 -04:00
Merge branch 'h2c' into 'master'
H2C Upgrade See merge request honeyryderchuck/httpx!10
This commit is contained in:
commit
539bb3c7d0
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
85
lib/httpx/plugins/compression/deflate.rb
Normal file
85
lib/httpx/plugins/compression/deflate.rb
Normal 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
|
||||
|
65
lib/httpx/plugins/compression/gzip.rb
Normal file
65
lib/httpx/plugins/compression/gzip.rb
Normal 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
|
@ -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
79
lib/httpx/plugins/h2c.rb
Normal 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
|
@ -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
|
||||
|
@ -67,6 +67,9 @@ module HTTPX
|
||||
@options = Options.new(options)
|
||||
end
|
||||
|
||||
def close
|
||||
end
|
||||
|
||||
def consume(*)
|
||||
end
|
||||
|
||||
|
@ -95,6 +95,9 @@ module HTTPX
|
||||
@options = Options.new(options)
|
||||
end
|
||||
|
||||
def close
|
||||
end
|
||||
|
||||
def consume(*)
|
||||
end
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -18,6 +18,7 @@ class HTTP1Test < HTTPTest
|
||||
include Plugins::FollowRedirects
|
||||
include Plugins::Cookies
|
||||
include Plugins::Compression
|
||||
include Plugins::H2C
|
||||
|
||||
private
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
22
test/support/requests/plugins/h2c.rb
Normal file
22
test/support/requests/plugins/h2c.rb
Normal 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
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user