http-2 0.6.1 → 0.6.3

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.
@@ -0,0 +1,55 @@
1
+ module HTTP2
2
+
3
+ # HTTP 2.0 server connection class that implements appropriate header
4
+ # compression / decompression algorithms and stream management logic.
5
+ #
6
+ # Your code is responsible for feeding request data to the server object,
7
+ # which in turn performs all of the necessary HTTP 2.0 decoding / encoding,
8
+ # state management, and the rest. A simple example:
9
+ #
10
+ # @example
11
+ # socket = YourTransport.new
12
+ #
13
+ # conn = HTTP2::Server.new
14
+ # conn.on(:stream) do |stream|
15
+ # ...
16
+ # end
17
+ #
18
+ # while bytes = socket.read
19
+ # conn << bytes
20
+ # end
21
+ #
22
+ class Server < Connection
23
+
24
+ # Initialize new HTTP 2.0 server object.
25
+ def initialize(*args)
26
+ @stream_id = 2
27
+ @state = :new
28
+ @compressor = Header::Compressor.new(:response)
29
+ @decompressor = Header::Decompressor.new(:request)
30
+
31
+ super
32
+ end
33
+
34
+ private
35
+
36
+ # Handle locally initiated server-push event emitted by the stream.
37
+ #
38
+ # @param args [Array]
39
+ # @param callback [Proc]
40
+ def promise(*args, &callback)
41
+ parent, headers, flags = *args
42
+ promise = new_stream(parent: parent)
43
+ promise.send({
44
+ type: :push_promise,
45
+ flags: flags,
46
+ stream: parent.id,
47
+ promise_stream: promise.id,
48
+ payload: headers.to_a
49
+ })
50
+
51
+ callback.call(promise)
52
+ end
53
+ end
54
+
55
+ end
@@ -467,7 +467,7 @@ module HTTP2
467
467
  close(error) if @state != :closed
468
468
 
469
469
  klass = error.to_s.split('_').map(&:capitalize).join
470
- raise Kernel.const_get(klass).new(msg)
470
+ raise Error.const_get(klass).new(msg)
471
471
  end
472
472
 
473
473
  end
@@ -1,3 +1,3 @@
1
1
  module HTTP2
2
- VERSION = "0.6.1"
2
+ VERSION = "0.6.3"
3
3
  end
