h2 0.8.1 → 0.8.2
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/CHANGELOG.md +5 -0
- data/Gemfile +1 -1
- data/examples/server/push_promise.rb +12 -6
- data/examples/server/sse.rb +14 -5
- data/exe/h2 +36 -17
- data/lib/h2.rb +30 -0
- data/lib/h2/client.rb +1 -12
- data/lib/h2/server.rb +9 -4
- data/lib/h2/server/stream.rb +37 -5
- data/lib/h2/server/stream/event_source.rb +43 -2
- data/lib/h2/server/stream/push_promise.rb +123 -0
- data/lib/h2/server/stream/response.rb +2 -0
- data/lib/h2/stream.rb +28 -2
- data/lib/h2/version.rb +1 -1
- metadata +3 -3
- data/lib/h2/server/push_promise.rb +0 -119
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 49b62827cc586f06022739232d3d7ea24a1b0744e1feb157ba24e0c65a0e9ba2
|
4
|
+
data.tar.gz: 90f546cf67e7502a0c64c6df96c3cf1b10c059efa1ab88e134cde4a34225e76e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2639436d0e0aa98250f097bca6e6250c9e6f30cf732f58bd8a18fa1accb81d026db7e34af9e2d98fc5cebc2487d503eb3b3c8f27f226c82a3f0356ac1bee40a2
|
7
|
+
data.tar.gz: 0aeae299e9d56b22962a344cf463ed9940bb93e58e82d00fdf2da052c768a508a53937136c71efc81ad82aa83e205e216726b1bf25d88cf5fe6cab9fbd7521e2
|
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
@@ -18,8 +18,9 @@ require File.expand_path '../../../test/support/create_certs', __FILE__
|
|
18
18
|
# crank up the logger level for testing/example purposes
|
19
19
|
#
|
20
20
|
H2::Logger.level = ::Logger::DEBUG
|
21
|
+
H2::Server::Connection.include H2::FrameDebugger
|
21
22
|
|
22
|
-
port = 1234
|
23
|
+
port = 4430#1234
|
23
24
|
addr = Socket.getaddrinfo('localhost', port).first[3]
|
24
25
|
puts "*** Starting server on https://localhost:#{port}"
|
25
26
|
|
@@ -69,6 +70,11 @@ sni = {
|
|
69
70
|
:cert => certs_dir + '/example.com.crt',
|
70
71
|
:key => certs_dir + '/example.com.key',
|
71
72
|
:extra_chain_cert => certs_dir + '/example.com-chain.pem'
|
73
|
+
},
|
74
|
+
'vux.nakamura.io' => {
|
75
|
+
cert: certs_dir + '/nakamuraio.crt',
|
76
|
+
key: certs_dir + '/nakamuraio.key',
|
77
|
+
extra_chain_cert: certs_dir + '/nakamuraio-chain.pem'
|
72
78
|
}
|
73
79
|
}
|
74
80
|
|
@@ -87,7 +93,7 @@ s = H2::Server::HTTPS.new host: addr, port: port, sni: sni do |connection|
|
|
87
93
|
#
|
88
94
|
# see +H2::Server::Stream#request+
|
89
95
|
#
|
90
|
-
if stream.request.path
|
96
|
+
if stream.request.path != '/'
|
91
97
|
|
92
98
|
# since this is a TLS-enabled server, we could actually test it with a
|
93
99
|
# real browser, which will undoubtedly request /favicon.ico.
|
@@ -133,9 +139,9 @@ s = H2::Server::HTTPS.new host: addr, port: port, sni: sni do |connection|
|
|
133
139
|
# have this +H2::Server::PushPromise+ initiate the sub-stream on this
|
134
140
|
# stream by sending initial headers.
|
135
141
|
#
|
136
|
-
# see +H2::Server::
|
142
|
+
# see +H2::Server::Stream::#make_promise+
|
137
143
|
#
|
138
|
-
|
144
|
+
stream.make_promise js_promise
|
139
145
|
|
140
146
|
# respond with 200 and HTML body
|
141
147
|
#
|
@@ -150,8 +156,8 @@ s = H2::Server::HTTPS.new host: addr, port: port, sni: sni do |connection|
|
|
150
156
|
# but for `js_promise`, we must keep it ourselves. in this case, we keep
|
151
157
|
# it "synchronously", but we may also call `#keep_async` to queue it.
|
152
158
|
#
|
153
|
-
# see +H2::Server::PushPromise#keep+
|
154
|
-
# see +H2::Server::PushPromise#keep_async+
|
159
|
+
# see +H2::Server::Stream::PushPromise#keep+
|
160
|
+
# see +H2::Server::Stream::PushPromise#keep_async+
|
155
161
|
#
|
156
162
|
js_promise.keep
|
157
163
|
|
data/examples/server/sse.rb
CHANGED
@@ -18,8 +18,9 @@ require File.expand_path '../../../test/support/create_certs', __FILE__
|
|
18
18
|
# crank up the logger level for testing/example purposes
|
19
19
|
#
|
20
20
|
H2::Logger.level = ::Logger::DEBUG
|
21
|
+
H2::Server::Connection.include H2::FrameDebugger
|
21
22
|
|
22
|
-
port = 1234
|
23
|
+
port = 4430#1234
|
23
24
|
addr = Socket.getaddrinfo('localhost', port).first[3]
|
24
25
|
puts "*** Starting server on https://localhost:#{port}"
|
25
26
|
|
@@ -32,9 +33,14 @@ puts "*** Starting server on https://localhost:#{port}"
|
|
32
33
|
certs_dir = File.expand_path '../../../tmp/certs', __FILE__
|
33
34
|
sni = {
|
34
35
|
'localhost' => {
|
35
|
-
:
|
36
|
-
:
|
37
|
-
# :
|
36
|
+
cert: certs_dir + '/server.crt',
|
37
|
+
key: certs_dir + '/server.key',
|
38
|
+
# extra_chain_cert: certs_dir + '/chain.pem'
|
39
|
+
},
|
40
|
+
'vux.nakamura.io' => {
|
41
|
+
cert: certs_dir + '/nakamuraio.crt',
|
42
|
+
key: certs_dir + '/nakamuraio.key',
|
43
|
+
extra_chain_cert: certs_dir + '/nakamuraio-chain.pem'
|
38
44
|
}
|
39
45
|
}
|
40
46
|
|
@@ -147,7 +153,7 @@ s = H2::Server::HTTPS.new host: addr, port: port, sni: sni do |connection|
|
|
147
153
|
stream.respond status: 404,
|
148
154
|
body: "should have been pushed..."
|
149
155
|
|
150
|
-
|
156
|
+
when '/'
|
151
157
|
|
152
158
|
# initiate a push promise sub-stream, and queue the "keep" handler.
|
153
159
|
# since a push promise may be canceled, we queue the handler on the server reactor,
|
@@ -166,6 +172,9 @@ s = H2::Server::HTTPS.new host: addr, port: port, sni: sni do |connection|
|
|
166
172
|
#
|
167
173
|
stream.respond status: 200, body: data[:html]
|
168
174
|
|
175
|
+
else
|
176
|
+
stream.respond status: 404
|
177
|
+
|
169
178
|
end
|
170
179
|
end
|
171
180
|
end
|
data/exe/h2
CHANGED
@@ -19,10 +19,13 @@ end # }}}
|
|
19
19
|
options = {
|
20
20
|
body: nil,
|
21
21
|
block: false,
|
22
|
-
|
22
|
+
celluloid: false,
|
23
|
+
concurrent: false,
|
24
|
+
deflate: false,
|
23
25
|
headers: {},
|
24
26
|
goaway: false,
|
25
|
-
|
27
|
+
gzip: false,
|
28
|
+
method: nil,
|
26
29
|
tls: {},
|
27
30
|
verbose: false
|
28
31
|
}
|
@@ -43,23 +46,31 @@ OptionParser.new do |o|
|
|
43
46
|
end
|
44
47
|
|
45
48
|
o.on '--celluloid', 'use celluloid actor pool' do
|
49
|
+
raise ArgumentError, '--celluloid and --concurrent are mutually exclusive' if options[:concurrent]
|
46
50
|
require 'h2/client/celluloid'
|
51
|
+
options[:celluloid] = true
|
47
52
|
end
|
48
53
|
|
49
54
|
o.on '--concurrent', 'use concurrent-ruby thread pool' do
|
55
|
+
raise ArgumentError, '--celluloid and --concurrent are mutually exclusive' if options[:celluloid]
|
50
56
|
require 'h2/client/concurrent'
|
57
|
+
options[:concurrent] = true
|
51
58
|
end
|
52
59
|
|
53
|
-
o.on '-d', '--data [DATA]', String, 'post body data' do |d|
|
54
|
-
options[:method]
|
60
|
+
o.on '-d', '--data [DATA]', String, 'post body data (implies POST, override with -X)' do |d|
|
61
|
+
options[:method] ||= :post
|
55
62
|
options[:body] = d
|
56
63
|
end
|
57
64
|
|
58
65
|
o.on '--debug', 'debug output' do
|
59
|
-
|
66
|
+
H2::Client.include H2::FrameDebugger
|
60
67
|
end
|
61
68
|
|
62
|
-
o.on '
|
69
|
+
o.on '--deflate', 'request "deflate" content-encoding' do
|
70
|
+
options[:deflate] = true
|
71
|
+
end
|
72
|
+
|
73
|
+
o.on '-s', '--sse', 'send event-stream headers and print messages as they arrive' do
|
63
74
|
options[:headers]['accept'] = H2::EVENT_SOURCE_CONTENT_TYPE
|
64
75
|
end
|
65
76
|
|
@@ -67,15 +78,19 @@ OptionParser.new do |o|
|
|
67
78
|
options[:goaway] = true
|
68
79
|
end
|
69
80
|
|
81
|
+
o.on '--gzip', 'request "gzip" content-encoding' do
|
82
|
+
options[:gzip] = true
|
83
|
+
end
|
84
|
+
|
70
85
|
o.on '-h', '--help', 'show this help/usage' do
|
71
86
|
puts o
|
72
87
|
exit
|
73
88
|
end
|
74
89
|
|
75
90
|
o.on '-H [VALUE]', '--header [VALUE]', String, 'include header in request (format: "key: value")' do |h|
|
76
|
-
|
91
|
+
raise ArgumentError, "psuedo-headers not supported via CLI" if h[0] == ':'
|
77
92
|
kv = h.split(':').map &:strip
|
78
|
-
options[:headers][kv[0]] = kv[1]
|
93
|
+
options[:headers][kv[0].downcase] = kv[1]
|
79
94
|
end
|
80
95
|
|
81
96
|
o.on '-v', '--verbose', 'turn on verbosity' do
|
@@ -98,6 +113,9 @@ OptionParser.new do |o|
|
|
98
113
|
|
99
114
|
end.parse!
|
100
115
|
|
116
|
+
# start with nil, allow -d and/or -X to set, but default to get
|
117
|
+
options[:method] ||= :get
|
118
|
+
|
101
119
|
# }}}
|
102
120
|
|
103
121
|
# --- parse URL {{{
|
@@ -116,17 +134,18 @@ client = {
|
|
116
134
|
|
117
135
|
client[:tls] = options[:tls] unless options[:tls].empty?
|
118
136
|
|
119
|
-
c = H2::Client.new **client
|
120
|
-
if options[:debug]
|
121
|
-
c.client.on(:frame_received) {|f| puts "<< #{f.inspect}" }
|
122
|
-
c.client.on(:frame_sent) {|f| puts ">> #{f.inspect}" }
|
123
|
-
end
|
124
|
-
end
|
137
|
+
c = H2::Client.new **client
|
125
138
|
|
126
139
|
# }}}
|
127
140
|
|
128
141
|
# --- build & send request {{{
|
129
142
|
|
143
|
+
ce = []
|
144
|
+
ce << :gzip if options[:gzip]
|
145
|
+
ce << :deflate if options[:deflate]
|
146
|
+
ce = ce.join ','
|
147
|
+
options[:headers][H2::ACCEPT_ENCODING_KEY] = ce unless ce.empty?
|
148
|
+
|
130
149
|
request = {
|
131
150
|
body: options[:body],
|
132
151
|
headers: options[:headers],
|
@@ -138,7 +157,7 @@ if options[:verbose]
|
|
138
157
|
method: options[:method],
|
139
158
|
path: url.request_uri,
|
140
159
|
headers: request[:headers]
|
141
|
-
).each {|k,v| puts ">> #{k}: #{v}"
|
160
|
+
).each {|k,v| STDERR.puts ">> #{k}: #{v}" }
|
142
161
|
end
|
143
162
|
|
144
163
|
s = c.__send__ options[:method], **request
|
@@ -148,7 +167,7 @@ s = c.__send__ options[:method], **request
|
|
148
167
|
# --- print response & close {{{
|
149
168
|
|
150
169
|
if options[:verbose]
|
151
|
-
s.headers.each {|k,v| puts "<< #{k}: #{v}"
|
170
|
+
s.headers.each {|k,v| STDERR.puts "<< #{k}: #{v}" }
|
152
171
|
end
|
153
172
|
|
154
173
|
if s.eventsource?
|
@@ -159,7 +178,7 @@ end
|
|
159
178
|
|
160
179
|
c.block! if options[:block] or !s.pushes.empty?
|
161
180
|
s.pushes.each do |p|
|
162
|
-
puts "push promise: #{p.headers[':path']}"
|
181
|
+
STDERR.puts "push promise: #{p.headers[':path']}"
|
163
182
|
end
|
164
183
|
|
165
184
|
c.goaway if options[:goaway]
|
data/lib/h2.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
require 'http/2'
|
4
4
|
require 'logger'
|
5
5
|
require 'uri'
|
6
|
+
require 'zlib'
|
7
|
+
|
6
8
|
require 'h2/version'
|
7
9
|
|
8
10
|
module H2
|
@@ -29,8 +31,13 @@ module H2
|
|
29
31
|
:put
|
30
32
|
]
|
31
33
|
|
34
|
+
ACCEPT_ENCODING_KEY = 'accept-encoding'
|
35
|
+
CONTENT_ENCODING_KEY = 'content-encoding'
|
32
36
|
CONTENT_TYPE_KEY = 'content-type'
|
37
|
+
|
33
38
|
EVENT_SOURCE_CONTENT_TYPE = 'text/event-stream'
|
39
|
+
GZIP_ENCODING = 'gzip'
|
40
|
+
DEFLATE_ENCODING = 'deflate'
|
34
41
|
|
35
42
|
Logger = ::Logger.new STDOUT
|
36
43
|
|
@@ -172,3 +179,26 @@ end
|
|
172
179
|
|
173
180
|
require 'h2/client'
|
174
181
|
require 'h2/stream'
|
182
|
+
|
183
|
+
if RUBY_VERSION < '2.4.0'
|
184
|
+
module Zlib
|
185
|
+
class << self
|
186
|
+
def gunzip string
|
187
|
+
sio = StringIO.new string
|
188
|
+
gz = Zlib::GzipReader.new sio, encoding: Encoding::ASCII_8BIT
|
189
|
+
gz.read
|
190
|
+
ensure
|
191
|
+
gz && gz.close
|
192
|
+
end
|
193
|
+
|
194
|
+
def gzip string, level: nil, strategy: nil
|
195
|
+
sio = StringIO.new
|
196
|
+
sio.binmode
|
197
|
+
gz = Zlib::GzipWriter.new sio, level, strategy
|
198
|
+
gz.write string
|
199
|
+
gz.close
|
200
|
+
sio.string
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
data/lib/h2/client.rb
CHANGED
@@ -5,6 +5,7 @@ require 'h2/client/tcp_socket'
|
|
5
5
|
module H2
|
6
6
|
class Client
|
7
7
|
include Blockable
|
8
|
+
include HeaderStringifier
|
8
9
|
include On
|
9
10
|
|
10
11
|
PARSER_EVENTS = [
|
@@ -152,18 +153,6 @@ module H2
|
|
152
153
|
stream
|
153
154
|
end
|
154
155
|
|
155
|
-
# mutates the given hash into +String+ keys and values
|
156
|
-
#
|
157
|
-
# @param [Hash] hash the headers +Hash+ to stringify
|
158
|
-
#
|
159
|
-
def stringify_headers hash
|
160
|
-
hash.keys.each do |key|
|
161
|
-
hash[key] = hash[key].to_s unless String === hash[key]
|
162
|
-
hash[key.to_s] = hash.delete key unless String === key
|
163
|
-
end
|
164
|
-
hash
|
165
|
-
end
|
166
|
-
|
167
156
|
# builds headers +Hash+ with appropriate ordering
|
168
157
|
#
|
169
158
|
# @see https://http2.github.io/http2-spec/#rfc.section.8.1.2.1
|
data/lib/h2/server.rb
CHANGED
@@ -11,18 +11,23 @@ module H2
|
|
11
11
|
class Server
|
12
12
|
include Celluloid::IO
|
13
13
|
|
14
|
-
|
14
|
+
DEFAULT_OPTIONS = {
|
15
|
+
backlog: 100,
|
16
|
+
deflate: true,
|
17
|
+
gzip: true
|
18
|
+
}
|
15
19
|
|
16
20
|
execute_block_on_receiver :initialize
|
17
21
|
finalizer :shutdown
|
18
22
|
|
23
|
+
attr_reader :options
|
24
|
+
|
19
25
|
def initialize server, **options, &on_connection
|
20
26
|
@server = server
|
21
|
-
@options = options
|
27
|
+
@options = DEFAULT_OPTIONS.merge options
|
22
28
|
@on_connection = on_connection
|
23
29
|
|
24
|
-
|
25
|
-
@server.listen backlog
|
30
|
+
@server.listen @options[:backlog]
|
26
31
|
async.run
|
27
32
|
end
|
28
33
|
|
data/lib/h2/server/stream.rb
CHANGED
@@ -1,8 +1,3 @@
|
|
1
|
-
require 'h2/server/stream/event_source'
|
2
|
-
require 'h2/server/stream/request'
|
3
|
-
require 'h2/server/stream/response'
|
4
|
-
require 'h2/server/push_promise'
|
5
|
-
|
6
1
|
module H2
|
7
2
|
class Server
|
8
3
|
class Stream
|
@@ -187,6 +182,43 @@ module H2
|
|
187
182
|
|
188
183
|
end
|
189
184
|
|
185
|
+
module ContentEncoder
|
186
|
+
|
187
|
+
# checks the request for accept-encoding headers and processes body
|
188
|
+
# accordingly
|
189
|
+
#
|
190
|
+
def check_accept_encoding
|
191
|
+
if accept = @stream.request.headers[ACCEPT_ENCODING_KEY]
|
192
|
+
accept.split(',').map(&:strip).each do |encoding|
|
193
|
+
case encoding
|
194
|
+
when GZIP_ENCODING
|
195
|
+
if @stream.connection.server.options[:gzip]
|
196
|
+
@body = ::Zlib.gzip @body
|
197
|
+
@headers[CONTENT_ENCODING_KEY] = GZIP_ENCODING
|
198
|
+
break
|
199
|
+
end
|
200
|
+
|
201
|
+
# "deflate" has issues: https://zlib.net/zlib_faq.html#faq39
|
202
|
+
#
|
203
|
+
when DEFLATE_ENCODING
|
204
|
+
if @stream.connection.server.options[:deflate]
|
205
|
+
@body = ::Zlib.deflate @body
|
206
|
+
@headers[CONTENT_ENCODING_KEY] = DEFLATE_ENCODING
|
207
|
+
break
|
208
|
+
end
|
209
|
+
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
|
190
217
|
class StreamError < StandardError; end
|
191
218
|
end
|
192
219
|
end
|
220
|
+
|
221
|
+
require 'h2/server/stream/event_source'
|
222
|
+
require 'h2/server/stream/request'
|
223
|
+
require 'h2/server/stream/response'
|
224
|
+
require 'h2/server/stream/push_promise'
|
@@ -25,8 +25,11 @@ module H2
|
|
25
25
|
@stream = stream
|
26
26
|
@parser = @stream.stream
|
27
27
|
@headers = headers
|
28
|
+
@gzip = false
|
29
|
+
@deflate = false
|
28
30
|
|
29
31
|
check_accept_header
|
32
|
+
check_accept_encoding
|
30
33
|
init_response
|
31
34
|
end
|
32
35
|
|
@@ -40,6 +43,34 @@ module H2
|
|
40
43
|
end
|
41
44
|
end
|
42
45
|
|
46
|
+
# checks the request for accept-encoding headers and processes data &
|
47
|
+
# events accordingly
|
48
|
+
#
|
49
|
+
def check_accept_encoding
|
50
|
+
if accept = @stream.request.headers[ACCEPT_ENCODING_KEY]
|
51
|
+
accept.split(',').map(&:strip).each do |encoding|
|
52
|
+
case encoding
|
53
|
+
when GZIP_ENCODING
|
54
|
+
if @stream.connection.server.options[:gzip]
|
55
|
+
@gzip = true
|
56
|
+
@headers[CONTENT_ENCODING_KEY] = GZIP_ENCODING
|
57
|
+
break
|
58
|
+
end
|
59
|
+
|
60
|
+
# "deflate" has issues: https://zlib.net/zlib_faq.html#faq39
|
61
|
+
#
|
62
|
+
when DEFLATE_ENCODING
|
63
|
+
if @stream.connection.server.options[:deflate]
|
64
|
+
@deflate = true
|
65
|
+
@headers[CONTENT_ENCODING_KEY] = DEFLATE_ENCODING
|
66
|
+
break
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
43
74
|
# responds with SSE headers on this stream
|
44
75
|
#
|
45
76
|
def init_response
|
@@ -57,7 +88,7 @@ module H2
|
|
57
88
|
# @param [String] data: data associated with this event
|
58
89
|
#
|
59
90
|
def event name:, data:
|
60
|
-
e = EVENT_TEMPL % [name, data]
|
91
|
+
e = encode_content(EVENT_TEMPL % [name, data])
|
61
92
|
@parser.data e, end_stream: false
|
62
93
|
end
|
63
94
|
|
@@ -68,7 +99,7 @@ module H2
|
|
68
99
|
# @param [String] data associated with this event
|
69
100
|
#
|
70
101
|
def data str
|
71
|
-
d = DATA_TEMPL % str
|
102
|
+
d = encode_content(DATA_TEMPL % str)
|
72
103
|
@parser.data d, end_stream: false
|
73
104
|
end
|
74
105
|
|
@@ -85,6 +116,16 @@ module H2
|
|
85
116
|
@closed
|
86
117
|
end
|
87
118
|
|
119
|
+
private
|
120
|
+
|
121
|
+
# content-encoding helper for #event and #data methods
|
122
|
+
#
|
123
|
+
def encode_content str
|
124
|
+
str = ::Zlib.gzip str if @gzip
|
125
|
+
str = ::Zlib.deflate str if @deflate
|
126
|
+
str
|
127
|
+
end
|
128
|
+
|
88
129
|
end
|
89
130
|
end
|
90
131
|
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module H2
|
2
|
+
class Server
|
3
|
+
class Stream
|
4
|
+
class PushPromise
|
5
|
+
include ContentEncoder
|
6
|
+
include HeaderStringifier
|
7
|
+
|
8
|
+
GET = 'GET'
|
9
|
+
STATUS = '200'
|
10
|
+
|
11
|
+
attr_reader :content_length, :path, :push_stream
|
12
|
+
|
13
|
+
# build a new +PushPromise+ for the path, with the headers and body given
|
14
|
+
#
|
15
|
+
def initialize path:, headers: {}, body: nil
|
16
|
+
@path = path
|
17
|
+
@body = body
|
18
|
+
@headers = headers
|
19
|
+
|
20
|
+
@promise_headers = {
|
21
|
+
METHOD_KEY => GET,
|
22
|
+
AUTHORITY_KEY => headers.delete(AUTHORITY_KEY),
|
23
|
+
PATH_KEY => @path,
|
24
|
+
SCHEME_KEY => headers.delete(SCHEME_KEY)
|
25
|
+
}
|
26
|
+
|
27
|
+
@fsm = FSM.new
|
28
|
+
end
|
29
|
+
|
30
|
+
# create a new promise stream from +stream+, send the headers and set
|
31
|
+
# +@push_stream+ from the callback
|
32
|
+
#
|
33
|
+
def make_on stream
|
34
|
+
return unless @fsm.state == :init
|
35
|
+
@stream = stream
|
36
|
+
@headers = {STATUS_KEY => STATUS}.merge stringify_headers(@headers)
|
37
|
+
|
38
|
+
check_accept_encoding
|
39
|
+
@content_length = @body.bytesize.to_s
|
40
|
+
@headers.merge! CONTENT_LENGTH_KEY => @content_length
|
41
|
+
|
42
|
+
@stream.stream.promise(@promise_headers, end_headers: false) do |push|
|
43
|
+
push.headers @headers
|
44
|
+
@push_stream = push
|
45
|
+
@push_stream.on(:close){|err| cancel! if err == :cancel}
|
46
|
+
end
|
47
|
+
@fsm.transition :made
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def keep_async
|
52
|
+
@stream.connection.server.async.handle_push_promise self
|
53
|
+
end
|
54
|
+
|
55
|
+
# deliver the body for this promise, optionally splicing into +size+ chunks
|
56
|
+
#
|
57
|
+
def keep size = nil
|
58
|
+
return false unless @fsm.state == :made
|
59
|
+
|
60
|
+
if size.nil?
|
61
|
+
@push_stream.data @body
|
62
|
+
else
|
63
|
+
body = @body
|
64
|
+
|
65
|
+
if body.bytesize > size
|
66
|
+
body = @body.dup
|
67
|
+
while body.bytesize > size
|
68
|
+
@push_stream.data body.slice!(0, size), end_stream: false
|
69
|
+
yield if block_given?
|
70
|
+
end
|
71
|
+
else
|
72
|
+
yield if block_given?
|
73
|
+
end
|
74
|
+
|
75
|
+
@push_stream.data body
|
76
|
+
end
|
77
|
+
|
78
|
+
@fsm.transition :kept
|
79
|
+
log :info, self
|
80
|
+
@stream.on_complete
|
81
|
+
end
|
82
|
+
|
83
|
+
def kept?
|
84
|
+
@fsm.state == :kept
|
85
|
+
end
|
86
|
+
|
87
|
+
def canceled?
|
88
|
+
@fsm.state == :canceled
|
89
|
+
end
|
90
|
+
|
91
|
+
# cancel this promise, most likely due to a RST_STREAM frame from the
|
92
|
+
# client (already in cache, etc...)
|
93
|
+
#
|
94
|
+
def cancel!
|
95
|
+
@fsm.transition :canceled
|
96
|
+
@stream.on_complete
|
97
|
+
end
|
98
|
+
|
99
|
+
def log level, msg
|
100
|
+
Logger.__send__ level, "[stream #{@push_stream.id}] #{msg}"
|
101
|
+
end
|
102
|
+
|
103
|
+
def to_s
|
104
|
+
request = @stream.request
|
105
|
+
%{#{request.addr} "push #{@path} HTTP/2" #{STATUS} #{@content_length}}
|
106
|
+
end
|
107
|
+
alias to_str to_s
|
108
|
+
|
109
|
+
# simple state machine to guarantee promise process
|
110
|
+
#
|
111
|
+
class FSM
|
112
|
+
include Celluloid::FSM
|
113
|
+
default_state :init
|
114
|
+
state :init, to: [:canceled, :made]
|
115
|
+
state :made, to: [:canceled, :kept]
|
116
|
+
state :kept
|
117
|
+
state :canceled
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -2,6 +2,7 @@ module H2
|
|
2
2
|
class Server
|
3
3
|
class Stream
|
4
4
|
class Response
|
5
|
+
include ContentEncoder
|
5
6
|
include HeaderStringifier
|
6
7
|
|
7
8
|
attr_reader :body, :content_length, :headers, :status, :stream
|
@@ -23,6 +24,7 @@ module H2
|
|
23
24
|
@body = body
|
24
25
|
@status = status
|
25
26
|
|
27
|
+
check_accept_encoding
|
26
28
|
init_content_length
|
27
29
|
end
|
28
30
|
|
data/lib/h2/stream.rb
CHANGED
@@ -33,6 +33,8 @@ module H2
|
|
33
33
|
@stream = stream
|
34
34
|
|
35
35
|
@eventsource = false
|
36
|
+
@gzip = false
|
37
|
+
@deflate = false
|
36
38
|
|
37
39
|
init_blocking
|
38
40
|
yield self if block_given?
|
@@ -104,6 +106,10 @@ module H2
|
|
104
106
|
loop do
|
105
107
|
event = @body.pop
|
106
108
|
break if event == :close
|
109
|
+
|
110
|
+
event = ::Zlib.gunzip event if @gzip
|
111
|
+
event = ::Zlib.inflate event if @deflate
|
112
|
+
|
107
113
|
yield event
|
108
114
|
end
|
109
115
|
else
|
@@ -119,7 +125,14 @@ module H2
|
|
119
125
|
@parent.add_push self if @parent && push?
|
120
126
|
@client.last_stream = self
|
121
127
|
@closed = true
|
122
|
-
|
128
|
+
|
129
|
+
if @eventsource
|
130
|
+
@body << :close
|
131
|
+
else
|
132
|
+
@body = Zlib.gunzip @body if @gzip
|
133
|
+
@body = Zlib.inflate @body if @deflate
|
134
|
+
end
|
135
|
+
|
123
136
|
unblock!
|
124
137
|
on :close
|
125
138
|
end
|
@@ -130,7 +143,6 @@ module H2
|
|
130
143
|
|
131
144
|
@stream.on(:data) do |d|
|
132
145
|
on :data, d
|
133
|
-
unblock! if @eventsource
|
134
146
|
@body << d
|
135
147
|
end
|
136
148
|
end
|
@@ -139,6 +151,7 @@ module H2
|
|
139
151
|
#
|
140
152
|
def add_headers h
|
141
153
|
check_event_source h
|
154
|
+
check_content_encoding h
|
142
155
|
h = Hash[h]
|
143
156
|
on :headers, h
|
144
157
|
@headers.merge! h
|
@@ -151,6 +164,19 @@ module H2
|
|
151
164
|
if h.any? {|e| e[0] == CONTENT_TYPE_KEY && e[1] == EVENT_SOURCE_CONTENT_TYPE }
|
152
165
|
@eventsource = true
|
153
166
|
@body = Queue.new
|
167
|
+
unblock!
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# checks for content encoding headers and sets flags
|
172
|
+
#
|
173
|
+
def check_content_encoding h
|
174
|
+
return if @gzip or @deflate
|
175
|
+
h.each do |e|
|
176
|
+
if e[0] == CONTENT_ENCODING_KEY
|
177
|
+
@gzip = true if e[1] == GZIP_ENCODING
|
178
|
+
@deflate = true if e[1] == DEFLATE_ENCODING
|
179
|
+
end
|
154
180
|
end
|
155
181
|
end
|
156
182
|
|
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.8.
|
4
|
+
version: 0.8.2
|
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-08-
|
11
|
+
date: 2018-08-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: http-2
|
@@ -104,9 +104,9 @@ files:
|
|
104
104
|
- lib/h2/server.rb
|
105
105
|
- lib/h2/server/connection.rb
|
106
106
|
- lib/h2/server/https.rb
|
107
|
-
- lib/h2/server/push_promise.rb
|
108
107
|
- lib/h2/server/stream.rb
|
109
108
|
- lib/h2/server/stream/event_source.rb
|
109
|
+
- lib/h2/server/stream/push_promise.rb
|
110
110
|
- lib/h2/server/stream/request.rb
|
111
111
|
- lib/h2/server/stream/response.rb
|
112
112
|
- lib/h2/stream.rb
|
@@ -1,119 +0,0 @@
|
|
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:, headers: {}, body: nil
|
13
|
-
@path = path
|
14
|
-
@body = body
|
15
|
-
|
16
|
-
@promise_headers = {
|
17
|
-
METHOD_KEY => GET,
|
18
|
-
AUTHORITY_KEY => headers.delete(AUTHORITY_KEY),
|
19
|
-
PATH_KEY => @path,
|
20
|
-
SCHEME_KEY => headers.delete(SCHEME_KEY)
|
21
|
-
}
|
22
|
-
|
23
|
-
@content_length = @body.bytesize.to_s
|
24
|
-
|
25
|
-
@push_headers = {
|
26
|
-
STATUS_KEY => STATUS,
|
27
|
-
CONTENT_LENGTH_KEY => @content_length
|
28
|
-
}.merge headers
|
29
|
-
|
30
|
-
@fsm = FSM.new
|
31
|
-
end
|
32
|
-
|
33
|
-
# create a new promise stream from +stream+, send the headers and set
|
34
|
-
# +@push_stream+ from the callback
|
35
|
-
#
|
36
|
-
def make_on stream
|
37
|
-
return unless @fsm.state == :init
|
38
|
-
@stream = stream
|
39
|
-
@stream.stream.promise(@promise_headers, end_headers: false) do |push|
|
40
|
-
push.headers @push_headers
|
41
|
-
@push_stream = push
|
42
|
-
@push_stream.on(:close){|err| cancel! if err == :cancel}
|
43
|
-
end
|
44
|
-
@fsm.transition :made
|
45
|
-
self
|
46
|
-
end
|
47
|
-
|
48
|
-
def keep_async
|
49
|
-
@stream.connection.server.async.handle_push_promise self
|
50
|
-
end
|
51
|
-
|
52
|
-
# deliver the body for this promise, optionally splicing into +size+ chunks
|
53
|
-
#
|
54
|
-
def keep size = nil
|
55
|
-
return false unless @fsm.state == :made
|
56
|
-
|
57
|
-
if size.nil?
|
58
|
-
@push_stream.data @body
|
59
|
-
else
|
60
|
-
body = @body
|
61
|
-
|
62
|
-
if body.bytesize > size
|
63
|
-
body = @body.dup
|
64
|
-
while body.bytesize > size
|
65
|
-
@push_stream.data body.slice!(0, size), end_stream: false
|
66
|
-
yield if block_given?
|
67
|
-
end
|
68
|
-
else
|
69
|
-
yield if block_given?
|
70
|
-
end
|
71
|
-
|
72
|
-
@push_stream.data body
|
73
|
-
end
|
74
|
-
|
75
|
-
@fsm.transition :kept
|
76
|
-
log :info, self
|
77
|
-
@stream.on_complete
|
78
|
-
end
|
79
|
-
|
80
|
-
def kept?
|
81
|
-
@fsm.state == :kept
|
82
|
-
end
|
83
|
-
|
84
|
-
def canceled?
|
85
|
-
@fsm.state == :canceled
|
86
|
-
end
|
87
|
-
|
88
|
-
# cancel this promise, most likely due to a RST_STREAM frame from the
|
89
|
-
# client (already in cache, etc...)
|
90
|
-
#
|
91
|
-
def cancel!
|
92
|
-
@fsm.transition :canceled
|
93
|
-
@stream.on_complete
|
94
|
-
end
|
95
|
-
|
96
|
-
def log level, msg
|
97
|
-
Logger.__send__ level, "[stream #{@push_stream.id}] #{msg}"
|
98
|
-
end
|
99
|
-
|
100
|
-
def to_s
|
101
|
-
request = @stream.request
|
102
|
-
%{#{request.addr} "push #{@path} HTTP/2" #{STATUS} #{@content_length}}
|
103
|
-
end
|
104
|
-
alias to_str to_s
|
105
|
-
|
106
|
-
# simple state machine to guarantee promise process
|
107
|
-
#
|
108
|
-
class FSM
|
109
|
-
include Celluloid::FSM
|
110
|
-
default_state :init
|
111
|
-
state :init, to: [:canceled, :made]
|
112
|
-
state :made, to: [:canceled, :kept]
|
113
|
-
state :kept
|
114
|
-
state :canceled
|
115
|
-
end
|
116
|
-
|
117
|
-
end
|
118
|
-
end
|
119
|
-
end
|