httpx 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +191 -0
  3. data/README.md +119 -0
  4. data/lib/httpx.rb +50 -0
  5. data/lib/httpx/buffer.rb +34 -0
  6. data/lib/httpx/callbacks.rb +32 -0
  7. data/lib/httpx/chainable.rb +51 -0
  8. data/lib/httpx/channel.rb +222 -0
  9. data/lib/httpx/channel/http1.rb +220 -0
  10. data/lib/httpx/channel/http2.rb +224 -0
  11. data/lib/httpx/client.rb +173 -0
  12. data/lib/httpx/connection.rb +74 -0
  13. data/lib/httpx/errors.rb +7 -0
  14. data/lib/httpx/extensions.rb +52 -0
  15. data/lib/httpx/headers.rb +152 -0
  16. data/lib/httpx/io.rb +240 -0
  17. data/lib/httpx/loggable.rb +11 -0
  18. data/lib/httpx/options.rb +138 -0
  19. data/lib/httpx/plugins/authentication.rb +14 -0
  20. data/lib/httpx/plugins/basic_authentication.rb +20 -0
  21. data/lib/httpx/plugins/compression.rb +123 -0
  22. data/lib/httpx/plugins/compression/brotli.rb +55 -0
  23. data/lib/httpx/plugins/compression/deflate.rb +50 -0
  24. data/lib/httpx/plugins/compression/gzip.rb +59 -0
  25. data/lib/httpx/plugins/cookies.rb +63 -0
  26. data/lib/httpx/plugins/digest_authentication.rb +141 -0
  27. data/lib/httpx/plugins/follow_redirects.rb +72 -0
  28. data/lib/httpx/plugins/h2c.rb +85 -0
  29. data/lib/httpx/plugins/proxy.rb +108 -0
  30. data/lib/httpx/plugins/proxy/http.rb +115 -0
  31. data/lib/httpx/plugins/proxy/socks4.rb +110 -0
  32. data/lib/httpx/plugins/proxy/socks5.rb +152 -0
  33. data/lib/httpx/plugins/push_promise.rb +67 -0
  34. data/lib/httpx/plugins/stream.rb +33 -0
  35. data/lib/httpx/registry.rb +88 -0
  36. data/lib/httpx/request.rb +222 -0
  37. data/lib/httpx/response.rb +225 -0
  38. data/lib/httpx/selector.rb +155 -0
  39. data/lib/httpx/timeout.rb +68 -0
  40. data/lib/httpx/transcoder.rb +12 -0
  41. data/lib/httpx/transcoder/body.rb +56 -0
  42. data/lib/httpx/transcoder/chunker.rb +38 -0
  43. data/lib/httpx/transcoder/form.rb +41 -0
  44. data/lib/httpx/transcoder/json.rb +36 -0
  45. data/lib/httpx/version.rb +5 -0
  46. 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