async-http 0.6.0 → 0.8.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: 013fbd13a7bf3d427d1fa5b21caff2f818050103
4
- data.tar.gz: 8d87d03698f68f4d7d69e639ca66726463c72f31
2
+ SHA256:
3
+ metadata.gz: 5159abaa1252d9eb0885989ab212b178e8bc8c672904b570bd26df5d5b077245
4
+ data.tar.gz: 6713e0b620617916076956129bed3c3de4338b83dba8bfbffe9f8306c90f564a
5
5
  SHA512:
6
- metadata.gz: 72444c8c1320f81dbedea9cf855ebc729a39989ee831d1e27633899242309ea4ab34645998c08f35b66cf41a75da58276622d4062ba3ef0f7c117e2ac4a8ccda
7
- data.tar.gz: 4dfb58bd27fed7d8fa4d61d06049c6d4aeeeb7a930c62808f46e9aa72600a1c594602a730da5e5cef03a9779567db6cf24b98970e2c68e4ac3bea721df370834
6
+ metadata.gz: 2e814e75bddb4cd9fee74c78d45a2da90ab01e7f37454871b5b1ec7d5f42a88fea57b02b6f28dcbdda079803511f2da4b4d9ed2fbf9ecd18b6d0cae1213b07eb
7
+ data.tar.gz: b08a59a9a07811a5cf91cee4a4bc650eb3bbb753c1076f27fd41c5a237d4684542677b8ed1ce32f2474fe3347f80d05730b8141f1fcd1c8e6c571ac33653b95b
@@ -1,20 +1,36 @@
1
1
  language: ruby
2
- sudo: false
2
+ sudo: required
3
3
  dist: trusty
4
4
  cache: bundler
5
5
 
6
+ addons:
7
+ apt:
8
+ packages:
9
+ # - wrk installed below from xenial
10
+ - apache2-utils
11
+
12
+ before_install:
13
+ - echo "deb http://us.archive.ubuntu.com/ubuntu/ xenial main restricted universe" | sudo tee -a /etc/apt/sources.list
14
+ - sudo apt-get update -qq
15
+ - sudo apt-get install openssl libssl-dev wrk
16
+ # Rebuild Ruby using the updated OpenSSL libraries which allows 2.5 to pass.
17
+ # - rvm reinstall --disable-binary `rvm current`
18
+ # Fix a bug with Ruby 2.5: LoadError: cannot load such file -- bundler/dep_proxy
19
+ - gem update --system
20
+ # This works for Ruby 2.3 and 2.4 but not 2.5 which doesn't seem to use the local gem.
21
+ - gem install openssl
22
+
6
23
  matrix:
7
24
  include:
8
- - rvm: 2.0
9
- - rvm: 2.1
10
- - rvm: 2.2
11
25
  - rvm: 2.3
12
26
  - rvm: 2.4
27
+ - rvm: 2.5
13
28
  - rvm: jruby-head
14
29
  env: JRUBY_OPTS="--debug -X+O"
15
30
  - rvm: ruby-head
16
31
  - rvm: rbx-3
17
32
  allow_failures:
33
+ - rvm: 2.5
18
34
  - rvm: ruby-head
19
35
  - rvm: jruby-head
20
36
  - rvm: rbx-3
data/Gemfile CHANGED
@@ -3,6 +3,12 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in async-io.gemspec
4
4
  gemspec
5
5
 
6
+ group :development do
7
+ gem 'trenni-sanitize'
8
+
9
+ gem 'async-await'
10
+ end
11
+
6
12
  group :test do
7
13
  gem 'simplecov'
8
14
  gem 'coveralls', require: false
data/README.md CHANGED
@@ -36,9 +36,7 @@ require 'async/http/server'
36
36
  require 'async/http/client'
37
37
  require 'async/reactor'
38
38
 
39
- endpoints = [
40
- Async::IO::Endpoint.tcp('127.0.0.1', 9294, reuse_port: true)
41
- ]
39
+ endpoint = Async::IO::Endpoint.tcp('127.0.0.1', 9294, reuse_port: true)
42
40
 
43
41
  class Server < Async::HTTP::Server
44
42
  def handle_request(request, peer, address)
@@ -46,8 +44,8 @@ class Server < Async::HTTP::Server
46
44
  end
47
45
  end
48
46
 
