async-http 0.9.0 → 0.10.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
2
  SHA256:
3
- metadata.gz: 52cf6dc85fcbfa045a17959e27ba77d8cc3ebeb211074598501ebfeb560f3ac6
4
- data.tar.gz: 34a8c1a0bfa896da593b901cc31eb80002d74e3d4422440f6ad24073156b009c
3
+ metadata.gz: c74bbff1b6216d57e8d60e5a54700f9461cef5aff2436db4abbf07c300a1af09
4
+ data.tar.gz: 1354a61e69f69706688f112fef4c958af285bec43172f00d7993bff7fcb9dc7b
5
5
  SHA512:
6
- metadata.gz: 630b0308b56222426254806595c485f1fbe75b98f2e5a7b85c2d889176e28e04068054672e1b74a302680b15a972d1b3d46a002d03da10a66af5a937e5f63dcd
7
- data.tar.gz: 17fe537532ce17b0e3103f1f645dd8c900a42aad8d1bffcca0a8da5fc4476debb924eb5319fc19a50e7ee6b2db5f0517eda3cab6b79ef94bedcedc9d529a1509
6
+ metadata.gz: d62959c37bc323c82d57ca4e745ef4d2a715f3ec0e580e0d4a1e5cbdb65766f6b4c671efab4f8e85fb0fabe6a5cac99be1de1aa2cc92c30bc39e5e51f9f40d51
7
+ data.tar.gz: 75b83edd5bd767e638ebeacbd3446e317b24b49eba17628af95e32e78690cc0b26c8c998ed1aaed7248c8e796c7e77fd43a8c80f391fcc5876a8cd84533bdd1d
data/Gemfile CHANGED
@@ -4,9 +4,7 @@ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  group :development do
7
- gem 'trenni-sanitize'
8
-
9
- gem 'async-await'
7
+ gem 'pry'
10
8
  end
11
9
 
12
10
  group :test do
data/async-http.gemspec CHANGED
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.require_paths = ["lib"]
18
18
 
19
19
  spec.add_dependency("async", "~> 1.4")
20
- spec.add_dependency("async-io", "~> 1.5")
20
+ spec.add_dependency("async-io", "~> 1.6")
21
21
 
22
22
  spec.add_dependency("http-2", "~> 0.8")
23
23
  # spec.add_dependency("openssl")
@@ -25,14 +25,19 @@ require_relative 'protocol'
25
25
  module Async
26
26
  module HTTP
27
27
  class Client
28
- def initialize(endpoint, protocol = Protocol::HTTPS, **options)
28
+ def initialize(endpoint, protocol = nil, authority = nil, **options)
29
29
  @endpoint = endpoint
30
30
 
31
- @protocol = protocol
31
+ @protocol = protocol || endpoint.protocol
32
+ @authority = authority || endpoint.hostname
32
33
 
33
34
  @connections = connect(**options)
34
35
  end
35
36
 
37
+ attr :endpoint
38
+ attr :protocol
39
+ attr :authority
40
+
36
41
  def self.open(*args, &block)
37
42
  client = self.new(*args)
38
43
 
@@ -59,7 +64,7 @@ module Async
59
64
 
60
65
  def request(*args)
61
66
  @connections.acquire do |connection|
62
- connection.send_request(*args)
67
+ connection.send_request(@authority, *args)
63
68
  end
64
69
  end
65
70
 
@@ -0,0 +1,85 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Async
22
+ module HTTP
23
+ class Headers
24
+ def initialize
25
+ @hash = {}
26
+ end
27
+
28
+ def freeze
29
+ return unless frozen?
30
+
31
+ @hash.freeze
32
+
33
+ super
34
+ end
35
+
36
+ def inspect
37
+ @hash.inspect
38
+ end
39
+
40
+ def []= key, value
41
+ @hash[symbolize(key)] = value
42
+ end
43
+
44
+ def [] key
45
+ @hash[key]
46
+ end
47
+
48
+ def == other
49
+ @hash == other.to_hash
50
+ end
51
+
52
+ def delete(key)
53
+ @hash.delete(key)
54
+ end
55
+
56
+ def each
57
+ return to_enum unless block_given?
58
+
59
+ @hash.each do |key, value|
60
+ yield stringify(key), value
61
+ end
62
+ end
63
+
64
+ def symbolize(value)
65
+ Headers[value]
66
+ end
67
+
68
+ def stringify(key)
69
+ key.to_s.tr('_', '-')
70
+ end
71
+
72
+ def to_hash
73
+ @hash
74
+ end
75
+
76
+ def to_http_hash
77
+ Hash[@hash.map{|key, value| ["HTTP_#{key.to_s.upcase}", value]}]
78
+ end
79
+
80
+ def self.[] value
81
+ value.downcase.tr('-', '_').to_sym
82
+ end
83
+ end
84
+ end
85
+ end
@@ -112,8 +112,10 @@ module Async
112
112
  end
