h2 0.4.1 → 0.5.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 +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
|
[](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: []
|