h2 0.8.0 → 0.8.1

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: b32f7af9ee3845b4a817610abf47cf8fdf4503fda1418b49ab323220fe1ccf49
4
- data.tar.gz: 5a6fc19c1d0480c6f0eeddb808d61a09dbdb1582c4aa31125cbe6a8e4ba0b8a0
3
+ metadata.gz: 2b78f8a3e3aeed1884179c684adaa31e16cb377bdaa123d0af10f4cb174ff86f
4
+ data.tar.gz: 9beec3d11d01b4848118b17e1572275e231f580a48c62f0db28f52c059801c80
5
5
  SHA512:
6
- metadata.gz: 20820c02ce3c3459deb468854498e3bf362c19d1b0f032979d77d679f44803bbd81db8d780714e428e192b9bbead83114da031af2c2b5f7983b79075345ced2c
7
- data.tar.gz: 236e9658e69e7fde118c0cc4b679b2deceb83bebe2a06c4c33aac14038a39232d20ca4cadfc995e34a885b3c808dd14faee3cdb47e9adbca587b218ebeb00732
6
+ metadata.gz: 0ce8d9218b3cf9fc848ba56563ccdb8760cf63e30c312825ada05652e30dbcc60405109785b29c9466effcc9a3713ba8c57ccdf73c4a2f2b7984772e9a5f8f5a
7
+ data.tar.gz: ec1413ce8638d7aa6db8ec4e75f8032581e72dd391c5124e410ad3f9896feaf0f9580ae97895d846407d10e1b8a4a82ff381b57d14929d1d76240655ee55df53
@@ -1,6 +1,11 @@
1
1
  h2 changelog
2
2
  ============
3
3
 
4
+ ### 0.8.1 11 aug 2018
5
+
6
+ * update rake / bundler dep versions
7
+ * refactors for interpreter warnings
8
+
4
9
  ### 0.8.0 10 aug 2018
5
10
 
