httpx 0.0.5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 3fd5760406dc7a2cb5abb040160144fb8a788a19
4
- data.tar.gz: 2fcf4228f1ff06b7621b7e688e9fe2a173e39096
2
+ SHA256:
3
+ metadata.gz: 34654a12cf778aeffd39429a869bbfb62dc487369ed50fdfc8b14c512e141c91
4
+ data.tar.gz: 055b3f79a33ecf43982264f2af5d61631d290aab74c7b63d30fc82501ba366c3
5
5
  SHA512:
6
- metadata.gz: adc85e755255e5e460bce9a84c5e5c3c03a8271702eed4373aded5002d71b28d99787bc4f4837d3bee4634f58fbe5541cf6f3a0282c54e4b3c5d86233f7b581c
7
- data.tar.gz: 58f6d7c64c527bb654a8ab7107eff77315ad4c3d6f17da06b081be0d25345169736b78082b9e4335373dde707e071823d2bafa74102336535f9af07234bbd3d6
6
+ metadata.gz: b9cc0fb05f09c33d2b18f7aa3b46313240744ecb0a8474ffb5345afc2e533768b987609eabd8f1461c00f64815610dcd83e68f340933ce1695ee3efcbc6653a2
7
+ data.tar.gz: 21bc5f92e8c8c639a2b0f0885d7652805a099f44a89900da8336993c1e033359245666204d2adccc9fbfb18e49c6dd883fc09ffac5c56b194df056a8cda4b0de
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # HTTPX: A Ruby HTTP HTTPX for tomorrow... and beyond!
1
+ # HTTPX: A Ruby HTTP library for tomorrow... and beyond!
2
2
 
