h2 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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