http-2 0.11.0 → 0.12.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +0 -2
  3. data/lib/http/2/buffer.rb +6 -4
  4. data/lib/http/2/client.rb +5 -1
  5. data/lib/http/2/compressor.rb +42 -34
  6. data/lib/http/2/connection.rb +72 -86
  7. data/lib/http/2/emitter.rb +4 -1
  8. data/lib/http/2/error.rb +2 -0
  9. data/lib/http/2/flow_buffer.rb +8 -3
  10. data/lib/http/2/framer.rb +83 -94
  11. data/lib/http/2/huffman.rb +19 -17
  12. data/lib/http/2/server.rb +9 -7
  13. data/lib/http/2/stream.rb +48 -48
  14. data/lib/http/2/version.rb +3 -1
  15. data/lib/http/2.rb +2 -0
  16. metadata +7 -60
  17. data/.autotest +0 -20
  18. data/.coveralls.yml +0 -1
  19. data/.gitignore +0 -20
  20. data/.gitmodules +0 -3
  21. data/.rspec +0 -5
  22. data/.rubocop.yml +0 -93
  23. data/.rubocop_todo.yml +0 -131
  24. data/.travis.yml +0 -17
  25. data/Gemfile +0 -16
  26. data/Guardfile +0 -18
  27. data/Guardfile.h2spec +0 -12
  28. data/Rakefile +0 -49
  29. data/example/Gemfile +0 -3
  30. data/example/README.md +0 -44
  31. data/example/client.rb +0 -122
  32. data/example/helper.rb +0 -19
  33. data/example/keys/server.crt +0 -20
  34. data/example/keys/server.key +0 -27
  35. data/example/server.rb +0 -139
  36. data/example/upgrade_client.rb +0 -153
  37. data/example/upgrade_server.rb +0 -203
  38. data/http-2.gemspec +0 -22
  39. data/lib/tasks/generate_huffman_table.rb +0 -166
  40. data/spec/buffer_spec.rb +0 -28
  41. data/spec/client_spec.rb +0 -188
  42. data/spec/compressor_spec.rb +0 -666
  43. data/spec/connection_spec.rb +0 -681
  44. data/spec/emitter_spec.rb +0 -54
  45. data/spec/framer_spec.rb +0 -487
  46. data/spec/h2spec/h2spec.darwin +0 -0
  47. data/spec/h2spec/output/non_secure.txt +0 -317
  48. data/spec/helper.rb +0 -147
  49. data/spec/hpack_test_spec.rb +0 -84
  50. data/spec/huffman_spec.rb +0 -68
  51. data/spec/server_spec.rb +0 -52
  52. data/spec/stream_spec.rb +0 -878
  53. data/spec/support/deep_dup.rb +0 -55
  54. data/spec/support/duplicable.rb +0 -98