49
- server = Server.new(endpoints)
50
- client = Async::HTTP::Client.new(endpoints)
47
+ server = Server.new(endpoint)
48
+ client = Async::HTTP::Client.new(endpoint)
51
49
 
52
50
  Async::Reactor.run do |task|
53
51
  server_task = task.async do
data/Rakefile CHANGED
@@ -5,18 +5,21 @@ RSpec::Core::RakeTask.new(:test)
5
5
 
6
6
  task :default => :test
7
7
 
8
+ require 'async/http/protocol'
9
+ PROTOCOL = Async::HTTP::Protocol::HTTP2
10
+
11
+ task :debug do
12
+ require 'async/logger'
13
+
14
+ Async.logger.level = Logger::DEBUG
15
+ end
16
+
8
17
  task :server do
9
18
  require 'async/reactor'
10
19
  require 'async/http/server'
11
20
 
12
- app = lambda do |env|
13
- [200, {}, ["Hello World"]]
14
- end
15
-
16
- server = Async::HTTP::Server.new([
17
- Async::IO::Endpoint.tcp('127.0.0.1', 9294, reuse_port: true)
18
- ], app)
19
-
21
+ server = Async::HTTP::Server.new(Async::IO::Endpoint.tcp('0.0.0.0', 9294, reuse_port: true), PROTOCOL)
22
+
20
23
  Async::Reactor.run do
21
24
  server.run
22
25
  end
@@ -26,9 +29,7 @@ task :client do
26
29
  require 'async/reactor'
27
30
  require 'async/http/client'
28
31
 
29
- client = Async::HTTP::Client.new([
30
- Async::IO::Endpoint.tcp('127.0.0.1', 9294, reuse_port: true)
31
- ])
32
+ client = Async::HTTP::Client.new(Async::IO::Endpoint.tcp('127.0.0.1', 9294, reuse_port: true), PROTOCOL)
32
33
 
33
34
  Async::Reactor.run do
34
35
  response = client.get("/")
@@ -44,13 +45,11 @@ task :wrk do
44
45
  app = lambda do |env|
45
46
  [200, {}, ["Hello World"]]
46
47
  end
47
-
48
- server = Async::HTTP::Server.new([
49
- Async::IO::Endpoint.tcp('127.0.0.1', 9294, reuse_port: true)
50
- ], app)
51
-
48
+
49
+ server = Async::HTTP::Server.new(Async::IO::Endpoint.tcp('127.0.0.1', 9294, reuse_port: true), app)
50
+
52
51
  process_count = Etc.nprocessors
53
-
52
+
54
53
  pids = process_count.times.collect do
55
54
  fork do
56
55
  Async::Reactor.run do
@@ -16,8 +16,11 @@ Gem::Specification.new do |spec|
16
16
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
17
  spec.require_paths = ["lib"]
18
18
 
19
- spec.add_dependency("async", "~> 1.1")
20
- spec.add_dependency("async-io", "~> 1.0")
19
+ spec.add_dependency("async", "~> 1.4")
20
+ spec.add_dependency("async-io", "~> 1.5")
21
+
22
+ spec.add_dependency("http-2", "~> 0.8")
23
+ spec.add_dependency("openssl")
21
24
 
22
25
  spec.add_development_dependency "async-rspec", "~> 1.1"
23
26
 
@@ -0,0 +1,129 @@
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."
@@ -25,52 +25,56 @@ require_relative 'protocol'
25
25
  module Async
26
26
  module HTTP
27
27
  class Client
28
- def initialize(endpoints, protocol_class = Protocol::HTTP11)
29
- @endpoints = endpoints
28
+ def initialize(endpoint, protocol = Protocol::HTTPS, **options)
29
+ @endpoint = endpoint
30
30
 
31
- @protocol_class = protocol_class
31
+ @protocol = protocol
32
+
33
+ @connections = connect(**options)
32
34
  end
33
35
 
34
- GET = 'GET'.freeze
35
- HEAD = 'HEAD'.freeze
36
- POST = 'POST'.freeze
37
-
38
- def get(path, *args)
39
- connect do |protocol|
40
- protocol.send_request(GET, path, *args)
36
+ def self.open(*args, &block)
37
+ client = self.new(*args)
38
+
39
+ return client unless block_given?
40
+
41
+ begin
42
+ yield client
43
+ ensure
44
+ client.close
41
45
  end
42
46
  end
43
47
 
