h2 0.8.0 → 0.8.1

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: 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