httpx 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/lib/httpx.rb +8 -2
  3. data/lib/httpx/adapters/faraday.rb +203 -0
  4. data/lib/httpx/altsvc.rb +4 -0
  5. data/lib/httpx/callbacks.rb +1 -4
  6. data/lib/httpx/chainable.rb +4 -3
  7. data/lib/httpx/connection.rb +326 -104
  8. data/lib/httpx/{channel → connection}/http1.rb +29 -15
  9. data/lib/httpx/{channel → connection}/http2.rb +12 -6
  10. data/lib/httpx/errors.rb +2 -0
  11. data/lib/httpx/headers.rb +4 -1
  12. data/lib/httpx/io/ssl.rb +5 -1
  13. data/lib/httpx/io/tcp.rb +13 -7
  14. data/lib/httpx/io/udp.rb +1 -0
  15. data/lib/httpx/io/unix.rb +1 -0
  16. data/lib/httpx/loggable.rb +34 -9
  17. data/lib/httpx/options.rb +57 -31
  18. data/lib/httpx/parser/http1.rb +8 -0
  19. data/lib/httpx/plugins/authentication.rb +4 -0
  20. data/lib/httpx/plugins/basic_authentication.rb +4 -0
  21. data/lib/httpx/plugins/compression.rb +22 -5
  22. data/lib/httpx/plugins/cookies.rb +89 -36
  23. data/lib/httpx/plugins/digest_authentication.rb +45 -26
  24. data/lib/httpx/plugins/follow_redirects.rb +61 -62
  25. data/lib/httpx/plugins/h2c.rb +78 -39
  26. data/lib/httpx/plugins/multipart.rb +5 -0
  27. data/lib/httpx/plugins/persistent.rb +29 -0
  28. data/lib/httpx/plugins/proxy.rb +125 -78
  29. data/lib/httpx/plugins/proxy/http.rb +31 -27
  30. data/lib/httpx/plugins/proxy/socks4.rb +30 -24
  31. data/lib/httpx/plugins/proxy/socks5.rb +49 -39
  32. data/lib/httpx/plugins/proxy/ssh.rb +81 -0
  33. data/lib/httpx/plugins/push_promise.rb +18 -9
  34. data/lib/httpx/plugins/retries.rb +43 -15
  35. data/lib/httpx/pool.rb +159 -0
  36. data/lib/httpx/registry.rb +2 -0
  37. data/lib/httpx/request.rb +10 -0
  38. data/lib/httpx/resolver.rb +2 -1
  39. data/lib/httpx/resolver/https.rb +62 -56
  40. data/lib/httpx/resolver/native.rb +48 -37
  41. data/lib/httpx/resolver/resolver_mixin.rb +16 -11
  42. data/lib/httpx/resolver/system.rb +11 -7
  43. data/lib/httpx/response.rb +24 -10
  44. data/lib/httpx/selector.rb +32 -39
  45. data/lib/httpx/{client.rb → session.rb} +99 -62
  46. data/lib/httpx/timeout.rb +7 -15
  47. data/lib/httpx/transcoder/body.rb +4 -0
  48. data/lib/httpx/transcoder/chunker.rb +4 -0
  49. data/lib/httpx/version.rb +1 -1
  50. metadata +10 -8
  51. data/lib/httpx/channel.rb +0 -367
@@ -2,53 +2,51 @@
2
2
 
3
3
  module HTTPX
4
4
  module Plugins
5
+ #
6
+ # This plugin adds support for upgrading a plaintext HTTP/1.1 connection to HTTP/2.
7
+ #
8
+ # https://tools.ietf.org/html/rfc7540#section-3.2
9
+ #
5
10
  module H2C
6
11
  def self.load_dependencies(*)
7
12
  require "base64"
8
13
  end
9
14
 
10
15
  module InstanceMethods