44
- def head(path, *args)
45
- connect do |protocol|
46
- protocol.send_request(HEAD, path, *args)
47
- end
48
+ def close
49
+ @connections.close
48
50
  end
49
51
 
50
- def post(path, *args)
51
- connect do |protocol|
52
- protocol.send_request(POST, path, *args)
52
+ VERBS = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']
53
+
54
+ VERBS.each do |verb|
55
+ define_method(verb.downcase) do |*args|
56
+ self.request(verb, *args)
53
57
  end
54
58
  end
55
59
 
56
- def send_request(*args)
57
- connect do |protocol|
58
- protocol.send_request(*args)
60
+ def request(*args)
61
+ @connections.acquire do |connection|
62
+ connection.send_request(*args)
59
63
  end
60
64
  end
61
65
 
62
- private
66
+ protected
63
67
 
64
- def connect
65
- Async::IO::Endpoint.each(@endpoints) do |endpoint|
66
- # puts "Connecting to #{address} on process #{Process.pid}"
68
+ def connect(connection_limit: nil)
69
+ Pool.new(connection_limit) do
70
+ Async.logger.debug(self) {"Making connection to #{@endpoint.inspect}"}
67
71
 
68
- endpoint.connect do |peer|
69
- stream = Async::IO::Stream.new(peer)
72
+ @endpoint.each do |endpoint|
73
+ peer = endpoint.connect
70
74
 
71
- # We only yield for first successful connection.
75
+ stream = IO::Stream.new(peer)
72
76
 
73
- return yield @protocol_class.new(stream)
77
+ break @protocol.client(stream)
74
78
  end
75
79
  end
76
80
  end
@@ -0,0 +1,120 @@
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
+ # Pool behaviours
24
+ #
25
+ # - Single request per connection (HTTP/1 without keep-alive)
26
+ # - Multiple sequential requests per connection (HTTP1 with keep-alive)
27
+ # - Multiplex requests per connection (HTTP2)
28
+ #
29
+ # In general we don't know the policy until connection is established.
30
+ #
31
+ # This pool doesn't impose a maximum number of open resources, but it WILL block if there are no available resources and trying to allocate another one fails.
32
+ #
33
+ # Resources must respond to
34
+ # #multiplex -> 1 or more.
35
+ # #reusable? -> can be used again.
36
+ #
37
+ class Pool
38
+ def initialize(limit = nil, &block)
39
+ @available = {} # resource => count
40
+ @waiting = []
41
+
42
+ @limit = limit
43
+
44
+ @constructor = block
45
+ end
46
+
47
+ def acquire
48
+ resource = wait_for_next_available
49
+
50
+ return resource unless block_given?
51
+
52
+ begin
53
+ yield resource
54
+ ensure
55
+ release(resource)
56
+ end
57
+ end
58
+
59
+ # Make the resource available and let waiting tasks know that there is something available.
60
+ def release(resource)
61
+ if resource.reusable?
62
+ Async.logger.debug(self) {"Reusing resource #{resource}"}
63
+
64
+ @available[resource] -= 1
65
+
66
+ if task = @waiting.pop
67
+ task.resume
68
+ end
69
+ else
70
+ Async.logger.debug(self) {"Closing resource: #{resource}"}
71
+ resource.close
72
+ end
73
+ end
74
+
75
+ def close
76
+ @available.each_key(&:close)
77
+ @available.clear
78
+ end
79
+
80
+ protected
81
+
82
+ def wait_for_next_available
83
+ until resource = next_available
84
+ @waiting << Fiber.current
85
+ Task.yield
86
+ end
87
+
88
+ return resource
89
+ end
90
+
91
+ def create_resource
92
+ begin
93
+ # This might fail, which is okay :)
94
+ resource = @constructor.call
95
+ rescue StandardError
96
+ Async.logger.error "#{$!}: #{$!.backtrace}"
97
+ return nil
98
+ end
99
+
100
+ @available[resource] = 1
101
+
102
+ return resource
103
+ end
104
+
105
+ # TODO this does not take into account resources that start off good but can fail.
106
+ def next_available
107
+ @available.each do |resource, count|
108
+ if count < resource.multiplex
109
+ @available[resource] += 1
110
+
111
+ return resource
112
+ end
113
+ end
114
+
115
+ Async.logger.debug(self) {"No available resources, allocating new one..."}
116
+ return create_resource
117
+ end
118
+ end
119
+ end
120
+ end
@@ -18,4 +18,5 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
- require_relative 'protocol/http1x'
21
+ require_relative 'protocol/http1'
22
+ require_relative 'protocol/https'
@@ -21,27 +21,33 @@
21
21
  require_relative 'http10'