113
113
  end
114
114
 
115
- Async.logger.debug(self) {"No available resources, allocating new one..."}
116
- return create_resource
115
+ if !@limit or @available.count < @limit
116
+ Async.logger.debug(self) {"No available resources, allocating new one..."}
117
+ return create_resource
118
+ end
117
119
  end
118
120
  end
119
121
  end
@@ -35,7 +35,7 @@ module Async
35
35
  end
36
36
 
37
37
  def keep_alive?(headers)
38
- headers[HTTP_CONNECTION] == KEEP_ALIVE
38
+ headers[:connection] == KEEP_ALIVE
39
39
  end
40
40
 
41
41
  # Server loop.
@@ -64,7 +64,7 @@ module Async
64
64
  end
65
65
 
66
66
  def read_body(headers)
67
- if content_length = headers[HTTP_CONTENT_LENGTH]
67
+ if content_length = headers[:content_length]
68
68
  return @stream.read(Integer(content_length))
69
69
  # elsif !keep_alive?(headers)
70
70
  # return @stream.read
@@ -22,14 +22,15 @@ require 'async/io/protocol/line'
22
22
 
23
23
  require_relative 'request'
24
24
  require_relative 'response'
25
+ require_relative '../headers'
25
26
 
26
27
  module Async
27
28
  module HTTP
28
29
  module Protocol
29
30
  # Implements basic HTTP/1.1 request/response.
30
31
  class HTTP11 < Async::IO::Protocol::Line
31
- HTTP_CONTENT_LENGTH = 'HTTP_CONTENT_LENGTH'.freeze
32
- HTTP_TRANSFER_ENCODING = 'HTTP_TRANSFER_ENCODING'.freeze
32
+ CONTENT_LENGTH = Headers['Content-Length']
33
+ TRANSFER_ENCODING = Headers['Transfer-Encoding']
33
34
 
34
35
  CRLF = "\r\n".freeze
35
36
 
@@ -53,7 +54,6 @@ module Async
53
54
  alias client new
54
55
  end
55
56
 
56
- HTTP_CONNECTION = 'HTTP_CONNECTION'.freeze
57
57
  KEEP_ALIVE = 'keep-alive'.freeze
58
58
  CLOSE = 'close'.freeze
59
59
 
@@ -64,7 +64,7 @@ module Async
64
64
  end
65
65
 
66
66
  def keep_alive?(headers)
67
- headers[HTTP_CONNECTION] != CLOSE
67
+ headers[:connection] != CLOSE
68
68
  end
69
69
 
70
70
  # Server loop.
@@ -88,16 +88,20 @@ module Async
88
88
  end
89
89
 
90
90
  # Client request.
91
- def send_request(method, path, headers = {}, body = [])
92
- write_request(method, path, version, headers, body)
91
+ def send_request(authority, method, path, headers = {}, body = [])
92
+ Async.logger.debug(self) {"#{method} #{path} #{headers.inspect}"}
93
+
94
+ write_request(authority, method, path, version, headers, body)
93
95
 
94
96
  return Response.new(*read_response)
95
97
  rescue EOFError
96
98
  return nil
97
99
  end
98
100
 
99
- def write_request(method, path, version, headers, body)
101
+ def write_request(authority, method, path, version, headers, body)
100
102
  @stream.write("#{method} #{path} #{version}\r\n")
103
+ @stream.write("Host: #{authority}\r\n")
104
+
101
105
  write_headers(headers)
