h2 0.6.1 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/Gemfile +2 -4
- data/README.md +5 -5
- data/Rakefile +16 -1
- data/examples/server/push_promise.rb +7 -2
- data/examples/server/sse.rb +103 -0
- data/exe/h2 +4 -1
- data/lib/h2.rb +16 -0
- data/lib/h2/server.rb +23 -5
- data/lib/h2/server/connection.rb +1 -1
- data/lib/h2/server/https.rb +0 -1
- data/lib/h2/server/push_promise.rb +2 -8
- data/lib/h2/server/stream.rb +15 -2
- data/lib/h2/server/stream/event_source.rb +91 -0
- data/lib/h2/server/stream/request.rb +12 -1
- data/lib/h2/server/stream/response.rb +25 -47
- data/lib/h2/version.rb +1 -1
- metadata +4 -3
- data/lib/h2/reel/ext.rb +0 -48
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a59b70f55abe247ca3ae333b72031a32f880fa1844bd8058585a4090bcc55969
|
4
|
+
data.tar.gz: d28023b7123b0819c1f540070831188fd99460f11fe9914c2d8c00c7e4dd4e6d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a7e502860f2f753ed37f419ba41b78c9399fe8a6278cbfe937ccd7c04f8da0c7fd53fdbc960454ac389a3b4a4073d3a39737a643fdf09235aac281b7f15f67eb
|
7
|
+
data.tar.gz: 2dfa20d7aafdc0e37cd61aba4c6a2d9696c4efb58de3786f87c628f08c65bb5437baebdc93dd2acd4f27abde49021f2ac82d58d06ea9d133359d6d9098c8c6b4
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,14 @@
|
|
1
1
|
h2 changelog
|
2
2
|
============
|
3
3
|
|
4
|
+
### 0.7.0 2 aug 2018
|
5
|
+
|
6
|
+
* `Server::Stream::Request#path` now removes query string
|
7
|
+
* add `H2::Server::Stream::Request#query_string`
|
8
|
+
* `H2::Server::Stream::Response` body now accepts any object that `respond_to? :each`
|
9
|
+
* remove Reel completely, base from Celluloid::IO
|
10
|
+
* add SSE support
|
11
|
+
|
4
12
|
### 0.6.1 27 jul 2018
|
5
13
|
|
6
14
|
* fix race between reading and sending first frame
|
data/Gemfile
CHANGED
@@ -2,14 +2,12 @@ source 'https://rubygems.org'
|
|
2
2
|
|
3
3
|
gemspec
|
4
4
|
|
5
|
-
gem 'http-2', path: '../http-2'
|
6
|
-
|
7
5
|
group :concurrent_ruby do
|
8
6
|
gem 'concurrent-ruby'
|
9
7
|
end
|
10
8
|
|
11
9
|
group :celluloid do
|
12
|
-
gem 'celluloid'
|
10
|
+
gem 'celluloid', '~> 0.17', '>= 0.17.3'
|
13
11
|
end
|
14
12
|
|
15
13
|
group :development, :test do
|
@@ -17,5 +15,5 @@ group :development, :test do
|
|
17
15
|
gem 'certificate_authority'
|
18
16
|
gem 'guard-rake'
|
19
17
|
gem 'pry-byebug', platforms: [:mri]
|
20
|
-
gem '
|
18
|
+
gem 'celluloid-io', '~> 0.17', '>= 0.17.3'
|
21
19
|
end
|
data/README.md
CHANGED
@@ -12,8 +12,8 @@ H2 uses:
|
|
12
12
|
## Server Usage
|
13
13
|
|
14
14
|
Server API is currently optional, and must be required separately. The server
|
15
|
-
uses [
|
16
|
-
|
15
|
+
uses [Celluloid::IO](https://github.com/celluloid/celluloid-io), but since this API is optional,
|
16
|
+
celluloid-io must be separately added to `Gemfile`. It is currently based on `celluloid-io-0.17.3`.
|
17
17
|
|
18
18
|
```ruby
|
19
19
|
require 'h2/server'
|
@@ -135,9 +135,9 @@ require 'h2/client/celluloid'
|
|
135
135
|
|
136
136
|
This will lazily fire up a celluloid pool, with defaults defined by Celluloid.
|
137
137
|
|
138
|
-
NOTE: if you've added
|
139
|
-
loaded in your Ruby VM already; however, you must still require this to
|
140
|
-
the client use Celluloid actor pools.
|
138
|
+
NOTE: if you've added celluloid-io and required the 'h2/server' API, Celluloid
|
139
|
+
will be loaded in your Ruby VM already; however, you must still require this to
|
140
|
+
have the client use Celluloid actor pools.
|
141
141
|
|
142
142
|
#### Concurrent-Ruby ThreadPoolExecutor
|
143
143
|
|
data/Rakefile
CHANGED
@@ -3,7 +3,7 @@ require "rake/testtask"
|
|
3
3
|
|
4
4
|
task default: :test
|
5
5
|
|
6
|
-
Rake::TestTask.new :test do |t|
|
6
|
+
Rake::TestTask.new :test => ['test:certs'] do |t|
|
7
7
|
t.test_files = FileList['test/**/*_test.rb']
|
8
8
|
end
|
9
9
|
|
@@ -18,4 +18,19 @@ namespace :test do
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
+
task :certs do
|
22
|
+
certs_dir = Pathname.new File.expand_path '../tmp/certs', __FILE__
|
23
|
+
ca_file = certs_dir.join('ca.crt').to_s
|
24
|
+
require_relative 'test/support/create_certs' unless File.exist? ca_file
|
25
|
+
end
|
26
|
+
|
27
|
+
task :nginx => [:certs] do
|
28
|
+
system "docker build -t h2_nginx_http2 test/support/nginx"
|
29
|
+
puts "\nstarting nginx with http/2 support"
|
30
|
+
puts "using document root: test/support/nginx/"
|
31
|
+
puts "using TLS certs: tmp/certs/server.*"
|
32
|
+
puts "listening at https://localhost:4430/"
|
33
|
+
system "docker run --rm -v `pwd`/tmp/certs:/usr/local/nginx/certs -v `pwd`/test/support/nginx:/usr/local/nginx/html -p 4430:443 -it h2_nginx_http2"
|
34
|
+
end
|
35
|
+
|
21
36
|
end
|
@@ -7,8 +7,8 @@ require 'h2/server'
|
|
7
7
|
H2::Logger.level = ::Logger::DEBUG
|
8
8
|
H2.verbose!
|
9
9
|
|
10
|
-
port =
|
11
|
-
addr = Socket.getaddrinfo('localhost', port).first[3]
|
10
|
+
port = 4430
|
11
|
+
addr = '127.0.0.1' #Socket.getaddrinfo('localhost', port).first[3]
|
12
12
|
certs_dir = File.expand_path '../../../tmp/certs', __FILE__
|
13
13
|
dog_png = File.read File.expand_path '../dog.png', __FILE__
|
14
14
|
push_promise = '<html><body>wait for it...<img src="/dog.png"/><script src="/pushed.js"></script></body></html>'
|
@@ -19,6 +19,11 @@ sni = {
|
|
19
19
|
:cert => certs_dir + '/server.crt',
|
20
20
|
:key => certs_dir + '/server.key',
|
21
21
|
# :extra_chain_cert => certs_dir + '/chain.pem'
|
22
|
+
},
|
23
|
+
'vux.nakamura.io' => {
|
24
|
+
:cert => certs_dir + '/nakamuraio.crt',
|
25
|
+
:key => certs_dir + '/nakamuraio.key',
|
26
|
+
:extra_chain_cert => certs_dir + '/nakamuraio-chain.pem'
|
22
27
|
}
|
23
28
|
}
|
24
29
|
|
@@ -0,0 +1,103 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Run with: bundle exec examples/server/sse/sse.rb
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'h2/server'
|
6
|
+
|
7
|
+
H2::Logger.level = ::Logger::DEBUG
|
8
|
+
H2.verbose!
|
9
|
+
|
10
|
+
port = 4430
|
11
|
+
addr = Socket.getaddrinfo('localhost', port).first[3]
|
12
|
+
certs_dir = File.expand_path '../../../tmp/certs', __FILE__
|
13
|
+
data, key = Hash.new {|h,k| h[k] = ''}, nil
|
14
|
+
|
15
|
+
sni = {
|
16
|
+
'localhost' => {
|
17
|
+
:cert => certs_dir + '/server.crt',
|
18
|
+
:key => certs_dir + '/server.key',
|
19
|
+
# :extra_chain_cert => certs_dir + '/chain.pem'
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
event_sources = []
|
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
|
+
case stream.request.path
|
29
|
+
when '/favicon.ico'
|
30
|
+
stream.respond status: 404
|
31
|
+
|
32
|
+
when '/events'
|
33
|
+
es = stream.to_eventsource
|
34
|
+
event_sources << es
|
35
|
+
|
36
|
+
when '/msg'
|
37
|
+
if stream.request.method == :post
|
38
|
+
msg = stream.request.body
|
39
|
+
event_sources.each {|es| es.event name: 'msg', data: msg}
|
40
|
+
stream.respond status: 201
|
41
|
+
else
|
42
|
+
stream.respond status: 404
|
43
|
+
end
|
44
|
+
|
45
|
+
when '/sse.js'
|
46
|
+
stream.respond status: 404,
|
47
|
+
body: "should have been pushed..."
|
48
|
+
|
49
|
+
else
|
50
|
+
stream.push_promise path: '/sse.js',
|
51
|
+
headers: { 'content-type' => 'application/javascript' },
|
52
|
+
body: data[:javascript]
|
53
|
+
|
54
|
+
stream.respond status: 200, body: data[:html]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
DATA.each_line do |l|
|
60
|
+
if l.start_with?('@@')
|
61
|
+
key = l.strip[2..-1].to_sym
|
62
|
+
else
|
63
|
+
data[key] << l
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
sleep
|
68
|
+
|
69
|
+
__END__
|
70
|
+
@@html
|
71
|
+
<!DOCTYPE html>
|
72
|
+
<html lang="en">
|
73
|
+
<head>
|
74
|
+
<meta charset="UTF-8">
|
75
|
+
<title>SSE example</title>
|
76
|
+
<script src="/sse.js"></script>
|
77
|
+
</head>
|
78
|
+
<body>
|
79
|
+
<form id="say">
|
80
|
+
say: <input type="text" id="words"/>
|
81
|
+
</form>
|
82
|
+
<br/>
|
83
|
+
<div><ol id="list"></ol></div>
|
84
|
+
</body>
|
85
|
+
</html>
|
86
|
+
|
87
|
+
@@javascript
|
88
|
+
document.addEventListener('DOMContentLoaded', () => {
|
89
|
+
let sse = new EventSource('/events');
|
90
|
+
sse.addEventListener('msg', (msg) => {
|
91
|
+
let item = document.createElement('li');
|
92
|
+
item.innerHTML = msg.data;
|
93
|
+
document.getElementById('list').appendChild(item);
|
94
|
+
});
|
95
|
+
|
96
|
+
let w = document.getElementById('words');
|
97
|
+
document.getElementById('say').onsubmit = (e) => {
|
98
|
+
e.preventDefault();
|
99
|
+
fetch('/msg', {method: 'post', body: w.value})
|
100
|
+
.then(() => { w.value = ''; });
|
101
|
+
};
|
102
|
+
w.focus();
|
103
|
+
});
|
data/exe/h2
CHANGED
@@ -136,8 +136,11 @@ if options[:verbose]
|
|
136
136
|
end
|
137
137
|
|
138
138
|
puts s.body
|
139
|
+
c.block! if options[:block] or !s.pushes.empty?
|
140
|
+
s.pushes.each do |p|
|
141
|
+
puts "push promise: #{p.headers[':path']}"
|
142
|
+
end
|
139
143
|
|
140
|
-
c.block! if options[:block]
|
141
144
|
c.goaway if options[:goaway]
|
142
145
|
c.close
|
143
146
|
|
data/lib/h2.rb
CHANGED
@@ -161,6 +161,22 @@ module H2
|
|
161
161
|
|
162
162
|
end
|
163
163
|
|
164
|
+
module HeaderStringifier
|
165
|
+
|
166
|
+
private
|
167
|
+
def stringify_headers hash
|
168
|
+
hash.keys.each do |k|
|
169
|
+
hash[k] = hash[k].to_s unless String === hash[k]
|
170
|
+
if Symbol === k
|
171
|
+
key = k.to_s.gsub '_', '-'
|
172
|
+
hash[key] = hash.delete k
|
173
|
+
end
|
174
|
+
end
|
175
|
+
hash
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
|
164
180
|
end
|
165
181
|
|
166
182
|
require 'h2/client'
|
data/lib/h2/server.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'celluloid/current'
|
2
|
-
require '
|
3
|
-
require 'h2/reel/ext'
|
2
|
+
require 'celluloid/io'
|
4
3
|
require 'h2'
|
5
4
|
|
6
5
|
module H2
|
@@ -20,13 +19,28 @@ module H2
|
|
20
19
|
|
21
20
|
end
|
22
21
|
|
23
|
-
# base H2 server, a
|
22
|
+
# base H2 server, a +Celluoid::IO+ production
|
24
23
|
#
|
25
|
-
class Server
|
24
|
+
class Server
|
25
|
+
include Celluloid::IO
|
26
|
+
|
27
|
+
TCP_DEFAULT_BACKLOG = 100
|
28
|
+
|
29
|
+
execute_block_on_receiver :initialize
|
30
|
+
finalizer :shutdown
|
26
31
|
|
27
32
|
def initialize server, **options, &on_connection
|
33
|
+
@server = server
|
34
|
+
@options = options
|
28
35
|
@on_connection = on_connection
|
29
|
-
|
36
|
+
|
37
|
+
backlog = options.fetch :backlog, TCP_DEFAULT_BACKLOG
|
38
|
+
@server.listen backlog
|
39
|
+
async.run
|
40
|
+
end
|
41
|
+
|
42
|
+
def shutdown
|
43
|
+
@server.close if @server
|
30
44
|
end
|
31
45
|
|
32
46
|
# build a new connection object, run it through the given block, and
|
@@ -74,6 +88,10 @@ module H2
|
|
74
88
|
super @tcpserver, options, &on_connection
|
75
89
|
end
|
76
90
|
|
91
|
+
def run
|
92
|
+
loop { async.handle_connection @server.accept }
|
93
|
+
end
|
94
|
+
|
77
95
|
end
|
78
96
|
|
79
97
|
end
|
data/lib/h2/server/connection.rb
CHANGED
data/lib/h2/server/https.rb
CHANGED
@@ -9,15 +9,9 @@ module H2
|
|
9
9
|
|
10
10
|
# build a new +PushPromise+ for the path, with the headers and body given
|
11
11
|
#
|
12
|
-
def initialize path
|
12
|
+
def initialize path:, headers: {}, body: nil
|
13
13
|
@path = path
|
14
|
-
|
15
|
-
headers = body_or_headers.dup
|
16
|
-
@body = body
|
17
|
-
else
|
18
|
-
headers = {}
|
19
|
-
@body = body_or_headers
|
20
|
-
end
|
14
|
+
@body = body
|
21
15
|
|
22
16
|
@promise_headers = {
|
23
17
|
METHOD_KEY => GET,
|
data/lib/h2/server/stream.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'h2/server/stream/event_source'
|
1
2
|
require 'h2/server/stream/request'
|
2
3
|
require 'h2/server/stream/response'
|
3
4
|
require 'h2/server/push_promise'
|
@@ -51,8 +52,8 @@ module H2
|
|
51
52
|
if @closed
|
52
53
|
log :warn, 'stream closed before response sent'
|
53
54
|
else
|
54
|
-
response.respond_on(stream)
|
55
55
|
log :info, response
|
56
|
+
response.respond_on(stream)
|
56
57
|
@responded = true
|
57
58
|
end
|
58
59
|
end
|
@@ -72,7 +73,7 @@ module H2
|
|
72
73
|
headers.merge! AUTHORITY_KEY => @request.authority,
|
73
74
|
SCHEME_KEY => @request.scheme
|
74
75
|
|
75
|
-
PushPromise.new path, headers, body
|
76
|
+
PushPromise.new path: path, headers: headers, body: body
|
76
77
|
end
|
77
78
|
|
78
79
|
# begin the new push promise stream from this +@stream+ by sending the
|
@@ -120,6 +121,16 @@ module H2
|
|
120
121
|
Logger.__send__ level, "[stream #{@stream.id}] #{msg}"
|
121
122
|
end
|
122
123
|
|
124
|
+
# make this stream into an SSE event source
|
125
|
+
#
|
126
|
+
# raises +StreamError+ if the request's content-type is not valid
|
127
|
+
#
|
128
|
+
# @return [H2::Server::Stream::EventSource]
|
129
|
+
#
|
130
|
+
def to_eventsource headers: {}
|
131
|
+
EventSource.new stream: self, headers: headers
|
132
|
+
end
|
133
|
+
|
123
134
|
protected
|
124
135
|
|
125
136
|
# bind parser events to this instance
|
@@ -174,5 +185,7 @@ module H2
|
|
174
185
|
end
|
175
186
|
|
176
187
|
end
|
188
|
+
|
189
|
+
class StreamError < StandardError; end
|
177
190
|
end
|
178
191
|
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module H2
|
2
|
+
class Server
|
3
|
+
class Stream
|
4
|
+
class EventSource
|
5
|
+
include HeaderStringifier
|
6
|
+
|
7
|
+
DATA_TEMPL = "data: %s\n\n"
|
8
|
+
EVENT_TEMPL = "event: %s\n#{DATA_TEMPL}"
|
9
|
+
SSE_HEADER = {
|
10
|
+
STATUS_KEY => '200',
|
11
|
+
:content_type => 'text/event-stream'
|
12
|
+
}
|
13
|
+
|
14
|
+
# build and return +EventSource+ instance, ready for pushing out data
|
15
|
+
# or named events. checks accept header in the request, then responds
|
16
|
+
# with valid headers for beginning an SSE stream
|
17
|
+
#
|
18
|
+
# @param [H2::Server::Stream] stream: the +Stream+ instance
|
19
|
+
# @param [Hash] headers: optional headers to add to the intial response
|
20
|
+
#
|
21
|
+
# @return [H2::Server::Stream::EventSource]
|
22
|
+
#
|
23
|
+
def initialize stream:, headers: {}
|
24
|
+
@closed = false
|
25
|
+
@stream = stream
|
26
|
+
@parser = @stream.stream
|
27
|
+
@headers = headers
|
28
|
+
|
29
|
+
check_accept_header
|
30
|
+
init_response
|
31
|
+
end
|
32
|
+
|
33
|
+
# checks accept header in the request and raises a +StreamError+ if not
|
34
|
+
# valid for SSE
|
35
|
+
#
|
36
|
+
def check_accept_header
|
37
|
+
accept = @stream.request.headers['accept']
|
38
|
+
unless accept == SSE_HEADER[:content_type]
|
39
|
+
raise StreamError, "invalid header accept: #{accept}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# responds with SSE headers on this stream
|
44
|
+
#
|
45
|
+
def init_response
|
46
|
+
headers = SSE_HEADER.merge @headers
|
47
|
+
@parser.headers stringify_headers(headers)
|
48
|
+
rescue ::HTTP2::Error::StreamClosed => sc
|
49
|
+
@stream.log :warn, "stream closed early by client"
|
50
|
+
end
|
51
|
+
|
52
|
+
# send out a named event with the given data
|
53
|
+
#
|
54
|
+
# this would be handled by `es.addEventListener('name', (msg)=>{})`
|
55
|
+
#
|
56
|
+
# @param [String] name: the name of the event
|
57
|
+
# @param [String] data: data associated with this event
|
58
|
+
#
|
59
|
+
def event name:, data:
|
60
|
+
e = EVENT_TEMPL % [name, data]
|
61
|
+
@parser.data e, end_stream: false
|
62
|
+
end
|
63
|
+
|
64
|
+
# send out a message with the given data
|
65
|
+
#
|
66
|
+
# this would be handled by `es.onmessage((msg)=>{})`
|
67
|
+
#
|
68
|
+
# @param [String] data associated with this event
|
69
|
+
#
|
70
|
+
def data str
|
71
|
+
d = DATA_TEMPL % str
|
72
|
+
@parser.data d, end_stream: false
|
73
|
+
end
|
74
|
+
|
75
|
+
# emit a final frame on this stream with +end_stream+ flag
|
76
|
+
#
|
77
|
+
def close
|
78
|
+
@parser.data ''
|
79
|
+
@closed = true
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [Boolean] true if this stream is closed
|
83
|
+
#
|
84
|
+
def closed?
|
85
|
+
@closed
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -44,7 +44,18 @@ module H2
|
|
44
44
|
# retreive the path from the stream request headers
|
45
45
|
#
|
46
46
|
def path
|
47
|
-
@path
|
47
|
+
return @path if defined?(@path)
|
48
|
+
@path = headers[PATH_KEY]
|
49
|
+
@path = @path.split('?').first if @path
|
50
|
+
end
|
51
|
+
|
52
|
+
# retreive the query string from the stream request headers
|
53
|
+
#
|
54
|
+
def query_string
|
55
|
+
return @query_string if defined?(@query_string)
|
56
|
+
@query_string = headers[PATH_KEY].index '?'
|
57
|
+
return if @query_string.nil?
|
58
|
+
@query_string = headers[PATH_KEY][(@query_string + 1)..-1]
|
48
59
|
end
|
49
60
|
|
50
61
|
# retreive the scheme from the stream request headers
|
@@ -2,16 +2,26 @@ module H2
|
|
2
2
|
class Server
|
3
3
|
class Stream
|
4
4
|
class Response
|
5
|
+
include HeaderStringifier
|
5
6
|
|
6
7
|
attr_reader :body, :content_length, :headers, :status, :stream
|
7
8
|
|
8
9
|
# build a new +Response+ object
|
9
10
|
#
|
11
|
+
# @param [H2::Server::Stream] stream: Stream instance associated with this response
|
12
|
+
# @param [Integer] status: HTTP status code
|
13
|
+
# @param [Hash] headers: response headers
|
14
|
+
# @param [String,#each] body: response body. NOTE: may be any object that
|
15
|
+
# `respond_to? :each` which both yields and returns
|
16
|
+
# String objects.
|
17
|
+
#
|
18
|
+
# @return [H2::Server::Stream::Response]
|
19
|
+
#
|
10
20
|
def initialize stream:, status:, headers: {}, body: ''
|
11
|
-
@stream
|
12
|
-
@headers
|
13
|
-
@body
|
14
|
-
|
21
|
+
@stream = stream
|
22
|
+
@headers = headers
|
23
|
+
@body = body
|
24
|
+
@status = status
|
15
25
|
|
16
26
|
init_content_length
|
17
27
|
end
|
@@ -19,20 +29,18 @@ module H2
|
|
19
29
|
# sets the content length in the headers by the byte size of +@body+
|
20
30
|
#
|
21
31
|
def init_content_length
|
22
|
-
@
|
23
|
-
|
32
|
+
return if @headers.any? {|k,_| k.downcase == CONTENT_LENGTH_KEY}
|
33
|
+
return if @body.respond_to?(:each)
|
34
|
+
@content_length = case
|
35
|
+
when String === @body
|
24
36
|
@body.bytesize
|
25
|
-
when IO
|
26
|
-
@body.stat.size
|
27
37
|
when NilClass
|
28
38
|
'0'
|
29
39
|
else
|
30
40
|
raise TypeError, "can't render #{@body.class} as a response body"
|
31
41
|
end
|
32
42
|
|
33
|
-
|
34
|
-
@headers[CONTENT_LENGTH_KEY] = @content_length
|
35
|
-
end
|
43
|
+
@headers[CONTENT_LENGTH_KEY] = @content_length
|
36
44
|
end
|
37
45
|
|
38
46
|
# the corresponding +Request+ to this +Response+
|
@@ -41,38 +49,21 @@ module H2
|
|
41
49
|
stream.request
|
42
50
|
end
|
43
51
|
|
44
|
-
# send the headers and body out on +s+, an +HTTP2::Stream+ object
|
52
|
+
# send the headers and body out on +s+, an +HTTP2::Stream+ object, and
|
53
|
+
# close the stream when complete.
|
45
54
|
#
|
46
55
|
# NOTE: +:status+ must come first?
|
47
56
|
#
|
48
57
|
def respond_on s
|
49
58
|
headers = { STATUS_KEY => @status.to_s }.merge @headers
|
50
59
|
s.headers stringify_headers(headers)
|
51
|
-
|
52
|
-
when String
|
60
|
+
if String === @body
|
53
61
|
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
62
|
else
|
74
|
-
|
63
|
+
stream.log :error, "unexpected @body: #{caller[0]}"
|
75
64
|
end
|
65
|
+
rescue ::HTTP2::Error::StreamClosed => sc
|
66
|
+
stream.log :warn, "stream closed early by client"
|
76
67
|
end
|
77
68
|
|
78
69
|
def to_s
|
@@ -80,19 +71,6 @@ module H2
|
|
80
71
|
end
|
81
72
|
alias to_str to_s
|
82
73
|
|
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
74
|
end
|
97
75
|
end
|
98
76
|
end
|
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.7.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-
|
11
|
+
date: 2018-08-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: http-2
|
@@ -108,6 +108,7 @@ files:
|
|
108
108
|
- examples/server/hello_world.rb
|
109
109
|
- examples/server/https_hello_world.rb
|
110
110
|
- examples/server/push_promise.rb
|
111
|
+
- examples/server/sse.rb
|
111
112
|
- exe/h2
|
112
113
|
- h2.gemspec
|
113
114
|
- lib/h2.rb
|
@@ -115,12 +116,12 @@ files:
|
|
115
116
|
- lib/h2/client/celluloid.rb
|
116
117
|
- lib/h2/client/concurrent.rb
|
117
118
|
- lib/h2/client/tcp_socket.rb
|
118
|
-
- lib/h2/reel/ext.rb
|
119
119
|
- lib/h2/server.rb
|
120
120
|
- lib/h2/server/connection.rb
|
121
121
|
- lib/h2/server/https.rb
|
122
122
|
- lib/h2/server/push_promise.rb
|
123
123
|
- lib/h2/server/stream.rb
|
124
|
+
- lib/h2/server/stream/event_source.rb
|
124
125
|
- lib/h2/server/stream/request.rb
|
125
126
|
- lib/h2/server/stream/response.rb
|
126
127
|
- lib/h2/stream.rb
|
data/lib/h2/reel/ext.rb
DELETED
@@ -1,48 +0,0 @@
|
|
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
|