22
22
  require_relative 'http11'
23
23
 
24
+ require_relative '../pool'
25
+
24
26
  module Async
25
27
  module HTTP
26
28
  module Protocol
27
29
  # A server that supports both HTTP1.0 and HTTP1.1 semantics by detecting the version of the request.
28
- class HTTP1x < Async::IO::Protocol::Line
30
+ class HTTP1 < Async::IO::Protocol::Line
29
31
  HANDLERS = {
30
32
  "HTTP/1.0" => HTTP10,
31
33
  "HTTP/1.1" => HTTP11,
32
34
  }
33
35
 
34
- def initialize(stream, handlers: HANDLERS)
36
+ def initialize(stream)
35
37
  super(stream, HTTP11::CRLF)
38
+ end
39
+
40
+ class << self
41
+ def client(*args)
42
+ HTTP11.new(*args)
43
+ end
36
44
 
37
- @handlers = handlers
38
-
39
- @handler = nil
45
+ alias server new
40
46
  end
41
47
 
42
48
  def create_handler(version)
43
- if klass = @handlers[version]
44
- klass.new(@stream)
49
+ if klass = HANDLERS[version]
50
+ klass.server(@stream)
45
51
  else
46
52
  raise RuntimeError, "Unsupported protocol version #{version}"
47
53
  end
@@ -52,10 +58,6 @@ module Async
52
58
 
53
59
  create_handler(version).receive_requests(&block)
54
60
  end
55
-
56
- def send_request(request, &block)
57
- create_handler(request.version).send_request(request, &block)
58
- end
59
61
  end
60
62
  end
61
63
  end
@@ -45,8 +45,14 @@ module Async
45
45
 
46
46
  write_response(request.version, status, headers, body)
47
47
 
48
- break unless keep_alive?(request.headers) && keep_alive?(headers)
48
+ unless keep_alive?(request.headers) && keep_alive?(headers)
49
+ @keep_alive = false
50
+
51
+ break
52
+ end
49
53
  end
54
+
55
+ return false
50
56
  end
51
57
 
52
58
  def write_body(body, chunked = true)
@@ -35,6 +35,22 @@ module Async
35
35
 
36
36
  def initialize(stream)
37
37
  super(stream, CRLF)
38
+
39
+ @keep_alive = true
40
+ end
41
+
42
+ # Only one simultaneous connection at a time.
43
+ def multiplex
44
+ 1
45
+ end
46
+
47
+ def reusable?
48
+ @keep_alive
49
+ end
50
+
51
+ class << self
52
+ alias server new
53
+ alias client new
38
54
  end
39
55
 
40
56
  HTTP_CONNECTION = 'HTTP_CONNECTION'.freeze
@@ -60,7 +76,11 @@ module Async
60
76
 
61
77
  write_response(request.version, status, headers, body)
62
78
 
63
- break unless keep_alive?(request.headers) && keep_alive?(headers)
79
+ unless keep_alive?(request.headers) and keep_alive?(headers)
80
+ @keep_alive = false
81
+
82
+ break
83
+ end
64
84
 
65
85
  # This ensures we yield at least once every iteration of the loop and allow other fibers to execute.
66
86
  task.yield
@@ -72,7 +92,6 @@ module Async
72
92
  write_request(method, path, version, headers, body)
73
93
 
74
94
  return Response.new(*read_response)
75
-
76
95
  rescue EOFError
77
96
  return nil
78
97
  end
@@ -92,6 +111,8 @@ module Async
92
111
  headers = read_headers
93
112
  body = read_body(headers)
94
113
 
114
+ @keep_alive = keep_alive?(headers)
115
+
95
116
  return version, Integer(status), reason, headers, body
96
117
  end
97
118
 
