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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a59b70f55abe247ca3ae333b72031a32f880fa1844bd8058585a4090bcc55969
4
- data.tar.gz: d28023b7123b0819c1f540070831188fd99460f11fe9914c2d8c00c7e4dd4e6d
3
+ metadata.gz: b32f7af9ee3845b4a817610abf47cf8fdf4503fda1418b49ab323220fe1ccf49
4
+ data.tar.gz: 5a6fc19c1d0480c6f0eeddb808d61a09dbdb1582c4aa31125cbe6a8e4ba0b8a0
5
5
  SHA512:
6
- metadata.gz: a7e502860f2f753ed37f419ba41b78c9399fe8a6278cbfe937ccd7c04f8da0c7fd53fdbc960454ac389a3b4a4073d3a39737a643fdf09235aac281b7f15f67eb
7
- data.tar.gz: 2dfa20d7aafdc0e37cd61aba4c6a2d9696c4efb58de3786f87c628f08c65bb5437baebdc93dd2acd4f27abde49021f2ac82d58d06ea9d133359d6d9098c8c6b4
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
@@ -1,6 +1,7 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
+ gem 'http-2', path: '../http-2'
4
5
 
5
6
  group :concurrent_ruby do
6
7
  gem 'concurrent-ruby'
data/README.md CHANGED
@@ -11,9 +11,10 @@ H2 uses:
11
11
 
12
12
  ## Server Usage
13
13
 
14
- Server API is currently optional, and must be required separately. The server
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`.
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 [examples](https://github.com/kenichi/h2/tree/master/examples/server/).
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',
@@ -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
- es = stream.to_eventsource
34
- event_sources << es
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
- puts s.body
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']}"
@@ -9,7 +9,6 @@ module H2
9
9
  include ::Celluloid
10
10
 
11
11
  def read client, maxlen = DEFAULT_MAXLEN
12
- client.reading!
13
12
  client._read maxlen
14
13
  end
15
14
  end
@@ -18,7 +18,7 @@ module H2
18
18
  def read maxlen = DEFAULT_MAXLEN
19
19
  main = Thread.current
20
20
  @reader = self.class.thread_pool.post do
21
- reading!
21
+ @read_gate.block!
22
22
  begin
23
23
  _read maxlen
24
24
  rescue => e
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 = tls
52
- @streams = {}
53
- @socket = TCPSocket.new(@host, @port)
54
- @socket = tls_socket @socket if @tls
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
- reading!
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
- @first = false if @first
305
- read unless @first or @reading
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 => '200',
11
- :content_type => 'text/event-stream'
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[:content_type]
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
- block!
95
- @body
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
- @headers
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
@@ -1,5 +1,5 @@
1
1
  module H2
2
- VERSION = '0.7.0'
2
+ VERSION = '0.8.0'
3
3
 
4
4
  class << self
5
5
 
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.7.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-02 00:00:00.000000000 Z
11
+ date: 2018-08-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http-2