11
- def request(*args, keep_open: @keep_open, **options)
12
- return super if @_h2c_probed
13
- begin
14
- requests = __build_reqs(*args, **options)
15
-
16
- upgrade_request = requests.first
17
- return super unless valid_h2c_upgrade_request?(upgrade_request)
18
- upgrade_request.headers["upgrade"] = "h2c"
19
- upgrade_request.headers.add("connection", "upgrade")
20
- upgrade_request.headers.add("connection", "http2-settings")
21
- upgrade_request.headers["http2-settings"] = HTTP2::Client.settings_header(@options.http2_settings)
22
- # TODO: validate!
23
- upgrade_response = __send_reqs(*upgrade_request, **options).first
24
-
25
- if upgrade_response.status == 101
26
- channel = find_channel(upgrade_request)
27
- parser = channel.upgrade_parser("h2")
28
- parser.extend(UpgradeExtensions)
29
- parser.upgrade(upgrade_request, upgrade_response, **options)
30
- data = upgrade_response.to_s
31
- parser << data
32
- response = upgrade_request.response
33
- if response.status == 200
34
- requests.delete(upgrade_request)
35
- return response if requests.empty?
36
- end
37
- responses = __send_reqs(*requests)
38
- else
39
- # proceed as usual
40
- responses = [upgrade_response] + __send_reqs(*requests[1..-1])
41
- end
42
- return responses.first if responses.size == 1
43
- responses
44
- ensure
45
- @_h2c_probed = true
46
- close unless keep_open
47
- end
16
+ def request(*args, **options)
17
+ h2c_options = options.merge(fallback_protocol: "h2c")
18
+
19
+ requests = build_requests(*args, h2c_options)
20
+
21
+ upgrade_request = requests.first
22
+ return super unless valid_h2c_upgrade_request?(upgrade_request)
23
+
24
+ upgrade_request.headers.add("connection", "upgrade")
25
+ upgrade_request.headers.add("connection", "http2-settings")
26
+ upgrade_request.headers["upgrade"] = "h2c"
27
+ upgrade_request.headers["http2-settings"] = HTTP2::Client.settings_header(upgrade_request.options.http2_settings)
28
+ wrap { send_requests(*upgrade_request, h2c_options).first }
29
+
30
+ responses = send_requests(*requests, h2c_options)
31
+
32
+ return responses.first if responses.size == 1
33
+
34
+ responses
48
35
  end
49
36
 
50
37
  private
51
38
 
39
+ def fetch_response(request, connections, options)
40
+ response = super
41
+ if response && valid_h2c_upgrade?(request, response, options)
42
+ log { "upgrading to h2c..." }
43
+ connection = find_connection(request, connections, options)
44
+ connections << connection unless connections.include?(connection)
45
+ connection.upgrade(request, response)
46
+ end
47
+ response
48
+ end
49
+
52
50
  VALID_H2C_METHODS = %i[get options head].freeze
53
51
  private_constant :VALID_H2C_METHODS
54
52
 
@@ -56,16 +54,57 @@ module HTTPX
56
54
  VALID_H2C_METHODS.include?(request.verb) &&
57
55
  request.scheme == "http"
58
56
  end
57
+
58
+ def valid_h2c_upgrade?(request, response, options)
59
+ options.fallback_protocol == "h2c" &&
60
+ request.headers.get("connection").include?("upgrade") &&
61
+ request.headers.get("upgrade").include?("h2c") &&
62
+ response.status == 101
63
+ end
59
64
  end
60
65
 
61
- module UpgradeExtensions
62
- def upgrade(request, _response, **)
66
+ class H2CParser < Connection::HTTP2
67
+ def upgrade(request, response)
63
68
  @connection.send_connection_preface
64
69
  # skip checks, it is assumed that this is the first
65
70
  # request in the connection
66
71
  stream = @connection.upgrade
67
72
  handle_stream(stream, request)
68
73
  @streams[request] = stream
74
+
75
+ # clean up data left behind in the buffer, if the server started
76
+ # sending frames
77
+ data = response.to_s
78
+ @connection << data
79
+ end
80
+ end
81
+
82
+ module ConnectionMethods
83
+ using URIExtensions
84
+
85
+ def match?(uri, options)
86
+ return super unless uri.scheme == "http" && @options.fallback_protocol == "h2c"
87
+
88
+ super && options.fallback_protocol == "h2c"
89
+ end
90
+
91
+ def coalescable?(connection)
92
+ return super unless @options.fallback_protocol == "h2c" && @origin.scheme == "http"
93
+
94
+ @origin == connection.origin && connection.options.fallback_protocol == "h2c"
95
+ end
96
+
97
+ def upgrade(request, response)
98
+ @parser.reset if @parser
99
+ @parser = H2CParser.new(@write_buffer, @options)
100
+ set_parser_callbacks(@parser)
101
+ @parser.upgrade(request, response)
102
+ end
103
+
104
+ def build_parser(*)
105
+ return super unless @origin.scheme == "http"
106
+
107
+ super("http/1.1")
69
108
  end
70
109
  end
71
110
 
@@ -2,6 +2,11 @@
2
2
 
3
3
  module HTTPX
4
4
  module Plugins
5
+ #
6
+ # This plugin adds support for passing `http-form_data` objects (like file objects) as "multipart/form-data";
7
+ #
8
+ # HTTPX.post(URL, form: form: { image: HTTP::FormData::File.new("path/to/file")})
9
+ #
5
10
  module Multipart
6
11
  module FormTranscoder