@@ -0,0 +1,205 @@
1
+ # Copyright, 2018, 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
+ require_relative 'request'
22
+ require_relative 'response'
23
+
24
+ require 'async/notification'
25
+
26
+ require 'http/2'
27
+
28
+ module Async
29
+ module HTTP
30
+ module Protocol
31
+ # A server that supports both HTTP1.0 and HTTP1.1 semantics by detecting the version of the request.
32
+ class HTTP2
33
+ def self.client(stream)
34
+ self.new(::HTTP2::Client.new, stream)
35
+ end
36
+
37
+ def self.server(stream)
38
+ self.new(::HTTP2::Server.new, stream)
39
+ end
40
+
41
+ def initialize(controller, stream)
42
+ @controller = controller
43
+ @stream = stream
44
+
45
+ @controller.on(:frame) do |data|
46
+ @stream.write(data)
47
+ @stream.flush
48
+ end
49
+
50
+ # @controller.on(:frame_sent) do |frame|
51
+ # Async.logger.debug(self) {"Sent frame: #{frame.inspect}"}
52
+ # end
53
+ #
54
+ # @controller.on(:frame_received) do |frame|
55
+ # Async.logger.debug(self) {"Received frame: #{frame.inspect}"}
56
+ # end
57
+
58
+ if @controller.is_a? ::HTTP2::Client
59
+ @controller.send_connection_preface
60
+ @reader = read_in_background
61
+ end
62
+ end
63
+
64
+ # Multiple requests can be processed at the same time.
65
+ def multiplex
66
+ @controller.remote_settings[:settings_max_concurrent_streams]
67
+ end
68
+
69
+ def reusable?
70
+ @reader.alive?
71
+ end
72
+
73
+ def read_in_background(task: Task.current)
74
+ 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
83
+ end
84
+ end
85
+ end
86
+
87
+ def close
88
+ Async.logger.debug(self) {"Closing connection"}
89
+ @reader.stop
90
+ @stream.close
91
+ end
92
+
93
+ def receive_requests(&block)
94
+ # emits new streams opened by the client
95
+ @controller.on(:stream) do |stream|
96
+ request = Request.new
97
+ request.version = "HTTP/2.0"
98
+ request.headers = {}
99
+
100
+ # stream.on(:active) { } # fires when stream transitions to open state
101
+ # stream.on(:close) { } # stream is closed by client and server
102
+
103
+ stream.on(:headers) do |headers|
104
+ headers.each do |key, value|
105
+ if key == ':method'
106
+ request.method = value
107
+ elsif key == ':path'
108
+ request.path = value
109
+ else
110
+ request.headers[key] = value
111
+ end
112
+ end
113
+ end
114
+
115
+ stream.on(:data) do |body|
116
+ request.body = body
117
+ end
118
+
119
+ stream.on(:half_close) do
120
+ response = yield request
121
+
122
+ # send response
123
+ stream.headers(':status' => response[0].to_s)
124
+
125
+ stream.headers(response[1]) unless response[1].empty?
126
+
127
+ response[2].each do |chunk|
128
+ stream.data(chunk, end_stream: false)
129
+ end
130
+
131
+ stream.data("", end_stream: true)
132
+ end
133
+ end
134
+
135
+ while data = @stream.io.read(1024)
136
+ @controller << data
137
+ end
138
+ end
139
+
140
+ def send_request(method, path, headers = {}, body = nil)
141
+ stream = @controller.new_stream
142
+
143
+ internal_headers = {
144
+ ':scheme' => 'https',
145
+ ':method' => method,
146
+ ':path' => path,
147
+ }.merge(headers)
148
+
149
+ stream.headers(internal_headers, end_stream: true)
150
+
151
+ # if body
152
+ # body.each do |chunk|
153
+ # stream.data(chunk, end_stream: false)
154
+ # end
155
+ #
156
+ # stream.data("", end_stream: true)
157
+ # end
158
+
159
+ response = Response.new
160
+ response.version = "HTTP/2"
161
+ response.headers = {}
162
+ response.body = Async::IO::BinaryString.new
163
+
164
+ stream.on(:headers) do |headers|
165
+ # Async.logger.debug(self) {"Stream headers: #{headers.inspect}"}
166
+
167
+ headers.each do |key, value|
168
+ if key == ':status'
169
+ response.status = value.to_i
170
+ elsif key == ':reason'
171
+ response.reason = value
172
+ else
173
+ response.headers[key] = value
174
+ end
175
+ end
176
+ end
177
+
178
+ stream.on(:data) do |body|
179
+ # Async.logger.debug(self) {"Stream data: #{body.size} bytes"}
180
+ response.body << body
181
+ end
182
+
183
+ finished = Async::Notification.new
184
+
185
+ stream.on(:half_close) do
186
+ # Async.logger.debug(self) {"Stream half-closed."}
187
+ end
188
+
189
+ stream.on(:close) do
190
+ # Async.logger.debug(self) {"Stream closed, sending signal."}
191
+ finished.signal
192
+ end
193
+
194
+ @stream.flush
195
+
196
+ # Async.logger.debug(self) {"Stream flushed, waiting for signal."}
197
+ finished.wait
198
+
199
+ # Async.logger.debug(self) {"Stream finished: #{response.inspect}"}
200
+ return response
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,67 @@
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
+ require_relative 'http1'
22
+ require_relative 'http2'
23
+
24
+ require_relative '../pool'
25
+
26
+ require 'openssl'
27
+
28
+ unless OpenSSL::SSL::SSLContext.instance_methods.include? :alpn_protocols=
29
+ abort "OpenSSL implementation doesn't support ALPN."
30
+ end
31
+
32
+ module Async
33
+ module HTTP
34
+ module Protocol
35
+ # A server that supports both HTTP1.0 and HTTP1.1 semantics by detecting the version of the request.
36
+ module HTTPS
37
+ HANDLERS = {
38
+ "http/1.0" => HTTP10,
39
+ "http/1.1" => HTTP11,
40
+ "h2" => HTTP2,
41
+ nil => HTTP11,
42
+ }
43
+
44
+ def self.protocol_for(stream)
45
+ # alpn_protocol is only available if openssl v1.0.2+
46
+ name = stream.io.alpn_protocol
47
+
48
+ Async.logger.debug(self) {"Negotiating protocol #{name.inspect}..."}
49
+
50
+ if protocol = HANDLERS[name]
51
+ return protocol
52
+ else
53
+ throw ArgumentError.new("Could not determine protocol for connection (#{name.inspect}).")
54
+ end
55
+ end
56
+
57
+ def self.client(stream)
58
+ protocol_for(stream).client(stream)
59
+ end
60
+
61
+ def self.server(stream)
62
+ protocol_for(stream).server(stream)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -25,45 +25,40 @@ require_relative 'protocol'
25
25
  module Async