@@ -1,203 +0,0 @@
1
- # frozen_string_literals: true
2
-
3
- require_relative 'helper'
4
- require 'http_parser'
5
-
6
- options = { port: 8080 }
7
- OptionParser.new do |opts|
8
- opts.banner = 'Usage: server.rb [options]'
9
-
10
- opts.on('-s', '--secure', 'HTTPS mode') do |v|
11
- options[:secure] = v
12
- end
13
-
14
- opts.on('-p', '--port [Integer]', 'listen port') do |v|
15
- options[:port] = v
16
- end
17
- end.parse!
18
-
19
- puts "Starting server on port #{options[:port]}"
20
- server = TCPServer.new(options[:port])
21
-
22
- if options[:secure]
23
- ctx = OpenSSL::SSL::SSLContext.new
24
- ctx.cert = OpenSSL::X509::Certificate.new(File.open('keys/server.crt'))
25
- ctx.key = OpenSSL::PKey::RSA.new(File.open('keys/server.key'))
26
- ctx.npn_protocols = [DRAFT]
27
-
28
- server = OpenSSL::SSL::SSLServer.new(server, ctx)
29
- end
30
-
31
- def request_header_hash
32
- Hash.new do |hash, key|
33
- k = key.to_s.downcase
34
- k.tr! '_', '-'
35
- _, value = hash.find { |header_key, _| header_key.downcase == k }
36
- hash[key] = value if value
37
- end
38
- end
39
-
40
- class UpgradeHandler
41
- VALID_UPGRADE_METHODS = %w(GET OPTIONS).freeze
42
- UPGRADE_RESPONSE = <<RESP.freeze
43
- HTTP/1.1 101 Switching Protocols
44
- Connection: Upgrade
45
- Upgrade: h2c
46
-
47
- RESP
48
-
49
- attr_reader :complete, :headers, :body, :parsing
50
-
51
- def initialize(conn, sock)
52
- @conn, @sock = conn, sock
53
- @complete, @parsing = false, false
54
- @headers = request_header_hash
55
- @body = ''
56
- @parser = ::HTTP::Parser.new(self)
57
- end
58
-
59
- def <<(data)
60
- @parsing ||= true
61
- @parser << data
62
- return unless complete
63
-
64
- @sock.write UPGRADE_RESPONSE
65
-
66
- settings = headers['http2-settings']
67
- request = {
68
- ':scheme' => 'http',
69
- ':method' => @parser.http_method,
70
- ':authority' => headers['Host'],
71
- ':path' => @parser.request_url,
72
- }.merge(headers)
73
-
74
- @conn.upgrade(settings, request, @body)
75
- end
76
-
77
- def complete!
78
- @complete = true
79
- end
80
-
81
- def on_headers_complete(headers)
82
- @headers.merge! headers
83
- end
84
-
85
- def on_body(chunk)
86
- @body << chunk
87
- end
88
-
89
- def on_message_complete
90
- fail unless VALID_UPGRADE_METHODS.include?(@parser.http_method)
91
- @parsing = false
92
- complete!
93
- end
94
- end
95
-
96
- loop do
97
- sock = server.accept
98
- puts 'New TCP connection!'
99
-
100
- conn = HTTP2::Server.new
101
- conn.on(:frame) do |bytes|
102
- # puts "Writing bytes: #{bytes.unpack("H*").first}"
103
- sock.write bytes
104
- end
105
- conn.on(:frame_sent) do |frame|
106
- puts "Sent frame: #{frame.inspect}"
107
- end
108
- conn.on(:frame_received) do |frame|
109
- puts "Received frame: #{frame.inspect}"
110
- end
111
-
112
- conn.on(:stream) do |stream|
113
- log = Logger.new(stream.id)
114
- req = request_header_hash
115
- buffer = ''
116
-
117
- stream.on(:active) { log.info 'client opened new stream' }
118
- stream.on(:close) do
119
- log.info 'stream closed'
120
- end
121
-
122
- stream.on(:headers) do |h|
123
- req.merge! Hash[*h.flatten]
124
- log.info "request headers: #{h}"
125
- end
126
-
127
- stream.on(:data) do |d|
128
- log.info "payload chunk: <<#{d}>>"
129
- buffer << d
130
- end
131
-
132
- stream.on(:half_close) do
133
- log.info 'client closed its end of the stream'
134
-
135
- if req['Upgrade']
136
- log.info "Processing h2c Upgrade request: #{req}"
137
- if req[':method'] != 'OPTIONS' # Don't respond to OPTIONS...
138
- response = 'Hello h2c world!'
139
- stream.headers({
140
- ':status' => '200',
141
- 'content-length' => response.bytesize.to_s,
142
- 'content-type' => 'text/plain',
143
- }, end_stream: false)
144
- stream.data(response)
145
- end
146
- else
147
-
148
- response = nil
149
- if req[':method'] == 'POST'
150
- log.info "Received POST request, payload: #{buffer}"
151
- response = "Hello HTTP 2.0! POST payload: #{buffer}"
152
- else
153
- log.info 'Received GET request'
154
- response = 'Hello HTTP 2.0! GET request'
155
- end
156
-
157
- stream.headers({
158
- ':status' => '200',
159
- 'content-length' => response.bytesize.to_s,
160
- 'content-type' => 'text/plain',
161
- }, end_stream: false)
162
-
163
- # split response into multiple DATA frames
164
- stream.data(response.slice!(0, 5), end_stream: false)
165
- stream.data(response)
166
- end
167
- end
168
- end
169
-
170
- uh = UpgradeHandler.new(conn, sock)
171
-
172
- while !sock.closed? && !(sock.eof? rescue true) # rubocop:disable Style/RescueModifier
173
- data = sock.readpartial(1024)
174
- # puts "Received bytes: #{data.unpack("H*").first}"
175
-
176
- begin
177
- case
178
- when !uh.parsing && !uh.complete
179
-
180
- if data.start_with?(*UpgradeHandler::VALID_UPGRADE_METHODS)
181
- uh << data
182
- else
183
- uh.complete!
184
- conn << data
185
- end
186
-
187
- when uh.parsing && !uh.complete
188
- uh << data
189
-
190
- when uh.complete
191
- conn << data
192
- end
193
-
194
- rescue StandardError => e
195
- puts "Exception: #{e}, #{e.message} - closing socket."
196
- puts e.backtrace.last(10).join("\n")
197
- sock.close
198
- end
199
- end
200
- end
201
-
202
- # echo foo=bar | nghttp -d - -t 0 -vu http://127.0.0.1:8080/
203
- # nghttp -vu http://127.0.0.1:8080/
data/http-2.gemspec DELETED
@@ -1,22 +0,0 @@
1
- lib = File.expand_path('./lib', __dir__)
2
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
- require 'http/2/version'
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = 'http-2'
7
- spec.version = HTTP2::VERSION
8
- spec.authors = ['Ilya Grigorik', 'Kaoru Maeda']
9
- spec.email = ['ilya@igvita.com']
10
- spec.description = 'Pure-ruby HTTP 2.0 protocol implementation'
11
- spec.summary = spec.description
12
- spec.homepage = 'https://github.com/igrigorik/http-2'
13
- spec.license = 'MIT'
14
- spec.required_ruby_version = '>=2.1.0'
15
-
16
- spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ['lib']
20
-
21
- spec.add_development_dependency 'bundler'
22
- end
@@ -1,166 +0,0 @@
1
- desc 'Generate Huffman precompiled table in huffman_statemachine.rb'
2
- task :generate_table do
3
- HuffmanTable::Node.generate_state_table
4
- end
5
-
6
- require_relative '../http/2/huffman'
7
-
8
- # @private
9
- module HuffmanTable
10
- BITS_AT_ONCE = HTTP2::Header::Huffman::BITS_AT_ONCE
11
- EOS = 256
12
-
13
- class Node
14
- attr_accessor :next, :emit, :final, :depth
15
- attr_accessor :transitions
16
- attr_accessor :id
17
- @@id = 0 # rubocop:disable Style/ClassVars
18
- def initialize(depth)
19
- @next = [nil, nil]
20
- @id = @@id
21
- @@id += 1 # rubocop:disable Style/ClassVars
22
- @final = false
23
- @depth = depth
24
- end
25
-
26
- def add(code, len, chr)
27
- self.final = true if chr == EOS && @depth <= 7
28
- if len.zero?
29
- @emit = chr
30
- else
31
- bit = (code & (1 << (len - 1))).zero? ? 0 : 1
32
- node = @next[bit] ||= Node.new(@depth + 1)
33
- node.add(code, len - 1, chr)
34
- end
35
- end
36
-
37
- class Transition
38
- attr_accessor :emit, :node
39
- def initialize(emit, node)
40
- @emit = emit
41
- @node = node
42
- end
43
- end
44
-
45
- def self.generate_tree
46
- @root = new(0)
47
- HTTP2::Header::Huffman::CODES.each_with_index do |c, chr|
48
- code, len = c
49
- @root.add(code, len, chr)
50
- end
51
- puts "#{@@id} nodes"
52
- @root
53
- end
54
-
55
- def self.generate_machine
56
- generate_tree
57
- togo = Set[@root]
58
- @states = Set[@root]
59
-
60
- until togo.empty?
61
- node = togo.first
62
- togo.delete(node)
63
-
64
- next if node.transitions
65
- node.transitions = Array[1 << BITS_AT_ONCE]
66
-
67
- (1 << BITS_AT_ONCE).times do |input|
68
- n = node
69
- emit = ''
70
- (BITS_AT_ONCE - 1).downto(0) do |i|
71
- bit = (input & (1 << i)).zero? ? 0 : 1
72
- n = n.next[bit]
73
- next unless n.emit
74
- if n.emit == EOS
75
- emit = EOS # cause error on decoding
76
- else
77
- emit << n.emit.chr(Encoding::BINARY) unless emit == EOS
78
- end
79
- n = @root
80
- end
81
- node.transitions[input] = Transition.new(emit, n)
82
- togo << n
83
- @states << n
84
- end
85
- end
86
- puts "#{@states.size} states"
87
- @root
88
- end
89
-
90
- def self.generate_state_table
91
- generate_machine
92
- state_id = {}
93
- id_state = {}
94
- state_id[@root] = 0
95
- id_state[0] = @root
96
- max_final = 0
97
- id = 1
98
- (@states - [@root]).sort_by { |s| s.final ? 0 : 1 }.each do |s|
99
- state_id[s] = id
100
- id_state[id] = s
101
- max_final = id if s.final
102
- id += 1
103
- end
104
-
105
- File.open(File.expand_path('../http/2/huffman_statemachine.rb', File.dirname(__FILE__)), 'w') do |f|
106
- f.print <<HEADER
107
- # Machine generated Huffman decoder state machine.
108
- # DO NOT EDIT THIS FILE.
109
-
110
- # The following task generates this file.
111
- # rake generate_huffman_table
112
-
113
- module HTTP2
114
- module Header
115
- class Huffman
116
- # :nodoc:
117
- MAX_FINAL_STATE = #{max_final}
118
- MACHINE = [
119
- HEADER
120
- id.times do |i|
121
- n = id_state[i]
122
- f.print ' ['
123
- string = (1 << BITS_AT_ONCE).times.map do |t|
124
- transition = n.transitions.fetch(t)
125
- emit = transition.emit
126
- unless emit == EOS
127
- bytes = emit.bytes
128
- fail ArgumentError if bytes.size > 1
129
- emit = bytes.first
130
- end
131
- "[#{emit.inspect}, #{state_id.fetch(transition.node)}]"
132
- end.join(', ')
133
- f.print(string)
134
- f.print "],\n"
135
- end
136
- f.print <<TAILER
137
- ].each { |arr| arr.each { |subarr| subarr.each(&:freeze) }.freeze }.freeze
138
- end
139
- end
140
- end
141
- TAILER
142
- end
143
- end
144
-
145
- class << self
146
- attr_reader :root
147
- end
148
-
149
- # Test decoder
150
- def self.decode(input)
151
- emit = ''
152
- n = root
153
- nibbles = input.unpack('C*').flat_map { |b| [((b & 0xf0) >> 4), b & 0xf] }
154
- until nibbles.empty?
155
- nb = nibbles.shift
156
- t = n.transitions[nb]
157
- emit << t.emit
158
- n = t.node
159
- end
160
- unless n.final && nibbles.all? { |x| x == 0xf }
161
- puts "len = #{emit.size} n.final = #{n.final} nibbles = #{nibbles}"
162
- end
163
- emit
164
- end
165
- end
166
- end
data/spec/buffer_spec.rb DELETED
@@ -1,28 +0,0 @@
1
- require 'helper'
2
-
3
- RSpec.describe HTTP2::Buffer do
4
- let(:b) { Buffer.new('émalgré') }
5
-
6
- it 'should force 8-bit encoding' do
7
- expect(b.encoding.to_s).to eq 'ASCII-8BIT'
8
- end
9
-
10
- it 'should force 8-bit encoding when adding data' do
11
- b << 'émalgré'
12
- expect(b.encoding.to_s).to eq 'ASCII-8BIT'
13
- b.prepend('émalgré')
14
- expect(b.encoding.to_s).to eq 'ASCII-8BIT'
15
- end
16
-
17
- it 'should return bytesize of the buffer' do
18
- expect(b.size).to eq 9
19
- end
20
-
21
- it 'should read single byte at a time' do
22
- 9.times { expect(b.read(1)).not_to be_nil }
23
- end
24
-
25
- it 'should unpack an unsigned 32-bit int' do
26
- expect(Buffer.new([256].pack('N')).read_uint32).to eq 256
27
- end
28
- end
data/spec/client_spec.rb DELETED
@@ -1,188 +0,0 @@
1
- require 'helper'
2
-
3
- RSpec.describe HTTP2::Client do
4
- include FrameHelpers
5
- before(:each) do
6
- @client = Client.new
7
- end
8
-
9
- let(:f) { Framer.new }
10
-
11
- context 'initialization and settings' do
12
- it 'should return odd stream IDs' do
13
- expect(@client.new_stream.id).not_to be_even
14
- end
15
-
16
- it 'should emit connection header and SETTINGS on new client connection' do
17
- frames = []
18
- @client.on(:frame) { |bytes| frames << bytes }
19
- @client.ping('12345678')
20
-
21
- expect(frames[0]).to eq CONNECTION_PREFACE_MAGIC
22
- expect(f.parse(frames[1])[:type]).to eq :settings
23
- end
24
-
25
- it 'should initialize client with custom connection settings' do
26
- frames = []
27
-
28
- @client = Client.new(settings_max_concurrent_streams: 200)
29
- @client.on(:frame) { |bytes| frames << bytes }
30
- @client.ping('12345678')
31
-
32
- frame = f.parse(frames[1])
33
- expect(frame[:type]).to eq :settings
34
- expect(frame[:payload]).to include([:settings_max_concurrent_streams, 200])
35
- end
36
-
37
- it 'should initialize client when receiving server settings before sending ack' do
38
- frames = []
39
- @client.on(:frame) { |bytes| frames << bytes }
40
- @client << f.generate(settings_frame)
41
-
42
- expect(frames[0]).to eq CONNECTION_PREFACE_MAGIC
43
- expect(f.parse(frames[1])[:type]).to eq :settings
44
- ack_frame = f.parse(frames[2])
45
- expect(ack_frame[:type]).to eq :settings
46
- expect(ack_frame[:flags]).to include(:ack)
47
- end
48
- end
49
-
50
- context 'upgrade' do
51
- it 'fails when client has already created streams' do
52
- @client.new_stream
53
- expect { @client.upgrade }.to raise_error(HTTP2::Error::ProtocolError)
54
- end
55
-
56
- it 'sends the preface' do
57
- expect(@client).to receive(:send_connection_preface)
58
- @client.upgrade
59
- end
60
-
61
- it 'initializes the first stream in the half-closed state' do
62
- stream = @client.upgrade
63
- expect(stream.state).to be(:half_closed_local)
64
- end
65
- end
66
-
67
- context 'push' do
68
- it 'should disallow client initiated push' do
69
- expect do
70
- @client.promise({}) {}
71
- end.to raise_error(NoMethodError)
72
- end
73
-
74
- it 'should raise error on PUSH_PROMISE against stream 0' do
75
- expect do
76
- @client << set_stream_id(f.generate(push_promise_frame), 0)
77
- end.to raise_error(ProtocolError)
78
- end
79
-
80
- it 'should raise error on PUSH_PROMISE against bogus stream' do
81
- expect do
82
- @client << set_stream_id(f.generate(push_promise_frame), 31_415)
83
- end.to raise_error(ProtocolError)
84
- end
85
-
86
- it 'should raise error on PUSH_PROMISE against non-idle stream' do
87
- expect do
88
- s = @client.new_stream
89
- s.send headers_frame
90
-
91
- @client << set_stream_id(f.generate(push_promise_frame), s.id)
92
- @client << set_stream_id(f.generate(push_promise_frame), s.id)
93
- end.to raise_error(ProtocolError)
94
- end
95
-
96
- it 'should emit stream object for received PUSH_PROMISE' do
97
- s = @client.new_stream
98
- s.send headers_frame
99
-
100
- promise = nil
101
- @client.on(:promise) { |stream| promise = stream }
102
- @client << set_stream_id(f.generate(push_promise_frame), s.id)
103
-
104
- expect(promise.id).to eq 2
105
- expect(promise.state).to eq :reserved_remote
106
- end
107
-
108
- it 'should emit promise headers for received PUSH_PROMISE' do
109
- header = nil
110
- s = @client.new_stream
111
- s.send headers_frame
112
-
113
- @client.on(:promise) do |stream|
114
- stream.on(:promise_headers) do |h|
115
- header = h
116
- end
117
- end
118
- @client << set_stream_id(f.generate(push_promise_frame), s.id)
119
-
120
- expect(header).to be_a(Array)
121
- # expect(header).to eq([%w(a b)])
122
- end
123
-
124
- it 'should auto RST_STREAM promises against locally-RST stream' do
125
- s = @client.new_stream
126
- s.send headers_frame
127
- s.close
128
-
129
- allow(@client).to receive(:send)
130
- expect(@client).to receive(:send) do |frame|
131
- expect(frame[:type]).to eq :rst_stream
132
- expect(frame[:stream]).to eq 2
133
- end
134
-
135
- @client << set_stream_id(f.generate(push_promise_frame), s.id)
136
- end
137
- end
138
-
139
- context 'alt-svc' do
140
- context 'received in the connection' do
141
- it 'should emit :altsvc when receiving one' do
142
- @client << f.generate(settings_frame)
143
- frame = nil
144
- @client.on(:altsvc) do |f|
145
- frame = f
146
- end
147
- @client << f.generate(altsvc_frame)
148
- expect(frame).to be_a(Hash)
149
- end
150
- it 'should not emit :altsvc when the frame when contains no host' do
151
- @client << f.generate(settings_frame)
152
- frame = nil
153
- @client.on(:altsvc) do |f|
154
- frame = f
155
- end
156
-
157
- @client << f.generate(altsvc_frame.merge(origin: nil))
158
- expect(frame).to be_nil
159
- end
160
- end
161
- context 'received in a stream' do
162
- it 'should emit :altsvc' do
163
- s = @client.new_stream
164
- s.send headers_frame
165
- s.close
166
-
167
- frame = nil
168
- s.on(:altsvc) { |f| frame = f }
169
-
170
- @client << set_stream_id(f.generate(altsvc_frame.merge(origin: nil)), s.id)
171
-
172
- expect(frame).to be_a(Hash)
173
- end
174
- it 'should not emit :alt_svc when the frame when contains a origin' do
175
- s = @client.new_stream
176
- s.send headers_frame
177
- s.close
178
-
179
- frame = nil
180
- s.on(:altsvc) { |f| frame = f }
181
-
182
- @client << set_stream_id(f.generate(altsvc_frame), s.id)
183
-
184
- expect(frame).to be_nil
185
- end
186
- end
187
- end
188
- end