@@ -0,0 +1,23 @@
1
+ require "helper"
2
+
3
+ describe HTTP2::Buffer do
4
+
5
+ let(:b) { Buffer.new("émalgré") }
6
+
7
+ it "should force 8-bit encoding" do
8
+ b.encoding.to_s.should eq "ASCII-8BIT"
9
+ end
10
+
11
+ it "should return bytesize of the buffer" do
12
+ b.size.should eq 9
13
+ end
14
+
15
+ it "should read single byte at a time" do
16
+ 9.times { b.read(1).should_not be_nil }
17
+ end
18
+
19
+ it "should unpack an unsigned 32-bit int" do
20
+ Buffer.new([256].pack("N")).read_uint32.should eq 256
21
+ end
22
+
23
+ end
@@ -0,0 +1,93 @@
1
+ require "helper"
2
+
3
+ describe HTTP2::Client do
4
+ before(:each) do
5
+ @client = Client.new
6
+ end
7
+
8
+ let(:f) { Framer.new }
9
+
10
+ context "initialization and settings" do
11
+ it "should return odd stream IDs" do
12
+ @client.new_stream.id.should_not be_even
13
+ end
14
+
15
+ it "should emit connection header and SETTINGS on new client connection" do
16
+ frames = []
17
+ @client.on(:frame) { |bytes| frames << bytes }
18
+ @client.ping("12345678")
19
+
20
+ frames[0].should eq CONNECTION_HEADER
21
+ f.parse(frames[1])[:type].should eq :settings
22
+ end
23
+
24
+ it "should initialize client with custom connection settings" do
25
+ frames = []
26
+
27
+ @client = Client.new(streams: 200)
28
+ @client.on(:frame) { |bytes| frames << bytes }
29
+ @client.ping("12345678")
30
+
31
+ frame = f.parse(frames[1])
32
+ frame[:type].should eq :settings
33
+ frame[:payload][:settings_max_concurrent_streams].should eq 200
34
+ end
35
+ end
36
+
37
+ context "push" do
38
+ it "should disallow client initiated push" do
39
+ expect do
40
+ @client.promise({}) {}
41
+ end.to raise_error(Exception)
42
+ end
43
+
44
+ it "should raise error on PUSH_PROMISE against stream 0" do
45
+ expect {
46
+ @client << set_stream_id(f.generate(PUSH_PROMISE), 0)
47
+ }.to raise_error(ProtocolError)
48
+ end
49
+
50
+ it "should raise error on PUSH_PROMISE against bogus stream" do
51
+ expect {
52
+ @client << set_stream_id(f.generate(PUSH_PROMISE), 31415)
53
+ }.to raise_error(ProtocolError)
54
+ end
55
+
56
+ it "should raise error on PUSH_PROMISE against non-idle stream" do
57
+ expect {
58
+ s = @client.new_stream
59
+ s.send HEADERS
60
+
61
+ @client << set_stream_id(f.generate(PUSH_PROMISE), s.id)
62
+ @client << set_stream_id(f.generate(PUSH_PROMISE), s.id)
63
+ }.to raise_error(ProtocolError)
64
+ end
65
+
66
+ it "should emit stream object for received PUSH_PROMISE" do
67
+ s = @client.new_stream
68
+ s.send HEADERS
69
+
70
+ promise = nil
71
+ @client.on(:promise) {|s| promise = s }
72
+ @client << set_stream_id(f.generate(PUSH_PROMISE), s.id)
73
+
74
+ promise.id.should eq 2
75
+ promise.state.should eq :reserved_remote
76
+ end
77
+
78
+ it "should auto RST_STREAM promises against locally-RST stream" do
79
+ s = @client.new_stream
80
+ s.send HEADERS
81
+ s.close
82
+
83
+ @client.stub(:send)
84
+ @client.should_receive(:send) do |frame|
85
+ frame[:type].should eq :rst_stream
86
+ frame[:stream].should eq 2
87
+ end
88
+
89
+ @client << set_stream_id(f.generate(PUSH_PROMISE), s.id)
90
+ end
91
+ end
92
+
93
+ end
@@ -10,25 +10,25 @@ describe HTTP2::Header do
10
10
  it "should encode 10 using a 5-bit prefix" do
11
11
  buf = c.integer(10, 5)
12
12
  buf.should eq [10].pack('C')
13
- d.integer(StringIO.new(buf), 5).should eq 10
13
+ d.integer(Buffer.new(buf), 5).should eq 10
14
14
  end
15
15
 
16
16
  it "should encode 10 using a 0-bit prefix" do
17
17
  buf = c.integer(10, 0)
18
18
  buf.should eq [10].pack('C')
19
- d.integer(StringIO.new(buf), 0).should eq 10
19
+ d.integer(Buffer.new(buf), 0).should eq 10
20
20
  end
21
21
 
22
22
  it "should encode 1337 using a 5-bit prefix" do
23
23
  buf = c.integer(1337, 5)
24
24
  buf.should eq [31,128+26,10].pack('C*')
25
- d.integer(StringIO.new(buf), 5).should eq 1337
25
+ d.integer(Buffer.new(buf), 5).should eq 1337
26
26
  end
27
27
 
28
28
  it "should encode 1337 using a 0-bit prefix" do
29
29
  buf = c.integer(1337,0)
30
30
  buf.should eq [128+57,10].pack('C*')
31
- d.integer(StringIO.new(buf), 0).should eq 1337
31
+ d.integer(Buffer.new(buf), 0).should eq 1337
32
32
  end
33
33
  end
34
34
 
@@ -37,7 +37,7 @@ describe HTTP2::Header do
37
37
  ascii = "abcdefghij"
38
38
  str = c.string(ascii)
39
39
 
40
- buf = StringIO.new(str+"trailer")
40
+ buf = Buffer.new(str+"trailer")
41
41
  d.string(buf).should eq ascii
42
42
  end
43
43
 