26
26
  module HTTP
27
27
  class Server
28
- def initialize(endpoints, protocol_class = Protocol::HTTP1x)
29
- @endpoints = endpoints
28
+ def initialize(endpoint, protocol_class = Protocol::HTTP1)
29
+ @endpoint = endpoint
30
30
  @protocol_class = protocol_class
31
31
  end
32
32
 
33
33
  def handle_request(request, peer, address)
34
- [200, {}, []]
34
+ [200, {"Content-Type" => "text/plain"}, ["Hello World"]]
35
35
  end
36
36
 
37
37
  def accept(peer, address)
38
38
  stream = Async::IO::Stream.new(peer)
39
+ protocol = @protocol_class.server(stream)
39
40
 
40
- protocol = @protocol_class.new(stream)
41
-
42
- # puts "Opening session on child pid #{Process.pid}"
41
+ # Async.logger.debug(self) {"Incoming connnection from #{address.inspect}"}
43
42
 
44
43
  hijack = catch(:hijack) do
45
44
  protocol.receive_requests do |request|
45
+ # Async.logger.debug(self) {"Incoming request from #{address.inspect}: #{request.method} #{request.path}"}
46
46
  handle_request(request, peer, address)
47
47
  end
48
48
  end
49
-
49
+
50
50
  if hijack
51
51
  hijack.call
52
52
  end
53
-
54
- # puts "Closing session"
55
-
56
53
  rescue EOFError, Errno::ECONNRESET, Errno::EPIPE
57
54
  # Sometimes client will disconnect without completing a result or reading the entire buffer.
58
55
  return nil
56
+ ensure
57
+ peer.close
59
58
  end
60
59
 
61
60
  def run
62
- Async::IO::Endpoint.each(@endpoints) do |endpoint|
63
- # puts "Binding to #{endpoint} on process #{Process.pid}"
64
-
65
- endpoint.accept(&self.method(:accept))
66
- end
61
+ @endpoint.accept(&self.method(:accept))
67
62
  end
68
63
  end
69
64
  end
@@ -24,16 +24,32 @@ require 'async/io/ssl_socket'
24
24
  module Async
25
25
  module HTTP
26
26
  class URLEndpoint < Async::IO::Endpoint
27
+ DEFAULT_ALPH_PROTOCOLS = ['h2', 'http/1.1'].freeze
28
+
27
29
  def self.parse(string, **options)
28
30
  self.new(URI.parse(string), **options)
