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 +5 -5
- data/README.md +1 -1
- data/lib/httpx/channel.rb +17 -12
- data/lib/httpx/channel/http1.rb +1 -1
- data/lib/httpx/client.rb +1 -7
- data/lib/httpx/connection.rb +4 -1
- data/lib/httpx/io.rb +4 -244
- data/lib/httpx/io/ssl.rb +107 -0
- data/lib/httpx/io/tcp.rb +148 -0
- data/lib/httpx/io/unix.rb +56 -0
- data/lib/httpx/options.rb +10 -4
- data/lib/httpx/plugins/follow_redirects.rb +31 -3
- data/lib/httpx/plugins/proxy.rb +37 -6
- data/lib/httpx/plugins/proxy/socks4.rb +9 -6
- data/lib/httpx/plugins/proxy/socks5.rb +11 -11
- data/lib/httpx/plugins/retries.rb +52 -0
- data/lib/httpx/request.rb +2 -2
- data/lib/httpx/response.rb +2 -8
- data/lib/httpx/version.rb +1 -1
- metadata +7 -5
- data/lib/httpx/io/resolver.rb +0 -135
- data/lib/httpx/io/udp.rb +0 -65
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 34654a12cf778aeffd39429a869bbfb62dc487369ed50fdfc8b14c512e141c91
|
4
|
+
data.tar.gz: 055b3f79a33ecf43982264f2af5d61631d290aab74c7b63d30fc82501ba366c3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b9cc0fb05f09c33d2b18f7aa3b46313240744ecb0a8474ffb5345afc2e533768b987609eabd8f1461c00f64815610dcd83e68f340933ce1695ee3efcbc6653a2
|
7
|
+
data.tar.gz: 21bc5f92e8c8c639a2b0f0885d7652805a099f44a89900da8336993c1e033359245666204d2adccc9fbfb18e49c6dd883fc09ffac5c56b194df056a8cda4b0de
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# HTTPX: A Ruby HTTP
|
1
|
+
# HTTPX: A Ruby HTTP library for tomorrow... and beyond!
|
2
2
|
|
3
3
|
[](https://gitlab.com/honeyryderchuck/httpx/commits/master)
|
4
4
|
[](https://honeyryderchuck.gitlab.io/httpx/coverage/#_AllFiles)
|
data/lib/httpx/channel.rb
CHANGED
@@ -42,14 +42,15 @@ module HTTPX
|
|
42
42
|
|
43
43
|
class << self
|
44
44
|
def by(uri, options)
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
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
|
-
|
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,
|
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,
|
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
|
data/lib/httpx/channel/http1.rb
CHANGED
data/lib/httpx/client.rb
CHANGED
@@ -51,13 +51,7 @@ module HTTPX
|
|
51
51
|
end
|
52
52
|
|
53
53
|
def fetch_response(request)
|
54
|
-
|
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)
|
data/lib/httpx/connection.rb
CHANGED
data/lib/httpx/io.rb
CHANGED
@@ -1,255 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "resolv"
|
4
3
|
require "socket"
|
5
|
-
require "
|
6
|
-
require "
|
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
|
data/lib/httpx/io/ssl.rb
ADDED
@@ -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
|
data/lib/httpx/io/tcp.rb
ADDED
@@ -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
|
data/lib/httpx/options.rb
CHANGED
@@ -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
|
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 =
|
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
|
data/lib/httpx/plugins/proxy.rb
CHANGED
@@ -34,10 +34,15 @@ module HTTPX
|
|
34
34
|
private
|
35
35
|
|
36
36
|
def proxy_params(uri)
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
94
|
+
on_socks_error("invalid SOCKS version (#{version})") if version != 5
|
95
95
|
end
|
96
96
|
|
97
|
-
def
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
data/lib/httpx/request.rb
CHANGED
@@ -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
|
data/lib/httpx/response.rb
CHANGED
@@ -216,14 +216,12 @@ module HTTPX
|
|
216
216
|
class ErrorResponse
|
217
217
|
include Loggable
|
218
218
|
|
219
|
-
attr_reader :error
|
219
|
+
attr_reader :error
|
220
220
|
|
221
|
-
def initialize(error,
|
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
|
data/lib/httpx/version.rb
CHANGED
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
|
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
|
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/
|
98
|
-
- lib/httpx/io/
|
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
|
150
|
+
rubygems_version: 2.7.6
|
149
151
|
signing_key:
|
150
152
|
specification_version: 4
|
151
153
|
summary: HTTPX, to the future, and beyond
|
data/lib/httpx/io/resolver.rb
DELETED
@@ -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
|
data/lib/httpx/io/udp.rb
DELETED
@@ -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
|