@@ -45,7 +45,7 @@ describe HTTP2::Header do
45
45
  utf8 = "éáűőúöüó€"
46
46
  str = c.string(utf8)
47
47
 
48
- buf = StringIO.new(str+"trailer")
48
+ buf = Buffer.new(str+"trailer")
49
49
  d.string(buf).should eq utf8
50
50
  end
51
51
 
@@ -53,7 +53,7 @@ describe HTTP2::Header do
53
53
  utf8 = "éáűőúöüó€"*100
54
54
  str = c.string(utf8)
55
55
 
56
- buf = StringIO.new(str+"trailer")
56
+ buf = Buffer.new(str+"trailer")
57
57
  d.string(buf).should eq utf8
58
58
  end
59
59
  end
@@ -62,120 +62,105 @@ describe HTTP2::Header do
62
62
  context "header representation" do
63
63
  it "should handle indexed representation" do
64
64
  h = {name: 10, type: :indexed}
65
-
66
- indexed = StringIO.new(c.header(h))
67
- d.header(indexed).should eq h
65
+ d.header(c.header(h)).should eq h
68
66
  end
69
67
 
70
68
  context "literal w/o indexing representation" do
71
69
  it "should handle indexed header" do
72
70
  h = {name: 10, value: "my-value", type: :noindex}
73
-
74
- literal = StringIO.new(c.header(h))
75
- d.header(literal).should eq h
71
+ d.header(c.header(h)).should eq h
76
72
  end
77
73
 
78
74
  it "should handle literal header" do
79
75
  h = {name: "x-custom", value: "my-value", type: :noindex}
80
-
81
- literal = StringIO.new(c.header(h))
82
- d.header(literal).should eq h
76
+ d.header(c.header(h)).should eq h
83
77
  end
84
78
  end
85
79
 
86
80
  context "literal w/ incremental indexing" do
87
81
  it "should handle indexed header" do
88
82
  h = {name: 10, value: "my-value", type: :incremental}
89
-
90
- literal = StringIO.new(c.header(h))
91
- d.header(literal).should eq h
83
+ d.header(c.header(h)).should eq h
92
84
  end
93
85
 
94
86
  it "should handle literal header" do
95
87
  h = {name: "x-custom", value: "my-value", type: :incremental}
96
-
97
- literal = StringIO.new(c.header(h))
98
- d.header(literal).should eq h
88
+ d.header(c.header(h)).should eq h
99
89
  end
100
90
  end
101
91
 
102
92
  context "literal w/ substitution indexing" do
103
93
  it "should handle indexed header" do
104
94
  h = {name: 1, value: "my-value", index: 10, type: :substitution}
105
-
106
- literal = StringIO.new(c.header(h))
107
- d.header(literal).should eq h
95
+ d.header(c.header(h)).should eq h
108
96
  end
109
97
 
110
98
  it "should handle literal header" do
111
99
  h = {name: "x-new", value: "my-value", index: 10, type: :substitution}
112
-
113
- literal = StringIO.new(c.header(h))
114
- d.header(literal).should eq h
100
+ d.header(c.header(h)).should eq h
115
101
  end
116
102
  end
117
103
  end
118
104
 
119
105
  context "differential coding" do
120
106
  context "shared compression context" do
121
- before(:each) { @cc = CompressionContext.new(:request) }
107
+ before(:each) { @cc = EncodingContext.new(:request) }
122
108
 
123
109
  it "should be initialized with pre-defined headers" do
124
- cc = CompressionContext.new(:request)
125
- cc.table.size.should eq 38
110
+ cc = EncodingContext.new(:request)
111
+ cc.table.size.should eq 30
126
112
 
127
- cc = CompressionContext.new(:response)
128
- cc.table.size.should eq 35
113
+ cc = EncodingContext.new(:response)
114
+ cc.table.size.should eq 30
129
115
  end
130
116
 
131
117
  it "should be initialized with empty working set" do
132
- @cc.workset.should be_empty
118
+ @cc.refset.should be_empty
133
119
  end
134
120
 
135
121
  it "should update working set based on prior state" do
136
- @cc.update_sets
137
- @cc.workset.should be_empty
122
+ @cc.refset.should be_empty
138
123
 