102
106
  write_body(body)
103
107
 
@@ -121,7 +125,7 @@ module Async
121
125
  headers = read_headers
122
126
  body = read_body(headers)
123
127
 
124
- return method, path, version, headers, body
128
+ return headers.delete(:host), method, path, version, headers, body
125
129
  end
126
130
 
127
131
  def write_response(version, status, headers, body)
@@ -142,11 +146,11 @@ module Async
142
146
  end
143
147
  end
144
148
 
145
- def read_headers(headers = {})
149
+ def read_headers(headers = Headers.new)
146
150
  # Parsing headers:
147
151
  each_line do |line|
148
152
  if line =~ /^([a-zA-Z\-]+):\s*(.+?)\s*$/
149
- headers["HTTP_#{$1.tr('-', '_').upcase}"] = $2
153
+ headers[$1] = $2
150
154
  else
151
155
  break
152
156
  end
@@ -178,7 +182,7 @@ module Async
178
182
  end
179
183
 
180
184
  def read_body(headers)
181
- if headers[HTTP_TRANSFER_ENCODING] == 'chunked'
185
+ if headers[:transfer_encoding] == 'chunked'
182
186
  buffer = Async::IO::BinaryString.new
183
187
 
184
188
  while true
@@ -195,7 +199,7 @@ module Async
195
199
  end
196
200
 
197
201
  return buffer
198
- elsif content_length = headers[HTTP_CONTENT_LENGTH]
202
+ elsif content_length = headers[:content_length]
199
203
  return @stream.read(Integer(content_length))
200
204
  end
201
205
  end
@@ -20,6 +20,7 @@
20
20
 
21
21
  require_relative 'request'
22
22
  require_relative 'response'
23
+ require_relative '../headers'
23
24
 
24
25
  require 'async/notification'
25
26
 
@@ -38,6 +39,14 @@ module Async
38
39
  self.new(::HTTP2::Server.new, stream)
39
40
  end
40
41
 
42
+ HTTPS = 'https'.freeze
43
+ SCHEME = ':scheme'.freeze
44
+ METHOD = ':method'.freeze
45
+ PATH = ':path'.freeze
46
+ AUTHORITY = ':authority'.freeze
47
+ REASON = ':reason'.freeze
48
+ STATUS = ':status'.freeze
49
+
41
50
  def initialize(controller, stream)
42
51
  @controller = controller
43
52
  @stream = stream
@@ -72,15 +81,13 @@ module Async
72
81
 
73
82
  def read_in_background(task: Task.current)
74
83
  task.async do |nested_task|
75
- while true
76
- if data = @stream.io.read(10)
77
- # Async.logger.debug(self) {"Reading data: #{data.size} bytes"}
78
- @controller << data
79
- else
80
- Async.logger.debug(self) {"Connection reset by peer!"}
81
- break
82
- end
84
+ buffer = Async::IO::BinaryString.new
85
+
86
+ while data = @stream.io.read(1024*8, buffer)
87
+ @controller << data
83
88
  end
89
+
90
+ Async.logger.debug(self) {"Connection reset by peer!"}
84
91
  end
85
92
  end
86
93
 
@@ -95,17 +102,19 @@ module Async
95
102
  @controller.on(:stream) do |stream|
96
103
  request = Request.new
97
104
  request.version = "HTTP/2.0"
98
- request.headers = {}
105
+ request.headers = Headers.new
99
106
 
100
107
  # stream.on(:active) { } # fires when stream transitions to open state
101
108
  # stream.on(:close) { } # stream is closed by client and server
102
109
 
103
110
  stream.on(:headers) do |headers|
104
111
  headers.each do |key, value|
105
- if key == ':method'
112
+ if key == METHOD
106
113
  request.method = value
107
- elsif key == ':path'
114
+ elsif key == PATH
108
115
  request.path = value
116
+ elsif key == AUTHORITY
117
+ request.authority = value
109
118
  else
110
119
  request.headers[key] = value
111
120
  end
@@ -120,7 +129,7 @@ module Async
120
129
  response = yield request
121
130
 
122
131
  # send response
