httpx 0.0.1 → 0.0.2
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 +4 -0
- data/lib/httpx/channel.rb +18 -23
- data/lib/httpx/channel/http1.rb +1 -1
- data/lib/httpx/channel/http2.rb +1 -1
- data/lib/httpx/client.rb +1 -1
- data/lib/httpx/connection.rb +4 -13
- data/lib/httpx/io.rb +4 -2
- data/lib/httpx/io/resolver.rb +135 -0
- data/lib/httpx/io/udp.rb +65 -0
- data/lib/httpx/plugins/proxy/http.rb +2 -2
- data/lib/httpx/selector.rb +0 -4
- data/lib/httpx/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9f092c95882dd48ff10a25b77c08b7dec71acc31
|
4
|
+
data.tar.gz: fc75a3d72c8151c28b2f24ea4617d64e92b56488
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1cb99410118ac67eb35bbaf212fda20e95745a77fa026b1ce6712e742ca985728b88e087fd397fd38310496f20fd15cd039827a104220b479a4c471e645b8ea8
|
7
|
+
data.tar.gz: b593dfe0b0b433d1d12fc8695581158ddcb2a3558d693df07f821efd160cdb61beac1a00fcc78da10fdda608f03464f3cb1bd3e6d32386be318cb5fea44dbd5e
|
data/README.md
CHANGED
@@ -90,6 +90,10 @@ It means that it loads the bare minimum to perform requests, and the user has to
|
|
90
90
|
|
91
91
|
It also means that it ships with the minimum amount of dependencies.
|
92
92
|
|
93
|
+
## Easy to test
|
94
|
+
|
95
|
+
The test suite runs against [httpbin proxied over nghttp2](https://nghttp2.org/httpbin/), so there is no mocking/stubbing going on. The test suite uses [minitest](https://github.com/seattlerb/minitest), but its matchers usage is limit to assert (assert is all you need).
|
96
|
+
|
93
97
|
## Supported Rubies
|
94
98
|
|
95
99
|
All Rubies greater or equal to 2.1, and always latest JRuby.
|
data/lib/httpx/channel.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "resolv"
|
3
4
|
require "forwardable"
|
4
5
|
require "httpx/io"
|
5
6
|
require "httpx/buffer"
|
@@ -68,13 +69,13 @@ module HTTPX
|
|
68
69
|
end
|
69
70
|
|
70
71
|
def match?(uri)
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
72
|
+
ips = begin
|
73
|
+
Resolv.getaddresses(uri.host)
|
74
|
+
rescue StandardError
|
75
|
+
[uri.host]
|
76
|
+
end
|
76
77
|
|
77
|
-
|
78
|
+
ips.include?(@io.ip) &&
|
78
79
|
uri.port == @io.port &&
|
79
80
|
uri.scheme == @io.scheme
|
80
81
|
end
|
@@ -98,18 +99,9 @@ module HTTPX
|
|
98
99
|
@io.to_io
|
99
100
|
end
|
100
101
|
|
101
|
-
def close
|
102
|
-
|
102
|
+
def close
|
103
|
+
@parser.close if @parser
|
103
104
|
transition(:closing)
|
104
|
-
if hard || (pr && pr.empty?)
|
105
|
-
pr.close
|
106
|
-
@parser = nil
|
107
|
-
else
|
108
|
-
transition(:idle)
|
109
|
-
@parser = pr
|
110
|
-
parser.reenqueue!
|
111
|
-
return
|
112
|
-
end
|
113
105
|
end
|
114
106
|
|
115
107
|
def reset
|
@@ -154,7 +146,10 @@ module HTTPX
|
|
154
146
|
def dread(wsize = @window_size)
|
155
147
|
loop do
|
156
148
|
siz = @io.read(wsize, @read_buffer)
|
157
|
-
|
149
|
+
unless siz
|
150
|
+
emit(:close)
|
151
|
+
return
|
152
|
+
end
|
158
153
|
return if siz.zero?
|
159
154
|
log { "READ: #{siz} bytes..." }
|
160
155
|
parser << @read_buffer.to_s
|
@@ -165,7 +160,10 @@ module HTTPX
|
|
165
160
|
loop do
|
166
161
|
return if @write_buffer.empty?
|
167
162
|
siz = @io.write(@write_buffer)
|
168
|
-
|
163
|
+
unless siz
|
164
|
+
emit(:close)
|
165
|
+
return
|
166
|
+
end
|
169
167
|
log { "WRITE: #{siz} bytes..." }
|
170
168
|
return if siz.zero?
|
171
169
|
end
|
@@ -190,11 +188,8 @@ module HTTPX
|
|
190
188
|
parser.on(:promise) do |*args|
|
191
189
|
emit(:promise, *args)
|
192
190
|
end
|
193
|
-
# parser.inherit_callbacks(self)
|
194
|
-
parser.on(:complete) { throw(:close, self) }
|
195
191
|
parser.on(:close) do
|
196
|
-
transition(:
|
197
|
-
emit(:close)
|
192
|
+
transition(:closing)
|
198
193
|
end
|
199
194
|
parser
|
200
195
|
end
|
data/lib/httpx/channel/http1.rb
CHANGED
data/lib/httpx/channel/http2.rb
CHANGED
data/lib/httpx/client.rb
CHANGED
data/lib/httpx/connection.rb
CHANGED
@@ -19,19 +19,15 @@ module HTTPX
|
|
19
19
|
def next_tick(timeout: @timeout.timeout)
|
20
20
|
@selector.select(timeout) do |monitor|
|
21
21
|
if (channel = monitor.value)
|
22
|
-
|
22
|
+
channel.call
|
23
23
|
end
|
24
24
|
monitor.interests = channel.interests
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
-
def close
|
29
|
-
|
30
|
-
|
31
|
-
else
|
32
|
-
@channels.each(&:close)
|
33
|
-
next_tick until @selector.empty?
|
34
|
-
end
|
28
|
+
def close
|
29
|
+
@channels.each(&:close)
|
30
|
+
next_tick until @channels.empty?
|
35
31
|
end
|
36
32
|
|
37
33
|
def reset
|
@@ -65,10 +61,5 @@ module HTTPX
|
|
65
61
|
end
|
66
62
|
@channels << channel
|
67
63
|
end
|
68
|
-
|
69
|
-
def consume(channel)
|
70
|
-
ch = catch(:close) { channel.call }
|
71
|
-
close(ch) if ch
|
72
|
-
end
|
73
64
|
end
|
74
65
|
end
|
data/lib/httpx/io.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "resolv"
|
3
4
|
require "socket"
|
4
5
|
require "openssl"
|
5
6
|
require "ipaddr"
|
@@ -12,13 +13,14 @@ module HTTPX
|
|
12
13
|
|
13
14
|
def initialize(hostname, port, options)
|
14
15
|
@state = :idle
|
16
|
+
@hostname = hostname
|
15
17
|
@options = Options.new(options)
|
16
18
|
@fallback_protocol = @options.fallback_protocol
|
17
19
|
@port = port
|
18
20
|
if @options.io
|
19
21
|
@io = case @options.io
|
20
22
|
when Hash
|
21
|
-
@ip =
|
23
|
+
@ip = Resolv.getaddress(@hostname)
|
22
24
|
@options.io[@ip] || @options.io["#{@ip}:#{@port}"]
|
23
25
|
else
|
24
26
|
@ip = hostname
|
@@ -29,7 +31,7 @@ module HTTPX
|
|
29
31
|
@state = :connected
|
30
32
|
end
|
31
33
|
else
|
32
|
-
@ip =
|
34
|
+
@ip = Resolv.getaddress(@hostname)
|
33
35
|
end
|
34
36
|
@io ||= build_socket
|
35
37
|
end
|
@@ -0,0 +1,135 @@
|
|
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 or 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.to_s}"}
|
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
|
+
return 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
|
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
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
data/lib/httpx/io/udp.rb
ADDED
@@ -0,0 +1,65 @@
|
|
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
|
@@ -34,7 +34,7 @@ module HTTPX
|
|
34
34
|
return if @io.closed?
|
35
35
|
@parser = ConnectProxyParser.new(@write_buffer, @options.merge(max_concurrent_requests: 1))
|
36
36
|
@parser.once(:response, &method(:on_connect))
|
37
|
-
@parser.on(:
|
37
|
+
@parser.on(:close) { transition(:closing) }
|
38
38
|
proxy_connect
|
39
39
|
return if @state == :open
|
40
40
|
when :open
|
@@ -45,7 +45,7 @@ module HTTPX
|
|
45
45
|
when :idle
|
46
46
|
@parser = ProxyParser.new(@write_buffer, @options)
|
47
47
|
@parser.inherit_callbacks(self)
|
48
|
-
@parser.on(:
|
48
|
+
@parser.on(:close) { transition(:closing) }
|
49
49
|
end
|
50
50
|
end
|
51
51
|
super
|
data/lib/httpx/selector.rb
CHANGED
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.0.2
|
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-
|
11
|
+
date: 2018-03-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: http-2
|
@@ -94,6 +94,8 @@ 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
99
|
- lib/httpx/loggable.rb
|
98
100
|
- lib/httpx/options.rb
|
99
101
|
- lib/httpx/plugins/authentication.rb
|
@@ -143,7 +145,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
143
145
|
version: '0'
|
144
146
|
requirements: []
|
145
147
|
rubyforge_project:
|
146
|
-
rubygems_version: 2.
|
148
|
+
rubygems_version: 2.6.14
|
147
149
|
signing_key:
|
148
150
|
specification_version: 4
|
149
151
|
summary: HTTPX, to the future, and beyond
|