7
12
  module_function
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ # This plugin implements a session that persists connections over the duration of the process.
6
+ #
7
+ # This will improve connection reuse in a long-running process.
8
+ #
9
+ # One important caveat to note is, although this session might not close connections,
10
+ # other sessions from the same process that don't have this plugin turned on might.
11
+ #
12
+ # This session will still be able to work with it, as if, when expecting a connection
13
+ # terminated by a different session, it will just retry on a new one and keep it open.
14
+ #
15
+ # This plugin is also not recommendable when connecting to >9000 (like, a lot) different origins.
16
+ # So when you use this, make sure that you don't fall into this trap.
17
+ #
18
+ module Persistent
19
+ def self.load_dependencies(klass, *)
20
+ klass.plugin(:retries) # TODO: pass default max_retries -> 1 as soon as this is a parameter
21
+ end
22
+
23
+ def self.extra_options(options)
24
+ options.merge(persistent: true)
25
+ end
26
+ end
27
+ register_plugin :persistent, Persistent
28
+ end
29
+ end
@@ -6,11 +6,19 @@ require "forwardable"
6
6
 
7
7
  module HTTPX
8
8
  module Plugins
9
+ #
10
+ # This plugin adds support for proxies. It ships with support for:
11
+ #
12
+ # * HTTP proxies
13
+ # * HTTPS proxies
14
+ # * Socks4/4a proxies
15
+ # * Socks5 proxies
16
+ #
9
17
  module Proxy
10
18
  Error = Class.new(Error)
11
- class Parameters
12
- extend Registry
19
+ PROXY_ERRORS = [TimeoutError, IOError, SystemCallError, Error].freeze
13
20
 
21
+ class Parameters
14
22
  attr_reader :uri, :username, :password
15
23
 
16
24
  def initialize(uri:, username: nil, password: nil)
@@ -26,6 +34,32 @@ module HTTPX
26
34
  def token_authentication
27
35
  Base64.strict_encode64("#{user}:#{password}")
28
36
  end
37
+
38
+ def ==(other)
39
+ if other.is_a?(Parameters)
40
+ @uri == other.uri &&
41
+ @username == other.username &&
42
+ @password == other.password
43
+ else
44
+ super
45
+ end
46
+ end
47
+ end
48
+
49
+ class << self
50
+ def configure(klass, *)
51
+ klass.plugin(:"proxy/http")
52
+ klass.plugin(:"proxy/socks4")
53
+ klass.plugin(:"proxy/socks5")
54
+ end
55
+
56
+ def extra_options(options)
57
+ Class.new(options.class) do
58
+ def_option(:proxy) do |pr|
59
+ Hash[pr]
60
+ end
61
+ end.new(options)
62
+ end
29
63
  end
30
64
 
31
65
  module InstanceMethods
@@ -35,118 +69,131 @@ module HTTPX
35
69
 
36
70
  private
37
71
 
38
- def proxy_params(uri)
72
+ def proxy_uris(uri, options)
39
73
  @_proxy_uris ||= begin
40
- uris = @options.proxy ? Array(@options.proxy[:uri]) : []
74
+ uris = options.proxy ? Array(options.proxy[:uri]) : []
41
75
  if uris.empty?
42
76
  uri = URI(uri).find_proxy
43
77
  uris << uri if uri
44
78
  end
45
79
  uris
46
80
  end
47
- @options.proxy.merge(uri: @_proxy_uris.shift) unless @_proxy_uris.empty?
81
+ options.proxy.merge(uri: @_proxy_uris.first) unless @_proxy_uris.empty?
48
82
  end
49
83
 
50
- def find_channel(request, **options)
84
+ def find_connection(request, connections, options)
85
+ return super unless options.respond_to?(:proxy)
86
+
51
87
  uri = URI(request.uri)
52
- proxy = proxy_params(uri)
53
- raise Error, "Failed to connect to proxy" unless proxy
54
- @connection.find_channel(proxy) || build_channel(proxy, options)
88
+ next_proxy = proxy_uris(uri, options)
89
+ raise Error, "Failed to connect to proxy" unless next_proxy
90
+
91
+ proxy_options = options.merge(proxy: Parameters.new(**next_proxy))
92
+ connection = pool.find_connection(uri, proxy_options) || build_connection(uri, proxy_options)
93
+ unless connections.nil? || connections.include?(connection)
94
+ connections << connection
95
+ set_connection_callbacks(connection, options)
96
+ end
97
+ connection
55
98
  end
56
99
 
57
- def build_channel(proxy, options)
58
- return super if proxy.is_a?(URI::Generic)
59
- channel = build_proxy_channel(proxy, **options)
60
- set_channel_callbacks(channel, options)
61
- channel
62
- end
100
+ def build_connection(uri, options)
101
+ proxy = options.proxy
102
+ return super unless proxy
63
103
 