123
- stream.headers(':status' => response[0].to_s)
132
+ stream.headers(STATUS => response[0].to_s)
124
133
 
125
134
  stream.headers(response[1]) unless response[1].empty?
126
135
 
@@ -137,13 +146,16 @@ module Async
137
146
  end
138
147
  end
139
148
 
140
- def send_request(method, path, headers = {}, body = nil)
149
+ RESPONSE_VERSION = 'HTTP/2'.freeze
150
+
151
+ def send_request(authority, method, path, headers = {}, body = nil)
141
152
  stream = @controller.new_stream
142
153
 
143
154
  internal_headers = {
144
- ':scheme' => 'https',
145
- ':method' => method,
146
- ':path' => path,
155
+ SCHEME => HTTPS,
156
+ METHOD => method,
157
+ PATH => path,
158
+ AUTHORITY => authority,
147
159
  }.merge(headers)
148
160
 
149
161
  stream.headers(internal_headers, end_stream: true)
@@ -157,17 +169,17 @@ module Async
157
169
  # end
158
170
 
159
171
  response = Response.new
160
- response.version = "HTTP/2"
161
- response.headers = {}
172
+ response.version = RESPONSE_VERSION
173
+ response.headers = Headers.new
162
174
  response.body = Async::IO::BinaryString.new
163
175
 
164
176
  stream.on(:headers) do |headers|
165
177
  # Async.logger.debug(self) {"Stream headers: #{headers.inspect}"}
166
178
 
167
179
  headers.each do |key, value|
168
- if key == ':status'
180
+ if key == STATUS
169
181
  response.status = value.to_i
170
- elsif key == ':reason'
182
+ elsif key == REASON
171
183
  response.reason = value
172
184
  else
173
185
  response.headers[key] = value
@@ -21,7 +21,7 @@
21
21
  module Async
22
22
  module HTTP
23
23
  module Protocol
24
- class Request < Struct.new(:method, :path, :version, :headers, :body)
24
+ class Request < Struct.new(:authority, :method, :path, :version, :headers, :body)
25
25
  def read
26
26
  @body
27
27
  end
@@ -34,7 +34,7 @@ module Async
34
34
  [200, {"Content-Type" => "text/plain"}, ["Hello World"]]
35
35
  end
36
36
 
37
- def accept(peer, address)
37
+ def accept(peer, address, task: Task.current)
38
38
  stream = Async::IO::Stream.new(peer)
39
39
  protocol = @protocol_class.server(stream)
40
40
 
@@ -31,9 +31,14 @@ module Async
31
31
  end
32
32
 
33
33
  def initialize(url, endpoint = nil, **options)
34
+ super(**options)
35
+
34
36
  @url = url
35
37
  @endpoint = endpoint
36
- @options = options
38
+ end
39
+
40
+ def to_s
41
+ "\#<#{self.class} #{@url} #{@options.inspect}>"
37
42
  end
38
43
 
39
44
  attr :url
@@ -64,7 +69,7 @@ module Async
64
69
  end
65
70
 
66
71
  def hostname
67
- @url.hostname
72
+ @options.fetch(:hostname, @url.hostname)
68
73
  end
69
74
 
70
75
  def ssl_context
@@ -79,8 +84,6 @@ module Async
79
84
  @endpoint = Async::IO::Endpoint.tcp(hostname, port)
80
85
 
81
86
  if secure?
82
- Async.logger.debug(self) {"Setting hostname: #{self.hostname}"}
83
-
84
87
  # Wrap it in SSL:
85
88
  @endpoint = Async::IO::SecureEndpoint.new(@endpoint,
86
89
  ssl_context: ssl_context,
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Async
22
22
  module HTTP
23
- VERSION = "0.9.0"
23
+ VERSION = "0.10.0"
24
24
  end
25
25
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-http
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-03-27 00:00:00.000000000 Z
11
+ date: 2018-03-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.5'
33
+ version: '1.6'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '1.5'
40
+ version: '1.6'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: http-2
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -123,9 +123,9 @@ files:
123
123
  - README.md
124
124
  - Rakefile
125
125
  - async-http.gemspec
126
- - examples/spider.rb
127
126
  - lib/async/http.rb
128
127
  - lib/async/http/client.rb
128
+ - lib/async/http/headers.rb
129
129
  - lib/async/http/pool.rb
130
130
  - lib/async/http/protocol.rb
131
131
  - lib/async/http/protocol/http1.rb
data/examples/spider.rb DELETED
@@ -1,129 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'async/await'
4
-
5
- require 'pry'
6
-
7
- require_relative '../lib/async/http/client'
8
- require '../lib/async/http/url_endpoint'
9
- require '../lib/async/http/protocol/https'
10
-
11
- require 'trenni/sanitize'
12
- require 'set'
13
-
14
- Async.logger.level = Logger::DEBUG
15
-
16
- class HTML < Trenni::Sanitize::Filter
17
- def initialize(*)
18
- super
19
-
20
- @base = nil
21
- @links = []
22
- end
23
-
24
- attr :base
25
- attr :links
26
-
27
- def filter(node)
28
- if node.name == 'base'
29
- @base = node['href']
30
- elsif node.name == 'a'
31
- @links << node['href']
32
- end
33
-
34
- node.skip!(TAG)
35
- end
36
- end
37
-
38
- class Cache
39
- def initialize
40
- @clients = {}
41
- end
42
-
43
- def close
44
- @clients.each(&:close)
45
- @clients.clear
46
- end
47
-
48
- def [] endpoint
49
- url = endpoint.specification
50
- key = "#{url.scheme}://#{url.userinfo}@#{url.hostname}"
51
-
52
- @clients[key] ||= Async::HTTP::Client.new(endpoint, endpoint.secure? ? Async::HTTP::Protocol::HTTPS : Async::HTTP::Protocol::HTTP1)
53
- end
54
- end
55
-
56
- class << self
57
- include Async::Await
58
-
59
- async def fetch(url, depth = 4, fetched = Set.new, clients = Cache.new)
60
- return if fetched.include?(url) or depth == 0 or url.host != "www.codeotaku.com"
61
- fetched << url
62
-
63
- endpoint = Async::HTTP::URLEndpoint.new(url)
64
- client = clients[endpoint]
65
-
66
- request_uri = endpoint.specification.request_uri
67
- puts "GET #{url} (depth = #{depth})"
68
-
69
- response = timeout(10) do
70
- client.get(request_uri, {
71
- ':authority' => endpoint.specification.hostname,
72
- 'accept' => '*/*',
73
- 'user-agent' => 'spider',
74
- })
75
- end
76
-
77
- if response.status >= 300 && response.status < 400
78
- location = url + response.headers['location']
79
- # puts "Following redirect to #{location}"
80
- return fetch(location, depth-1, fetched)
81
- end
82
-
83
- content_type = response.headers['content-type']
84
- unless content_type&.start_with? 'text/html'
85
- # puts "Unsupported content type: #{response.headers['content-type']}"
86
- return
87
- end
88
-
89
- base = endpoint.specification
90
-
91
- begin
92
- html = HTML.parse(response.body)
93
- rescue
94
- # Async.logger.error($!)
95
- return
96
- end
97
-
98
- if html.base
99
- base = base + html.base
100
- end
101
-
102
- html.links.each do |href|
103
- begin
104
- full_url = base + href
105
-
106
- fetch(full_url, depth - 1, fetched) if full_url.kind_of? URI::HTTP
107
- rescue ArgumentError, URI::InvalidURIError
108
- # puts "Could not fetch #{href}, relative to #{base}."
109
- end
110
- end
111
- rescue Async::TimeoutError
112
- Async.logger.error("Timeout while fetching #{url}")
113
- rescue StandardError
114
- Async.logger.error($!)
115
- ensure
116
- puts "Closing client from spider..."
117
- client.close if client
118
- end
119
-
120
- async def fetch_one(url)
121
- endpoint = Async::HTTP::URLEndpoint.new(url)
122
- client = Async::HTTP::Client.new(endpoint, endpoint.secure? ? Async::HTTP::Protocol::HTTPS : Async::HTTP::Protocol::HTTP1)
123
-
124
- binding.pry
125
- end
126
- end
127
-
128
- fetch_one(URI.parse("https://www.codeotaku.com"))
129
- #puts "Finished."