http-2 0.6.1 → 0.6.3

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