httpx 0.0.5 → 0.1.0

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