httpx 0.0.1 → 0.0.2

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
- SHA256:
3
- metadata.gz: 76ac74f383584bdbf20b60f5a969ceb2827379fb9bd5d58bda52474b4ce22cdb
4
- data.tar.gz: 045ae543497d77f74dfc7aea2e317fa5521b3b9bc31d4ca0733298d1555e773c
2
+ SHA1:
3
+ metadata.gz: 9f092c95882dd48ff10a25b77c08b7dec71acc31
4
+ data.tar.gz: fc75a3d72c8151c28b2f24ea4617d64e92b56488
5
5
  SHA512:
6
- metadata.gz: f3986c2e0e756b2e41904f33eb3116067557c3eb82852d1d2c222a0d2afdf4281766a331216e3d95e69113cffbe812974b18793b90344dfc97e942d8b6a2fc33
7
- data.tar.gz: 97f6ee0db636710c51850d359e82302a49a33c7f05d7584492e7a43e33fb7b04d7fc34e9e8209b4b763044362ef75326aa8b482140e9beda2113748b60a8e729
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.
@@ -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
- ip = begin
72
- TCPSocket.getaddress(uri.host)
73
- rescue StandardError
74
- uri.host
75
- end
72
+ ips = begin
73
+ Resolv.getaddresses(uri.host)
74
+ rescue StandardError
75
+ [uri.host]
76
+ end
76
77
 
77
- ip == @io.ip &&
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(hard = false)
102
- pr = @parser
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
- throw(:close, self) unless siz
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
- throw(:close, self) unless siz
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(:closed)
197
- emit(:close)
192
+ transition(:closing)
198
193
  end
199
194
  parser
200
195
  end
@@ -151,7 +151,7 @@ module HTTPX
151
151
  # 1 keep alive request.
152
152
  @max_concurrent_requests = 1
153
153
  end
154
- emit(:complete)
154
+ emit(:close)
155
155
  end
156
156
 
157
157
  private
@@ -170,7 +170,7 @@ module HTTPX
170
170
 
171
171
  def on_close(*)
172
172
  return unless @connection.state == :closed && @connection.active_stream_count.zero?
173
- emit(:complete)
173
+ emit(:close)
174
174
  end
175
175
 
176
176
  def on_frame_sent(frame)
@@ -112,7 +112,7 @@ module HTTPX
112
112
  break
113
113
  end
114
114
  end
115
- requests.size == 1 ? responses.first : responses
115
+ responses
116
116
  end
117
117
 
118
118
  def __build_req(verb, uri, options = {})
@@ -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
- consume(channel)
22
+ channel.call
23
23
  end
24
24
  monitor.interests = channel.interests
25
25
  end
26
26
  end
27
27
 
28
- def close(channel = nil)
29
- if channel
30
- channel.close
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
@@ -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 = TCPSocket.getaddress(hostname)
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 = TCPSocket.getaddress(hostname)
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
@@ -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(:complete) { throw(:close, self) }
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(:complete) { throw(:close, self) }
48
+ @parser.on(:close) { transition(:closing) }
49
49
  end
50
50
  end
51
51
  super
@@ -48,10 +48,6 @@ class HTTPX::Selector
48
48
  @closed = false
49
49
  end
50
50
 
51
- def empty?
52
- @readers.empty? && @writers.empty?
53
- end
54
-
55
51
  # deregisters +io+ from selectables.
56
52
  def deregister(io)
57
53
  @lock.synchronize do
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.2"
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.1
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-02-24 00:00:00.000000000 Z
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.7.6
148
+ rubygems_version: 2.6.14
147
149
  signing_key:
148
150
  specification_version: 4
149
151
  summary: HTTPX, to the future, and beyond