httpx 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|