httpx 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +191 -0
- data/README.md +119 -0
- data/lib/httpx.rb +50 -0
- data/lib/httpx/buffer.rb +34 -0
- data/lib/httpx/callbacks.rb +32 -0
- data/lib/httpx/chainable.rb +51 -0
- data/lib/httpx/channel.rb +222 -0
- data/lib/httpx/channel/http1.rb +220 -0
- data/lib/httpx/channel/http2.rb +224 -0
- data/lib/httpx/client.rb +173 -0
- data/lib/httpx/connection.rb +74 -0
- data/lib/httpx/errors.rb +7 -0
- data/lib/httpx/extensions.rb +52 -0
- data/lib/httpx/headers.rb +152 -0
- data/lib/httpx/io.rb +240 -0
- data/lib/httpx/loggable.rb +11 -0
- data/lib/httpx/options.rb +138 -0
- data/lib/httpx/plugins/authentication.rb +14 -0
- data/lib/httpx/plugins/basic_authentication.rb +20 -0
- data/lib/httpx/plugins/compression.rb +123 -0
- data/lib/httpx/plugins/compression/brotli.rb +55 -0
- data/lib/httpx/plugins/compression/deflate.rb +50 -0
- data/lib/httpx/plugins/compression/gzip.rb +59 -0
- data/lib/httpx/plugins/cookies.rb +63 -0
- data/lib/httpx/plugins/digest_authentication.rb +141 -0
- data/lib/httpx/plugins/follow_redirects.rb +72 -0
- data/lib/httpx/plugins/h2c.rb +85 -0
- data/lib/httpx/plugins/proxy.rb +108 -0
- data/lib/httpx/plugins/proxy/http.rb +115 -0
- data/lib/httpx/plugins/proxy/socks4.rb +110 -0
- data/lib/httpx/plugins/proxy/socks5.rb +152 -0
- data/lib/httpx/plugins/push_promise.rb +67 -0
- data/lib/httpx/plugins/stream.rb +33 -0
- data/lib/httpx/registry.rb +88 -0
- data/lib/httpx/request.rb +222 -0
- data/lib/httpx/response.rb +225 -0
- data/lib/httpx/selector.rb +155 -0
- data/lib/httpx/timeout.rb +68 -0
- data/lib/httpx/transcoder.rb +12 -0
- data/lib/httpx/transcoder/body.rb +56 -0
- data/lib/httpx/transcoder/chunker.rb +38 -0
- data/lib/httpx/transcoder/form.rb +41 -0
- data/lib/httpx/transcoder/json.rb +36 -0
- data/lib/httpx/version.rb +5 -0
- metadata +150 -0
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module HTTPX
|
6
|
+
module Plugins
|
7
|
+
module Proxy
|
8
|
+
Error = Class.new(Error)
|
9
|
+
class Parameters
|
10
|
+
extend Registry
|
11
|
+
|
12
|
+
attr_reader :uri, :username, :password
|
13
|
+
|
14
|
+
def initialize(uri:, username: nil, password: nil)
|
15
|
+
@uri = uri.is_a?(URI::Generic) ? uri : URI(uri)
|
16
|
+
@username = username || @uri.user
|
17
|
+
@password = password || @uri.password
|
18
|
+
end
|
19
|
+
|
20
|
+
def authenticated?
|
21
|
+
@username && @password
|
22
|
+
end
|
23
|
+
|
24
|
+
def token_authentication
|
25
|
+
Base64.strict_encode64("#{user}:#{password}")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module InstanceMethods
|
30
|
+
def with_proxy(*args)
|
31
|
+
branch(default_options.with_proxy(*args))
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def proxy_params(uri)
|
37
|
+
return @options.proxy if @options.proxy
|
38
|
+
uri = URI(uri).find_proxy
|
39
|
+
return unless uri
|
40
|
+
{ uri: uri }
|
41
|
+
end
|
42
|
+
|
43
|
+
def find_channel(request, **options)
|
44
|
+
uri = URI(request.uri)
|
45
|
+
proxy = proxy_params(uri)
|
46
|
+
return super unless proxy
|
47
|
+
@connection.find_channel(proxy) || begin
|
48
|
+
channel = build_proxy_channel(proxy, **options)
|
49
|
+
set_channel_callbacks(channel)
|
50
|
+
channel
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def build_proxy_channel(proxy, **options)
|
55
|
+
parameters = Parameters.new(**proxy)
|
56
|
+
uri = parameters.uri
|
57
|
+
log { "proxy: #{uri}" }
|
58
|
+
io = TCP.new(uri.host, uri.port, @options)
|
59
|
+
proxy_type = Parameters.registry(parameters.uri.scheme)
|
60
|
+
channel = proxy_type.new(io, parameters, @options.merge(options), &method(:on_response))
|
61
|
+
@connection.__send__(:register_channel, channel)
|
62
|
+
channel
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
module OptionsMethods
|
67
|
+
def self.included(klass)
|
68
|
+
super
|
69
|
+
klass.def_option(:proxy) do |pr|
|
70
|
+
Hash[pr]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.configure(klass, *)
|
76
|
+
klass.plugin(:"proxy/http")
|
77
|
+
klass.plugin(:"proxy/socks4")
|
78
|
+
klass.plugin(:"proxy/socks5")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
register_plugin :proxy, Proxy
|
82
|
+
end
|
83
|
+
|
84
|
+
class ProxyChannel < Channel
|
85
|
+
def initialize(io, parameters, options, &blk)
|
86
|
+
super(io, options, &blk)
|
87
|
+
@parameters = parameters
|
88
|
+
end
|
89
|
+
|
90
|
+
def match?(*)
|
91
|
+
true
|
92
|
+
end
|
93
|
+
|
94
|
+
def to_io
|
95
|
+
transition(:connecting) if @state == :idle
|
96
|
+
@io.to_io
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class ProxySSL < SSL
|
101
|
+
def initialize(tcp, request_uri, options)
|
102
|
+
@io = tcp.to_io
|
103
|
+
super(tcp.ip, tcp.port, options)
|
104
|
+
@hostname = request_uri.host
|
105
|
+
@state = :connected
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
|
5
|
+
module HTTPX
|
6
|
+
module Plugins
|
7
|
+
module Proxy
|
8
|
+
module HTTP
|
9
|
+
class HTTPProxyChannel < ProxyChannel
|
10
|
+
private
|
11
|
+
|
12
|
+
def proxy_connect
|
13
|
+
req, _ = @pending.first
|
14
|
+
# if the first request after CONNECT is to an https address, it is assumed that
|
15
|
+
# all requests in the queue are not only ALL HTTPS, but they also share the certificate,
|
16
|
+
# and therefore, will share the connection.
|
17
|
+
#
|
18
|
+
if req.uri.scheme == "https"
|
19
|
+
connect_request = ConnectRequest.new(req.uri)
|
20
|
+
if @parameters.authenticated?
|
21
|
+
connect_request.headers["proxy-authentication"] = "Basic #{@parameters.token_authentication}"
|
22
|
+
end
|
23
|
+
parser.send(connect_request)
|
24
|
+
else
|
25
|
+
transition(:open)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def transition(nextstate)
|
30
|
+
case nextstate
|
31
|
+
when :connecting
|
32
|
+
return unless @state == :idle
|
33
|
+
@io.connect
|
34
|
+
return if @io.closed?
|
35
|
+
@parser = ConnectProxyParser.new(@write_buffer, @options.merge(max_concurrent_requests: 1))
|
36
|
+
@parser.once(:response, &method(:on_connect))
|
37
|
+
@parser.on(:complete) { throw(:close, self) }
|
38
|
+
proxy_connect
|
39
|
+
return if @state == :open
|
40
|
+
when :open
|
41
|
+
case @state
|
42
|
+
when :connecting
|
43
|
+
@parser.close
|
44
|
+
@parser = nil
|
45
|
+
when :idle
|
46
|
+
@parser = ProxyParser.new(@write_buffer, @options)
|
47
|
+
@parser.inherit_callbacks(self)
|
48
|
+
@parser.on(:complete) { throw(:close, self) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
super
|
52
|
+
end
|
53
|
+
|
54
|
+
def on_connect(_request, response)
|
55
|
+
if response.status == 200
|
56
|
+
req, _ = @pending.first
|
57
|
+
request_uri = req.uri
|
58
|
+
@io = ProxySSL.new(@io, request_uri, @options)
|
59
|
+
transition(:open)
|
60
|
+
throw(:called)
|
61
|
+
else
|
62
|
+
pending = @pending.map(&:first) + @parser.pending
|
63
|
+
while (req = pending.shift)
|
64
|
+
emit(:response, req, response)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class ProxyParser < Channel::HTTP1
|
71
|
+
def headline_uri(request)
|
72
|
+
request.uri.to_s
|
73
|
+
end
|
74
|
+
|
75
|
+
def set_request_headers(request)
|
76
|
+
super
|
77
|
+
request.headers["proxy-connection"] = request.headers["connection"]
|
78
|
+
request.headers.delete("connection")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class ConnectProxyParser < ProxyParser
|
83
|
+
attr_reader :pending
|
84
|
+
|
85
|
+
def headline_uri(request)
|
86
|
+
return super unless request.verb == :connect
|
87
|
+
uri = request.uri
|
88
|
+
tunnel = "#{uri.hostname}:#{uri.port}"
|
89
|
+
log { "establishing HTTP proxy tunnel to #{tunnel}" }
|
90
|
+
tunnel
|
91
|
+
end
|
92
|
+
|
93
|
+
def empty?
|
94
|
+
@requests.reject { |r| r.verb == :connect }.empty? ||
|
95
|
+
@requests.all? { |request| !request.response.nil? }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class ConnectRequest < Request
|
100
|
+
def initialize(uri, options = {})
|
101
|
+
super(:connect, uri, options)
|
102
|
+
@headers.delete("accept")
|
103
|
+
end
|
104
|
+
|
105
|
+
def path
|
106
|
+
"#{@uri.hostname}:#{@uri.port}"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
Parameters.register("http", HTTPProxyChannel)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
register_plugin :"proxy/http", Proxy::HTTP
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "resolv"
|
4
|
+
require "ipaddr"
|
5
|
+
|
6
|
+
module HTTPX
|
7
|
+
module Plugins
|
8
|
+
module Proxy
|
9
|
+
module Socks4
|
10
|
+
VERSION = 4
|
11
|
+
CONNECT = 1
|
12
|
+
GRANTED = 90
|
13
|
+
|
14
|
+
Error = Class.new(Error)
|
15
|
+
|
16
|
+
class Socks4ProxyChannel < ProxyChannel
|
17
|
+
private
|
18
|
+
|
19
|
+
def proxy_connect
|
20
|
+
@parser = SocksParser.new(@write_buffer, @options)
|
21
|
+
@parser.once(:packet, &method(:on_packet))
|
22
|
+
end
|
23
|
+
|
24
|
+
def on_packet(packet)
|
25
|
+
_version, status, _port, _ip = packet.unpack("CCnN")
|
26
|
+
if status == GRANTED
|
27
|
+
req, _ = @pending.first
|
28
|
+
request_uri = req.uri
|
29
|
+
@io = ProxySSL.new(@io, request_uri, @options) if request_uri.scheme == "https"
|
30
|
+
transition(:open)
|
31
|
+
throw(:called)
|
32
|
+
else
|
33
|
+
response = ErrorResponse.new("socks error: #{status}", 0)
|
34
|
+
until @pending.empty?
|
35
|
+
req, _ = @pending.shift
|
36
|
+
emit(:response, req, response)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def transition(nextstate)
|
42
|
+
case nextstate
|
43
|
+
when :connecting
|
44
|
+
return unless @state == :idle
|
45
|
+
@io.connect
|
46
|
+
return if @io.closed?
|
47
|
+
req, _ = @pending.first
|
48
|
+
return unless req
|
49
|
+
request_uri = req.uri
|
50
|
+
@write_buffer << Packet.connect(@parameters, request_uri)
|
51
|
+
proxy_connect
|
52
|
+
when :open
|
53
|
+
return unless @state == :connecting
|
54
|
+
@parser = nil
|
55
|
+
end
|
56
|
+
log(1, "SOCKS4: ") { "#{nextstate}: #{@write_buffer.to_s.inspect}" }
|
57
|
+
super
|
58
|
+
end
|
59
|
+
end
|
60
|
+
Parameters.register("socks4", Socks4ProxyChannel)
|
61
|
+
Parameters.register("socks4a", Socks4ProxyChannel)
|
62
|
+
|
63
|
+
class SocksParser
|
64
|
+
include Callbacks
|
65
|
+
|
66
|
+
def initialize(buffer, options)
|
67
|
+
@buffer = buffer
|
68
|
+
@options = Options.new(options)
|
69
|
+
end
|
70
|
+
|
71
|
+
def close; end
|
72
|
+
|
73
|
+
def consume(*); end
|
74
|
+
|
75
|
+
def empty?
|
76
|
+
true
|
77
|
+
end
|
78
|
+
|
79
|
+
def <<(packet)
|
80
|
+
emit(:packet, packet)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
module Packet
|
85
|
+
module_function
|
86
|
+
|
87
|
+
def connect(parameters, uri)
|
88
|
+
packet = [VERSION, CONNECT, uri.port].pack("CCn")
|
89
|
+
begin
|
90
|
+
ip = IPAddr.new(uri.host)
|
91
|
+
raise Error, "Socks4 connection to #{ip} not supported" unless ip.ipv4?
|
92
|
+
packet << [ip.to_i].pack("N")
|
93
|
+
rescue IPAddr::InvalidAddressError
|
94
|
+
if parameters.uri.scheme == "socks4"
|
95
|
+
# resolv defaults to IPv4, and socks4 doesn't support IPv6 otherwise
|
96
|
+
ip = IPAddr.new(Resolv.getaddress(uri.host))
|
97
|
+
packet << [ip.to_i].pack("N")
|
98
|
+
else
|
99
|
+
packet << "\x0\x0\x0\x1" << "\x7\x0" << uri.host
|
100
|
+
end
|
101
|
+
end
|
102
|
+
packet << [parameters.username].pack("Z*")
|
103
|
+
packet
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
register_plugin :"proxy/socks4", Proxy::Socks4
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins
|
5
|
+
module Proxy
|
6
|
+
module Socks5
|
7
|
+
VERSION = 5
|
8
|
+
NOAUTH = 0
|
9
|
+
PASSWD = 2
|
10
|
+
NONE = 0xff
|
11
|
+
CONNECT = 1
|
12
|
+
IPV4 = 1
|
13
|
+
DOMAIN = 3
|
14
|
+
IPV6 = 4
|
15
|
+
SUCCESS = 0
|
16
|
+
|
17
|
+
Error = Class.new(Error)
|
18
|
+
|
19
|
+
class Socks5ProxyChannel < ProxyChannel
|
20
|
+
private
|
21
|
+
|
22
|
+
def proxy_connect
|
23
|
+
@parser = SocksParser.new(@write_buffer, @options)
|
24
|
+
@parser.on(:packet, &method(:on_packet))
|
25
|
+
transition(:negotiating)
|
26
|
+
end
|
27
|
+
|
28
|
+
def on_packet(packet)
|
29
|
+
case @state
|
30
|
+
when :connecting
|
31
|
+
version, method = packet.unpack("CC")
|
32
|
+
check_version(version)
|
33
|
+
case method
|
34
|
+
when PASSWD
|
35
|
+
transition(:authenticating)
|
36
|
+
return
|
37
|
+
when NONE
|
38
|
+
on_error_response("no supported authorization methods")
|
39
|
+
else
|
40
|
+
transition(:negotiating)
|
41
|
+
end
|
42
|
+
when :authenticating
|
43
|
+
version, status = packet.unpack("CC")
|
44
|
+
check_version(version)
|
45
|
+
return transition(:negotiating) if status == SUCCESS
|
46
|
+
on_error_response("socks authentication error: #{status}")
|
47
|
+
when :negotiating
|
48
|
+
version, reply, = packet.unpack("CC")
|
49
|
+
check_version(version)
|
50
|
+
return on_error_response("socks5 negotiation error: #{reply}") unless reply == SUCCESS
|
51
|
+
req, _ = @pending.first
|
52
|
+
request_uri = req.uri
|
53
|
+
@io = ProxySSL.new(@io, request_uri, @options) if request_uri.scheme == "https"
|
54
|
+
transition(:open)
|
55
|
+
throw(:called)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def transition(nextstate)
|
60
|
+
case nextstate
|
61
|
+
when :connecting
|
62
|
+
return unless @state == :idle
|
63
|
+
@io.connect
|
64
|
+
return if @io.closed?
|
65
|
+
@write_buffer << Packet.negotiate(@parameters)
|
66
|
+
proxy_connect
|
67
|
+
when :authenticating
|
68
|
+
return unless @state == :connecting
|
69
|
+
@write_buffer << Packet.authenticate(@parameters)
|
70
|
+
when :negotiating
|
71
|
+
return unless @state == :connecting || @state == :authenticating
|
72
|
+
req, _ = @pending.first
|
73
|
+
request_uri = req.uri
|
74
|
+
@write_buffer << Packet.connect(request_uri)
|
75
|
+
when :open
|
76
|
+
return unless @state == :negotiating
|
77
|
+
@parser = nil
|
78
|
+
end
|
79
|
+
log(1, "SOCKS5: ") { "#{nextstate}: #{@write_buffer.to_s.inspect}" }
|
80
|
+
super
|
81
|
+
end
|
82
|
+
|
83
|
+
def check_version(version)
|
84
|
+
raise Error, "invalid SOCKS version (#{version})" if version != 5
|
85
|
+
end
|
86
|
+
|
87
|
+
def on_error_response(error)
|
88
|
+
response = ErrorResponse.new(error, 0)
|
89
|
+
until @pending.empty?
|
90
|
+
req, _ = @pending.shift
|
91
|
+
emit(:response, req, response)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
Parameters.register("socks5", Socks5ProxyChannel)
|
96
|
+
|
97
|
+
class SocksParser
|
98
|
+
include Callbacks
|
99
|
+
|
100
|
+
def initialize(buffer, options)
|
101
|
+
@buffer = buffer
|
102
|
+
@options = Options.new(options)
|
103
|
+
end
|
104
|
+
|
105
|
+
def close; end
|
106
|
+
|
107
|
+
def consume(*); end
|
108
|
+
|
109
|
+
def empty?
|
110
|
+
true
|
111
|
+
end
|
112
|
+
|
113
|
+
def <<(packet)
|
114
|
+
emit(:packet, packet)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
module Packet
|
119
|
+
module_function
|
120
|
+
|
121
|
+
def negotiate(parameters)
|
122
|
+
methods = [NOAUTH]
|
123
|
+
methods << PASSWD if parameters.authenticated?
|
124
|
+
methods.unshift(methods.size)
|
125
|
+
methods.unshift(VERSION)
|
126
|
+
methods.pack("C*")
|
127
|
+
end
|
128
|
+
|
129
|
+
def authenticate(parameters)
|
130
|
+
user = parameters.username
|
131
|
+
pass = parameters.password
|
132
|
+
[0x01, user.bytesize, user, pass.bytesize, password].pack("CCA*CA*")
|
133
|
+
end
|
134
|
+
|
135
|
+
def connect(uri)
|
136
|
+
packet = [VERSION, CONNECT, 0].pack("C*")
|
137
|
+
begin
|
138
|
+
ip = IPAddr.new(uri.host)
|
139
|
+
raise Error, "Socks4 connection to #{ip} not supported" unless ip.ipv4?
|
140
|
+
packet << [IPV4, ip.to_i].pack("CN")
|
141
|
+
rescue IPAddr::InvalidAddressError
|
142
|
+
packet << [DOMAIN, uri.host.bytesize, uri.host].pack("CCA*")
|
143
|
+
end
|
144
|
+
packet << [uri.port].pack("n")
|
145
|
+
packet
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
register_plugin :"proxy/socks5", Proxy::Socks5
|
151
|
+
end
|
152
|
+
end
|