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.
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