139
124
  @cc.process({name: 0, type: :indexed})
140
- @cc.update_sets
141
- @cc.workset.should eq [[0, [":scheme", "http"]]]
125
+ @cc.refset.should eq [[0, [":scheme", "http"]]]
142
126
 
143
127
  @cc.process({name: 0, type: :indexed})
144
- @cc.update_sets
145
- @cc.workset.should be_empty
128
+ @cc.refset.should be_empty
146
129
  end
147
130
 
148
131
  context "processing" do
149
132
  it "should toggle index representation headers in working set" do
150
133
  @cc.process({name: 0, type: :indexed})
151
- @cc.workset.first.should eq [0, [":scheme", "http"]]
134
+ @cc.refset.first.should eq [0, [":scheme", "http"]]
152
135
 
153
136
  @cc.process({name: 0, type: :indexed})
154
- @cc.workset.should be_empty
137
+ @cc.refset.should be_empty
155
138
  end
156
139
 
157
140
  context "no indexing" do
158
141
  it "should process indexed header with literal value" do
159
142
  original_table = @cc.table
160
143
 
161
- @cc.process({name: 3, value: "/path", type: :noindex})
162
- @cc.workset.first.should eq [3, [":path", "/path"]]
144
+ emit = @cc.process({name: 3, value: "/path", type: :noindex})
145
+ emit.should eq [":path", "/path"]
146
+ @cc.refset.should be_empty
163
147
  @cc.table.should eq original_table
164
148
  end
165
149
 
166
150
  it "should process indexed header with default value" do
167
151
  original_table = @cc.table
168
152
 
169
- @cc.process({name: 3, type: :noindex})
170
- @cc.workset.first.should eq [3, [":path", "/"]]
153
+ emit = @cc.process({name: 3, type: :noindex})
154
+ emit.should eq [":path", "/"]
171
155
  @cc.table.should eq original_table
172
156
  end
173
157
 
174
158
  it "should process literal header with literal value" do
175
159
  original_table = @cc.table
176
160
 
177
- @cc.process({name: "x-custom", value: "random", type: :noindex})
178
- @cc.workset.first.should eq [nil, ["x-custom", "random"]]
161
+ emit = @cc.process({name: "x-custom", value: "random", type: :noindex})
162
+ emit.should eq ["x-custom", "random"]
163
+ @cc.refset.should be_empty
179
164
  @cc.table.should eq original_table
180
165
  end
181
166
  end
@@ -185,7 +170,7 @@ describe HTTP2::Header do
185
170
  original_table = @cc.table.dup
186
171
 
187
172
  @cc.process({name: "x-custom", value: "random", type: :incremental})
188
- @cc.workset.first.should eq [original_table.size, ["x-custom", "random"]]
173
+ @cc.refset.first.should eq [original_table.size, ["x-custom", "random"]]
189
174
  (@cc.table - original_table).should eq [["x-custom", "random"]]
190
175
  end
191
176
  end
@@ -200,9 +185,9 @@ describe HTTP2::Header do
200
185
  index: idx, type: :substitution
201
186
  })
202
187
 
203
- @cc.workset.first.should eq [idx, ["x-custom", "random"]]
188
+ @cc.refset.first.should eq [idx, ["x-custom", "random"]]
204
189
  (@cc.table - original_table).should eq [["x-custom", "random"]]
205
- (original_table - @cc.table).should eq [["warning", ""]]
190
+ (original_table - @cc.table).should eq [["via", ""]]
206
191
  end
207
192
 
208
193
  it "should raise error on invalid substitution index" do
@@ -217,7 +202,7 @@ describe HTTP2::Header do
217
202
 
218
203
  context "size bounds" do
219
204
  it "should drop headers from beginning of table" do
220
- cc = CompressionContext.new(:request, 2048)
205
+ cc = EncodingContext.new(:request, 2048)
221
206
  original_table = cc.table.dup
222
207
  original_size = original_table.join.bytesize +
223
208
  original_table.size * 32
@@ -233,7 +218,7 @@ describe HTTP2::Header do
233
218
  end
