h2 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/Gemfile +1 -0
- data/README.md +9 -4
- data/examples/server/https_hello_world.rb +3 -0
- data/examples/server/sse.rb +7 -2
- data/exe/h2 +20 -1
- data/lib/h2/client/celluloid.rb +0 -1
- data/lib/h2/client/concurrent.rb +1 -1
- data/lib/h2/client.rb +48 -23
- data/lib/h2/server/stream/event_source.rb +3 -3
- data/lib/h2/stream.rb +32 -3
- data/lib/h2/version.rb +1 -1
- data/lib/h2.rb +5 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b32f7af9ee3845b4a817610abf47cf8fdf4503fda1418b49ab323220fe1ccf49
|
4
|
+
data.tar.gz: 5a6fc19c1d0480c6f0eeddb808d61a09dbdb1582c4aa31125cbe6a8e4ba0b8a0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 20820c02ce3c3459deb468854498e3bf362c19d1b0f032979d77d679f44803bbd81db8d780714e428e192b9bbead83114da031af2c2b5f7983b79075345ced2c
|
7
|
+
data.tar.gz: 236e9658e69e7fde118c0cc4b679b2deceb83bebe2a06c4c33aac14038a39232d20ca4cadfc995e34a885b3c808dd14faee3cdb47e9adbca587b218ebeb00732
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,11 @@
|
|
1
1
|
h2 changelog
|
2
2
|
============
|
3
3
|
|
4
|
+
### 0.8.0 10 aug 2018
|
5
|
+
|
6
|
+
* fix read/settings ack race (https://httpwg.org/specs/rfc7540.html#ConnectionHeader)
|
7
|
+
* add SSE/EventSource support
|
8
|
+
|
4
9
|
### 0.7.0 2 aug 2018
|
5
10
|
|
6
11
|
* `Server::Stream::Request#path` now removes query string
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -11,9 +11,10 @@ H2 uses:
|
|
11
11
|
|
12
12
|
## Server Usage
|
13
13
|
|
14
|
-
Server API is currently optional,
|
15
|
-
uses [Celluloid::IO](https://github.com/celluloid/celluloid-io), but
|
16
|
-
celluloid-io must be separately added to
|
14
|
+
Server API is currently optional, so `h2/server` must be required separately.
|
15
|
+
The server uses [Celluloid::IO](https://github.com/celluloid/celluloid-io), but
|
16
|
+
h2's gemspec does not require it, so celluloid-io must be separately added to
|
17
|
+
`Gemfile`. It is currently based on `celluloid-io-0.17.3`.
|
17
18
|
|
18
19
|
```ruby
|
19
20
|
require 'h2/server'
|
@@ -29,7 +30,11 @@ stream = H2.get url: "http://#{addr}:#{port}", tls: false
|
|
29
30
|
stream.body #=> "hello, world!\n"
|
30
31
|
```
|
31
32
|
|
32
|
-
See
|
33
|
+
See more server examples:
|
34
|
+
|
35
|
+
* [HTTPS Hello World](https://github.com/kenichi/h2/blob/master/examples/server/https_hello_world.rb)
|
36
|
+
* [Push Promises](https://github.com/kenichi/h2/blob/master/examples/server/push_promise.rb)
|
37
|
+
* [SSE/EventSource](https://github.com/kenichi/h2/blob/master/examples/server/sse.rb)
|
33
38
|
|
34
39
|
## Client Usage
|
35
40
|
|
@@ -8,6 +8,9 @@ port = 1234
|
|
8
8
|
addr = Socket.getaddrinfo('localhost', port).first[3]
|
9
9
|
certs_dir = File.expand_path '../../../tmp/certs', __FILE__
|
10
10
|
|
11
|
+
# if not using SNI, we may pass the underlying opts directly, and the same TLS
|
12
|
+
# cert/key will be used for all incoming connections.
|
13
|
+
#
|
11
14
|
tls = {
|
12
15
|
cert: certs_dir + '/server.crt',
|
13
16
|
key: certs_dir + '/server.key',
|
data/examples/server/sse.rb
CHANGED
@@ -30,8 +30,13 @@ s = H2::Server::HTTPS.new host: addr, port: port, sni: sni do |connection|
|
|
30
30
|
stream.respond status: 404
|
31
31
|
|
32
32
|
when '/events'
|
33
|
-
|
34
|
-
|
33
|
+
if stream.request.method == :delete
|
34
|
+
event_sources.each &:close
|
35
|
+
event_sources.clear
|
36
|
+
stream.respond status: 200
|
37
|
+
else
|
38
|
+
event_sources << stream.to_eventsource
|
39
|
+
end
|
35
40
|
|
36
41
|
when '/msg'
|
37
42
|
if stream.request.method == :post
|
data/exe/h2
CHANGED
@@ -58,10 +58,24 @@ OptionParser.new do |o|
|
|
58
58
|
options[:debug] = true
|
59
59
|
end
|
60
60
|
|
61
|
+
o.on '-sse', '--eventsource', 'send event-stream headers and print messages as they arrive' do
|
62
|
+
options[:headers]['accept'] = H2::EVENT_SOURCE_CONTENT_TYPE
|
63
|
+
end
|
64
|
+
|
61
65
|
o.on '-g', '--goaway', 'send GOAWAY frame when stream is complete' do
|
62
66
|
options[:goaway] = true
|
63
67
|
end
|
64
68
|
|
69
|
+
o.on '-h', '--help', 'show this help/usage' do
|
70
|
+
puts o
|
71
|
+
exit
|
72
|
+
end
|
73
|
+
|
74
|
+
o.on '-H [VALUE]', '--header [VALUE]', String, 'include header in request (format: "key: value")' do |h|
|
75
|
+
kv = h.split(':').map &:strip
|
76
|
+
options[:headers][kv[0]] = kv[1]
|
77
|
+
end
|
78
|
+
|
65
79
|
o.on '-v', '--verbose', 'turn on verbosity' do
|
66
80
|
options[:verbose] = true
|
67
81
|
end
|
@@ -135,7 +149,12 @@ if options[:verbose]
|
|
135
149
|
s.headers.each {|k,v| puts "<< #{k}: #{v}".yellow}
|
136
150
|
end
|
137
151
|
|
138
|
-
|
152
|
+
if s.eventsource?
|
153
|
+
s.body {|e| puts e}
|
154
|
+
else
|
155
|
+
puts s.body
|
156
|
+
end
|
157
|
+
|
139
158
|
c.block! if options[:block] or !s.pushes.empty?
|
140
159
|
s.pushes.each do |p|
|
141
160
|
puts "push promise: #{p.headers[':path']}"
|
data/lib/h2/client/celluloid.rb
CHANGED
data/lib/h2/client/concurrent.rb
CHANGED
data/lib/h2/client.rb
CHANGED
@@ -10,6 +10,7 @@ module H2
|
|
10
10
|
PARSER_EVENTS = [
|
11
11
|
:close,
|
12
12
|
:frame,
|
13
|
+
:frame_sent,
|
13
14
|
:goaway,
|
14
15
|
:promise
|
15
16
|
]
|
@@ -28,12 +29,13 @@ module H2
|
|
28
29
|
# @param [String] host IP address or hostname
|
29
30
|
# @param [Integer] port TCP port (default: 443)
|
30
31
|
# @param [String,URI] url full URL to parse (optional: existing +URI+ instance)
|
32
|
+
# @param [Boolean] lazy if true, awaits first stream to initiate connection (default: true)
|
31
33
|
# @param [Hash,FalseClass] tls TLS options (optional: +false+ do not use TLS)
|
32
34
|
# @option tls [String] :cafile path to CA file
|
33
35
|
#
|
34
36
|
# @return [H2::Client]
|
35
37
|
#
|
36
|
-
def initialize host: nil, port: 443, url: nil, tls: {}
|
38
|
+
def initialize host: nil, port: 443, url: nil, lazy: true, tls: {}
|
37
39
|
raise ArgumentError if url.nil? && (host.nil? || port.nil?)
|
38
40
|
|
39
41
|
if url
|
@@ -48,24 +50,34 @@ module H2
|
|
48
50
|
@scheme = tls ? 'https' : 'http'
|
49
51
|
end
|
50
52
|
|
51
|
-
@tls
|
52
|
-
@streams
|
53
|
-
@
|
54
|
-
@
|
55
|
-
@client = HTTP2::Client.new
|
56
|
-
|
57
|
-
@first = true
|
58
|
-
@reading = false
|
53
|
+
@tls = tls
|
54
|
+
@streams = {}
|
55
|
+
@client = HTTP2::Client.new
|
56
|
+
@read_gate = ReadGate.new
|
59
57
|
|
60
58
|
init_blocking
|
61
59
|
yield self if block_given?
|
62
60
|
bind_events
|
61
|
+
|
62
|
+
connect unless lazy
|
63
|
+
end
|
64
|
+
|
65
|
+
# initiate the connection
|
66
|
+
#
|
67
|
+
def connect
|
68
|
+
@socket = TCPSocket.new(@host, @port)
|
69
|
+
@socket = tls_socket @socket if @tls
|
70
|
+
read
|
71
|
+
end
|
72
|
+
|
73
|
+
def connected?
|
74
|
+
!!@socket
|
63
75
|
end
|
64
76
|
|
65
77
|
# @return true if the connection is closed
|
66
78
|
#
|
67
79
|
def closed?
|
68
|
-
@socket.closed?
|
80
|
+
connected? && @socket.closed?
|
69
81
|
end
|
70
82
|
|
71
83
|
# close the connection
|
@@ -79,14 +91,6 @@ module H2
|
|
79
91
|
@socket.eof?
|
80
92
|
end
|
81
93
|
|
82
|
-
def reading?
|
83
|
-
@mutex.synchronize { @reading }
|
84
|
-
end
|
85
|
-
|
86
|
-
def reading!
|
87
|
-
@mutex.synchronize { @reading = true }
|
88
|
-
end
|
89
|
-
|
90
94
|
# send a goaway frame and wait until the connection is closed
|
91
95
|
#
|
92
96
|
def goaway!
|
@@ -137,9 +141,10 @@ module H2
|
|
137
141
|
# @return [H2::Stream]
|
138
142
|
#
|
139
143
|
def request method:, path:, headers: {}, params: {}, body: nil, &block
|
144
|
+
connect unless connected?
|
140
145
|
s = @client.new_stream
|
141
|
-
stream = add_stream method: method, path: path, stream: s, &block
|
142
146
|
add_params params, path unless params.empty?
|
147
|
+
stream = add_stream method: method, path: path, stream: s, &block
|
143
148
|
|
144
149
|
h = build_headers method: method, path: path, headers: headers
|
145
150
|
s.headers h, end_stream: body.nil?
|
@@ -205,7 +210,8 @@ module H2
|
|
205
210
|
# creates a new +Thread+ to read the given number of bytes each loop from
|
206
211
|
# the current +@socket+
|
207
212
|
#
|
208
|
-
# NOTE: initial client frames (settings, etc) should be sent first
|
213
|
+
# NOTE: initial client frames (settings, etc) should be sent first, since
|
214
|
+
# this is a separate thread, take care to block until this happens
|
209
215
|
#
|
210
216
|
# NOTE: this is the override point for celluloid actor pool or concurrent
|
211
217
|
# ruby threadpool support
|
@@ -215,7 +221,7 @@ module H2
|
|
215
221
|
def read maxlen = DEFAULT_MAXLEN
|
216
222
|
main = Thread.current
|
217
223
|
@reader = Thread.new do
|
218
|
-
|
224
|
+
@read_gate.block!
|
219
225
|
begin
|
220
226
|
_read maxlen
|
221
227
|
rescue => e
|
@@ -300,9 +306,17 @@ module H2
|
|
300
306
|
@socket.write bytes
|
301
307
|
end
|
302
308
|
@socket.flush
|
309
|
+
end
|
303
310
|
|
304
|
-
|
305
|
-
|
311
|
+
# frame_sent callback for parser: used to wait for initial settings frame
|
312
|
+
# to be sent by the client (post-connection-preface) before the read thread
|
313
|
+
# responds to server settings frame with ack
|
314
|
+
#
|
315
|
+
def on_frame_sent frame
|
316
|
+
if @read_gate.first && frame[:type] == :settings
|
317
|
+
@read_gate.first = false
|
318
|
+
@read_gate.unblock!
|
319
|
+
end
|
306
320
|
end
|
307
321
|
|
308
322
|
# fake exceptionless IO for writing on older ruby versions
|
@@ -402,5 +416,16 @@ module H2
|
|
402
416
|
|
403
417
|
prepend ExceptionlessIO if H2.exceptionless_io?
|
404
418
|
|
419
|
+
class ReadGate
|
420
|
+
include Blockable
|
421
|
+
|
422
|
+
attr_accessor :first
|
423
|
+
|
424
|
+
def initialize
|
425
|
+
init_blocking
|
426
|
+
@first = true
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
405
430
|
end
|
406
431
|
end
|
@@ -7,8 +7,8 @@ module H2
|
|
7
7
|
DATA_TEMPL = "data: %s\n\n"
|
8
8
|
EVENT_TEMPL = "event: %s\n#{DATA_TEMPL}"
|
9
9
|
SSE_HEADER = {
|
10
|
-
STATUS_KEY
|
11
|
-
|
10
|
+
STATUS_KEY => '200',
|
11
|
+
CONTENT_TYPE_KEY => EVENT_SOURCE_CONTENT_TYPE
|
12
12
|
}
|
13
13
|
|
14
14
|
# build and return +EventSource+ instance, ready for pushing out data
|
@@ -35,7 +35,7 @@ module H2
|
|
35
35
|
#
|
36
36
|
def check_accept_header
|
37
37
|
accept = @stream.request.headers['accept']
|
38
|
-
unless accept == SSE_HEADER[
|
38
|
+
unless accept == SSE_HEADER[CONTENT_TYPE_KEY]
|
39
39
|
raise StreamError, "invalid header accept: #{accept}"
|
40
40
|
end
|
41
41
|
end
|
data/lib/h2/stream.rb
CHANGED
@@ -32,6 +32,8 @@ module H2
|
|
32
32
|
@pushes = Set.new
|
33
33
|
@stream = stream
|
34
34
|
|
35
|
+
@eventsource = false
|
36
|
+
|
35
37
|
init_blocking
|
36
38
|
yield self if block_given?
|
37
39
|
bind_events
|
@@ -61,6 +63,13 @@ module H2
|
|
61
63
|
@push
|
62
64
|
end
|
63
65
|
|
66
|
+
# @return [Boolean] true if this +Stream+ is connected to an +EventSource+
|
67
|
+
#
|
68
|
+
def eventsource?
|
69
|
+
block!
|
70
|
+
@eventsource
|
71
|
+
end
|
72
|
+
|
64
73
|
# add a push promise +Stream+ to this +Stream+'s list of "child" pushes
|
65
74
|
#
|
66
75
|
def add_push stream
|
@@ -91,8 +100,16 @@ module H2
|
|
91
100
|
# @return [String] response headers (blocks)
|
92
101
|
#
|
93
102
|
def body
|
94
|
-
|
95
|
-
|
103
|
+
if @eventsource
|
104
|
+
loop do
|
105
|
+
event = @body.pop
|
106
|
+
break if event == :close
|
107
|
+
yield event
|
108
|
+
end
|
109
|
+
else
|
110
|
+
block!
|
111
|
+
@body
|
112
|
+
end
|
96
113
|
end
|
97
114
|
|
98
115
|
# binds all stream events to their respective on_ handlers
|
@@ -102,6 +119,7 @@ module H2
|
|
102
119
|
@parent.add_push self if @parent && push?
|
103
120
|
@client.last_stream = self
|
104
121
|
@closed = true
|
122
|
+
@body << :close if @eventsource
|
105
123
|
unblock!
|
106
124
|
on :close
|
107
125
|
end
|
@@ -112,6 +130,7 @@ module H2
|
|
112
130
|
|
113
131
|
@stream.on(:data) do |d|
|
114
132
|
on :data, d
|
133
|
+
unblock! if @eventsource
|
115
134
|
@body << d
|
116
135
|
end
|
117
136
|
end
|
@@ -119,10 +138,20 @@ module H2
|
|
119
138
|
# builds +Hash+ from associative array, merges into response headers
|
120
139
|
#
|
121
140
|
def add_headers h
|
141
|
+
check_event_source h
|
122
142
|
h = Hash[h]
|
123
143
|
on :headers, h
|
124
144
|
@headers.merge! h
|
125
|
-
|
145
|
+
end
|
146
|
+
|
147
|
+
# checks for event source headers and reconfigures +@body+
|
148
|
+
#
|
149
|
+
def check_event_source h
|
150
|
+
return if @eventsource
|
151
|
+
if h.any? {|e| e[0] == CONTENT_TYPE_KEY && e[1] == EVENT_SOURCE_CONTENT_TYPE }
|
152
|
+
@eventsource = true
|
153
|
+
@body = Queue.new
|
154
|
+
end
|
126
155
|
end
|
127
156
|
|
128
157
|
# @return [Hash] a simple +Hash+ with +:headers+ and +:body+ keys/values
|
data/lib/h2/version.rb
CHANGED
data/lib/h2.rb
CHANGED
@@ -29,6 +29,9 @@ module H2
|
|
29
29
|
:put
|
30
30
|
]
|
31
31
|
|
32
|
+
CONTENT_TYPE_KEY = 'content-type'
|
33
|
+
EVENT_SOURCE_CONTENT_TYPE = 'text/event-stream'
|
34
|
+
|
32
35
|
Logger = ::Logger.new STDOUT
|
33
36
|
|
34
37
|
class << self
|
@@ -108,11 +111,11 @@ module H2
|
|
108
111
|
@mutex.synchronize { @condition.wait @mutex, timeout } if @condition
|
109
112
|
end
|
110
113
|
|
111
|
-
def unblock!
|
114
|
+
def unblock! remove_condition: true
|
112
115
|
return unless @condition
|
113
116
|
@mutex.synchronize do
|
114
117
|
@condition.broadcast
|
115
|
-
@condition = nil
|
118
|
+
@condition = nil if remove_condition
|
116
119
|
end
|
117
120
|
end
|
118
121
|
|
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.8.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-08-
|
11
|
+
date: 2018-08-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: http-2
|