6
11
  * fix read/settings ack race (https://httpwg.org/specs/rfc7540.html#ConnectionHeader)
data/Gemfile CHANGED
@@ -1,7 +1,7 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
- gem 'http-2', path: '../http-2'
4
+ # gem 'http-2', path: '../http-2'
5
5
 
6
6
  group :concurrent_ruby do
7
7
  gem 'concurrent-ruby'
@@ -14,7 +14,6 @@ end
14
14
  group :development, :test do
15
15
  gem 'awesome_print'
16
16
  gem 'certificate_authority'
17
- gem 'guard-rake'
18
17
  gem 'pry-byebug', platforms: [:mri]
19
18
  gem 'celluloid-io', '~> 0.17', '>= 0.17.3'
20
19
  end
data/Rakefile CHANGED
@@ -1,14 +1,28 @@
1
- require "bundler/gem_tasks"
2
- require "rake/testtask"
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+ require 'fileutils'
4
+ require File.expand_path '../test/support/create_certs', __FILE__
3
5
 
4
6
  task default: :test
5
7
 
6
- Rake::TestTask.new :test => ['test:certs'] do |t|
8
+ Rake::TestTask.new :test do |t|
7
9
  t.test_files = FileList['test/**/*_test.rb']
8
10
  end
9
11
 
10
12
  namespace :test do
11
13
 
14
+ desc 'run tests via official ruby docker image'
15
+ task :docker, [:tag] do |_,args|
16
+ tag = args.fetch :tag, 'ruby:2.5'
17
+
18
+ FileUtils.mkdir_p 'tmp/docker'
19
+ system "docker pull #{tag}"
20
+ system "docker run --rm -v `pwd`:/opt/src/h2 -it #{tag} /bin/sh -c '" +
21
+ "cd /opt/src/h2 && " +
22
+ "bundle install --path tmp/docker && " +
23
+ "bundle exec rake test'"
24
+ end
25
+
12
26
  desc 'send TTIN signal to test process'
13
27
  task :ttin do
14
28
  pid = `ps -ef | grep -v grep | grep -e 'ruby.*_test\.rb' | awk '{print $2}'`.strip
@@ -18,19 +32,16 @@ namespace :test do
18
32
  end
19
33
  end
20
34
 
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
35
+ task :nginx, [:tag, :ctx] do |_,args|
36
+ tag = args.fetch :tag, 'h2_nginx_http2'
37
+ ctx = args.fetch :ctx, 'test/support/nginx'
26
38
 
27
- task :nginx => [:certs] do
28
- system "docker build -t h2_nginx_http2 test/support/nginx"
39
+ system "docker build -t #{tag} #{ctx}"
29
40
  puts "\nstarting nginx with http/2 support"
30
- puts "using document root: test/support/nginx/"
41
+ puts "using docker context/document root: #{ctx}"
31
42
  puts "using TLS certs: tmp/certs/server.*"
32
43
  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"
44
+ system "docker run --rm -v `pwd`/tmp/certs:/usr/local/nginx/certs -v `pwd`/#{ctx}:/usr/local/nginx/html -p 4430:443 -it #{tag}"
34
45
  end
35
46
 
36
47
  end
@@ -4,17 +4,48 @@
4
4
  require 'bundler/setup'
5
5
  require 'h2/server'
6
6
 
7
+ # hello world example
8
+ #
9
+ # NOTE: this is a plaintext "h2c" type of HTTP/2 server. browsers probably will
10
+ # never support this, but it may be useful for testing, or for behind
11
+ # TLS-enabled proxies.
12
+
13
+ # crank up the logger level for testing/example purposes
14
+ #
7
15
  H2::Logger.level = ::Logger::DEBUG
8
- H2.verbose!
9
16
 
10
- addr, port = '127.0.0.1', 1234
17
+ port = 1234
18
+ addr = Socket.getaddrinfo('localhost', port).first[3]
19
+ puts "*** Starting server on http://localhost:#{port}"
11
20
 
12
- puts "*** Starting server on http://#{addr}:#{port}"
21
+ # create h2c server on the given address and port.
22
+ # the constructor requires a block that will be called on each connection.
23
+ #
13
24
  s = H2::Server::HTTP.new host: addr, port: port do |connection|
25
+
26
+ # each connection will have 0 or more streams, so we must give the
27
+ # connection a stream handler block via the +#each_stream+ method.
28
+ #
14
29
  connection.each_stream do |stream|
30
+
31
+ # here, without checking anything about the request, we respond with 200
32
+ # and a "hello, world\n" body
33
+ #
34
+ # see +H2::Server::Stream#respond+
35
+ #
15
36
  stream.respond status: 200, body: "hello, world!\n"
37
+
38
+ # since HTTP/2 connections are sort of intrinsically "keep-alive", we
39
+ # tell the client to close immediately with a GOAWAY frame
40
+ #
41
+ # see also +H2::Server::Connection#goaway_on_complete+
42
+ #
16
43
  stream.connection.goaway
44
+
17
45
  end
18
46
  end
19
47
 
48
+ # now that our server reactor (Celluloid::IO instance) is configured and listening,
49
+ # we can put the "main" thread to sleep.
50
+ #
20
51
  sleep
@@ -4,31 +4,76 @@
4
4
  require 'bundler/setup'
5
5
  require 'h2/server'
6
6
 
7
- port = 1234
8
- addr = Socket.getaddrinfo('localhost', port).first[3]
9
- certs_dir = File.expand_path '../../../tmp/certs', __FILE__
7
+ # hello world TLS example
8
+ #
9
+ # NOTE: this is a TLS-enabled "h2" type of HTTP/2 server. we will need some
10
+ # cryptography to get going. this will check for the existence of a git-
11
+ # ignored set of testing CA, server, and client certs/keys, creating
12
+ # them as needed.
13
+ #
14
+ # see: test/support/create_certs.rb
15
+ #
16
+ require File.expand_path '../../../test/support/create_certs', __FILE__
17
+
18
+ # crank up the logger level for testing/example purposes
19
+ #
20
+ H2::Logger.level = ::Logger::DEBUG
21
+
22
+ port = 1234
23
+ addr = Socket.getaddrinfo('localhost', port).first[3]
24
+ puts "*** Starting server on https://localhost:#{port}"
10
25
 
11
26
  # if not using SNI, we may pass the underlying opts directly, and the same TLS
12
27
  # cert/key will be used for all incoming connections.
13
28
  #
29
+ certs_dir = File.expand_path '../../../tmp/certs', __FILE__
14
30
  tls = {
15
31
  cert: certs_dir + '/server.crt',
16
32
  key: certs_dir + '/server.key',
17
33
  # :extra_chain_cert => certs_dir + '/chain.pem'
18
34
  }
19
35
 
20
- puts "*** Starting server on https://#{addr}:#{port}"
21
-
36
+ # create h2 server on the given address and port using the given certificate
37
+ # and private key for all TLS negotiation. the constructor requires a block
38
+ # that will be called on each connection.
39
+ #
22
40
  s = H2::Server::HTTPS.new host: addr, port: port, **tls do |connection|
41
+
42
+ # each connection will have 0 or more streams, so we must give the
43
+ # connection a stream handler block via the +#each_stream+ method.
44
+ #
23
45
  connection.each_stream do |stream|
24
- stream.goaway_on_complete
25
46
 
47
+ # check the request path (HTTP/2 psuedo-header ':path')
48
+ #
49
+ # see +H2::Server::Stream#request+ - access the +H2::Server::Stream::Request+ instance
50
+ #
26
51
  if stream.request.path == '/favicon.ico'
52
+
53
+ # since this is a TLS-enabled server, we could actually test it with a
54
+ # real browser, which will undoubtedly request /favicon.ico.
55
+ #
56
+ # see +H2::Server::Stream#respond+
57
+ #
27
58
  stream.respond status: 404
59
+
28
60
  else
61
+
62
+ # since HTTP/2 connections are sort of intrinsically "keep-alive", we
63
+ # tell the client to close when this stream is complete with a GOAWAY frame
64
+ #
65
+ stream.goaway_on_complete
66
+
67
+ # we respond with 200 and a "hello, world\n" body
68
+ #
69
+ # see +H2::Server::Stream#respond+
70
+ #
29
71
  stream.respond status: 200, body: "hello, world!\n"
30
72
  end
31
73
  end
32
74
  end
33
75
 
76
+ # now that our server reactor (Celluloid::IO instance) is configured and listening,
77
+ # we can put the "main" thread to sleep.
78
+ #
34
79
  sleep
@@ -4,53 +4,162 @@
4
4
  require 'bundler/setup'
5
5
  require 'h2/server'
6
6
 
7
+ # push promise example
8
+ #
9
+ # NOTE: this is a TLS-enabled "h2" type of HTTP/2 server. we will need some
10
+ # cryptography to get going. this will check for the existence of a git-
11
+ # ignored set of testing CA, server, and client certs/keys, creating
12
+ # them as needed.
13
+ #
14
+ # see: test/support/create_certs.rb
15
+ #
16
+ require File.expand_path '../../../test/support/create_certs', __FILE__
17
+
18
+ # crank up the logger level for testing/example purposes
19
+ #
7
20
  H2::Logger.level = ::Logger::DEBUG
8
- H2.verbose!
9
21
 
10
- port = 4430
11
- addr = '127.0.0.1' #Socket.getaddrinfo('localhost', port).first[3]
12
- certs_dir = File.expand_path '../../../tmp/certs', __FILE__
13
- dog_png = File.read File.expand_path '../dog.png', __FILE__
14
- push_promise = '<html><body>wait for it...<img src="/dog.png"/><script src="/pushed.js"></script></body></html>'
15
- pushed_js = '(()=>{ alert("hello h2 push promise!"); })();'
22
+ port = 1234
23
+ addr = Socket.getaddrinfo('localhost', port).first[3]
24
+ puts "*** Starting server on https://localhost:#{port}"
25
+
26
+ # NOTE: since we're going to try a real-world push promise, let's load up some
27
+ # "useful" example bits to play with: full valid HTML, a PNG of a dog, and
28
+ # some javscript. we will respond directly with the HTML, but push promise
29
+ # the dog and js.
30
+ #
31
+ html = <<~EOHTML
32
+ <!DOCTYPE html>
33
+ <html lang="en">
34
+ <head>
35
+ <meta charset="UTF-8">
36
+ <title>HTTP/2 Push Promise Example</title>
37
+ </head>
38
+ <body>
39
+ wait for it...
40
+ <img src="/dog.png"/>
41
+ <script src="/pushed.js"></script>
42
+ </body>
43
+ </html>
44
+ EOHTML
45
+
46
+ dog_png_file = File.expand_path '../dog.png', __FILE__
47
+ dog_png = File.read dog_png_file
48
+ dog_png_fs = File::Stat.new dog_png_file
49
+ dog_png_etag = OpenSSL::Digest::SHA.hexdigest dog_png_fs.ino.to_s +
50
+ dog_png_fs.size.to_s +
51
+ dog_png_fs.mtime.to_s
16
52
 
53
+ pushed_js = '(()=>{ alert("hello h2 push promise!"); })();'
54
+
55
+ # using SNI, we can negotiate TLS for multiple certificates based on the
56
+ # requested servername.
57
+ #
58
+ # see: https://en.wikipedia.org/wiki/Server_Name_Indication
59
+ # see: https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html#servername_cb
60
+ #
61
+ certs_dir = File.expand_path '../../../tmp/certs', __FILE__
17
62
  sni = {
18
63
  'localhost' => {
19
64
  :cert => certs_dir + '/server.crt',
20
65
  :key => certs_dir + '/server.key',
21
66
  # :extra_chain_cert => certs_dir + '/chain.pem'
22
67
  },
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'
68
+ 'example.com' => {
69
+ :cert => certs_dir + '/example.com.crt',
70
+ :key => certs_dir + '/example.com.key',
71
+ :extra_chain_cert => certs_dir + '/example.com-chain.pem'
27
72
  }
28
73
  }
29
74
 
30
- puts "*** Starting server on https://#{addr}:#{port}"
75
+ # create h2 server on the given address and port using the given SNI +Hash+
76
+ # for configuring TLS negotiation. the constructor requires a block that will
77
+ # be called on each connection.
78
+ #
31
79
  s = H2::Server::HTTPS.new host: addr, port: port, sni: sni do |connection|
80
+
81
+ # each connection will have 0 or more streams, so we must give the
82
+ # connection a stream handler block via the +#each_stream+ method.
83
+ #
32
84
  connection.each_stream do |stream|
33
85
 
86
+ # check the request path (HTTP/2 psuedo-header ':path')
87
+ #
88
+ # see +H2::Server::Stream#request+
89
+ #
34
90
  if stream.request.path == '/favicon.ico'
91
+
92
+ # since this is a TLS-enabled server, we could actually test it with a
93
+ # real browser, which will undoubtedly request /favicon.ico.
94
+ #
95
+ # see +H2::Server::Stream#respond+
96
+ #
35
97
  stream.respond status: 404
36
98
 
37
99
  else
100
+
101
+ # since HTTP/2 connections are sort of intrinsically "keep-alive", we
102
+ # tell the client to close when this stream is complete with a GOAWAY frame
103
+ #
38
104
  stream.goaway_on_complete
39
105
 
40
- # one-line convenience with async "keep" handler
41
- stream.push_promise path: '/dog.png', headers: { 'content-type' => 'image/png' }, body: dog_png
106
+ # initiate a push promise sub-stream, and queue the "keep" handler.
107
+ # since a push promise may be canceled, we queue the handler on the server reactor,
108
+ # after initiating the stream with headers, so that the client has a chance to
109
+ # cancel with a RST_STREAM frame.
110
+ #
111
+ # see +H2::Server::Stream#push_promise+
112
+ #
113
+ stream.push_promise path: '/dog.png',
114
+ headers: {
115
+ 'content-type' => 'image/png',
116
+
117
+ # NOTE: "etag" headers are not supplied by the server.
118
+ #
119
+ 'etag' => dog_png_etag
120
+
121
+ },
122
+ body: dog_png
42
123
 
43
- # more control over when promises are "kept"...
124
+ # instantiate a push promise sub-stream, but do not send initial headers
125
+ # nor "keep" the promise by sending the body.
126
+ #
127
+ # see +H2::Server::Stream#push_promise_for+
128
+ #
44
129
  js_promise = stream.push_promise_for path: '/pushed.js',
45
130
  headers: { 'content-type' => 'application/javascript' },
46
131
  body: pushed_js
132
+
133
+ # have this +H2::Server::PushPromise+ initiate the sub-stream on this
134
+ # stream by sending initial headers.
135
+ #
136
+ # see +H2::Server::PushPromise#make_on+
137
+ #
47
138
  js_promise.make_on stream
48
139
 
49
- stream.respond status: 200, body: push_promise
140
+ # respond with 200 and HTML body
141
+ #
142
+ # see +H2::Server::Stream#respond+
143
+ #
144
+ stream.respond status: 200, body: html
50
145
 
146
+ # we have now waited until we've sent the entire body of the original
147
+ # response, so the client probably has received that and the push promise
148
+ # headers for both the dog and the script. our convenient +H2::Server::Stream#push_promise+
149
+ # method above queued the actual sending of the body on the server reactor,
150
+ # but for `js_promise`, we must keep it ourselves. in this case, we keep
151
+ # it "synchronously", but we may also call `#keep_async` to queue it.
152
+ #
153
+ # see +H2::Server::PushPromise#keep+
154
+ # see +H2::Server::PushPromise#keep_async+
155
+ #
51
156
  js_promise.keep
157
+
52
158
  end
53
159
  end
54
160
  end
55
161
 
162
+ # now that our server reactor (Celluloid::IO instance) is configured and listening,
163
+ # we can put the "main" thread to sleep.
164
+ #
56
165
  sleep
@@ -1,17 +1,35 @@
1
1
  #!/usr/bin/env ruby
2
- # Run with: bundle exec examples/server/sse/sse.rb
2
+ # Run with: bundle exec examples/server/sse.rb
3
3
 
4
4
  require 'bundler/setup'
5
5
  require 'h2/server'
6
6
 
7
+ # SSE / event source example
8
+ #
9
+ # NOTE: this is a TLS-enabled "h2" type of HTTP/2 server. we will need some
10
+ # cryptography to get going. this will check for the existence of a git-
11
+ # ignored set of testing CA, server, and client certs/keys, creating
12
+ # them as needed.
13
+ #
14
+ # see: test/support/create_certs.rb
15
+ #
16
+ require File.expand_path '../../../test/support/create_certs', __FILE__
17
+
18
+ # crank up the logger level for testing/example purposes
19
+ #
7
20
  H2::Logger.level = ::Logger::DEBUG
8
- H2.verbose!
9
21
 
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
22
+ port = 1234
23
+ addr = Socket.getaddrinfo('localhost', port).first[3]
24
+ puts "*** Starting server on https://localhost:#{port}"
14
25
 
26
+ # using SNI, we can negotiate TLS for multiple certificates based on the
27
+ # requested servername.
28
+ #
29
+ # see: https://en.wikipedia.org/wiki/Server_Name_Indication
30
+ # see: https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html#servername_cb
31
+ #
32
+ certs_dir = File.expand_path '../../../tmp/certs', __FILE__
15
33
  sni = {
16
34
  'localhost' => {
17
35
  :cert => certs_dir + '/server.crt',
@@ -20,52 +38,145 @@ sni = {
20
38
  }
21
39
  }
22
40
 
41
+ # this example is a bit more involved and requires more complicated
42
+ # html and javascript. these two vars are the base for a "poor-man's"
43
+ # implemenation of sinatra-style inline templates.
44
+ #
45
+ data, key = Hash.new {|h,k| h[k] = ''}, nil
46
+
47
+ # for example purposes, we're just going to use a top-level array for
48
+ # keeping track of connected +H2::Server::Stream::EventSource+ objects.
49
+ #
50
+ # NOTE: these are not "hijacked" sockets, like a websocket might be. since
51
+ # streams are multiplexed over one HTTP/2 TCP connection, each object
52
+ # only represents that stream, and should not hold up other streams
53
+ # on the connection.
54
+ #
23
55
  event_sources = []
24
56
 
25
- puts "*** Starting server on https://#{addr}:#{port}"
57
+ # create h2 server on the given address and port using the given SNI +Hash+
58
+ # for configuring TLS negotiation. the constructor requires a block that will
59
+ # be called on each connection.
60
+ #
26
61
  s = H2::Server::HTTPS.new host: addr, port: port, sni: sni do |connection|
62
+
63
+ # each connection will have 0 or more streams, so we must give the
64
+ # connection a stream handler block via the +#each_stream+ method.
65
+ #
27
66
  connection.each_stream do |stream|
67
+
68
+ # check the request path (HTTP/2 psuedo-header ':path')
69
+ #
70
+ # see +H2::Server::Stream#request+
71
+ #
28
72
  case stream.request.path
29
73
  when '/favicon.ico'
74
+
75
+ # since this is a TLS-enabled server, we could actually test it with a
76
+ # real browser, which will undoubtedly request /favicon.ico.
77
+ #
78
+ # see +H2::Server::Stream#respond+
79
+ #
30
80
  stream.respond status: 404
31
81
 
32
82
  when '/events'
33
- if stream.request.method == :delete
83
+
84
+ # check request method
85
+ #
86
+ case stream.request.method
87
+ when :get
88
+
89
+ # respond with headers turning this stream into an event source, and
90
+ # stash it in our top-level array.
91
+ #
92
+ # see +H2::Server::Stream#to_eventsource+
93
+ # see +H2::Server::Stream::EventSource+
94
+ #
95
+ begin
96
+ event_sources << stream.to_eventsource
97
+ rescue H2::Server::StreamError
98
+ stream.respond status: 400
99
+ end
100
+
101
+ when :delete
102
+
103
+ # handle a DELETE /events request by sending a final "die" event, then
104
+ # closing all connected event sources.
105
+ #
106
+ event_sources.each {|es| es.event name: 'die', data: 'later!' rescue nil }
34
107
  event_sources.each &:close
35
108
  event_sources.clear
109
+
110
+ # respond with the 200 "ok" status
111
+ #
36
112
  stream.respond status: 200
113
+
37
114
  else
38
- event_sources << stream.to_eventsource
115
+ stream.respond status: 404
39
116
  end
40
117
 
41
118
  when '/msg'
119
+
120
+ # check to make sure this is a POST request
121
+ #
42
122
  if stream.request.method == :post
123
+
124
+ # handle a POST /msg request and send the received body to all
125
+ # connected event sources as the data of an event named "msg".
126
+ #
43
127
  msg = stream.request.body
44
- event_sources.each {|es| es.event name: 'msg', data: msg}
128
+ event_sources.each {|es| es.event name: 'msg', data: msg rescue nil }
129
+
130
+ # respond with the 201 "created" status
131
+ #
45
132
  stream.respond status: 201
133
+
46
134
  else
135
+
136
+ # 404 if not post
137
+ #
47
138
  stream.respond status: 404
48
139
  end
49
140
 
50
141
  when '/sse.js'
142
+
143
+ # to further the push promise example a bit, here we respond with a 404
144
+ # if the client requests the script we've linked in the HTML. this means
145
+ # the *only* way for a client to get that script is to receive the push.
146
+ #
51
147
  stream.respond status: 404,
52
148
  body: "should have been pushed..."
53
149
 
54
150
  else
151
+
152
+ # initiate a push promise sub-stream, and queue the "keep" handler.
153
+ # since a push promise may be canceled, we queue the handler on the server reactor,
154
+ # after initiating the stream with headers, so that the client has a chance to
155
+ # cancel with a RST_STREAM frame.
156
+ #
157
+ # see +H2::Server::Stream#push_promise+
158
+ #
55
159
  stream.push_promise path: '/sse.js',
56
160
  headers: { 'content-type' => 'application/javascript' },
57
161
  body: data[:javascript]
58
162
 
163
+ # respond with 200 and HTML body
164
+ #
165
+ # see +H2::Server::Stream#respond+
166
+ #
59
167
  stream.respond status: 200, body: data[:html]
168
+
60
169
  end
61
170
  end
62
171
  end
63
172
 
173
+ # "poor-man's" sinatra-style inline "templates"
174
+ #
64
175
  DATA.each_line do |l|
65
176
  if l.start_with?('@@')
66
177
  key = l.strip[2..-1].to_sym
67
178
  else
68
- data[key] << l
179
+ data[key] << l unless l.empty?
69
180
  end
70
181
  end
71
182
 
@@ -86,18 +197,54 @@ __END__
86
197
  </form>
87
198
  <br/>
88
199
  <div><ol id="list"></ol></div>
200
+ <hr/>
201
+ <input id="delete" type="button" value="close all"/>
89
202
  </body>
90
203
  </html>
91
204
 
92
205
  @@javascript
206
+ //
207
+ // client code for SSE/eventsource example
208
+ //
209
+ var sse;
93
210
  document.addEventListener('DOMContentLoaded', () => {
94
- let sse = new EventSource('/events');
211
+
212
+ // fire up a new EventSource instance. this will initiate the GET /events
213
+ // request with 'text/event-stream' accept header. it will also continue to
214
+ // try reconnecting if the connection closes enexpectedly.
215
+ //
216
+ sse = new EventSource('/events');
217
+
218
+ // add event listeners as normal client JS event handler functions, where the
219
+ // event "name" is the value given with the `name:` keyword to
220
+ // H2::Server::Stream::EventSource#event.
221
+ //
95
222
  sse.addEventListener('msg', (msg) => {
223
+
224
+ // in this case, we're listening for the "msg" event and simply adding a
225
+ // new item to the list already in the DOM with the given data.
226
+ //
96
227
  let item = document.createElement('li');
97
228
  item.innerHTML = msg.data;
98
229
  document.getElementById('list').appendChild(item);
99
230
  });
100
231
 
232
+ // since SSE will keep trying to reconnect, we want a way to signal a stop
233
+ // to that. listen for the "die" event and close the EventSource.
234
+ //
235
+ sse.addEventListener('die', (e) => {
236
+ console.log('got die event:', e.data);
237
+ sse.close();
238
+ console.log('closed:', sse);
239
+ document.getElementById('words').setAttribute('disabled', 'disabled');
240
+ });
241
+ document.getElementById('delete').onclick = (e) => {
242
+ fetch('/events', {method: 'delete'});
243
+ };
244
+
245
+ // for the sake of the example, we supply a input field/form and hijack
246
+ // the "submit" event to post it to our server.
247
+ //
101
248
  let w = document.getElementById('words');
102
249
  document.getElementById('say').onsubmit = (e) => {
103
250
  e.preventDefault();
data/exe/h2 CHANGED
@@ -5,7 +5,6 @@
5
5
  #
6
6
  # ---
7
7
 
8
- require 'colored'
9
8
  require 'optparse'
10
9
 
11
10
  begin # {{{
@@ -23,11 +22,13 @@ options = {
23
22
  debug: false,
24
23
  headers: {},
25
24
  goaway: false,
26
- method: nil,
25
+ method: :get,
27
26
  tls: {},
28
27
  verbose: false
29
28
  }
30
29
 
30
+ options[:tls][:ca_file] = ENV['H2_CAFILE'] if ENV['H2_CAFILE']
31
+
31
32
  OptionParser.new do |o|
32
33
 
33
34
  o.banner = 'Usage: h2 [options] URL'
@@ -36,7 +37,7 @@ OptionParser.new do |o|
36
37
  options[:block] = true
37
38
  end
38
39
 
39
- o.on '--cafile [FILE]', String, 'certificate authority bundle' do |ca|
40
+ o.on '--cafile [FILE]', String, "certificate authority bundle (overrides ENV['H2_CAFILE'])" do |ca|
40
41
  raise ArgumentError, "invalid CA file: #{ca}" unless ca && File.exist?(ca)
41
42
  options[:tls][:ca_file] = ca
42
43
  end
@@ -72,6 +73,7 @@ OptionParser.new do |o|
72
73
  end
73
74
 
74
75
  o.on '-H [VALUE]', '--header [VALUE]', String, 'include header in request (format: "key: value")' do |h|
76
+ warn "psuedo-headers not supported via CLI" if h[0] == ':'
75
77
  kv = h.split(':').map &:strip
76
78
  options[:headers][kv[0]] = kv[1]
77
79
  end
@@ -86,16 +88,16 @@ OptionParser.new do |o|
86
88
  options[:method] = meth
87
89
  end
88
90
 
89
- o.on '--version', 'print version information' do
90
- puts "#{H2::USER_AGENT['user-agent']} using http-2-#{HTTP2::VERSION}"
91
+ o.on '-V', '--version', 'print version information' do
92
+ puts "#{H2::USER_AGENT['user-agent']}"
93
+ puts " * http-2-#{HTTP2::VERSION}"
94
+ puts " * #{OpenSSL::OPENSSL_VERSION}"
95
+ puts " * CA file: #{options[:tls][:ca_file]}" if options[:tls][:ca_file]
91
96
  exit
92
97
  end
93
98
 
94
99
  end.parse!
95
100
 
96
- options[:method] ||= :get
97
- options[:tls][:ca_file] ||= ENV['H2_CAFILE'] if ENV['H2_CAFILE']
98
-
99
101
  # }}}
100
102
 
101
103
  # --- parse URL {{{
@@ -116,8 +118,8 @@ client[:tls] = options[:tls] unless options[:tls].empty?
116
118
 
117
119
  c = H2::Client.new **client do |c|
118
120
  if options[:debug]
119
- c.client.on(:frame_received) {|f| puts "<< #{f.inspect}".yellow}
120
- c.client.on(:frame_sent) {|f| puts ">> #{f.inspect}".green}
121
+ c.client.on(:frame_received) {|f| puts "<< #{f.inspect}" }
122
+ c.client.on(:frame_sent) {|f| puts ">> #{f.inspect}" }
121
123
  end
122
124
  end
123
125
 
data/h2.gemspec CHANGED
@@ -1,5 +1,5 @@
1
1
  # coding: utf-8
2
- require_relative './lib/h2/version'
2
+ require File.expand_path '../lib/h2/version', __FILE__
3
3
 
4
4
  Gem::Specification.new do |spec|
5
5
  spec.name = "h2"
@@ -18,9 +18,8 @@ Gem::Specification.new do |spec|
18
18
  spec.required_ruby_version = '>= 2.2'
19
19
 
20
20
  spec.add_dependency 'http-2', '~> 0.10', '>= 0.10.0'
21
- spec.add_dependency 'colored', '1.2'
22
21
 
23
- spec.add_development_dependency "bundler", "~> 1.15"
24
- spec.add_development_dependency "rake", "~> 10.0"
22
+ spec.add_development_dependency "bundler", "~> 1.16"
23
+ spec.add_development_dependency "rake", "~> 12.0"
25
24
  spec.add_development_dependency "minitest", "~> 5.0"
26
25
  end
data/lib/h2.rb CHANGED
@@ -36,17 +36,6 @@ module H2
36
36
 
37
37
  class << self
38
38
 
39
- # turn on extra verbose debug logging
40
- #
41
- def verbose!
42
- @verbose = true
43
- end
44
-
45
- def verbose?
46
- @verbose = false unless defined?(@verbose)
47
- @verbose
48
- end
49
-
50
39
  # convenience wrappers to make requests with HTTP methods
51
40
  #
52
41
  # @see H2.request
@@ -140,7 +129,6 @@ module H2
140
129
  module FrameDebugger
141
130
 
142
131
  def self.included base
143
- H2.verbose!
144
132
  base::PARSER_EVENTS.push :frame_sent, :frame_received
145
133
  end
146
134
 
@@ -66,29 +66,29 @@ module H2
66
66
  #
67
67
  def connect
68
68
  @socket = TCPSocket.new(@host, @port)
69
- @socket = tls_socket @socket if @tls
69
+ @socket = tls_socket socket if @tls
70
70
  read
71
71
  end
72
72
 
73
73
  def connected?
74
- !!@socket
74
+ !!socket
75
75
  end
76
76
 
77
77
  # @return true if the connection is closed
78
78
  #
79
79
  def closed?
80
- connected? && @socket.closed?
80
+ connected? && socket.closed?
81
81
  end
82
82
 
83
83
  # close the connection
84
84
  #
85
85
  def close
86
86
  unblock!
87
- @socket.close unless closed?
87
+ socket.close unless closed?
88
88
  end
89
89
 
90
90
  def eof?
91
- @socket.eof?
91
+ socket.eof?
92
92
  end
93
93
 
94
94
  # send a goaway frame and wait until the connection is closed
@@ -204,7 +204,7 @@ module H2
204
204
  # maintain a ivar for the +Array+ to send to +IO.select+
205
205
  #
206
206
  def selector
207
- @selector ||= [@socket]
207
+ @selector ||= [socket]
208
208
  end
209
209
 
210
210
  # creates a new +Thread+ to read the given number of bytes each loop from
@@ -268,7 +268,7 @@ module H2
268
268
  # @param [Integer] maxlen maximum number of bytes to read
269
269
  #
270
270
  def read_from_socket maxlen
271
- @socket.read_nonblock maxlen
271
+ socket.read_nonblock maxlen
272
272
  rescue IO::WaitReadable
273
273
  :wait_readable
274
274
  end
@@ -290,12 +290,12 @@ module H2
290
290
  def on_frame bytes
291
291
  on :frame, bytes
292
292
 
293
- if ::H2::Client::TCPSocket === @socket
293
+ if ::H2::Client::TCPSocket === socket
294
294
  total = bytes.bytesize
295
295
  loop do
296
296
  n = write_to_socket bytes
297
297
  if n == :wait_writable
298
- IO.select nil, @socket.selector
298
+ IO.select nil, socket.selector
299
299
  elsif n < total
300
300
  bytes = bytes.byteslice n, total
301
301
  else
@@ -303,9 +303,9 @@ module H2
303
303
  end
304
304
  end
305
305
  else
306
- @socket.write bytes
306
+ socket.write bytes
307
307
  end
308
- @socket.flush
308
+ socket.flush
309
309
  end
310
310
 
311
311
  # frame_sent callback for parser: used to wait for initial settings frame
@@ -324,7 +324,7 @@ module H2
324
324
  # @param [String] bytes
325
325
  #
326
326
  def write_to_socket bytes
327
- @socket.write_nonblock bytes
327
+ socket.write_nonblock bytes
328
328
  rescue IO::WaitWritable
329
329
  :wait_writable
330
330
  end
@@ -405,11 +405,11 @@ module H2
405
405
  module ExceptionlessIO
406
406
 
407
407
  def read_from_socket maxlen
408
- @socket.read_nonblock maxlen, exception: false
408
+ socket.read_nonblock maxlen, exception: false
409
409
  end
410
410
 
411
411
  def write_to_socket bytes
412
- @socket.write_nonblock bytes, exception: false
412
+ socket.write_nonblock bytes, exception: false
413
413
  end
414
414
 
415
415
  end
@@ -6,19 +6,6 @@ module H2
6
6
 
7
7
  CONTENT_LENGTH_KEY = 'content-length'
8
8
 
9
- class << self
10
-
11
- def alpn?
12
- !jruby? && OpenSSL::OPENSSL_VERSION_NUMBER >= ALPN_OPENSSL_MIN_VERSION && RUBY_VERSION >= '2.3'
13
- end
14
-
15
- def jruby?
16
- return @jruby if defined? @jruby
17
- @jruby = RUBY_ENGINE == 'jruby'
18
- end
19
-
20
- end
21
-
22
9
  # base H2 server, a +Celluoid::IO+ production
23
10
  #
24
11
  class Server
@@ -27,11 +27,15 @@ module H2
27
27
  @parser = ::HTTP2::Server.new
28
28
  @attached = true
29
29
 
30
+ # set a default stream handler that raises +NotImplementedError+
31
+ #
32
+ @each_stream = ->(s){ raise NotImplementedError }
33
+
30
34
  yield self if block_given?
31
35
 
32
36
  bind_events
33
37
 
34
- Logger.debug "new H2::Connection: #{self}" if H2.verbose?
38
+ Logger.debug "new H2::Connection: #{self}"
35
39
  end
36
40
 
37
41
  # is this connection still attached to the server reactor?
@@ -94,7 +98,7 @@ module H2
94
98
 
95
99
  rescue => e
96
100
  Logger.error "Exception: #{e.message} - closing socket"
97
- STDERR.puts e.backtrace if H2.verbose?
101
+ STDERR.puts e.backtrace if H2::Logger.level == ::Logger::DEBUG
98
102
  close
99
103
 
100
104
  end
@@ -24,7 +24,8 @@ module H2
24
24
  :data
25
25
  ]
26
26
 
27
- attr_reader :connection,
27
+ attr_reader :complete,
28
+ :connection,
28
29
  :push_promises,
29
30
  :request,
30
31
  :response,
@@ -62,7 +63,7 @@ module H2
62
63
  # task on the reactor to deliver the data
63
64
  #
64
65
  def push_promise *args
65
- pp = push_promise_for *args
66
+ pp = push_promise_for(*args)
66
67
  make_promise pp
67
68
  @connection.server.async.handle_push_promise pp
68
69
  end
@@ -95,7 +96,7 @@ module H2
95
96
  if block
96
97
  @complete = block
97
98
  elsif @completed = (@responded and push_promises_complete?)
98
- @complete[] if Proc === @complete
99
+ @complete[] if Proc === complete
99
100
  true
100
101
  else
101
102
  false
@@ -149,14 +150,14 @@ module H2
149
150
  # called by +@stream+ when this stream is activated
150
151
  #
151
152
  def on_active
152
- log :debug, 'active' if H2.verbose?
153
+ log :debug, 'active'
153
154
  @request = H2::Server::Stream::Request.new self
154
155
  end
155
156
 
156
157
  # called by +@stream+ when this stream is closed
157
158
  #
158
159
  def on_close
159
- log :debug, 'close' if H2.verbose?
160
+ log :debug, 'close'
160
161
  on_complete
161
162
  @closed = true
162
163
  end
@@ -165,14 +166,14 @@ module H2
165
166
  #
166
167
  def on_headers h
167
168
  incoming_headers = Hash[h]
168
- log :debug, "headers: #{incoming_headers}" if H2.verbose?
169
+ log :debug, "headers: #{incoming_headers}"
169
170
  @request.headers.merge! incoming_headers
170
171
  end
171
172
 
172
173
  # called by +@stream+ with a +String+ body part
173
174
  #
174
175
  def on_data d
175
- log :debug, "data: <<#{d}>>" if H2.verbose?
176
+ log :debug, "data: <<#{d}>>"
176
177
  @request.body << d
177
178
  end
178
179
 
@@ -180,7 +181,7 @@ module H2
180
181
  # is ready for response(s)
181
182
  #
182
183
  def on_half_close
183
- log :debug, 'half_close' if H2.verbose?
184
+ log :debug, 'half_close'
184
185
  connection.server.async.handle_stream self
185
186
  end
186
187
 
@@ -45,7 +45,7 @@ module H2
45
45
  def init_response
46
46
  headers = SSE_HEADER.merge @headers
47
47
  @parser.headers stringify_headers(headers)
48
- rescue ::HTTP2::Error::StreamClosed => sc
48
+ rescue ::HTTP2::Error::StreamClosed
49
49
  @stream.log :warn, "stream closed early by client"
50
50
  end
51
51
 
@@ -35,7 +35,7 @@ module H2
35
35
  # retreive the HTTP method as a lowercase +Symbol+
36
36
  #
37
37
  def method
38
- return @method unless @method.nil?
38
+ return @method if defined? @method
39
39
  @method = headers[METHOD_KEY]
40
40
  @method = @method.downcase.to_sym if @method
41
41
  @method
@@ -62,7 +62,7 @@ module H2
62
62
  else
63
63
  stream.log :error, "unexpected @body: #{caller[0]}"
64
64
  end
65
- rescue ::HTTP2::Error::StreamClosed => sc
65
+ rescue ::HTTP2::Error::StreamClosed
66
66
  stream.log :warn, "stream closed early by client"
67
67
  end
68
68
 
@@ -11,7 +11,7 @@ module H2
11
11
  :data
12
12
  ]
13
13
 
14
- attr_reader :body, :client, :headers, :parent, :pushes, :stream
14
+ attr_reader :client, :parent, :pushes, :stream
15
15
 
16
16
  # create a new h2 stream
17
17
  #
@@ -1,5 +1,5 @@
1
1
  module H2
2
- VERSION = '0.8.0'
2
+ VERSION = '0.8.1'
3
3
 
4
4
  class << self
5
5
 
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.0
4
+ version: 0.8.1
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-10 00:00:00.000000000 Z
11
+ date: 2018-08-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http-2
@@ -30,48 +30,34 @@ dependencies:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
32
  version: 0.10.0
33
- - !ruby/object:Gem::Dependency
34
- name: colored
35
- requirement: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - '='
38
- - !ruby/object:Gem::Version
39
- version: '1.2'
40
- type: :runtime
41
- prerelease: false
42
- version_requirements: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - '='
45
- - !ruby/object:Gem::Version
46
- version: '1.2'
47
33
  - !ruby/object:Gem::Dependency
48
34
  name: bundler
49
35
  requirement: !ruby/object:Gem::Requirement
50
36
  requirements:
51
37
  - - "~>"
52
38
  - !ruby/object:Gem::Version
53
- version: '1.15'
39
+ version: '1.16'
54
40
  type: :development
55
41
  prerelease: false
56
42
  version_requirements: !ruby/object:Gem::Requirement
57
43
  requirements:
58
44
  - - "~>"
59
45
  - !ruby/object:Gem::Version
60
- version: '1.15'
46
+ version: '1.16'
61
47
  - !ruby/object:Gem::Dependency
62
48
  name: rake
63
49
  requirement: !ruby/object:Gem::Requirement
64
50
  requirements:
65
51
  - - "~>"
66
52
  - !ruby/object:Gem::Version
67
- version: '10.0'
53
+ version: '12.0'
68
54
  type: :development
69
55
  prerelease: false
70
56
  version_requirements: !ruby/object:Gem::Requirement
71
57
  requirements:
72
58
  - - "~>"
73
59
  - !ruby/object:Gem::Version
74
- version: '10.0'
60
+ version: '12.0'
75
61
  - !ruby/object:Gem::Dependency
76
62
  name: minitest
77
63
  requirement: !ruby/object:Gem::Requirement
@@ -99,7 +85,6 @@ files:
99
85
  - CHANGELOG.md
100
86
  - CODE_OF_CONDUCT.md
101
87
  - Gemfile
102
- - Guardfile
103
88
  - LICENSE.txt
104
89
  - README.md
105
90
  - Rakefile
data/Guardfile DELETED
@@ -1,3 +0,0 @@
1
- guard :rake, task: 'test' do
2
- watch %r{^(lib|test).*}
3
- end