mirror of
https://github.com/HoneyryderChuck/httpx.git
synced 2025-10-08 00:02:42 -04:00
turned the channel into a state machine: this had the nice side-effect of solving a lot of API inconsistencies from the proxies
This commit is contained in:
parent
539bb3c7d0
commit
8797eebbf2
@ -64,6 +64,7 @@ module HTTPX
|
|||||||
@write_buffer = Buffer.new(BUFFER_SIZE)
|
@write_buffer = Buffer.new(BUFFER_SIZE)
|
||||||
@pending = []
|
@pending = []
|
||||||
@on_response = on_response
|
@on_response = on_response
|
||||||
|
@state = :idle
|
||||||
end
|
end
|
||||||
|
|
||||||
def match?(uri)
|
def match?(uri)
|
||||||
@ -75,18 +76,17 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
|
|
||||||
def to_io
|
def to_io
|
||||||
connect
|
case @state
|
||||||
|
when :idle
|
||||||
|
transition(:open)
|
||||||
|
when :open
|
||||||
|
end
|
||||||
@io.to_io
|
@io.to_io
|
||||||
end
|
end
|
||||||
|
|
||||||
def close(hard=false)
|
def close(hard=false)
|
||||||
if pr = @parser
|
pr = @parser
|
||||||
pr.close
|
transition(:closed)
|
||||||
@parser = nil
|
|
||||||
end
|
|
||||||
@io.close
|
|
||||||
@read_buffer.clear
|
|
||||||
@write_buffer.clear
|
|
||||||
return true if hard
|
return true if hard
|
||||||
unless pr && pr.empty?
|
unless pr && pr.empty?
|
||||||
connect
|
connect
|
||||||
@ -106,7 +106,7 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
|
|
||||||
def call
|
def call
|
||||||
return if closed?
|
return if @state == :closed
|
||||||
catch(:called) do
|
catch(:called) do
|
||||||
dread
|
dread
|
||||||
dwrite
|
dwrite
|
||||||
@ -122,11 +122,6 @@ module HTTPX
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def connect
|
|
||||||
@io.connect
|
|
||||||
send_pending
|
|
||||||
end
|
|
||||||
|
|
||||||
def dread(wsize = @window_size)
|
def dread(wsize = @window_size)
|
||||||
loop do
|
loop do
|
||||||
siz = @io.read(wsize, @read_buffer)
|
siz = @io.read(wsize, @read_buffer)
|
||||||
@ -148,7 +143,6 @@ module HTTPX
|
|||||||
end
|
end
|
||||||
|
|
||||||
def send_pending
|
def send_pending
|
||||||
return if @io.closed?
|
|
||||||
while !@write_buffer.full? && (req_args = @pending.shift)
|
while !@write_buffer.full? && (req_args = @pending.shift)
|
||||||
request, args = req_args
|
request, args = req_args
|
||||||
parser.send(request, **args)
|
parser.send(request, **args)
|
||||||
@ -165,5 +159,27 @@ module HTTPX
|
|||||||
parser.on(:close) { throw(:close, self) }
|
parser.on(:close) { throw(:close, self) }
|
||||||
parser
|
parser
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def transition(nextstate)
|
||||||
|
case nextstate
|
||||||
|
when :idle
|
||||||
|
|
||||||
|
when :open
|
||||||
|
return if @state == :closed
|
||||||
|
@io.connect
|
||||||
|
return if @io.closed?
|
||||||
|
send_pending
|
||||||
|
when :closed
|
||||||
|
return if @state == :idle
|
||||||
|
if pr = @parser
|
||||||
|
pr.close
|
||||||
|
@parser = nil
|
||||||
|
end
|
||||||
|
@io.close
|
||||||
|
@read_buffer.clear
|
||||||
|
@write_buffer.clear
|
||||||
|
end
|
||||||
|
@state = nextstate
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -81,25 +81,15 @@ module HTTPX
|
|||||||
def initialize(io, parameters, options, &blk)
|
def initialize(io, parameters, options, &blk)
|
||||||
super(io, options, &blk)
|
super(io, options, &blk)
|
||||||
@parameters = parameters
|
@parameters = parameters
|
||||||
@state = :idle
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def match?(*)
|
def match?(*)
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_pending
|
def to_io
|
||||||
return if @pending.empty?
|
transition(:connecting) if @state == :idle
|
||||||
case @state
|
super
|
||||||
when :open
|
|
||||||
# normal flow after connection
|
|
||||||
return super
|
|
||||||
when :connecting
|
|
||||||
# do NOT enqueue requests if proxy is connecting
|
|
||||||
return
|
|
||||||
when :idle
|
|
||||||
proxy_connect
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -17,7 +17,6 @@ module HTTPX
|
|||||||
# and therefore, will share the connection.
|
# and therefore, will share the connection.
|
||||||
#
|
#
|
||||||
if req.uri.scheme == "https"
|
if req.uri.scheme == "https"
|
||||||
transition(:connecting)
|
|
||||||
connect_request = ConnectRequest.new(req.uri)
|
connect_request = ConnectRequest.new(req.uri)
|
||||||
if @parameters.authenticated?
|
if @parameters.authenticated?
|
||||||
connect_request.headers["proxy-authentication"] = "Basic #{@parameters.token_authentication}"
|
connect_request.headers["proxy-authentication"] = "Basic #{@parameters.token_authentication}"
|
||||||
@ -25,18 +24,20 @@ module HTTPX
|
|||||||
parser.send(connect_request)
|
parser.send(connect_request)
|
||||||
else
|
else
|
||||||
transition(:open)
|
transition(:open)
|
||||||
send_pending
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def transition(nextstate)
|
def transition(nextstate)
|
||||||
case nextstate
|
case nextstate
|
||||||
when :idle
|
|
||||||
when :connecting
|
when :connecting
|
||||||
return unless @state == :idle
|
return unless @state == :idle
|
||||||
|
@io.connect
|
||||||
|
return if @io.closed?
|
||||||
@parser = ConnectProxyParser.new(@write_buffer, @options.merge(max_concurrent_requests: 1))
|
@parser = ConnectProxyParser.new(@write_buffer, @options.merge(max_concurrent_requests: 1))
|
||||||
@parser.once(:response, &method(:on_connect))
|
@parser.once(:response, &method(:on_connect))
|
||||||
@parser.on(:close) { throw(:close, self) }
|
@parser.on(:close) { throw(:close, self) }
|
||||||
|
proxy_connect
|
||||||
|
return if @state == :open
|
||||||
when :open
|
when :open
|
||||||
case @state
|
case @state
|
||||||
when :connecting
|
when :connecting
|
||||||
@ -46,19 +47,17 @@ module HTTPX
|
|||||||
@parser = ProxyParser.new(@write_buffer, @options)
|
@parser = ProxyParser.new(@write_buffer, @options)
|
||||||
@parser.on(:response, &@on_response)
|
@parser.on(:response, &@on_response)
|
||||||
@parser.on(:close) { throw(:close, self) }
|
@parser.on(:close) { throw(:close, self) }
|
||||||
else
|
|
||||||
return
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@state = nextstate
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
def on_connect(request, response)
|
def on_connect(request, response)
|
||||||
if response.status == 200
|
if response.status == 200
|
||||||
transition(:open)
|
|
||||||
req, _ = @pending.first
|
req, _ = @pending.first
|
||||||
request_uri = req.uri
|
request_uri = req.uri
|
||||||
@io = ProxySSL.new(@io, request_uri, @options)
|
@io = ProxySSL.new(@io, request_uri, @options)
|
||||||
|
transition(:open)
|
||||||
throw(:called)
|
throw(:called)
|
||||||
else
|
else
|
||||||
pending = @parser.pending
|
pending = @parser.pending
|
||||||
|
@ -19,22 +19,21 @@ module HTTPX
|
|||||||
def proxy_connect
|
def proxy_connect
|
||||||
@parser = SocksParser.new(@write_buffer, @options)
|
@parser = SocksParser.new(@write_buffer, @options)
|
||||||
@parser.once(:packet, &method(:on_packet))
|
@parser.once(:packet, &method(:on_packet))
|
||||||
transition(:connecting)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def on_packet(packet)
|
def on_packet(packet)
|
||||||
version, status, port, ip = packet.unpack("CCnN")
|
version, status, port, ip = packet.unpack("CCnN")
|
||||||
if status == GRANTED
|
if status == GRANTED
|
||||||
transition(:open)
|
|
||||||
req, _ = @pending.first
|
req, _ = @pending.first
|
||||||
request_uri = req.uri
|
request_uri = req.uri
|
||||||
if request_uri.scheme == "https"
|
if request_uri.scheme == "https"
|
||||||
@io = ProxySSL.new(@io, request_uri, @options)
|
@io = ProxySSL.new(@io, request_uri, @options)
|
||||||
end
|
end
|
||||||
|
transition(:open)
|
||||||
throw(:called)
|
throw(:called)
|
||||||
else
|
else
|
||||||
pending = @parser.instance_variable_get(:@pending)
|
response = ErrorResponse.new("socks error: #{status}", 0)
|
||||||
while req = pending.shift
|
while (req, _ = @pending.shift)
|
||||||
@on_response.call(req, response)
|
@on_response.call(req, response)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -42,18 +41,20 @@ module HTTPX
|
|||||||
|
|
||||||
def transition(nextstate)
|
def transition(nextstate)
|
||||||
case nextstate
|
case nextstate
|
||||||
when :idle
|
|
||||||
when :connecting
|
when :connecting
|
||||||
return unless @state == :idle
|
return unless @state == :idle
|
||||||
|
@io.connect
|
||||||
|
return if @io.closed?
|
||||||
req, _ = @pending.first
|
req, _ = @pending.first
|
||||||
request_uri = req.uri
|
request_uri = req.uri
|
||||||
@write_buffer << Packet.connect(@parameters, request_uri)
|
@write_buffer << Packet.connect(@parameters, request_uri)
|
||||||
|
proxy_connect
|
||||||
when :open
|
when :open
|
||||||
return unless :connecting
|
return unless @state == :connecting
|
||||||
@parser = nil
|
@parser = nil
|
||||||
end
|
end
|
||||||
log { "#{nextstate.to_s}: #{@write_buffer.to_s.inspect}" }
|
log { "#{nextstate.to_s}: #{@write_buffer.to_s.inspect}" }
|
||||||
@state = nextstate
|
super
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
Parameters.register("socks4", Socks4ProxyChannel)
|
Parameters.register("socks4", Socks4ProxyChannel)
|
||||||
@ -73,6 +74,10 @@ module HTTPX
|
|||||||
def consume(*)
|
def consume(*)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def empty?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
def <<(packet)
|
def <<(packet)
|
||||||
emit(:packet, packet)
|
emit(:packet, packet)
|
||||||
end
|
end
|
||||||
|
@ -28,7 +28,7 @@ module HTTPX
|
|||||||
|
|
||||||
def on_packet(packet)
|
def on_packet(packet)
|
||||||
case @state
|
case @state
|
||||||
when :negotiating
|
when :connecting
|
||||||
version, method = packet.unpack("CC")
|
version, method = packet.unpack("CC")
|
||||||
check_version(version)
|
check_version(version)
|
||||||
case method
|
case method
|
||||||
@ -36,54 +36,63 @@ module HTTPX
|
|||||||
transition(:authenticating)
|
transition(:authenticating)
|
||||||
return
|
return
|
||||||
when NONE
|
when NONE
|
||||||
raise Error, "no supported authorization methods"
|
on_error_response("no supported authorization methods")
|
||||||
else
|
else
|
||||||
transition(:connecting)
|
transition(:negotiating)
|
||||||
end
|
end
|
||||||
when :authenticating
|
when :authenticating
|
||||||
version, status = packet.unpack("CC")
|
version, status = packet.unpack("CC")
|
||||||
check_version(version)
|
check_version(version)
|
||||||
raise Error, "could not authorize" if status != SUCCESS
|
return transition(:negotiating) if status == SUCCESS
|
||||||
transition(:connecting)
|
on_error_response("socks authentication error: #{status}")
|
||||||
when :connecting
|
when :negotiating
|
||||||
version, reply, = packet.unpack("CC")
|
version, reply, = packet.unpack("CC")
|
||||||
check_version(version)
|
check_version(version)
|
||||||
raise Error, "Illegal response type" unless reply == SUCCESS
|
return on_error_response("socks5 negotiation error: #{reply}") unless reply == SUCCESS
|
||||||
transition(:open)
|
|
||||||
req, _ = @pending.first
|
req, _ = @pending.first
|
||||||
request_uri = req.uri
|
request_uri = req.uri
|
||||||
if request_uri.scheme == "https"
|
if request_uri.scheme == "https"
|
||||||
@io = ProxySSL.new(@io, request_uri, @options)
|
@io = ProxySSL.new(@io, request_uri, @options)
|
||||||
end
|
end
|
||||||
|
transition(:open)
|
||||||
throw(:called)
|
throw(:called)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def transition(nextstate)
|
def transition(nextstate)
|
||||||
case nextstate
|
case nextstate
|
||||||
when :idle
|
|
||||||
when :negotiating
|
|
||||||
return unless @state == :idle
|
|
||||||
@write_buffer << Packet.negotiate(@parameters)
|
|
||||||
when :authenticating
|
|
||||||
return unless @state == :negotiating
|
|
||||||
@write_buffer << Packet.authenticate(@parameters)
|
|
||||||
when :connecting
|
when :connecting
|
||||||
return unless @state == :negotiating || @state == :authenticating
|
return unless @state == :idle
|
||||||
|
@io.connect
|
||||||
|
return if @io.closed?
|
||||||
|
@write_buffer << Packet.negotiate(@parameters)
|
||||||
|
proxy_connect
|
||||||
|
when :authenticating
|
||||||
|
return unless @state == :connecting
|
||||||
|
@write_buffer << Packet.authenticate(@parameters)
|
||||||
|
when :negotiating
|
||||||
|
return unless @state == :connecting || @state == :authenticating
|
||||||
req, _ = @pending.first
|
req, _ = @pending.first
|
||||||
request_uri = req.uri
|
request_uri = req.uri
|
||||||
@write_buffer << Packet.connect(request_uri)
|
@write_buffer << Packet.connect(request_uri)
|
||||||
when :open
|
when :open
|
||||||
return unless :connecting
|
return unless @state == :negotiating
|
||||||
@parser = nil
|
@parser = nil
|
||||||
end
|
end
|
||||||
log { "#{nextstate.to_s}: #{@write_buffer.to_s.inspect}" }
|
log { "#{nextstate.to_s}: #{@write_buffer.to_s.inspect}" }
|
||||||
@state = nextstate
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_version(version)
|
def check_version(version)
|
||||||
raise Error, "invalid SOCKS version (#{version})" if version != 5
|
raise Error, "invalid SOCKS version (#{version})" if version != 5
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def on_error_response(error)
|
||||||
|
response = ErrorResponse.new(error, 0)
|
||||||
|
while (req, _ = @pending.shift)
|
||||||
|
@on_response.call(req, response)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
Parameters.register("socks5", Socks5ProxyChannel)
|
Parameters.register("socks5", Socks5ProxyChannel)
|
||||||
|
|
||||||
@ -101,6 +110,10 @@ module HTTPX
|
|||||||
def consume(*)
|
def consume(*)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def empty?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
def <<(packet)
|
def <<(packet)
|
||||||
emit(:packet, packet)
|
emit(:packet, packet)
|
||||||
end
|
end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user