234
219
 
235
220
  it "should prepend on dropped substitution index" do
236
- cc = CompressionContext.new(:request, 2048)
221
+ cc = EncodingContext.new(:request, 2048)
237
222
  original_table = cc.table.dup
238
223
  original_size = original_table.join.bytesize +
239
224
  original_table.size * 32
@@ -248,39 +233,54 @@ describe HTTP2::Header do
248
233
  cc.table[1][0].should eq ":scheme"
249
234
  end
250
235
  end
236
+
237
+ it "should clear table if entry exceeds table size" do
238
+ cc = EncodingContext.new(:request, 2048)
239
+
240
+ h = { name: "x-custom", value: "a", index: 0, type: :incremental }
241
+ e = { name: "large", value: "a" * 2048, index: 0}
242
+
243
+ cc.process(h)
244
+ cc.process(e.merge({type: :substitution}))
245
+ cc.table.should be_empty
246
+
247
+ cc.process(h)
248
+ cc.process(e.merge({type: :incremental}))
249
+ cc.table.should be_empty
250
+ end
251
251
  end
252
252
  end
253
253
 
254
254
  context "integration" do
255
- before (:all) { @cc = CompressionContext.new(:request) }
255
+ before (:all) { @cc = EncodingContext.new(:request) }
256
256
 
257
257
  it "should match first header set in spec appendix" do
258
258
  req_headers = [
259
259
  {name: 3, value: "/my-example/index.html"},
260
- {name: 12, value: "my-user-agent"},
261
- {name: "x-my-header", value: "first"}
260
+ {name: 11, value: "my-user-agent"},
261
+ {name: "mynewheader", value: "first"}
262
262
  ]
263
263
 
264
264
  req_headers.each {|h| @cc.process(h.merge({type: :incremental})) }
265
265
 
266
- @cc.table[38].should eq [":path", "/my-example/index.html"]
267
- @cc.table[39].should eq ["user-agent", "my-user-agent"]
268
- @cc.table[40].should eq req_headers[2].values
266
+ @cc.table[30].should eq [":path", "/my-example/index.html"]
267
+ @cc.table[31].should eq ["user-agent", "my-user-agent"]
268
+ @cc.table[32].should eq req_headers[2].values
269
269
  end
270
270
 
271
271
  it "should match second header set in spec appendix" do
272
- @cc.process({name: 38, type: :indexed})
273
- @cc.process({name: 39, type: :indexed})
272
+ @cc.process({name: 30, type: :indexed})
273
+ @cc.process({name: 31, type: :indexed})
274
274
  @cc.process({
275
275
  name: 3, value: "/my-example/resources/script.js",
276
- index: 38, type: :substitution
276
+ index: 30, type: :substitution
277
277
  })
278
- @cc.process({name: 40, value: "second", type: :incremental})
278
+ @cc.process({name: 32, value: "second", type: :incremental})
279
279
 
280
- @cc.table[38].should eq [":path", "/my-example/resources/script.js"]
281
- @cc.table[39].should eq ["user-agent", "my-user-agent"]
282
- @cc.table[40].should eq ["x-my-header", "first"]
283
- @cc.table[41].should eq ["x-my-header", "second"]
280
+ @cc.table[30].should eq [":path", "/my-example/resources/script.js"]
281
+ @cc.table[31].should eq ["user-agent", "my-user-agent"]
282
+ @cc.table[32].should eq ["mynewheader", "first"]
283
+ @cc.table[33].should eq ["mynewheader", "second"]
284
284
  end
285
285
  end
286
286
  end
@@ -297,12 +297,12 @@ describe HTTP2::Header do
297
297
  0x44, # (literal header with incremental indexing, name index = 3)
298
298
  0x16, # (header value string length = 22)
299
299
  "/my-example/index.html".bytes,
300
- 0x4D, # (literal header with incremental indexing, name index = 12)
300
+ 0x4C, # (literal header with incremental indexing, name index = 11)
301
301
  0x0D, # (header value string length = 13)
302
302
  "my-user-agent".bytes,
303
303
  0x40, # (literal header with incremental indexing, new name)
304
304
  0x0B, # (header name string length = 11)