29
31
  end
30
32
 
33
+ def initialize(url, endpoint = nil, **options)
34
+ @url = url
35
+ @endpoint = endpoint
36
+ @options = options
37
+ end
38
+
31
39
  def address
32
40
  endpoint.address
33
41
  end
34
42
 
35
43
  def secure?
36
- ['https', 'wss'].include?(specification.scheme)
44
+ ['https', 'wss'].include?(@url.scheme)
45
+ end
46
+
47
+ def protocol
48
+ if secure?
49
+ Protocol::HTTPS
50
+ else
51
+ Protocol::HTTP1
52
+ end
37
53
  end
38
54
 
39
55
  def default_port
@@ -41,24 +57,27 @@ module Async
41
57
  end
42
58
 
43
59
  def port
44
- specification.port || default_port
60
+ @url.port || default_port
45
61
  end
46
62
 
47
63
  def hostname
48
- specification.hostname
64
+ @url.hostname
49
65
  end
50
66
 
51
67
  def ssl_context
52
- options[:ssl_context] || ::OpenSSL::SSL::SSLContext.new.tap do |context|
68
+ @options[:ssl_context] || ::OpenSSL::SSL::SSLContext.new.tap do |context|
69
+ context.alpn_protocols = @options.fetch(:alpn_protocols, DEFAULT_ALPH_PROTOCOLS)
53
70
  context.set_params
54
71
  end
55
72
  end
56
73
 
57
74
  def endpoint
58
- unless defined? @endpoint
75
+ unless @endpoint
59
76
  @endpoint = Async::IO::Endpoint.tcp(hostname, port)
60
77
 
61
78
  if secure?
79
+ Async.logger.debug(self) {"Setting hostname: #{self.hostname}"}
80
+
62
81
  # Wrap it in SSL:
63
82
  @endpoint = Async::IO::SecureEndpoint.new(@endpoint,
64
83
  ssl_context: ssl_context,
@@ -77,6 +96,14 @@ module Async
77
96
  def connect(*args, &block)
78
97
  endpoint.connect(*args, &block)
79
98
  end
99
+
100
+ def each
101
+ return to_enum unless block_given?
102
+
103
+ self.endpoint.each do |endpoint|
104
+ yield self.class.new(@url, endpoint, @options)
105
+ end
106
+ end
80
107
  end
81
108
  end
82
109
  end
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Async
22
22
  module HTTP
23
- VERSION = "0.6.0"
23
+ VERSION = "0.8.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.6.0
4
+ version: 0.8.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-02-02 00:00:00.000000000 Z
11
+ date: 2018-03-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -16,28 +16,56 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.1'
19
+ version: '1.4'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.1'
26
+ version: '1.4'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: async-io
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.0'
33
+ version: '1.5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: http-2
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.8'
34
48
  type: :runtime
35
49
  prerelease: false
36
50
  version_requirements: !ruby/object:Gem::Requirement
37
51
  requirements:
38
52
  - - "~>"
39
53
  - !ruby/object:Gem::Version
40
- version: '1.0'
54
+ version: '0.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: openssl
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
41
69
  - !ruby/object:Gem::Dependency
42
70
  name: async-rspec
43
71
  requirement: !ruby/object:Gem::Requirement
@@ -109,12 +137,16 @@ files:
109
137
  - README.md
110
138
  - Rakefile
111
139
  - async-http.gemspec
140
+ - examples/spider.rb
112
141
  - lib/async/http.rb
113
142
  - lib/async/http/client.rb
143
+ - lib/async/http/pool.rb
114
144
  - lib/async/http/protocol.rb
145
+ - lib/async/http/protocol/http1.rb
115
146
  - lib/async/http/protocol/http10.rb
116
147
  - lib/async/http/protocol/http11.rb
117
- - lib/async/http/protocol/http1x.rb
148
+ - lib/async/http/protocol/http2.rb
149
+ - lib/async/http/protocol/https.rb
118
150
  - lib/async/http/protocol/request.rb
119
151
  - lib/async/http/protocol/response.rb
120
152
  - lib/async/http/server.rb
@@ -139,7 +171,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
139
171
  version: '0'
140
172
  requirements: []
141
173
  rubyforge_project:
142
- rubygems_version: 2.6.12
174
+ rubygems_version: 2.7.6
143
175
  signing_key:
144
176
  specification_version: 4
145
177
  summary: ''