3
3
  [![pipeline status](https://gitlab.com/honeyryderchuck/httpx/badges/master/pipeline.svg)](https://gitlab.com/honeyryderchuck/httpx/commits/master)
4
4
  [![coverage report](https://gitlab.com/honeyryderchuck/httpx/badges/master/coverage.svg)](https://honeyryderchuck.gitlab.io/httpx/coverage/#_AllFiles)
@@ -42,14 +42,15 @@ module HTTPX
42
42
 
43
43
  class << self
44
44
  def by(uri, options)
45
- io = case uri.scheme
46
- when "http"
47
- IO.registry("tcp").new(uri.host, uri.port, options)
48
- when "https"
49
- IO.registry("ssl").new(uri.host, uri.port, options)
50
- else
51
- raise Error, "#{uri}: #{uri.scheme}: unrecognized channel"
45
+ type = options.transport || begin
46
+ case uri.scheme
47
+ when "http" then "tcp"
48
+ when "https" then "ssl"
49
+ else
50
+ raise Error, "#{uri}: #{uri.scheme}: unrecognized channel"
51
+ end
52
52
  end
53
+ io = IO.registry(type).new(uri, options)
53
54
  new(io, options)
54
55
  end
55
56
  end
@@ -153,7 +154,9 @@ module HTTPX
153
154
  loop do
154
155
  siz = @io.read(wsize, @read_buffer)
155
156
  unless siz
156
- emit(:close)
157
+ ex = EOFError.new("descriptor closed")
158
+ ex.set_backtrace(caller)
159
+ on_error(ex)
157
160
  return
158
161
  end
159
162
  return if siz.zero?
@@ -167,7 +170,9 @@ module HTTPX
167
170
  return if @write_buffer.empty?
168
171
  siz = @io.write(@write_buffer)
169
172
  unless siz
170
- emit(:close)
173
+ ex = EOFError.new("descriptor closed")
174
+ ex.set_backtrace(caller)
175
+ on_error(ex)
171
176
  return
172
177
  end
173
178
  log { "WRITE: #{siz} bytes..." }
@@ -206,7 +211,7 @@ module HTTPX
206
211
  end
207
212
  end
208
213
  parser.on(:error) do |request, ex|
209
- response = ErrorResponse.new(ex, 0, @options)
214
+ response = ErrorResponse.new(ex, @options)
210
215
  emit(:response, request, response)
211
216
  end
212
217
  parser
@@ -246,8 +251,8 @@ module HTTPX
246
251
  end
247
252
 
248
253
  def handle_error(e)
249
- parser.handle_error(e)
250
- response = ErrorResponse.new(e, 0, @options)
254
+ parser.handle_error(e) if parser.respond_to?(:handle_error)
255
+ response = ErrorResponse.new(e, @options)
251
256
  @pending.each do |request, _|
252
257
  emit(:response, request, response)
253
258
  end
@@ -101,7 +101,7 @@ module HTTPX
101
101
 
102
102
  response << chunk
103
103
 
104
- # dispatch if response.complete?
104
+ @has_response = response.complete?
105
105
  end
106
106
 
107
107
  def on_message_complete
@@ -51,13 +51,7 @@ module HTTPX
51
51
  end
52
52
 
53
53
  def fetch_response(request)
54
- response = @responses.delete(request)
55
- if response.is_a?(ErrorResponse) && response.retryable?
56
- channel = find_channel(request)
57
- channel.send(request, retries: response.retries - 1)
58
- return
59
- end
60
- response
54
+ @responses.delete(request)
61
55
  end
62
56
 
63
57
  def find_channel(request, **options)
@@ -24,7 +24,10 @@ module HTTPX
24
24
  end
25
25
  monitor.interests = channel.interests
26
26
  end
27
- rescue TimeoutError => ex
27
+ rescue TimeoutError,
28
+ Errno::ECONNRESET,
29
+ Errno::ECONNABORTED,
30
+ Errno::EPIPE => ex
28
31
  @channels.each do |ch|
29
32
  ch.emit(:error, ex)
30
33
  end
@@ -1,255 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "resolv"
4
3
  require "socket"
5
- require "openssl"
6
- require "ipaddr"
4
+ require "httpx/io/tcp"
5
+ require "httpx/io/ssl"
6
+ require "httpx/io/unix"
7
7
 
8
8
  module HTTPX
9
- class TCP
10
- include Loggable
11
-
12
- attr_reader :ip, :port
13
-
14
- def initialize(hostname, port, options)
15
- @state = :idle
16
- @hostname = hostname
17
- @options = Options.new(options)
18
- @fallback_protocol = @options.fallback_protocol
19
- @port = port
20
- if @options.io
21
- @io = case @options.io
22
- when Hash
23
- @ip = Resolv.getaddress(@hostname)
24
- @options.io[@ip] || @options.io["#{@ip}:#{@port}"]
25
- else
26
- @ip = hostname
27
- @options.io
28
- end
29
- unless @io.nil?
30
- @keep_open = true
31
- @state = :connected
32
- end
33
- else
34
- @ip = Resolv.getaddress(@hostname)
35
- end
36
- @io ||= build_socket
37
- end
38
-
39
- def scheme
40
- "http"
41
- end
42
-
43
- def to_io
44
- @io.to_io
45
- end
46
-
47
- def protocol
48
- @fallback_protocol
49
- end
50
-
51
- def connect
52
- return unless closed?
53
- begin
54
- if @io.closed?
55
- transition(:idle)
56
- @io = build_socket
57
- end
58
- @io.connect_nonblock(Socket.sockaddr_in(@port, @ip))
59
- rescue Errno::EISCONN
60
- end
61
- transition(:connected)
62
- rescue Errno::EINPROGRESS,
63
- Errno::EALREADY,
64
- ::IO::WaitReadable
65
- end
66
-
67
- if RUBY_VERSION < "2.3"
68
- def read(size, buffer)
69
- @io.read_nonblock(size, buffer)
70
- buffer.bytesize
71
- rescue ::IO::WaitReadable
72
- 0
73
- rescue EOFError
74
- nil
75
- end
76
-
77
- def write(buffer)
78
- siz = @io.write_nonblock(buffer)
79
- buffer.slice!(0, siz)
80
- siz
81
- rescue ::IO::WaitWritable
82
- 0
83
- rescue EOFError
84
- nil
85
- end
86
- else
87
- def read(size, buffer)
88
- ret = @io.read_nonblock(size, buffer, exception: false)
89
- return 0 if ret == :wait_readable
90
- return if ret.nil?
91
- buffer.bytesize
92
- end
93
-
94
- def write(buffer)
95
- siz = @io.write_nonblock(buffer, exception: false)
96
- return 0 if siz == :wait_writable
97
- return if siz.nil?
98
- buffer.slice!(0, siz)
99
- siz
100
- end
101
- end
102
-
103
- def close
104
- return if @keep_open || closed?
105
- begin
106
- @io.close
107
- ensure
108
- transition(:closed)
109
- end
110
- end
111
-
112
- def connected?
113
- @state == :connected
114
- end
115
-
116
- def closed?
117
- @state == :idle || @state == :closed
118
- end
119
-
120
- def inspect
121
- id = @io.closed? ? "closed" : @io.fileno
122
- "#<TCP(fd: #{id}): #{@ip}:#{@port} (state: #{@state})>"
123
- end
124
-
125
- private
126
-
127
- def build_socket
128
- addr = IPAddr.new(@ip)
129
- Socket.new(addr.family, :STREAM, 0)
130
- end
131
-
132
- def transition(nextstate)
133
- case nextstate
134
- # when :idle
135
- when :connected
136
- return unless @state == :idle
137
- when :closed
138
- return unless @state == :connected
139
- end
140
- do_transition(nextstate)
141
- end
142
-
143
- def do_transition(nextstate)
144
- log(level: 1, label: "#{inspect}: ") { nextstate.to_s }
145
- @state = nextstate
146
- end
147
- end
148
-
149
- class SSL < TCP
150
- TLS_OPTIONS = if OpenSSL::SSL::SSLContext.instance_methods.include?(:alpn_protocols)
151
- { alpn_protocols: %w[h2 http/1.1] }
152
- else
153
- {}
154
- end
155
-
156
- def initialize(_, _, options)
157
- @ctx = OpenSSL::SSL::SSLContext.new
158
- ctx_options = TLS_OPTIONS.merge(options.ssl)
159
- @ctx.set_params(ctx_options) unless ctx_options.empty?
160
- super
161
- @state = :negotiated if @keep_open
162
- end
163
-
164
- def scheme
165
- "https"
166
- end
167
-
168
- def protocol
169
- @io.alpn_protocol || super
170
- rescue StandardError
171
- super
172
- end
173
-
174
- def close
175
- super
176
- # allow reconnections
177
- # connect only works if initial @io is a socket
178
- @io = @io.io if @io.respond_to?(:io)
179
- @negotiated = false
180
- end
181
-
182
- def connected?
183
- @state == :negotiated
184
- end
185
-
186
- def connect
187
- super
188
- if @keep_open
189
- @state = :negotiated
190
- return
191
- end
192
- return if @state == :negotiated ||
193
- @state != :connected
194
- unless @io.is_a?(OpenSSL::SSL::SSLSocket)
195
- @io = OpenSSL::SSL::SSLSocket.new(@io, @ctx)
196
- @io.hostname = @hostname
197
- @io.sync_close = true
198
- end
199
- # TODO: this might block it all
200
- @io.connect_nonblock
201
- transition(:negotiated)
202
- rescue ::IO::WaitReadable,
203
- ::IO::WaitWritable
204
- end
205
-
206
- if RUBY_VERSION < "2.3"
207
- def read(*)
208
- super
209
- rescue ::IO::WaitWritable
210
- 0
211
- end
212
-
213
- def write(*)
214
- super
215
- rescue ::IO::WaitReadable
216
- 0
217
- end
218
- else
219
- if OpenSSL::VERSION < "2.0.6"
220
- def read(size, buffer)
221
- @io.read_nonblock(size, buffer)
222
- buffer.bytesize
223
- rescue ::IO::WaitReadable,
224
- ::IO::WaitWritable
225
- 0
226
- rescue EOFError
227
- nil
228
- end
229
- end
230
- end
231
-
232
- def inspect
233
- id = @io.closed? ? "closed" : @io.to_io.fileno
234
- "#<SSL(fd: #{id}): #{@ip}:#{@port} state: #{@state}>"
235
- end
236
-
237
- private
238
-
239
- def transition(nextstate)
240
- case nextstate
241
- when :negotiated
242
- return unless @state == :connected
243
- when :closed
244
- return unless @state == :negotiated ||
245
- @state == :connected
246
- end
247
- do_transition(nextstate)
248
- end
249
- end
250
9
  module IO
251
10
  extend Registry
252
11
  register "tcp", TCP
253
12
  register "ssl", SSL
13
+ register "unix", HTTPX::UNIX
254
14
  end
255
15
  end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module HTTPX
6
+ class SSL < TCP
7
+ TLS_OPTIONS = if OpenSSL::SSL::SSLContext.instance_methods.include?(:alpn_protocols)
8
+ { alpn_protocols: %w[h2 http/1.1] }
9
+ else
10
+ {}
11
+ end
12
+
13
+ def initialize(_, options)
14
+ @ctx = OpenSSL::SSL::SSLContext.new
15
+ ctx_options = TLS_OPTIONS.merge(options.ssl)
16
+ @ctx.set_params(ctx_options) unless ctx_options.empty?
17
+ super
18
+ @state = :negotiated if @keep_open
19
+ end
20
+
21
+ def scheme
22
+ "https"
23
+ end
24
+
25
+ def protocol
26
+ @io.alpn_protocol || super
27
+ rescue StandardError
28
+ super
29
+ end
30
+
31
+ def close
32
+ super
33
+ # allow reconnections
34
+ # connect only works if initial @io is a socket
35
+ @io = @io.io if @io.respond_to?(:io)
36
+ @negotiated = false
37
+ end
38
+
39
+ def connected?
40
+ @state == :negotiated
41
+ end
42
+
43
+ def connect
44
+ super
45
+ if @keep_open
46
+ @state = :negotiated
47
+ return
48
+ end
49
+ return if @state == :negotiated ||
50
+ @state != :connected
51
+ unless @io.is_a?(OpenSSL::SSL::SSLSocket)
52
+ @io = OpenSSL::SSL::SSLSocket.new(@io, @ctx)
53
+ @io.hostname = @hostname
54
+ @io.sync_close = true
55
+ end
56
+ @io.connect_nonblock
57
+ @io.post_connection_check(@hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
58
+ transition(:negotiated)
59
+ rescue ::IO::WaitReadable,
60
+ ::IO::WaitWritable
61
+ end
62
+
63
+ if RUBY_VERSION < "2.3"
64
+ def read(*)
65
+ super
66
+ rescue ::IO::WaitWritable
67
+ 0
68
+ end
69
+
70
+ def write(*)
71
+ super
72
+ rescue ::IO::WaitReadable
73
+ 0
74
+ end
75
+ else
76
+ if OpenSSL::VERSION < "2.0.6"
77
+ def read(size, buffer)
78
+ @io.read_nonblock(size, buffer)
79
+ buffer.bytesize
80
+ rescue ::IO::WaitReadable,
81
+ ::IO::WaitWritable
82
+ 0
83
+ rescue EOFError
84
+ nil
85
+ end
86
+ end
87
+ end
88
+
89
+ def inspect
90
+ id = @io.closed? ? "closed" : @io.to_io.fileno
91
+ "#<SSL(fd: #{id}): #{@ip}:#{@port} state: #{@state}>"
92
+ end
93
+
94
+ private
95
+
96
+ def transition(nextstate)
97
+ case nextstate
98
+ when :negotiated
99
+ return unless @state == :connected
100
+ when :closed
101
+ return unless @state == :negotiated ||
102
+ @state == :connected
103
+ end
104
+ do_transition(nextstate)
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "resolv"
4
+ require "ipaddr"
5
+
6
+ module HTTPX
7
+ class TCP
8
+ include Loggable
9
+
10
+ attr_reader :ip, :port
11
+
12
+ alias_method :host, :ip
13
+
14
+ def initialize(uri, options)
15
+ @state = :idle
16
+ @hostname = uri.host
17
+ @options = Options.new(options)
18
+ @fallback_protocol = @options.fallback_protocol
19
+ @port = uri.port
20
+ if @options.io
21
+ @io = case @options.io
22
+ when Hash
23
+ @ip = Resolv.getaddress(@hostname)
24
+ @options.io[@ip] || @options.io["#{@ip}:#{@port}"]
25
+ else
26
+ @ip = @hostname
27
+ @options.io
28
+ end
29
+ unless @io.nil?
30
+ @keep_open = true
31
+ @state = :connected
32
+ end
33
+ else
34
+ @ip = Resolv.getaddress(@hostname)
35
+ end
36
+ @io ||= build_socket
37
+ end
38
+
39
+ def scheme
40
+ "http"
41
+ end
42
+
43
+ def to_io
44
+ @io.to_io
45
+ end
46
+
47
+ def protocol
48
+ @fallback_protocol
49
+ end
50
+
51
+ def connect
52
+ return unless closed?
53
+ begin
54
+ if @io.closed?
55
+ transition(:idle)
56
+ @io = build_socket
57
+ end
58
+ @io.connect_nonblock(Socket.sockaddr_in(@port, @ip))
59
+ rescue Errno::EISCONN
60
+ end
61
+ transition(:connected)
62
+ rescue Errno::EINPROGRESS,
63
+ Errno::EALREADY,
64
+ ::IO::WaitReadable
65
+ end
66
+
67
+ if RUBY_VERSION < "2.3"
68
+ def read(size, buffer)
69
+ @io.read_nonblock(size, buffer)
70
+ buffer.bytesize
71
+ rescue ::IO::WaitReadable
72
+ 0
73
+ rescue EOFError
74
+ nil
75
+ end
76
+
77
+ def write(buffer)
78
+ siz = @io.write_nonblock(buffer)
79
+ buffer.slice!(0, siz)
80
+ siz
81
+ rescue ::IO::WaitWritable
82
+ 0
83
+ rescue EOFError
84
+ nil
85
+ end
86
+ else
87
+ def read(size, buffer)
88
+ ret = @io.read_nonblock(size, buffer, exception: false)
89
+ return 0 if ret == :wait_readable
90
+ return if ret.nil?
91
+ buffer.bytesize
92
+ end
93
+
94
+ def write(buffer)
95
+ siz = @io.write_nonblock(buffer, exception: false)
96
+ return 0 if siz == :wait_writable
97
+ return if siz.nil?
98
+ buffer.slice!(0, siz)
99
+ siz
100
+ end
101
+ end
102
+
103
+ def close
104
+ return if @keep_open || closed?
105
+ begin
106
+ @io.close
107
+ ensure
108
+ transition(:closed)
109
+ end
110
+ end
111
+
112
+ def connected?
113
+ @state == :connected
114
+ end
115
+
116
+ def closed?
117
+ @state == :idle || @state == :closed
118
+ end
119
+
120
+ def inspect
121
+ id = @io.closed? ? "closed" : @io.fileno
122
+ "#<TCP(fd: #{id}): #{@ip}:#{@port} (state: #{@state})>"
123
+ end
124
+
125
+ private
126
+
127
+ def build_socket
128
+ addr = IPAddr.new(@ip)
129
+ Socket.new(addr.family, :STREAM, 0)
130
+ end
131
+
132
+ def transition(nextstate)
133
+ case nextstate
134
+ # when :idle
135
+ when :connected
136
+ return unless @state == :idle
137
+ when :closed
138
+ return unless @state == :connected
139
+ end
140
+ do_transition(nextstate)
141
+ end
142
+
143
+ def do_transition(nextstate)
144
+ log(level: 1, label: "#{inspect}: ") { nextstate.to_s }
145
+ @state = nextstate
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,56 @@
1
+ require "forwardable"
2
+
3
+ module HTTPX
4
+ class UNIX < TCP
5
+ extend Forwardable
6
+
7
+ def_delegator :@uri, :port, :scheme
8
+
9
+ def initialize(uri, options)
10
+ @uri = uri
11
+ @state = :idle
12
+ @options = Options.new(options)
13
+ @path = @options.transport_options[:path]
14
+ @fallback_protocol = @options.fallback_protocol
15
+ if @options.io
16
+ @io = case @options.io
17
+ when Hash
18
+ @options.io[@path]
19
+ else
20
+ @options.io
21
+ end
22
+ unless @io.nil?
23
+ @keep_open = true
24
+ @state = :connected
25
+ end
26
+ end
27
+ @io ||= build_socket
28
+ end
29
+
30
+ def hostname
31
+ @uri.host
32
+ end
33
+
34
+ def connect
35
+ return unless closed?
36
+ begin
37
+ if @io.closed?
38
+ transition(:idle)
39
+ @io = build_socket
40
+ end
41
+ @io.connect_nonblock(Socket.sockaddr_un(@path))
42
+ rescue Errno::EISCONN
43
+ end
44
+ transition(:connected)
45
+ rescue Errno::EINPROGRESS,
46
+ Errno::EALREADY,
47
+ ::IO::WaitReadable
48
+ end
49
+
50
+ private
51
+
52
+ def build_socket
53
+ Socket.new(Socket::PF_UNIX, :STREAM, 0)
54
+ end
55
+ end
56
+ end
@@ -3,7 +3,6 @@
3
3
  module HTTPX
4
4
  class Options
5
5
  MAX_CONCURRENT_REQUESTS = 100
6
- MAX_RETRIES = 3
7
6
  WINDOW_SIZE = 1 << 14 # 16K
8
7
  MAX_BODY_THRESHOLD_SIZE = (1 << 10) * 112 # 112K
9
8
 
@@ -47,7 +46,6 @@ module HTTPX
47
46
  :timeout => Timeout.new,
48
47
  :headers => {},
49
48
  :max_concurrent_requests => MAX_CONCURRENT_REQUESTS,
50
- :max_retries => MAX_RETRIES,
51
49
  :window_size => WINDOW_SIZE,
52
50
  :body_threshold_size => MAX_BODY_THRESHOLD_SIZE,
53
51
  :request_class => Class.new(Request),
@@ -55,6 +53,8 @@ module HTTPX
55
53
  :headers_class => Class.new(Headers),
56
54
  :request_body_class => Class.new(Request::Body),
57
55
  :response_body_class => Class.new(Response::Body),
56
+ :transport => nil,
57
+ :transport_options => nil,
58
58
  }
59
59
 
60
60
  defaults.merge!(options)
@@ -84,11 +84,17 @@ module HTTPX
84
84
  self.body_threshold_size = Integer(num)
85
85
  end
86
86
 
87
+ def_option(:transport) do |tr|
88
+ transport = tr.to_s
89
+ raise Error, "#{transport} is an unsupported transport type" unless IO.registry.keys.include?(transport)
90
+ self.transport = transport
91
+ end
92
+
87
93
  %w[
88
94
  params form json body
89
- follow ssl http2_settings max_retries
95
+ follow ssl http2_settings
90
96
  request_class response_class headers_class request_body_class response_body_class
91
- io fallback_protocol debug debug_level
97
+ io fallback_protocol debug debug_level transport_options
92
98
  ].each do |method_name|
93
99
  def_option(method_name)
94
100
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
+ InsecureRedirectError = Class.new(Error)
4
5
  module Plugins
5
6
  module FollowRedirects
6
7
  module InstanceMethods
@@ -48,9 +49,24 @@ module HTTPX
48
49
 
49
50
  private
50
51
 
52
+ def fetch_response(request)
53
+ response = super
54
+ if response &&
55
+ REDIRECT_STATUS.include?(response.status) &&
56
+ !@options.follow_insecure_redirects
57
+ redirect_uri = __get_location_from_response(response)
58
+ if response.uri.scheme == "https" &&
59
+ redirect_uri.scheme == "http"
60
+ error = InsecureRedirectError.new(redirect_uri.to_s)
61
+ error.set_backtrace(caller)
62
+ response = ErrorResponse.new(error, @options)
63
+ end
64
+ end
65
+ response
66
+ end
67
+
51
68
  def __build_redirect_req(request, response, options)
52
- redirect_uri = URI(response.headers["location"])
53
- redirect_uri = response.uri.merge(redirect_uri) if redirect_uri.relative?
69
+ redirect_uri = __get_location_from_response(response)
54
70
 
55
71
  # TODO: integrate cookies in the next request
56
72
  # redirects are **ALWAYS** GET
@@ -58,12 +74,24 @@ module HTTPX
58
74
  body: request.body)
59
75
  __build_req(:get, redirect_uri, retry_options)
60
76
  end
77
+
78
+ def __get_location_from_response(response)
79
+ location_uri = URI(response.headers["location"])
80
+ location_uri = response.uri.merge(location_uri) if location_uri.relative?
81
+ location_uri
82
+ end
61
83
  end
62
84
 
63
85
  module OptionsMethods
64
86
  def self.included(klass)
65
87
  super
66
- klass.def_option(:max_redirects)
88
+ klass.def_option(:max_redirects) do |num|
89
+ num = Integer(num)
90
+ raise Error, ":max_redirects must be positive" unless num.positive?
91
+ num
92
+ end
93
+
94
+ klass.def_option(:follow_insecure_redirects)
67
95
  end
68
96
  end
69
97
  end
@@ -34,10 +34,15 @@ module HTTPX
34
34
  private
35
35
 
36
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 }
37
+ @_proxy_uris ||= begin
38
+ uris = @options.proxy ? Array(@options.proxy[:uri]) : []
39
+ if uris.empty?
40
+ uri = URI(uri).find_proxy
41
+ uris << uri if uri
42
+ end
43
+ uris
44
+ end
45
+ @options.proxy.merge(uri: @_proxy_uris.shift) unless @_proxy_uris.empty?
41
46
  end
42
47
 
43
48
  def find_channel(request, **options)
@@ -55,12 +60,27 @@ module HTTPX
55
60
  parameters = Parameters.new(**proxy)
56
61
  uri = parameters.uri
57
62
  log { "proxy: #{uri}" }
58
- io = TCP.new(uri.host, uri.port, @options)
63
+ io = TCP.new(uri, @options)
59
64
  proxy_type = Parameters.registry(parameters.uri.scheme)
60
65
  channel = proxy_type.new(io, parameters, @options.merge(options), &method(:on_response))
61
66
  @connection.__send__(:register_channel, channel)
62
67
  channel
63
68
  end
69
+
70
+ def fetch_response(request)
71
+ response = super
72
+ if response.is_a?(ErrorResponse) &&
73
+ # either it was a timeout error connecting, or it was a proxy error
74
+ (((response.error.is_a?(TimeoutError) || response.error.is_a?(IOError)) && request.state == :idle) ||
75
+ response.error.is_a?(Error)) &&
76
+ !@_proxy_uris.empty?
77
+ log { "failed connecting to proxy, trying next..." }
78
+ channel = find_channel(request)
79
+ channel.send(request)
80
+ return
81
+ end
82
+ response
83
+ end
64
84
  end
65
85
 
66
86
  module OptionsMethods
@@ -91,6 +111,10 @@ module HTTPX
91
111
  true
92
112
  end
93
113
 
114
+ def send(request, **args)
115
+ @pending << [request, args]
116
+ end
117
+
94
118
  def to_io
95
119
  case @state
96
120
  when :idle
@@ -108,12 +132,19 @@ module HTTPX
108
132
  consume
109
133
  end
110
134
  end
135
+
136
+ def reset
137
+ @state = :open
138
+ transition(:closing)
139
+ transition(:closed)
140
+ emit(:close)
141
+ end
111
142
  end
112
143
 
113
144
  class ProxySSL < SSL
114
145
  def initialize(tcp, request_uri, options)
115
146
  @io = tcp.to_io
116
- super(tcp.ip, tcp.port, options)
147
+ super(tcp, options)
117
148
  @hostname = request_uri.host
118
149
  @state = :connected
119
150
  end
@@ -30,11 +30,7 @@ module HTTPX
30
30
  transition(:connected)
31
31
  throw(:called)
32
32
  else
33
- response = ErrorResponse.new(Error.new("socks error: #{status}"), 0, @options)
34
- until @pending.empty?
35
- req, _ = @pending.shift
36
- emit(:response, req, response)
37
- end
33
+ on_socks_error("socks error: #{status}")
38
34
  end
39
35
  end
40
36
 
@@ -53,9 +49,16 @@ module HTTPX
53
49
  return unless @state == :connecting
54
50
  @parser = nil
55
51
  end
56
- log(level: 1, label: "SOCKS4: ") { "#{nextstate}: #{@write_buffer.to_s.inspect}" }
52
+ log(level: 1, label: "SOCKS4: ") { "#{nextstate}: #{@write_buffer.to_s.inspect}" } unless nextstate == :open
57
53
  super
58
54
  end
55
+
56
+ def on_socks_error(message)
57
+ ex = Error.new(message)
58
+ ex.set_backtrace(caller)
59
+ on_error(ex)
60
+ throw(:called)
61
+ end
59
62
  end
60
63
  Parameters.register("socks4", Socks4ProxyChannel)
61
64
  Parameters.register("socks4a", Socks4ProxyChannel)
@@ -45,7 +45,7 @@ module HTTPX
45
45
  transition(:authenticating)
46
46
  return
47
47
  when NONE
48
- on_error_response("no supported authorization methods")
48
+ on_socks_error("no supported authorization methods")
49
49
  else
50
50
  transition(:negotiating)
51
51
  end
@@ -53,11 +53,11 @@ module HTTPX
53
53
  version, status = packet.unpack("CC")
54
54
  check_version(version)
55
55
  return transition(:negotiating) if status == SUCCESS
56
- on_error_response("socks authentication error: #{status}")
56
+ on_socks_error("socks authentication error: #{status}")
57
57
  when :negotiating
58
58
  version, reply, = packet.unpack("CC")
59
59
  check_version(version)
60
- return on_error_response("socks5 negotiation error: #{reply}") unless reply == SUCCESS
60
+ on_socks_error("socks5 negotiation error: #{reply}") unless reply == SUCCESS
61
61
  req, _ = @pending.first
62
62
  request_uri = req.uri
63
63
  @io = ProxySSL.new(@io, request_uri, @options) if request_uri.scheme == "https"
@@ -86,22 +86,22 @@ module HTTPX
86
86
  return unless @state == :negotiating
87
87
  @parser = nil
88
88
  end
89
- log(level: 1, label: "SOCKS5: ") { "#{nextstate}: #{@write_buffer.to_s.inspect}" }
89
+ log(level: 1, label: "SOCKS5: ") { "#{nextstate}: #{@write_buffer.to_s.inspect}" } unless nextstate == :open
90
90
  super
91
91
  end
92
92
 
93
93
  def check_version(version)
94
- raise Error, "invalid SOCKS version (#{version})" if version != 5
94
+ on_socks_error("invalid SOCKS version (#{version})") if version != 5
95
95
  end
96
96
 
97
- def on_error_response(error)
98
- response = ErrorResponse.new(Error.new(error), 0, @options)
99
- until @pending.empty?
100
- req, _ = @pending.shift
101
- emit(:response, req, response)
102
- end
97
+ def on_socks_error(message)
98
+ ex = Error.new(message)
99
+ ex.set_backtrace(caller)
100
+ on_error(ex)
101
+ throw(:called)
103
102
  end
104
103
  end
104
+
105
105
  Parameters.register("socks5", Socks5ProxyChannel)
106
106
 
107
107
  class SocksParser
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ module Retries
6
+ MAX_RETRIES = 3
7
+ IDEMPOTENT_METHODS = %i[get options head put delete].freeze
8
+
9
+ module InstanceMethods
10
+ def max_retries(n)
11
+ branch(default_options.with_max_retries(n.to_i))
12
+ end
13
+
14
+ private
15
+
16
+ def fetch_response(request)
17
+ response = super
18
+ if response.is_a?(ErrorResponse) &&
19
+ request.retries.positive? &&
20
+ IDEMPOTENT_METHODS.include?(request.verb)
21
+ request.retries -= 1
22
+ channel = find_channel(request)
23
+ channel.send(request)
24
+ return
25
+ end
26
+ response
27
+ end
28
+ end
29
+
30
+ module RequestMethods
31
+ attr_accessor :retries
32
+
33
+ def initialize(*args)
34
+ super
35
+ @retries = @options.max_retries || MAX_RETRIES
36
+ end
37
+ end
38
+
39
+ module OptionsMethods
40
+ def self.included(klass)
41
+ super
42
+ klass.def_option(:max_retries) do |num|
43
+ num = Integer(num)
44
+ raise Error, ":max_retries must be positive" unless num.positive?
45
+ num
46
+ end
47
+ end
48
+ end
49
+ end
50
+ register_plugin :retries, Retries
51
+ end
52
+ end
@@ -43,7 +43,7 @@ module HTTPX
43
43
 
44
44
  def initialize(verb, uri, options = {})
45
45
  @verb = verb.to_s.downcase.to_sym
46
- @uri = URI(uri)
46
+ @uri = URI(URI.escape(uri.to_s))
47
47
  @options = Options.new(options)
48
48
 
49
49
  raise(Error, "unknown method: #{verb}") unless METHODS.include?(@verb)
@@ -65,7 +65,7 @@ module HTTPX
65
65
  end
66
66
 
67
67
  def path
68
- path = uri.path
68
+ path = uri.path.dup
69
69
  path << "/" if path.empty?
70
70
  path << "?#{query}" unless query.empty?
71
71
  path
@@ -216,14 +216,12 @@ module HTTPX
216
216
  class ErrorResponse
217
217
  include Loggable
218
218
 
219
- attr_reader :error, :retries
219
+ attr_reader :error
220
220
 
221
- def initialize(error, retries, options)
221
+ def initialize(error, options)
222
222
  @error = error
223
- @retries = retries
224
223
  @options = Options.new(options)
225
224
  log { "#{error.class}: #{error}" }
226
- log { caller.join("\n") }
227
225
  end
228
226
 
229
227
  def status
@@ -233,9 +231,5 @@ module HTTPX
233
231
  def raise_for_status
234
232
  raise @error
235
233
  end
236
-
237
- def retryable?
238
- @retries.positive?
239
- end
240
234
  end
241
235
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- VERSION = "0.0.5"
4
+ VERSION = "0.1.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: httpx
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-03 00:00:00.000000000 Z
11
+ date: 2018-07-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http-2
@@ -94,8 +94,9 @@ files:
94
94
  - lib/httpx/extensions.rb
95
95
  - lib/httpx/headers.rb
96
96
  - lib/httpx/io.rb
97
- - lib/httpx/io/resolver.rb
98
- - lib/httpx/io/udp.rb
97
+ - lib/httpx/io/ssl.rb
98
+ - lib/httpx/io/tcp.rb
99
+ - lib/httpx/io/unix.rb
99
100
  - lib/httpx/loggable.rb
100
101
  - lib/httpx/options.rb
101
102
  - lib/httpx/plugins/authentication.rb
@@ -113,6 +114,7 @@ files:
113
114
  - lib/httpx/plugins/proxy/socks4.rb
114
115
  - lib/httpx/plugins/proxy/socks5.rb
115
116
  - lib/httpx/plugins/push_promise.rb
117
+ - lib/httpx/plugins/retries.rb
116
118
  - lib/httpx/plugins/stream.rb
117
119
  - lib/httpx/registry.rb
118
120
  - lib/httpx/request.rb
@@ -145,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
145
147
  version: '0'
146
148
  requirements: []
147
149
  rubyforge_project:
148
- rubygems_version: 2.6.14.1
150
+ rubygems_version: 2.7.6
149
151
  signing_key:
150
152
  specification_version: 4
151
153
  summary: HTTPX, to the future, and beyond
@@ -1,135 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "forwardable"
4
- require "ipaddr"
5
- require "resolv"
6
-
7
- module HTTPX
8
- class Resolver
9
- include Loggable
10
- extend Forwardable
11
- # Maximum UDP packet we'll accept
12
- MAX_PACKET_SIZE = 512
13
- DNS_PORT = 53
14
-
15
- @mutex = Mutex.new
16
- @identifier = 1
17
-
18
- def self.generate_id
19
- @mutex.synchronize { @identifier = (@identifier + 1) & 0xFFFF }
20
- end
21
-
22
- def self.nameservers
23
- Resolv::DNS::Config.default_config_hash[:nameserver]
24
- end
25
-
26
- def_delegator :@io, :closed?
27
- def initialize(options)
28
- @options = Options.new(options)
29
- # early return for edge case when there are no nameservers configured
30
- # but we still want to be able to static lookups using #resolve_hostname
31
- (@nameservers = self.class.nameservers) || return
32
- server = IPAddr.new(@nameservers.sample)
33
- @io = UDP.new(server, DNS_PORT)
34
- @read_buffer = "".b
35
- @addresses = {}
36
- @hostnames = []
37
- @callbacks = []
38
- @state = :idle
39
- end
40
-
41
- def to_io
42
- @io.to_io
43
- end
44
-
45
- def resolve(hostname, &action)
46
- if host = resolve_hostname(hostname)
47
- unless ip_address = resolve_host(host)
48
- raise Resolv::ResolvError, "invalid entry in hosts file: #{host}"
49
- end
50
- @addresses[hostname] = ip_address
51
- action.call(ip_address)
52
- end
53
- @hostnames << hostname
54
- @callbacks << action
55
- query = build_query(hostname).encode
56
- log { "resolving #{hostname}: #{query.inspect}" }
57
- siz = @io.write(query)
58
- log { "WRITE: #{siz} bytes..." }
59
- end
60
-
61
- def call
62
- return if @state == :closed
63
- return if @hostnames.empty?
64
- dread
65
- end
66
-
67
- def dread(wsize = MAX_PACKET_SIZE)
68
- loop do
69
- siz = @io.read(wsize, @read_buffer)
70
- throw(:close, self) unless siz
71
- return if siz.zero?
72
- log { "READ: #{siz} bytes..." }
73
- addrs = parse(@read_buffer)
74
- @read_buffer.clear
75
- next if addrs.empty?
76
-
77
- hostname = @hostnames.shift
78
- callback = @callbacks.shift
79
- addr = addrs.index(addrs.rand(addrs.size))
80
- log { "resolved #{hostname}: #{addr}" }
81
- @addresses[hostname] = addr
82
- callback.call(addr)
83
- end
84
- end
85
-
86
- private
87
-
88
- def parse(frame)
89
- response = Resolv::DNS::Message.decode(frame)
90
-
91
- addrs = []
92
- # The answer might include IN::CNAME entries so filters them out
93
- # to include IN::A & IN::AAAA entries only.
94
- response.each_answer { |_name, _ttl, value| addrs << value.address if value.respond_to?(:address) }
95
-
96
- addrs
97
- end
98
-
99
- def resolve_hostname(hostname)
100
- # Resolv::Hosts#getaddresses pushes onto a stack
101
- # so since we want the first occurance, simply
102
- # pop off the stack.
103
- resolv.getaddresses(hostname).pop
104
- rescue StandardError
105
- end
106
-
107
- def resolv
108
- @resolv ||= Resolv::Hosts.new
109
- end
110
-
111
- def build_query(hostname)
112
- Resolv::DNS::Message.new.tap do |query|
113
- query.id = self.class.generate_id
114
- query.rd = 1
115
- query.add_question hostname, Resolv::DNS::Resource::IN::A
116
- end
117
- end
118
-
119
- def resolve_host(host)
120
- resolve_ip(Resolv::IPv4, host) || get_address(host) || resolve_ip(Resolv::IPv6, host)
121
- end
122
-
123
- def resolve_ip(klass, host)
124
- klass.create(host)
125
- rescue ArgumentError
126
- end
127
-
128
- private
129
-
130
- def get_address(host)
131
- Resolv::Hosts.new(host).getaddress
132
- rescue StandardError
133
- end
134
- end
135
- end
@@ -1,65 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "socket"
4
- require "ipaddr"
5
-
6
- module HTTPX
7
- class UDP
8
- include Loggable
9
-
10
- attr_reader :ip, :port
11
-
12
- def initialize(ip, port)
13
- @ip = ip.to_s
14
- @port = port
15
- @io = UDPSocket.new(ip.family)
16
- @closed = false
17
- end
18
-
19
- def to_io
20
- @io.to_io
21
- end
22
-
23
- if RUBY_VERSION < "2.3"
24
- def read(size, buffer)
25
- data, _ = @io.recvfrom_nonblock(size)
26
- buffer.replace(data)
27
- buffer.bytesize
28
- rescue ::IO::WaitReadable
29
- 0
30
- rescue EOFError
31
- nil
32
- end
33
- else
34
- def read(size, buffer)
35
- @io.recvfrom_nonblock(size, 0, buffer, exception: false)
36
- buffer.bytesize
37
- rescue ::IO::WaitReadable
38
- 0
39
- rescue EOFError
40
- nil
41
- end
42
- end
43
-
44
- def write(buffer)
45
- siz = @io.send(buffer, 0, @ip, @port)
46
- buffer.slice!(0, siz)
47
- siz
48
- end
49
-
50
- def close
51
- return if @closed
52
- @io.close
53
- ensure
54
- @closed = true
55
- end
56
-
57
- def closed?
58
- @closed
59
- end
60
-
61
- def inspect
62
- "#<(fd: #{@io.fileno}): #{@ip}:#{@port})>"
63
- end
64
- end
65
- end