305
- "x-my-header".bytes,
305
+ "mynewheader".bytes,
306
306
  0x05, # (header value string length = 5)
307
307
  "first".bytes
308
308
  ].flatten
@@ -310,7 +310,7 @@ describe HTTP2::Header do
310
310
  E1_HEADERS = [
311
311
  [":path", "/my-example/index.html"],
312
312
  ["user-agent", "my-user-agent"],
313
- ["x-my-header", "first"]
313
+ ["mynewheader", "first"]
314
314
  ]
315
315
 
316
316
  it "should match first header set in spec appendix" do
@@ -318,18 +318,18 @@ describe HTTP2::Header do
318
318
  end
319
319
 
320
320
  it "should decode first header set in spec appendix" do
321
- @dc.decode(StringIO.new(E1_BYTES.pack("C*"))).should eq E1_HEADERS
321
+ @dc.decode(Buffer.new(E1_BYTES.pack("C*"))).should eq E1_HEADERS
322
322
  end
323
323
 
324
324
  E2_BYTES = [
325
- 0xa6, # (indexed header, index = 38: removal from reference set)
326
- 0xa8, # (indexed header, index = 40: removal from reference set)
325
+ 0x9e, # (indexed header, index = 30: removal from reference set)
326
+ 0xa0, # (indexed header, index = 32: removal from reference set)
327
327
  0x04, # (literal header, substitution indexing, name index = 3)
328
- 0x26, # (replaced entry index = 38)
328
+ 0x1e, # (replaced entry index = 30)
329
329
  0x1f, # (header value string length = 31)
330
330
  "/my-example/resources/script.js".bytes,
331
331
  0x5f,
332
- 0x0a, # (literal header, incremental indexing, name index = 40)
332
+ 0x02, # (literal header, incremental indexing, name index = 32)
333
333
  0x06, # (header value string length = 6)
334
334
  "second".bytes
335
335
  ].flatten
@@ -337,7 +337,7 @@ describe HTTP2::Header do
337
337
  E2_HEADERS = [
338
338
  [":path", "/my-example/resources/script.js"],
339
339
  ["user-agent", "my-user-agent"],
340
- ["x-my-header", "second"]
340
+ ["mynewheader", "second"]
341
341
  ]
342
342
 
343
343
  it "should match second header set in spec appendix" do
@@ -356,15 +356,15 @@ describe HTTP2::Header do
356
356
  end
357
357
 
358
358
  it "should decode second header set in spec appendix" do
359
- @dc.decode(StringIO.new(E2_BYTES.pack("C*"))).should match_array E2_HEADERS
359
+ @dc.decode(Buffer.new(E2_BYTES.pack("C*"))).should match_array E2_HEADERS
360
360
  end
361
361
 
362
362
  it "encode-decode should be invariant" do
363
363
  cc = Compressor.new(:request)
364
364
  dc = Decompressor.new(:request)
365
365
 
366
- E1_HEADERS.should match_array dc.decode(StringIO.new(cc.encode(E1_HEADERS)))
367
- E2_HEADERS.should match_array dc.decode(StringIO.new(cc.encode(E2_HEADERS)))
366
+ E1_HEADERS.should match_array dc.decode(cc.encode(E1_HEADERS))
367
+ E2_HEADERS.should match_array dc.decode(cc.encode(E2_HEADERS))
368
368
  end
369
369
 
370
370
  it "should encode-decode request set of headers" do
@@ -378,7 +378,16 @@ describe HTTP2::Header do
378
378
  ["accept", "*/*"]
379
379
  ]
380
380
 
381
- dc.decode(StringIO.new(cc.encode(req))).should eq req
381
+ dc.decode(cc.encode(req)).should eq req
382
+ end
383
+
384
+ it "should downcase all request header names" do
385
+ cc = Compressor.new(:request)
386
+ dc = Decompressor.new(:request)
387
+
388
+ req = [["Accept", "IMAGE/PNG"]]
389
+ recv = dc.decode(cc.encode(req))
390
+ recv.should eq [["accept", "IMAGE/PNG"]]
382
391
  end
383
392
  end
384
393
  end