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 +5 -5
- data/.travis.yml +20 -4
- data/Gemfile +6 -0
- data/README.md +3 -5
- data/Rakefile +16 -17
- data/async-http.gemspec +5 -2
- data/examples/spider.rb +129 -0
- data/lib/async/http/client.rb +32 -28
- data/lib/async/http/pool.rb +120 -0
- data/lib/async/http/protocol.rb +2 -1
- data/lib/async/http/protocol/{http1x.rb → http1.rb} +13 -11
- data/lib/async/http/protocol/http10.rb +7 -1
- data/lib/async/http/protocol/http11.rb +23 -2
- data/lib/async/http/protocol/http2.rb +205 -0
- data/lib/async/http/protocol/https.rb +67 -0
- data/lib/async/http/server.rb +10 -15
- data/lib/async/http/url_endpoint.rb +32 -5
- data/lib/async/http/version.rb +1 -1
- metadata +40 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5159abaa1252d9eb0885989ab212b178e8bc8c672904b570bd26df5d5b077245
|
4
|
+
data.tar.gz: 6713e0b620617916076956129bed3c3de4338b83dba8bfbffe9f8306c90f564a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2e814e75bddb4cd9fee74c78d45a2da90ab01e7f37454871b5b1ec7d5f42a88fea57b02b6f28dcbdda079803511f2da4b4d9ed2fbf9ecd18b6d0cae1213b07eb
|
7
|
+
data.tar.gz: b08a59a9a07811a5cf91cee4a4bc650eb3bbb753c1076f27fd41c5a237d4684542677b8ed1ce32f2474fe3347f80d05730b8141f1fcd1c8e6c571ac33653b95b
|
data/.travis.yml
CHANGED
@@ -1,20 +1,36 @@
|
|
1
1
|
language: ruby
|
2
|
-
sudo:
|
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
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
|
-
|
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(
|
50
|
-
client = Async::HTTP::Client.new(
|
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
|
-
|
13
|
-
|
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
|
-
|
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
|
data/async-http.gemspec
CHANGED
@@ -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.
|
20
|
-
spec.add_dependency("async-io", "~> 1.
|
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
|
|
data/examples/spider.rb
ADDED
@@ -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."
|
data/lib/async/http/client.rb
CHANGED
@@ -25,52 +25,56 @@ require_relative 'protocol'
|
|
25
25
|
module Async
|
26
26
|
module HTTP
|
27
27
|
class Client
|
28
|
-
def initialize(
|
29
|
-
@
|
28
|
+
def initialize(endpoint, protocol = Protocol::HTTPS, **options)
|
29
|
+
@endpoint = endpoint
|
30
30
|
|
31
|
-
@
|
31
|
+
@protocol = protocol
|
32
|
+
|
33
|
+
@connections = connect(**options)
|
32
34
|
end
|
33
35
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
45
|
-
|
46
|
-
protocol.send_request(HEAD, path, *args)
|
47
|
-
end
|
48
|
+
def close
|
49
|
+
@connections.close
|
48
50
|
end
|
49
51
|
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
57
|
-
|
58
|
-
|
60
|
+
def request(*args)
|
61
|
+
@connections.acquire do |connection|
|
62
|
+
connection.send_request(*args)
|
59
63
|
end
|
60
64
|
end
|
61
65
|
|
62
|
-
|
66
|
+
protected
|
63
67
|
|
64
|
-
def connect
|
65
|
-
|
66
|
-
|
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.
|
69
|
-
|
72
|
+
@endpoint.each do |endpoint|
|
73
|
+
peer = endpoint.connect
|
70
74
|
|
71
|
-
|
75
|
+
stream = IO::Stream.new(peer)
|
72
76
|
|
73
|
-
|
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
|
data/lib/async/http/protocol.rb
CHANGED
@@ -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
|
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
|
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
|
-
|
38
|
-
|
39
|
-
@handler = nil
|
45
|
+
alias server new
|
40
46
|
end
|
41
47
|
|
42
48
|
def create_handler(version)
|
43
|
-
if klass =
|
44
|
-
klass.
|
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
|
-
|
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
|
-
|
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
|
data/lib/async/http/server.rb
CHANGED
@@ -25,45 +25,40 @@ require_relative 'protocol'
|
|
25
25
|
module Async
|
26
26
|
module HTTP
|
27
27
|
class Server
|
28
|
-
def initialize(
|
29
|
-
@
|
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
|
-
|
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
|
-
|
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?(
|
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
|
-
|
60
|
+
@url.port || default_port
|
45
61
|
end
|
46
62
|
|
47
63
|
def hostname
|
48
|
-
|
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
|
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
|
data/lib/async/http/version.rb
CHANGED
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.
|
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-
|
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.
|
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.
|
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.
|
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: '
|
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/
|
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
|
174
|
+
rubygems_version: 2.7.6
|
143
175
|
signing_key:
|
144
176
|
specification_version: 4
|
145
177
|
summary: ''
|