h2 0.4.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +2 -1
- data/README.md +36 -9
- data/bin/console +1 -0
- data/examples/server/dog.png +0 -0
- data/examples/server/hello_world.rb +20 -0
- data/examples/server/https_hello_world.rb +31 -0
- data/examples/server/push_promise.rb +47 -0
- data/h2.gemspec +3 -3
- data/lib/h2/reel/ext.rb +48 -0
- data/lib/h2/server/connection.rb +153 -0
- data/lib/h2/server/https.rb +171 -0
- data/lib/h2/server/push_promise.rb +125 -0
- data/lib/h2/server/stream/request.rb +65 -0
- data/lib/h2/server/stream/response.rb +99 -0
- data/lib/h2/server/stream.rb +208 -0
- data/lib/h2/server.rb +98 -0
- data/lib/h2/version.rb +1 -1
- metadata +20 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b2ce1f4f619d3bd9eb680ad49ad0181ef4a5e81b0a9fa07f0b8e544b85b77028
|
4
|
+
data.tar.gz: 34de290127df74e156b02e595f2b1ea91ab3da6edf38cfff263a4734e16297cc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2a89223df4da4f8effb784628e711bd9341b63b096ecfa5d1bb970f2d9a447c59e51871167c7da1f48eb3fc70436970fe64a84400ab93ab7e835e7d165cc203b
|
7
|
+
data.tar.gz: 81cd03234bc8eff7a0acc6d69105ba5cf9599b8ad9b8193c28679d6c1fb504a612cb9c5062bba7f4c3ebe5448636908c8c87e75666de0575b78bb28d9a4b0e8f
|
data/Gemfile
CHANGED
@@ -12,7 +12,8 @@ end
|
|
12
12
|
|
13
13
|
group :development, :test do
|
14
14
|
gem 'awesome_print'
|
15
|
+
gem 'certificate_authority'
|
15
16
|
gem 'guard-rake'
|
16
17
|
gem 'pry-byebug', platforms: [:mri]
|
17
|
-
gem 'reel',
|
18
|
+
gem 'reel', '0.6.1'
|
18
19
|
end
|
data/README.md
CHANGED
@@ -2,14 +2,36 @@
|
|
2
2
|
|
3
3
|
[![Build Status](https://travis-ci.org/kenichi/h2.svg?branch=master)](https://travis-ci.org/kenichi/h2)
|
4
4
|
|
5
|
-
H2 is
|
5
|
+
H2 is an HTTP/2 client and server based on the [http-2](https://github.com/igrigorik/http-2) gem.
|
6
6
|
|
7
7
|
H2 uses:
|
8
8
|
|
9
9
|
* keyword arguments (>=2.0)
|
10
10
|
* exception-less socket IO (>=2.3).
|
11
11
|
|
12
|
-
## Usage
|
12
|
+
## Server Usage
|
13
|
+
|
14
|
+
Server API is currently optional, and must be required separately. The server
|
15
|
+
uses [Reel](https://github.com/celluloid/reel), but since this API is optional,
|
16
|
+
reel must be separately added to `Gemfile`. It is currently based on `reel-0.6.1`.
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
require 'h2/server'
|
20
|
+
|
21
|
+
server = H2::Server::HTTP.new host: addr, port: port do |connection|
|
22
|
+
connection.each_stream do |stream|
|
23
|
+
stream.respond :ok, "hello, world!\n"
|
24
|
+
stream.connection.goaway
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
stream = H2.get url: "http://#{addr}:#{port}", tls: false
|
29
|
+
stream.body #=> "hello, world!\n"
|
30
|
+
```
|
31
|
+
|
32
|
+
See [examples](https://github.com/kenichi/h2/tree/master/examples/server/)
|
33
|
+
|
34
|
+
## Client Usage
|
13
35
|
|
14
36
|
```ruby
|
15
37
|
require 'h2'
|
@@ -44,10 +66,10 @@ stream.closed? #=> true
|
|
44
66
|
|
45
67
|
client.closed? #=> false unless server sent GOAWAY
|
46
68
|
|
47
|
-
|
48
|
-
|
49
|
-
if h['
|
50
|
-
|
69
|
+
client.on :promise do |p| # check/cancel a promise
|
70
|
+
p.on :headers do |h|
|
71
|
+
if h['etag'] == 'some_value'
|
72
|
+
p.cancel! # already have
|
51
73
|
end
|
52
74
|
end
|
53
75
|
end
|
@@ -69,13 +91,13 @@ end
|
|
69
91
|
client.goaway!
|
70
92
|
```
|
71
93
|
|
72
|
-
## CLI
|
94
|
+
## Client CLI
|
73
95
|
|
74
96
|
For more info on using the CLI `h2` installed with this gem:
|
75
97
|
|
76
98
|
`$ h2 --help`
|
77
99
|
|
78
|
-
## TLS CA Certificates
|
100
|
+
## Using TLS CA Certificates with the Client
|
79
101
|
|
80
102
|
If you're running on macOS and using Homebrew's openssl package, you may need to
|
81
103
|
specify the CA file in the TLS options:
|
@@ -101,7 +123,7 @@ Neither of these gems are hard dependencies. If you want to use either one, you
|
|
101
123
|
have it available to your Ruby VM, most likely via Bundler, *and* require the
|
102
124
|
sub-component of h2 that will prepend and extend `H2::Client`. They are also intended
|
103
125
|
to be mutually exclusive: you can have both in your VM, but you can only use one at a
|
104
|
-
time with h2.
|
126
|
+
time with h2's client.
|
105
127
|
|
106
128
|
#### Celluloid Pool
|
107
129
|
|
@@ -113,6 +135,10 @@ require 'h2/client/celluloid'
|
|
113
135
|
|
114
136
|
This will lazily fire up a celluloid pool, with defaults defined by Celluloid.
|
115
137
|
|
138
|
+
NOTE: if you've added reel and required the 'h2/server' API, Celluloid will be
|
139
|
+
loaded in your Ruby VM already; however, you must still require this to have
|
140
|
+
the client use Celluloid actor pools.
|
141
|
+
|
116
142
|
#### Concurrent-Ruby ThreadPoolExecutor
|
117
143
|
|
118
144
|
To use a concurrent-ruby thread pool executor for reading from `H2::Client` connections:
|
@@ -137,6 +163,7 @@ max_queue: procs * 5
|
|
137
163
|
* [x] push promise cancellation
|
138
164
|
* [x] alternate concurrency models
|
139
165
|
* [ ] fix up CLI to be more curlish
|
166
|
+
* [ ] update server API
|
140
167
|
|
141
168
|
## Contributing
|
142
169
|
|
data/bin/console
CHANGED
Binary file
|
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Run with: bundle exec examples/server/hello_world.rb
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'h2/server'
|
6
|
+
|
7
|
+
H2::Logger.level = ::Logger::DEBUG
|
8
|
+
H2.verbose!
|
9
|
+
|
10
|
+
addr, port = '127.0.0.1', 1234
|
11
|
+
|
12
|
+
puts "*** Starting server on http://#{addr}:#{port}"
|
13
|
+
s = H2::Server::HTTP.new host: addr, port: port do |connection|
|
14
|
+
connection.each_stream do |stream|
|
15
|
+
stream.respond :ok, "hello, world!\n"
|
16
|
+
stream.connection.goaway
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
sleep
|
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Run with: bundle exec examples/server/https_hello_world.rb
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'h2/server'
|
6
|
+
|
7
|
+
port = 1234
|
8
|
+
addr = Socket.getaddrinfo('localhost', port).first[3]
|
9
|
+
certs_dir = File.expand_path '../../../tmp/certs', __FILE__
|
10
|
+
|
11
|
+
tls = {
|
12
|
+
cert: certs_dir + '/server.crt',
|
13
|
+
key: certs_dir + '/server.key',
|
14
|
+
# :extra_chain_cert => certs_dir + '/chain.pem'
|
15
|
+
}
|
16
|
+
|
17
|
+
puts "*** Starting server on https://#{addr}:#{port}"
|
18
|
+
|
19
|
+
s = H2::Server::HTTPS.new host: addr, port: port, **tls do |connection|
|
20
|
+
connection.each_stream do |stream|
|
21
|
+
stream.goaway_on_complete
|
22
|
+
|
23
|
+
if stream.request.path == '/favicon.ico'
|
24
|
+
stream.respond :not_found
|
25
|
+
else
|
26
|
+
stream.respond :ok, "hello, world!\n"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
sleep
|
@@ -0,0 +1,47 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Run with: bundle exec examples/server/push_promise.rb
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'h2/server'
|
6
|
+
|
7
|
+
H2::Logger.level = ::Logger::DEBUG
|
8
|
+
H2.verbose!
|
9
|
+
|
10
|
+
port = 1234
|
11
|
+
addr = Socket.getaddrinfo('localhost', port).first[3]
|
12
|
+
certs_dir = File.expand_path '../../../tmp/certs', __FILE__
|
13
|
+
dog_png = File.read File.expand_path '../dog.png', __FILE__
|
14
|
+
push_promise = '<html>wait for it...<img src="/dog.png"/><script src="/pushed.js"></script></html>'
|
15
|
+
pushed_js = '(()=>{ alert("hello h2 push promise!"); })();'
|
16
|
+
|
17
|
+
sni = {
|
18
|
+
'localhost' => {
|
19
|
+
:cert => certs_dir + '/server.crt',
|
20
|
+
:key => certs_dir + '/server.key',
|
21
|
+
# :extra_chain_cert => certs_dir + '/chain.pem'
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
puts "*** Starting server on https://#{addr}:#{port}"
|
26
|
+
s = H2::Server::HTTPS.new host: addr, port: port, sni: sni do |connection|
|
27
|
+
connection.each_stream do |stream|
|
28
|
+
|
29
|
+
if stream.request.path == '/favicon.ico'
|
30
|
+
stream.respond :not_found
|
31
|
+
|
32
|
+
else
|
33
|
+
stream.goaway_on_complete
|
34
|
+
|
35
|
+
stream.push_promise '/dog.png', { 'content-type' => 'image/png' }, dog_png
|
36
|
+
|
37
|
+
js_promise = stream.push_promise_for '/pushed.js', { 'content-type' => 'application/javascript' }, pushed_js
|
38
|
+
js_promise.make_on stream
|
39
|
+
|
40
|
+
stream.respond :ok, push_promise
|
41
|
+
|
42
|
+
js_promise.keep
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
sleep
|
data/h2.gemspec
CHANGED
@@ -6,8 +6,8 @@ Gem::Specification.new do |spec|
|
|
6
6
|
spec.version = H2::VERSION
|
7
7
|
spec.authors = ["Kenichi Nakamura"]
|
8
8
|
spec.email = ["kenichi.nakamura@gmail.com"]
|
9
|
-
spec.summary = 'an http/2 client based on http-2'
|
10
|
-
spec.description = 'a pure ruby http/2 client based on http-2'
|
9
|
+
spec.summary = 'an http/2 client & server based on http-2'
|
10
|
+
spec.description = 'a pure ruby http/2 client & server based on http-2'
|
11
11
|
spec.homepage = 'https://github.com/kenichi/h2'
|
12
12
|
spec.license = 'MIT'
|
13
13
|
spec.bindir = 'exe'
|
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
|
|
17
17
|
|
18
18
|
spec.required_ruby_version = '>= 2.2'
|
19
19
|
|
20
|
-
spec.add_dependency 'http-2', '~> 0.
|
20
|
+
spec.add_dependency 'http-2', '~> 0.9', '>= 0.9.1'
|
21
21
|
spec.add_dependency 'colored', '1.2'
|
22
22
|
|
23
23
|
spec.add_development_dependency "bundler", "~> 1.15"
|
data/lib/h2/reel/ext.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'reel/connection'
|
2
|
+
require 'reel/request'
|
3
|
+
require 'reel/server'
|
4
|
+
|
5
|
+
# see also: https://github.com/celluloid/reel/pull/228
|
6
|
+
|
7
|
+
|
8
|
+
# this is a little sneaky, not as direct as the PR above, but the least
|
9
|
+
# invasive way i could come up with to get access to the server from the
|
10
|
+
# request.
|
11
|
+
|
12
|
+
module Reel
|
13
|
+
|
14
|
+
# we add a `server` accessor to +Connection+...
|
15
|
+
#
|
16
|
+
class Request
|
17
|
+
attr_reader :connection
|
18
|
+
end
|
19
|
+
|
20
|
+
# ... and a `connection` reader to +Request+.
|
21
|
+
#
|
22
|
+
class Connection
|
23
|
+
attr_accessor :server
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
module H2
|
29
|
+
module Reel
|
30
|
+
module ServerConnection
|
31
|
+
|
32
|
+
# then we hijack +Server+ construction, and wrap the callback at the last
|
33
|
+
# minute with one that sets the server on every connection, before
|
34
|
+
# calling the original.
|
35
|
+
#
|
36
|
+
def initialize server, options = {}, &callback
|
37
|
+
super
|
38
|
+
@og_callback = @callback
|
39
|
+
@callback = ->(conn) {
|
40
|
+
conn.server = self
|
41
|
+
@og_callback[conn]
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
::Reel::Server.prepend ServerConnection
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'h2/server/stream'
|
2
|
+
|
3
|
+
module H2
|
4
|
+
class Server
|
5
|
+
|
6
|
+
# handles reading data from the +@socket+ into the +HTTP2::Server+ +@parser+,
|
7
|
+
# callbacks from the +@parser+, and closing of the +@socket+
|
8
|
+
#
|
9
|
+
class Connection
|
10
|
+
|
11
|
+
# each +@parser+ event method is wrapped in a block to call a local instance
|
12
|
+
# method of the same name
|
13
|
+
#
|
14
|
+
PARSER_EVENTS = [
|
15
|
+
:frame,
|
16
|
+
:frame_sent,
|
17
|
+
:frame_received,
|
18
|
+
:stream,
|
19
|
+
:goaway
|
20
|
+
]
|
21
|
+
|
22
|
+
attr_reader :parser, :server, :socket
|
23
|
+
|
24
|
+
def initialize socket:, server:
|
25
|
+
@socket = socket
|
26
|
+
@server = server
|
27
|
+
@parser = ::HTTP2::Server.new
|
28
|
+
@attached = true
|
29
|
+
|
30
|
+
yield self if block_given?
|
31
|
+
|
32
|
+
bind_events
|
33
|
+
|
34
|
+
Logger.debug "new H2::Connection: #{self}" if H2.verbose?
|
35
|
+
end
|
36
|
+
|
37
|
+
# is this connection still attached to the server reactor?
|
38
|
+
#
|
39
|
+
def attached?
|
40
|
+
@attached
|
41
|
+
end
|
42
|
+
|
43
|
+
# bind parser events to this instance
|
44
|
+
#
|
45
|
+
def bind_events
|
46
|
+
PARSER_EVENTS.each do |e|
|
47
|
+
on = "on_#{e}".to_sym
|
48
|
+
@parser.on(e) { |x| __send__ on, x }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# closes this connection's socket if attached
|
53
|
+
#
|
54
|
+
def close
|
55
|
+
socket.close if socket && attached?
|
56
|
+
end
|
57
|
+
|
58
|
+
# is this connection's socket closed?
|
59
|
+
#
|
60
|
+
def closed?
|
61
|
+
socket.closed?
|
62
|
+
end
|
63
|
+
|
64
|
+
# prevent this server reactor from handling this connection
|
65
|
+
#
|
66
|
+
def detach
|
67
|
+
@attached = false
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
# accessor for stream handler
|
72
|
+
#
|
73
|
+
def each_stream &block
|
74
|
+
@each_stream = block if block_given?
|
75
|
+
@each_stream
|
76
|
+
end
|
77
|
+
|
78
|
+
# queue a goaway frame
|
79
|
+
#
|
80
|
+
def goaway
|
81
|
+
server.async.goaway self
|
82
|
+
end
|
83
|
+
|
84
|
+
# begins the read loop, handling all errors with a log message,
|
85
|
+
# backtrace, and closing the +@socket+
|
86
|
+
#
|
87
|
+
def read
|
88
|
+
begin
|
89
|
+
while attached? && !@socket.closed? && !(@socket.eof? rescue true)
|
90
|
+
data = @socket.readpartial(4096)
|
91
|
+
@parser << data
|
92
|
+
end
|
93
|
+
close
|
94
|
+
|
95
|
+
rescue => e
|
96
|
+
Logger.error "Exception: #{e.message} - closing socket"
|
97
|
+
STDERR.puts e.backtrace
|
98
|
+
close
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
protected
|
104
|
+
|
105
|
+
# +@parser+ event methods
|
106
|
+
|
107
|
+
# called by +@parser+ with a binary frame to write to the +@socket+
|
108
|
+
#
|
109
|
+
def on_frame bytes
|
110
|
+
Logger.debug "Writing bytes: #{truncate_string(bytes.unpack("H*").first)}" if H2.verbose?
|
111
|
+
|
112
|
+
# N.B. this is the important bit
|
113
|
+
#
|
114
|
+
@socket.write bytes
|
115
|
+
rescue IOError, Errno::EPIPE => e
|
116
|
+
Logger.error e.message
|
117
|
+
close
|
118
|
+
end
|
119
|
+
|
120
|
+
def on_frame_sent f
|
121
|
+
Logger.debug "Sent frame: #{truncate_frame(f).inspect}" if H2.verbose?
|
122
|
+
end
|
123
|
+
|
124
|
+
def on_frame_received f
|
125
|
+
Logger.debug "Received frame: #{truncate_frame(f).inspect}" if H2.verbose?
|
126
|
+
end
|
127
|
+
|
128
|
+
# the +@parser+ calls this when a new stream has been initiated by the
|
129
|
+
# client
|
130
|
+
#
|
131
|
+
def on_stream stream
|
132
|
+
H2::Server::Stream.new connection: self, stream: stream
|
133
|
+
end
|
134
|
+
|
135
|
+
# the +@parser+ calls this when a goaway frame is received from the client
|
136
|
+
#
|
137
|
+
def on_goaway event
|
138
|
+
close
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
def truncate_string s
|
144
|
+
(String === s && s.length > 64) ? "#{s[0,64]}..." : s
|
145
|
+
end
|
146
|
+
|
147
|
+
def truncate_frame f
|
148
|
+
f.reduce({}) { |h, (k, v)| h[k] = truncate_string(v); h }
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
module H2
|
2
|
+
class Server
|
3
|
+
|
4
|
+
# 'h2' server - for TLS 1.2 ALPN HTTP/2 connection
|
5
|
+
#
|
6
|
+
# @see https://tools.ietf.org/html/rfc7540#section-3.3
|
7
|
+
#
|
8
|
+
class HTTPS < H2::Server
|
9
|
+
|
10
|
+
ALPN_PROTOCOL = 'h2'
|
11
|
+
ALPN_SELECT_CALLBACK = ->(ps){ ps.find { |p| ALPN_PROTOCOL == p }}
|
12
|
+
ECDH_CURVES = 'P-256'
|
13
|
+
TMP_ECDH_CALLBACK = ->(*_){ OpenSSL::PKey::EC.new 'prime256v1' }
|
14
|
+
|
15
|
+
ECDH_OPENSSL_MIN_VERSION = '2.0'
|
16
|
+
|
17
|
+
# create a new h2 server that uses SNI to determine TLS cert/key to use
|
18
|
+
#
|
19
|
+
# @see https://en.wikipedia.org/wiki/Server_Name_Indication
|
20
|
+
#
|
21
|
+
# @param [String] host the IP address for this server to listen on
|
22
|
+
# @param [Integer] port the TCP port for this server to listen on
|
23
|
+
# @param [Hash] sni the SNI option hash with certs/keys for domains
|
24
|
+
# @param [Hash] options
|
25
|
+
#
|
26
|
+
# @option [String] :cert TLS certificate
|
27
|
+
# @option [String] :extra_chain_cert TLS certificate
|
28
|
+
# @option [String] :key TLS key
|
29
|
+
#
|
30
|
+
# == SNI options with default callback
|
31
|
+
#
|
32
|
+
# [:sni] Hash with domain name +String+ keys and +Hash+ values:
|
33
|
+
# [:cert] +String+ TLS certificate
|
34
|
+
# [:extra_chain_cert] +String+ TLS certificate
|
35
|
+
# [:key] +String+ TLS key
|
36
|
+
#
|
37
|
+
# == SNI options with _custom_ callback
|
38
|
+
#
|
39
|
+
# [:sni] Hash:
|
40
|
+
# [:callback] +Proc+ creates +OpenSSL::SSL::SSLContext+ for each
|
41
|
+
# connection
|
42
|
+
#
|
43
|
+
def initialize host:, port:, sni: {}, **options, &on_connection
|
44
|
+
@sni = sni
|
45
|
+
@sni_callback = @sni[:callback] || method(:sni_callback)
|
46
|
+
@tcpserver = Celluloid::IO::TCPServer.new host, port
|
47
|
+
@sslserver = Celluloid::IO::SSLServer.new @tcpserver, create_ssl_context(options)
|
48
|
+
options.merge! host: host, port: port, sni: sni
|
49
|
+
super @sslserver, options, &on_connection
|
50
|
+
end
|
51
|
+
|
52
|
+
# accept a socket connection, possibly attach spy, hand off to +#handle_connection+
|
53
|
+
# asyncronously, repeat
|
54
|
+
#
|
55
|
+
def run
|
56
|
+
loop do
|
57
|
+
begin
|
58
|
+
socket = @server.accept
|
59
|
+
rescue OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::EPIPE,
|
60
|
+
Errno::ETIMEDOUT, Errno::EHOSTUNREACH => ex
|
61
|
+
Logger.warn "Error accepting SSLSocket: #{ex.class}: #{ex.to_s}"
|
62
|
+
retry
|
63
|
+
end
|
64
|
+
|
65
|
+
socket = ::Reel::Spy.new(socket, @spy) if @spy
|
66
|
+
async.handle_connection socket
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# default SNI callback - builds SSLContext from cert/key by domain name in +@sni+
|
73
|
+
# or returns existing one if name is not found
|
74
|
+
#
|
75
|
+
def sni_callback args
|
76
|
+
socket, name = args
|
77
|
+
@contexts ||= {}
|
78
|
+
if @contexts[name]
|
79
|
+
@contexts[name]
|
80
|
+
elsif sni_opts = @sni[name] and Hash === sni_opts
|
81
|
+
@contexts[name] = create_ssl_context sni_opts
|
82
|
+
else
|
83
|
+
socket.context
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# builds a new SSLContext suitable for use in 'h2' connections
|
88
|
+
#
|
89
|
+
def create_ssl_context **opts
|
90
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
91
|
+
ctx.ca_file = opts[:ca_file] if opts[:ca_file]
|
92
|
+
ctx.ca_path = opts[:ca_path] if opts[:ca_path]
|
93
|
+
ctx.cert = context_cert opts[:cert]
|
94
|
+
ctx.ciphers = opts[:ciphers] || OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:ciphers]
|
95
|
+
ctx.extra_chain_cert = context_extra_chain_cert opts[:extra_chain_cert]
|
96
|
+
ctx.key = context_key opts[:key]
|
97
|
+
ctx.options = opts[:options] || OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options]
|
98
|
+
ctx.servername_cb = @sni_callback
|
99
|
+
ctx.ssl_version = :TLSv1_2
|
100
|
+
context_ecdh ctx
|
101
|
+
|
102
|
+
# https://github.com/jruby/jruby-openssl/issues/99
|
103
|
+
context_set_protocols ctx unless H2.jruby?
|
104
|
+
|
105
|
+
ctx
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
if OpenSSL::VERSION >= ECDH_OPENSSL_MIN_VERSION && RUBY_VERSION >= '2.3'
|
111
|
+
def context_ecdh ctx
|
112
|
+
ctx.ecdh_curves = ECDH_CURVES
|
113
|
+
end
|
114
|
+
else
|
115
|
+
if H2.jruby? || RUBY_VERSION < '2.3'
|
116
|
+
def context_ecdh ctx
|
117
|
+
end
|
118
|
+
else
|
119
|
+
def context_ecdh ctx
|
120
|
+
ctx.tmp_ecdh_callback = TMP_ECDH_CALLBACK
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def context_cert cert
|
126
|
+
case cert
|
127
|
+
when String
|
128
|
+
cert = File.read cert if File.exist? cert
|
129
|
+
OpenSSL::X509::Certificate.new cert
|
130
|
+
when OpenSSL::X509::Certificate
|
131
|
+
cert
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def context_key key
|
136
|
+
case key
|
137
|
+
when String
|
138
|
+
key = File.read key if File.exist? key
|
139
|
+
OpenSSL::PKey::RSA.new key
|
140
|
+
when OpenSSL::PKey::RSA
|
141
|
+
key
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def context_extra_chain_cert chain
|
146
|
+
case chain
|
147
|
+
when String
|
148
|
+
chain = File.read chain if File.exist? chain
|
149
|
+
[OpenSSL::X509::Certificate.new(chain)]
|
150
|
+
when OpenSSL::X509::Certificate
|
151
|
+
[chain]
|
152
|
+
when Array
|
153
|
+
chain
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
if H2.alpn?
|
158
|
+
def context_set_protocols ctx
|
159
|
+
ctx.alpn_protocols = [ALPN_PROTOCOL]
|
160
|
+
ctx.alpn_select_cb = ALPN_SELECT_CALLBACK
|
161
|
+
end
|
162
|
+
else
|
163
|
+
def context_set_protocols ctx
|
164
|
+
ctx.npn_protocols = [ALPN_PROTOCOL]
|
165
|
+
ctx.npn_select_cb = ALPN_SELECT_CALLBACK
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module H2
|
2
|
+
class Server
|
3
|
+
class PushPromise
|
4
|
+
|
5
|
+
GET = 'GET'
|
6
|
+
STATUS = '200'
|
7
|
+
|
8
|
+
attr_reader :content_length, :path, :push_stream
|
9
|
+
|
10
|
+
# build a new +PushPromise+ for the path, with the headers and body given
|
11
|
+
#
|
12
|
+
def initialize path, body_or_headers = {}, body = nil
|
13
|
+
@path = path
|
14
|
+
if Hash === body_or_headers
|
15
|
+
headers = body_or_headers.dup
|
16
|
+
@body = body
|
17
|
+
else
|
18
|
+
headers = {}
|
19
|
+
@body = body_or_headers
|
20
|
+
end
|
21
|
+
|
22
|
+
@promise_headers = {
|
23
|
+
METHOD_KEY => GET,
|
24
|
+
AUTHORITY_KEY => headers.delete(AUTHORITY_KEY),
|
25
|
+
PATH_KEY => @path,
|
26
|
+
SCHEME_KEY => headers.delete(SCHEME_KEY)
|
27
|
+
}
|
28
|
+
|
29
|
+
@content_length = @body.bytesize.to_s
|
30
|
+
|
31
|
+
@push_headers = {
|
32
|
+
STATUS_KEY => STATUS,
|
33
|
+
CONTENT_LENGTH_KEY => @content_length
|
34
|
+
}.merge headers
|
35
|
+
|
36
|
+
@fsm = FSM.new
|
37
|
+
end
|
38
|
+
|
39
|
+
# create a new promise stream from +stream+, send the headers and set
|
40
|
+
# +@push_stream+ from the callback
|
41
|
+
#
|
42
|
+
def make_on stream
|
43
|
+
return unless @fsm.state == :init
|
44
|
+
@stream = stream
|
45
|
+
@stream.stream.promise(@promise_headers, end_headers: false) do |push|
|
46
|
+
push.headers @push_headers
|
47
|
+
@push_stream = push
|
48
|
+
@push_stream.on(:close){|err| cancel! if err == :cancel}
|
49
|
+
end
|
50
|
+
@fsm.transition :made
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
def keep_async
|
55
|
+
@stream.connection.server.async.handle_push_promise self
|
56
|
+
end
|
57
|
+
|
58
|
+
# deliver the body for this promise, optionally splicing into +size+ chunks
|
59
|
+
#
|
60
|
+
def keep size = nil
|
61
|
+
return false unless @fsm.state == :made
|
62
|
+
|
63
|
+
if size.nil?
|
64
|
+
@push_stream.data @body
|
65
|
+
else
|
66
|
+
body = @body
|
67
|
+
|
68
|
+
if body.bytesize > size
|
69
|
+
body = @body.dup
|
70
|
+
while body.bytesize > size
|
71
|
+
@push_stream.data body.slice!(0, size), end_stream: false
|
72
|
+
yield if block_given?
|
73
|
+
end
|
74
|
+
else
|
75
|
+
yield if block_given?
|
76
|
+
end
|
77
|
+
|
78
|
+
@push_stream.data body
|
79
|
+
end
|
80
|
+
|
81
|
+
@fsm.transition :kept
|
82
|
+
log :info, self
|
83
|
+
@stream.on_complete
|
84
|
+
end
|
85
|
+
|
86
|
+
def kept?
|
87
|
+
@fsm.state == :kept
|
88
|
+
end
|
89
|
+
|
90
|
+
def canceled?
|
91
|
+
@fsm.state == :canceled
|
92
|
+
end
|
93
|
+
|
94
|
+
# cancel this promise, most likely due to a RST_STREAM frame from the
|
95
|
+
# client (already in cache, etc...)
|
96
|
+
#
|
97
|
+
def cancel!
|
98
|
+
@fsm.transition :canceled
|
99
|
+
@stream.on_complete
|
100
|
+
end
|
101
|
+
|
102
|
+
def log level, msg
|
103
|
+
Logger.__send__ level, "[stream #{@push_stream.id}] #{msg}"
|
104
|
+
end
|
105
|
+
|
106
|
+
def to_s
|
107
|
+
request = @stream.request
|
108
|
+
%{#{request.addr} "push #{@path} HTTP/2" #{STATUS} #{@content_length}}
|
109
|
+
end
|
110
|
+
alias to_str to_s
|
111
|
+
|
112
|
+
# simple state machine to guarantee promise process
|
113
|
+
#
|
114
|
+
class FSM
|
115
|
+
include Celluloid::FSM
|
116
|
+
default_state :init
|
117
|
+
state :init, to: [:canceled, :made]
|
118
|
+
state :made, to: [:canceled, :kept]
|
119
|
+
state :kept
|
120
|
+
state :canceled
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module H2
|
2
|
+
class Server
|
3
|
+
class Stream
|
4
|
+
class Request
|
5
|
+
|
6
|
+
# a case-insensitive hash that also handles symbol translation i.e. s/_/-/
|
7
|
+
#
|
8
|
+
HEADER_HASH = Hash.new do |hash, key|
|
9
|
+
k = key.to_s.upcase
|
10
|
+
k.gsub! '_', '-' if Symbol === key
|
11
|
+
_, value = hash.find {|header_key,v| header_key.upcase == k}
|
12
|
+
hash[key] = value if value
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :body, :headers, :stream
|
16
|
+
|
17
|
+
def initialize stream
|
18
|
+
@stream = stream
|
19
|
+
@headers = HEADER_HASH.dup
|
20
|
+
@body = ''
|
21
|
+
end
|
22
|
+
|
23
|
+
# retreive the IP address of the connection
|
24
|
+
#
|
25
|
+
def addr
|
26
|
+
@addr ||= @stream.connection.socket.peeraddr[3] rescue nil
|
27
|
+
end
|
28
|
+
|
29
|
+
# retreive the authority from the stream request headers
|
30
|
+
#
|
31
|
+
def authority
|
32
|
+
@authority ||= headers[AUTHORITY_KEY]
|
33
|
+
end
|
34
|
+
|
35
|
+
# retreive the HTTP method as a lowercase +Symbol+
|
36
|
+
#
|
37
|
+
def method
|
38
|
+
return @method unless @method.nil?
|
39
|
+
@method = headers[METHOD_KEY]
|
40
|
+
@method = @method.downcase.to_sym if @method
|
41
|
+
@method
|
42
|
+
end
|
43
|
+
|
44
|
+
# retreive the path from the stream request headers
|
45
|
+
#
|
46
|
+
def path
|
47
|
+
@path ||= headers[PATH_KEY]
|
48
|
+
end
|
49
|
+
|
50
|
+
# retreive the scheme from the stream request headers
|
51
|
+
#
|
52
|
+
def scheme
|
53
|
+
@scheme ||= headers[SCHEME_KEY]
|
54
|
+
end
|
55
|
+
|
56
|
+
# respond to this request on its stream
|
57
|
+
#
|
58
|
+
def respond response, body_or_headers = nil, body = nil
|
59
|
+
@stream.respond response, body_or_headers, body
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module H2
|
2
|
+
class Server
|
3
|
+
class Stream
|
4
|
+
class Response
|
5
|
+
|
6
|
+
attr_reader :body, :content_length, :headers, :status, :stream
|
7
|
+
|
8
|
+
# build a new +Response+ object
|
9
|
+
#
|
10
|
+
def initialize stream:, status:, headers: {}, body: ''
|
11
|
+
@stream = stream
|
12
|
+
@headers = headers
|
13
|
+
@body = body
|
14
|
+
self.status = status
|
15
|
+
|
16
|
+
init_content_length
|
17
|
+
end
|
18
|
+
|
19
|
+
# sets the content length in the headers by the byte size of +@body+
|
20
|
+
#
|
21
|
+
def init_content_length
|
22
|
+
@content_length = case @body
|
23
|
+
when String
|
24
|
+
@body.bytesize
|
25
|
+
when IO
|
26
|
+
@body.stat.size
|
27
|
+
when NilClass
|
28
|
+
'0'
|
29
|
+
else
|
30
|
+
raise TypeError, "can't render #{@body.class} as a response body"
|
31
|
+
end
|
32
|
+
|
33
|
+
unless @headers.any? {|k,_| k.downcase == CONTENT_LENGTH_KEY}
|
34
|
+
@headers[CONTENT_LENGTH_KEY] = @content_length
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# the corresponding +Request+ to this +Response+
|
39
|
+
#
|
40
|
+
def request
|
41
|
+
stream.request
|
42
|
+
end
|
43
|
+
|
44
|
+
# send the headers and body out on +s+, an +HTTP2::Stream+ object
|
45
|
+
#
|
46
|
+
# NOTE: +:status+ must come first?
|
47
|
+
#
|
48
|
+
def respond_on s
|
49
|
+
headers = { STATUS_KEY => @status.to_s }.merge @headers
|
50
|
+
s.headers stringify_headers(headers)
|
51
|
+
case @body
|
52
|
+
when String
|
53
|
+
s.data @body
|
54
|
+
when IO
|
55
|
+
raise NotImplementedError # TODO
|
56
|
+
else
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# sets +@status+ either from given integer value (HTTP status code) or by
|
61
|
+
# mapping a +Symbol+ in +Reel::Response::SYMBOL_TO_STATUS_CODE+ to one
|
62
|
+
#
|
63
|
+
def status= status
|
64
|
+
case status
|
65
|
+
when Integer
|
66
|
+
@status = status
|
67
|
+
when Symbol
|
68
|
+
if code = ::Reel::Response::SYMBOL_TO_STATUS_CODE[status]
|
69
|
+
self.status = code
|
70
|
+
else
|
71
|
+
raise ArgumentError, "unrecognized status symbol: #{status}"
|
72
|
+
end
|
73
|
+
else
|
74
|
+
raise TypeError, "invalid status type: #{status.inspect}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_s
|
79
|
+
%{#{request.addr} "#{request.method} #{request.path} HTTP/2" #{status} #{content_length}}
|
80
|
+
end
|
81
|
+
alias to_str to_s
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def stringify_headers hash
|
86
|
+
hash.keys.each do |k|
|
87
|
+
hash[k] = hash[k].to_s unless String === hash[k]
|
88
|
+
if Symbol === k
|
89
|
+
key = k.to_s.gsub '_', '-'
|
90
|
+
hash[key] = hash.delete k
|
91
|
+
end
|
92
|
+
end
|
93
|
+
hash
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
require 'h2/server/stream/request'
|
2
|
+
require 'h2/server/stream/response'
|
3
|
+
require 'h2/server/push_promise'
|
4
|
+
|
5
|
+
module H2
|
6
|
+
class Server
|
7
|
+
class Stream
|
8
|
+
|
9
|
+
# each stream event method is wrapped in a block to call a local instance
|
10
|
+
# method of the same name
|
11
|
+
#
|
12
|
+
STREAM_EVENTS = [
|
13
|
+
:active,
|
14
|
+
:close,
|
15
|
+
:half_close
|
16
|
+
]
|
17
|
+
|
18
|
+
# the above take only the event, the following receive both the event
|
19
|
+
# and the data
|
20
|
+
#
|
21
|
+
STREAM_DATA_EVENTS = [
|
22
|
+
:headers,
|
23
|
+
:data
|
24
|
+
]
|
25
|
+
|
26
|
+
attr_reader :connection,
|
27
|
+
:push_promises,
|
28
|
+
:request,
|
29
|
+
:response,
|
30
|
+
:stream
|
31
|
+
|
32
|
+
def initialize connection:, stream:
|
33
|
+
@closed = false
|
34
|
+
@completed = false
|
35
|
+
@connection = connection
|
36
|
+
@push_promises = Set.new
|
37
|
+
@responded = false
|
38
|
+
@stream = stream
|
39
|
+
|
40
|
+
bind_events
|
41
|
+
end
|
42
|
+
|
43
|
+
# mimicing Reel::Connection#respond
|
44
|
+
#
|
45
|
+
# write status, headers, and data to +@stream+
|
46
|
+
#
|
47
|
+
def respond response, body_or_headers = nil, body = nil
|
48
|
+
|
49
|
+
# :/
|
50
|
+
#
|
51
|
+
if Hash === body_or_headers
|
52
|
+
headers = body_or_headers
|
53
|
+
body ||= ''
|
54
|
+
else
|
55
|
+
headers = {}
|
56
|
+
body = body_or_headers ||= ''
|
57
|
+
end
|
58
|
+
|
59
|
+
@response = case response
|
60
|
+
when Symbol, Integer
|
61
|
+
response = Response.new stream: self,
|
62
|
+
status: response,
|
63
|
+
headers: headers,
|
64
|
+
body: body
|
65
|
+
when Response
|
66
|
+
response
|
67
|
+
else raise TypeError, "invalid response: #{response.inspect}"
|
68
|
+
end
|
69
|
+
|
70
|
+
if @closed
|
71
|
+
log :warn, 'stream closed before response sent'
|
72
|
+
else
|
73
|
+
response.respond_on(stream)
|
74
|
+
log :info, response
|
75
|
+
@responded = true
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# create a push promise, send the headers, then queue an asynchronous
|
80
|
+
# task on the reactor to deliver the data
|
81
|
+
#
|
82
|
+
def push_promise *args
|
83
|
+
pp = push_promise_for *args
|
84
|
+
make_promise pp
|
85
|
+
@connection.server.async.handle_push_promise pp
|
86
|
+
end
|
87
|
+
|
88
|
+
# create a push promise - mimicing Reel::Connection#respond
|
89
|
+
#
|
90
|
+
def push_promise_for path, body_or_headers = {}, body = nil
|
91
|
+
|
92
|
+
# :/
|
93
|
+
#
|
94
|
+
case body_or_headers
|
95
|
+
when Hash
|
96
|
+
headers = body_or_headers
|
97
|
+
else
|
98
|
+
headers = {}
|
99
|
+
body = body_or_headers
|
100
|
+
end
|
101
|
+
|
102
|
+
headers.merge! AUTHORITY_KEY => @request.authority,
|
103
|
+
SCHEME_KEY => @request.scheme
|
104
|
+
|
105
|
+
PushPromise.new path, headers, body
|
106
|
+
end
|
107
|
+
|
108
|
+
# begin the new push promise stream from this +@stream+ by sending the
|
109
|
+
# initial headers frame
|
110
|
+
#
|
111
|
+
# @see +PushPromise#make_on!+
|
112
|
+
# @see +HTTP2::Stream#promise+
|
113
|
+
#
|
114
|
+
def make_promise p
|
115
|
+
p.make_on self
|
116
|
+
push_promises << p
|
117
|
+
p
|
118
|
+
end
|
119
|
+
|
120
|
+
# set or call +@complete+ callback
|
121
|
+
#
|
122
|
+
def on_complete &block
|
123
|
+
return true if @completed
|
124
|
+
if block
|
125
|
+
@complete = block
|
126
|
+
elsif @completed = (@responded and push_promises_complete?)
|
127
|
+
@complete[] if Proc === @complete
|
128
|
+
true
|
129
|
+
else
|
130
|
+
false
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# check for push promises completion
|
135
|
+
#
|
136
|
+
def push_promises_complete?
|
137
|
+
@push_promises.empty? or @push_promises.all? {|p| p.kept? or p.canceled?}
|
138
|
+
end
|
139
|
+
|
140
|
+
# trigger a GOAWAY frame when this stream is responded to and any/all push
|
141
|
+
# promises are complete
|
142
|
+
#
|
143
|
+
def goaway_on_complete
|
144
|
+
on_complete { connection.goaway }
|
145
|
+
end
|
146
|
+
|
147
|
+
# logging helper
|
148
|
+
#
|
149
|
+
def log level, msg
|
150
|
+
Logger.__send__ level, "[stream #{@stream.id}] #{msg}"
|
151
|
+
end
|
152
|
+
|
153
|
+
protected
|
154
|
+
|
155
|
+
# bind parser events to this instance
|
156
|
+
#
|
157
|
+
def bind_events
|
158
|
+
STREAM_EVENTS.each do |e|
|
159
|
+
on = "on_#{e}".to_sym
|
160
|
+
@stream.on(e) { __send__ on }
|
161
|
+
end
|
162
|
+
STREAM_DATA_EVENTS.each do |e|
|
163
|
+
on = "on_#{e}".to_sym
|
164
|
+
@stream.on(e) { |x| __send__ on, x }
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# called by +@stream+ when this stream is activated
|
169
|
+
#
|
170
|
+
def on_active
|
171
|
+
log :debug, 'active' if H2.verbose?
|
172
|
+
@request = H2::Server::Stream::Request.new self
|
173
|
+
end
|
174
|
+
|
175
|
+
# called by +@stream+ when this stream is closed
|
176
|
+
#
|
177
|
+
def on_close
|
178
|
+
log :debug, 'close' if H2.verbose?
|
179
|
+
on_complete
|
180
|
+
@closed = true
|
181
|
+
end
|
182
|
+
|
183
|
+
# called by +@stream+ with a +Hash+
|
184
|
+
#
|
185
|
+
def on_headers h
|
186
|
+
incoming_headers = Hash[h]
|
187
|
+
log :debug, "headers: #{incoming_headers}" if H2.verbose?
|
188
|
+
@request.headers.merge! incoming_headers
|
189
|
+
end
|
190
|
+
|
191
|
+
# called by +@stream+ with a +String+ body part
|
192
|
+
#
|
193
|
+
def on_data d
|
194
|
+
log :debug, "data: <<#{d}>>" if H2.verbose?
|
195
|
+
@request.body << d
|
196
|
+
end
|
197
|
+
|
198
|
+
# called by +@stream+ when body/request is complete, signaling that client
|
199
|
+
# is ready for response(s)
|
200
|
+
#
|
201
|
+
def on_half_close
|
202
|
+
log :debug, 'half_close' if H2.verbose?
|
203
|
+
connection.server.async.handle_stream self
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
data/lib/h2/server.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'celluloid/current'
|
2
|
+
require 'logger'
|
3
|
+
require 'reel'
|
4
|
+
require 'h2/reel/ext'
|
5
|
+
require 'h2'
|
6
|
+
|
7
|
+
module H2
|
8
|
+
|
9
|
+
CONTENT_LENGTH_KEY = 'content-length'
|
10
|
+
|
11
|
+
Logger = ::Logger.new STDOUT
|
12
|
+
|
13
|
+
class << self
|
14
|
+
|
15
|
+
def alpn?
|
16
|
+
!jruby? && OpenSSL::OPENSSL_VERSION_NUMBER >= ALPN_OPENSSL_MIN_VERSION && RUBY_VERSION >= '2.3'
|
17
|
+
end
|
18
|
+
|
19
|
+
def jruby?
|
20
|
+
return @jruby if defined? @jruby
|
21
|
+
@jruby = RUBY_ENGINE == 'jruby'
|
22
|
+
end
|
23
|
+
|
24
|
+
# turn on extra verbose debug logging
|
25
|
+
#
|
26
|
+
def verbose!
|
27
|
+
@verbose = true
|
28
|
+
end
|
29
|
+
|
30
|
+
def verbose?
|
31
|
+
@verbose = false unless defined?(@verbose)
|
32
|
+
@verbose
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
# base H2 server, a direct subclass of +Reel::Server+
|
38
|
+
#
|
39
|
+
class Server < ::Reel::Server
|
40
|
+
|
41
|
+
def initialize server, **options, &on_connection
|
42
|
+
@on_connection = on_connection
|
43
|
+
super server, options
|
44
|
+
end
|
45
|
+
|
46
|
+
# build a new connection object, run it through the given block, and
|
47
|
+
# start reading from the socket if still attached
|
48
|
+
#
|
49
|
+
def handle_connection socket
|
50
|
+
connection = H2::Server::Connection.new socket: socket, server: self
|
51
|
+
@on_connection[connection]
|
52
|
+
connection.read if connection.attached?
|
53
|
+
end
|
54
|
+
|
55
|
+
# async stream handling
|
56
|
+
#
|
57
|
+
def handle_stream stream
|
58
|
+
stream.connection.each_stream[stream]
|
59
|
+
end
|
60
|
+
|
61
|
+
# async push promise
|
62
|
+
#
|
63
|
+
def handle_push_promise push_promise
|
64
|
+
push_promise.keep
|
65
|
+
end
|
66
|
+
|
67
|
+
# async goaway
|
68
|
+
#
|
69
|
+
def goaway connection
|
70
|
+
sleep 0.25
|
71
|
+
connection.parser.goaway unless connection.closed?
|
72
|
+
end
|
73
|
+
|
74
|
+
# 'h2c' server - for plaintext HTTP/2 connection
|
75
|
+
#
|
76
|
+
# NOTE: browsers don't support this and probably never will
|
77
|
+
#
|
78
|
+
# @see https://tools.ietf.org/html/rfc7540#section-3.4
|
79
|
+
# @see https://hpbn.co/http2/#upgrading-to-http2
|
80
|
+
#
|
81
|
+
class HTTP < H2::Server
|
82
|
+
|
83
|
+
# create a new h2c server
|
84
|
+
#
|
85
|
+
def initialize host:, port:, **options, &on_connection
|
86
|
+
@tcpserver = Celluloid::IO::TCPServer.new host, port
|
87
|
+
options.merge! host: host, port: port
|
88
|
+
super @tcpserver, options, &on_connection
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
require 'h2/server/connection'
|
98
|
+
require 'h2/server/https'
|
data/lib/h2/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: h2
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kenichi Nakamura
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-07-
|
11
|
+
date: 2018-07-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: http-2
|
@@ -16,20 +16,20 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '0.
|
19
|
+
version: '0.9'
|
20
20
|
- - ">="
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: 0.
|
22
|
+
version: 0.9.1
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
26
26
|
requirements:
|
27
27
|
- - "~>"
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: '0.
|
29
|
+
version: '0.9'
|
30
30
|
- - ">="
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: 0.
|
32
|
+
version: 0.9.1
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
34
|
name: colored
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -86,7 +86,7 @@ dependencies:
|
|
86
86
|
- - "~>"
|
87
87
|
- !ruby/object:Gem::Version
|
88
88
|
version: '5.0'
|
89
|
-
description: a pure ruby http/2 client based on http-2
|
89
|
+
description: a pure ruby http/2 client & server based on http-2
|
90
90
|
email:
|
91
91
|
- kenichi.nakamura@gmail.com
|
92
92
|
executables:
|
@@ -104,6 +104,10 @@ files:
|
|
104
104
|
- README.md
|
105
105
|
- Rakefile
|
106
106
|
- bin/console
|
107
|
+
- examples/server/dog.png
|
108
|
+
- examples/server/hello_world.rb
|
109
|
+
- examples/server/https_hello_world.rb
|
110
|
+
- examples/server/push_promise.rb
|
107
111
|
- exe/h2
|
108
112
|
- h2.gemspec
|
109
113
|
- lib/h2.rb
|
@@ -111,6 +115,14 @@ files:
|
|
111
115
|
- lib/h2/client/celluloid.rb
|
112
116
|
- lib/h2/client/concurrent.rb
|
113
117
|
- lib/h2/client/tcp_socket.rb
|
118
|
+
- lib/h2/reel/ext.rb
|
119
|
+
- lib/h2/server.rb
|
120
|
+
- lib/h2/server/connection.rb
|
121
|
+
- lib/h2/server/https.rb
|
122
|
+
- lib/h2/server/push_promise.rb
|
123
|
+
- lib/h2/server/stream.rb
|
124
|
+
- lib/h2/server/stream/request.rb
|
125
|
+
- lib/h2/server/stream/response.rb
|
114
126
|
- lib/h2/stream.rb
|
115
127
|
- lib/h2/version.rb
|
116
128
|
homepage: https://github.com/kenichi/h2
|
@@ -136,5 +148,5 @@ rubyforge_project:
|
|
136
148
|
rubygems_version: 2.7.6
|
137
149
|
signing_key:
|
138
150
|
specification_version: 4
|
139
|
-
summary: an http/2 client based on http-2
|
151
|
+
summary: an http/2 client & server based on http-2
|
140
152
|
test_files: []
|