async-http 0.6.0 → 0.8.0

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
- 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: ''