64
- def build_proxy_channel(proxy, **options)
65
- parameters = Parameters.new(**proxy)
66
- uri = parameters.uri
67
- log { "proxy: #{uri}" }
68
- proxy_type = Parameters.registry(parameters.uri.scheme)
69
- channel = proxy_type.new("tcp", uri, parameters, @options.merge(options), &method(:on_response))
70
- @connection.__send__(:resolve_channel, channel)
71
- channel
104
+ connection = options.connection_class.new("tcp", uri, options)
105
+ pool.init_connection(connection, options)
106
+ connection
72
107
  end
73
108
 
74
- def fetch_response(request)
109
+ def fetch_response(request, connections, options)
75
110
  response = super
76
111
  if response.is_a?(ErrorResponse) &&
77
112
  # either it was a timeout error connecting, or it was a proxy error
78
- (((response.error.is_a?(TimeoutError) || response.error.is_a?(IOError)) && request.state == :idle) ||
79
- response.error.is_a?(Error)) &&
80
- !@_proxy_uris.empty?
113
+ PROXY_ERRORS.any? { |ex| response.error.is_a?(ex) } && !@_proxy_uris.empty?
114
+ @_proxy_uris.shift
81
115
  log { "failed connecting to proxy, trying next..." }
82
- channel = find_channel(request)
83
- channel.send(request)
116
+ request.transition(:idle)
117
+ connection = find_connection(request, connections, options)
118
+ connections << connection unless connections.include?(connection)
119
+ connection.send(request)
84
120
  return
85
121
  end
86
122
  response
87
123
  end
88
124
  end
89
125
 
90
- module OptionsMethods
91
- def self.included(klass)
126
+ module ConnectionMethods
127
+ using URIExtensions
128
+
129
+ def initialize(*)
92
130
  super
93
- klass.def_option(:proxy) do |pr|
94
- Hash[pr]
95
- end
131
+ return unless @options.proxy
132
+
133
+ # redefining the connection origin as the proxy's URI,
134
+ # as this will be used as the tcp peer ip.
135
+ @origin = URI(@options.proxy.uri.origin)
96
136
  end
97
- end
98
137
 
99
- def self.configure(klass, *)
100
- klass.plugin(:"proxy/http")
101
- klass.plugin(:"proxy/socks4")
102
- klass.plugin(:"proxy/socks5")
103
- end
104
- end
105
- register_plugin :proxy, Proxy
106
- end
138
+ def match?(uri, options)
139
+ return super unless @options.proxy
107
140
 
108
- class ProxyChannel < Channel
109
- def initialize(type, uri, parameters, options, &blk)
110
- super(type, uri, options, &blk)
111
- @parameters = parameters
112
- end
141
+ super && @options.proxy == options.proxy
142
+ end
113
143
 
114
- def match?(*)
115
- true
116
- end
144
+ # should not coalesce connections here, as the IP is the IP of the proxy
145
+ def coalescable?(*)
146
+ return super unless @options.proxy
117
147
 
118
- def send(request, **args)
119
- @pending << [request, args]
120
- end
148
+ false
149
+ end
121
150
 
122
- def connecting?
123
- super || @state == :connecting || @state == :connected
124
- end
151
+ def send(request)
152
+ return super unless @options.proxy
153
+ return super unless connecting?
125
154
 
126
- def to_io
127
- case @state
128
- when :idle
129
- transition(:connecting)
130
- when :connected
131
- transition(:open)
132
- end
133
- @io.to_io
134
- end
155
+ @pending << request
156
+ end
135
157
 
136
- def call
137
- super
138
- case @state
139
- when :connecting
140
- consume
141
- end
142
- end
158
+ def connecting?
159
+ return super unless @options.proxy
160
+
161
+ super || @state == :connecting || @state == :connected
162
+ end
163
+
164
+ def to_io
165
+ return super unless @options.proxy
143
166
 
144
- def reset
145
- @state = :open
146
- transition(:closing)
147
- transition(:closed)
148
- emit(:close)
167
+ case @state
168
+ when :idle
169
+ transition(:connecting)
170
+ when :connected
171
+ transition(:open)
172
+ end
173
+ @io.to_io
174
+ end
175
+
176
+ def call
177
+ super
178
+ return unless @options.proxy
179
+
180
+ case @state
181
+ when :connecting
182
+ consume
183
+ end
184
+ end
185
+
186
+ def reset
187
+ return super unless @options.proxy
188
+
189
+ @state = :open
190
+ transition(:closing)
191
+ transition(:closed)
192
+ emit(:close)
193
+ end
194
+ end
149
195
  end
196
+ register_plugin :proxy, Proxy
150
197
  end
151
198
 
152
199
  class ProxySSL < SSL