http-2 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.autotest +19 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/README.md +280 -0
- data/Rakefile +11 -0
- data/example/client.rb +46 -0
- data/example/helper.rb +14 -0
- data/example/server.rb +50 -0
- data/http-2.gemspec +24 -0
- data/lib/http/2/buffer.rb +21 -0
- data/lib/http/2/compressor.rb +493 -0
- data/lib/http/2/connection.rb +516 -0
- data/lib/http/2/emitter.rb +47 -0
- data/lib/http/2/error.rb +45 -0
- data/lib/http/2/flow_buffer.rb +64 -0
- data/lib/http/2/framer.rb +302 -0
- data/lib/http/2/stream.rb +474 -0
- data/lib/http/2/version.rb +3 -0
- data/lib/http/2.rb +9 -0
- data/spec/compressor_spec.rb +384 -0
- data/spec/connection_spec.rb +448 -0
- data/spec/emitter_spec.rb +46 -0
- data/spec/framer_spec.rb +325 -0
- data/spec/helper.rb +98 -0
- data/spec/stream_spec.rb +683 -0
- metadata +120 -0
data/spec/framer_spec.rb
ADDED
@@ -0,0 +1,325 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
describe HTTP2::Framer do
|
4
|
+
|
5
|
+
let(:f) { Framer.new }
|
6
|
+
|
7
|
+
context "common header" do
|
8
|
+
let(:frame) {
|
9
|
+
{
|
10
|
+
length: 4,
|
11
|
+
type: :headers,
|
12
|
+
flags: [:end_stream, :reserved, :end_headers],
|
13
|
+
stream: 15,
|
14
|
+
}
|
15
|
+
}
|
16
|
+
|
17
|
+
let(:bytes) { [0x04, 0x01, 0x7, 0x0000000F].pack("SCCL") }
|
18
|
+
|
19
|
+
it "should generate common 8 byte header" do
|
20
|
+
f.commonHeader(frame).should eq bytes
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should parse common 8 byte header" do
|
24
|
+
f.readCommonHeader(Buffer.new(bytes)).should eq frame
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should raise exception on invalid frame type" do
|
28
|
+
expect {
|
29
|
+
frame[:type] = :bogus
|
30
|
+
f.commonHeader(frame)
|
31
|
+
}.to raise_error(CompressionError, /invalid.*type/i)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should raise exception on invalid stream ID" do
|
35
|
+
expect {
|
36
|
+
frame[:stream] = Framer::MAX_STREAM_ID + 1
|
37
|
+
f.commonHeader(frame)
|
38
|
+
}.to raise_error(CompressionError, /stream/i)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should raise exception on invalid frame flag" do
|
42
|
+
expect {
|
43
|
+
frame[:flags] = [:bogus]
|
44
|
+
f.commonHeader(frame)
|
45
|
+
}.to raise_error(CompressionError, /frame flag/)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should raise exception on invalid frame size" do
|
49
|
+
expect {
|
50
|
+
frame[:length] = 2**16
|
51
|
+
f.commonHeader(frame)
|
52
|
+
}.to raise_error(CompressionError, /too large/)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context "DATA" do
|
57
|
+
it "should generate and parse bytes" do
|
58
|
+
frame = {
|
59
|
+
length: 4,
|
60
|
+
type: :data,
|
61
|
+
flags: [:end_stream, :reserved],
|
62
|
+
stream: 1,
|
63
|
+
payload: 'text'
|
64
|
+
}
|
65
|
+
|
66
|
+
bytes = f.generate(frame)
|
67
|
+
bytes.should eq [0x4,0x0,0x3,0x1,*'text'.bytes].pack("SCCLC*")
|
68
|
+
|
69
|
+
f.parse(Buffer.new(bytes)).should eq frame
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "HEADERS" do
|
74
|
+
it "should generate and parse bytes" do
|
75
|
+
frame = {
|
76
|
+
length: 12,
|
77
|
+
type: :headers,
|
78
|
+
flags: [:end_stream, :reserved, :end_headers],
|
79
|
+
stream: 1,
|
80
|
+
payload: 'header-block'
|
81
|
+
}
|
82
|
+
|
83
|
+
bytes = f.generate(frame)
|
84
|
+
bytes.should eq [0xc,0x1,0x7,0x1,*'header-block'.bytes].pack("SCCLC*")
|
85
|
+
f.parse(Buffer.new(bytes)).should eq frame
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should carry an optional stream priority" do
|
89
|
+
frame = {
|
90
|
+
length: 16,
|
91
|
+
type: :headers,
|
92
|
+
flags: [:end_headers, :priority],
|
93
|
+
stream: 1,
|
94
|
+
priority: 15,
|
95
|
+
payload: 'header-block'
|
96
|
+
}
|
97
|
+
|
98
|
+
bytes = f.generate(frame)
|
99
|
+
bytes.should eq [0x10,0x1,0xc,0x1,0xf,*'header-block'.bytes].pack("SCCLLC*")
|
100
|
+
f.parse(Buffer.new(bytes)).should eq frame
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context "PRIORITY" do
|
105
|
+
it "should generate and parse bytes" do
|
106
|
+
frame = {
|
107
|
+
length: 4,
|
108
|
+
type: :priority,
|
109
|
+
stream: 1,
|
110
|
+
priority: 15
|
111
|
+
}
|
112
|
+
|
113
|
+
bytes = f.generate(frame)
|
114
|
+
bytes.should eq [0x4,0x2,0x0,0x1,0xf].pack("SCCLL")
|
115
|
+
f.parse(Buffer.new(bytes)).should eq frame
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context "RST_STREAM" do
|
120
|
+
it "should generate and parse bytes" do
|
121
|
+
frame = {
|
122
|
+
length: 4,
|
123
|
+
type: :rst_stream,
|
124
|
+
stream: 1,
|
125
|
+
error: :stream_closed
|
126
|
+
}
|
127
|
+
|
128
|
+
bytes = f.generate(frame)
|
129
|
+
bytes.should eq [0x4,0x3,0x0,0x1,0x5].pack("SCCLL")
|
130
|
+
f.parse(Buffer.new(bytes)).should eq frame
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
context "SETTINGS" do
|
135
|
+
let(:frame) {
|
136
|
+
{
|
137
|
+
length: 8,
|
138
|
+
type: :settings,
|
139
|
+
stream: 0,
|
140
|
+
payload: {
|
141
|
+
settings_max_concurrent_streams: 10
|
142
|
+
}
|
143
|
+
}
|
144
|
+
}
|
145
|
+
|
146
|
+
it "should generate and parse bytes" do
|
147
|
+
|
148
|
+
bytes = f.generate(frame)
|
149
|
+
bytes.should eq [0x8,0x4,0x0,0x0,0x4,0xa].pack("SCCLLL")
|
150
|
+
f.parse(Buffer.new(bytes)).should eq frame
|
151
|
+
end
|
152
|
+
|
153
|
+
it "should encode custom settings" do
|
154
|
+
frame[:length] = 8*3
|
155
|
+
frame[:payload] = {
|
156
|
+
settings_max_concurrent_streams: 10,
|
157
|
+
settings_initial_window_size: 20,
|
158
|
+
55 => 30
|
159
|
+
}
|
160
|
+
|
161
|
+
f.parse(Buffer.new(f.generate(frame))).should eq frame
|
162
|
+
|
163
|
+
end
|
164
|
+
|
165
|
+
it "should raise exception on invalid stream ID" do
|
166
|
+
expect {
|
167
|
+
frame[:stream] = 1
|
168
|
+
f.generate(frame)
|
169
|
+
}.to raise_error(CompressionError, /Invalid stream ID/)
|
170
|
+
end
|
171
|
+
|
172
|
+
it "should raise exception on invalid setting" do
|
173
|
+
expect {
|
174
|
+
frame[:payload] = {random: 23}
|
175
|
+
f.generate(frame)
|
176
|
+
}.to raise_error(CompressionError, /Unknown settings ID/)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
context "PUSH_PROMISE" do
|
181
|
+
it "should generate and parse bytes" do
|
182
|
+
frame = {
|
183
|
+
length: 11,
|
184
|
+
type: :push_promise,
|
185
|
+
flags: [:end_push_promise],
|
186
|
+
stream: 1,
|
187
|
+
promise_stream: 2,
|
188
|
+
payload: 'headers'
|
189
|
+
}
|
190
|
+
|
191
|
+
bytes = f.generate(frame)
|
192
|
+
bytes.should eq [0xb,0x5,0x1,0x1,0x2,*'headers'.bytes].pack("SCCLLC*")
|
193
|
+
f.parse(Buffer.new(bytes)).should eq frame
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
context "PING" do
|
198
|
+
let(:frame) {
|
199
|
+
{
|
200
|
+
length: 8,
|
201
|
+
stream: 1,
|
202
|
+
type: :ping,
|
203
|
+
flags: [:pong],
|
204
|
+
payload: '12345678'
|
205
|
+
}
|
206
|
+
}
|
207
|
+
|
208
|
+
it "should generate and parse bytes" do
|
209
|
+
bytes = f.generate(frame)
|
210
|
+
bytes.should eq [0x8,0x6,0x1,0x1,*'12345678'.bytes].pack("SCCLC*")
|
211
|
+
f.parse(Buffer.new(bytes)).should eq frame
|
212
|
+
end
|
213
|
+
|
214
|
+
it "should raise exception on invalid payload" do
|
215
|
+
expect {
|
216
|
+
frame[:payload] = "1234"
|
217
|
+
f.generate(frame)
|
218
|
+
}.to raise_error(CompressionError, /Invalid payload size/)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
context "GOAWAY" do
|
223
|
+
let(:frame) {
|
224
|
+
{
|
225
|
+
length: 13,
|
226
|
+
stream: 1,
|
227
|
+
type: :goaway,
|
228
|
+
last_stream: 2,
|
229
|
+
error: :no_error,
|
230
|
+
payload: 'debug'
|
231
|
+
}
|
232
|
+
}
|
233
|
+
|
234
|
+
it "should generate and parse bytes" do
|
235
|
+
bytes = f.generate(frame)
|
236
|
+
bytes.should eq [0xd,0x7,0x0,0x1,0x2,0x0,*'debug'.bytes].pack("SCCLLLC*")
|
237
|
+
f.parse(Buffer.new(bytes)).should eq frame
|
238
|
+
end
|
239
|
+
|
240
|
+
it "should treat debug payload as optional" do
|
241
|
+
frame.delete :payload
|
242
|
+
frame[:length] = 0x8
|
243
|
+
|
244
|
+
bytes = f.generate(frame)
|
245
|
+
bytes.should eq [0x8,0x7,0x0,0x1,0x2,0x0].pack("SCCLLL")
|
246
|
+
f.parse(Buffer.new(bytes)).should eq frame
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
context "WINDOW_UPDATE" do
|
251
|
+
it "should generate and parse bytes" do
|
252
|
+
frame = {
|
253
|
+
length: 4,
|
254
|
+
type: :window_update,
|
255
|
+
increment: 10
|
256
|
+
}
|
257
|
+
|
258
|
+
bytes = f.generate(frame)
|
259
|
+
bytes.should eq [0x4,0x9,0x0,0x0,0xa].pack("SCCLL")
|
260
|
+
f.parse(Buffer.new(bytes)).should eq frame
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
context "CONTINUATION" do
|
265
|
+
it "should generate and parse bytes" do
|
266
|
+
frame = {
|
267
|
+
length: 12,
|
268
|
+
type: :continuation,
|
269
|
+
stream: 1,
|
270
|
+
flags: [:end_stream, :end_headers],
|
271
|
+
payload: 'header-block'
|
272
|
+
}
|
273
|
+
|
274
|
+
bytes = f.generate(frame)
|
275
|
+
bytes.should eq [0xc,0xa,0x3,0x1,*'header-block'.bytes].pack("SCCLC*")
|
276
|
+
f.parse(Buffer.new(bytes)).should eq frame
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
it "should determine frame length" do
|
281
|
+
frames = [
|
282
|
+
[{type: :data, stream: 1, flags: [:end_stream], payload: "abc"}, 3],
|
283
|
+
[{type: :headers, stream: 1, payload: "abc"}, 3],
|
284
|
+
[{type: :priority, stream: 3, priority: 30}, 4],
|
285
|
+
[{type: :rst_stream, stream: 3, error: 100}, 4],
|
286
|
+
[{type: :settings, payload: {settings_max_concurrent_streams: 10}}, 8],
|
287
|
+
[{type: :push_promise, promise_stream: 5, payload: "abc"}, 7],
|
288
|
+
[{type: :ping, payload: "blob"*2}, 8],
|
289
|
+
[{type: :goaway, last_stream: 5, error: 20, payload: "blob"}, 12],
|
290
|
+
[{type: :window_update, stream: 1, increment: 1024}, 4],
|
291
|
+
[{type: :continuation, stream: 1, payload: "abc"}, 3]
|
292
|
+
]
|
293
|
+
|
294
|
+
frames.each do |(frame, size)|
|
295
|
+
bytes = f.generate(frame)
|
296
|
+
Buffer.new(bytes).slice(0,2).unpack("S").first.should eq size
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
it "should parse single frame at a time" do
|
301
|
+
frames = [
|
302
|
+
{type: :headers, stream: 1, payload: "headers"},
|
303
|
+
{type: :data, stream: 1, flags: [:end_stream], payload: "abc"}
|
304
|
+
]
|
305
|
+
|
306
|
+
buf = Buffer.new(f.generate(frames[0]) + f.generate(frames[1]))
|
307
|
+
|
308
|
+
f.parse(buf).should eq frames[0]
|
309
|
+
f.parse(buf).should eq frames[1]
|
310
|
+
end
|
311
|
+
|
312
|
+
it "should process full frames only" do
|
313
|
+
frame = {type: :headers, stream: 1, payload: "headers"}
|
314
|
+
bytes = f.generate(frame)
|
315
|
+
|
316
|
+
buf = Buffer.new(bytes[0...-1])
|
317
|
+
f.parse(buf).should be_nil
|
318
|
+
buf.should eq bytes[0...-1]
|
319
|
+
|
320
|
+
buf = Buffer.new(bytes)
|
321
|
+
f.parse(buf).should eq frame
|
322
|
+
buf.should be_empty
|
323
|
+
end
|
324
|
+
|
325
|
+
end
|
data/spec/helper.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'http/2'
|
2
|
+
|
3
|
+
include HTTP2
|
4
|
+
include HTTP2::Header
|
5
|
+
include HTTP2::Error
|
6
|
+
|
7
|
+
DATA = {
|
8
|
+
type: :data,
|
9
|
+
flags: [:end_stream],
|
10
|
+
stream: 1,
|
11
|
+
payload: 'text'
|
12
|
+
}
|
13
|
+
|
14
|
+
HEADERS = {
|
15
|
+
type: :headers,
|
16
|
+
flags: [:end_headers],
|
17
|
+
stream: 1,
|
18
|
+
payload: 'header-block'
|
19
|
+
}
|
20
|
+
|
21
|
+
HEADERS_END_STREAM = {
|
22
|
+
type: :headers,
|
23
|
+
flags: [:end_headers, :end_stream],
|
24
|
+
stream: 1,
|
25
|
+
payload: 'header-block'
|
26
|
+
}
|
27
|
+
|
28
|
+
PRIORITY = {
|
29
|
+
type: :priority,
|
30
|
+
stream: 1,
|
31
|
+
priority: 15
|
32
|
+
}
|
33
|
+
|
34
|
+
RST_STREAM = {
|
35
|
+
type: :rst_stream,
|
36
|
+
stream: 1,
|
37
|
+
error: :stream_closed
|
38
|
+
}
|
39
|
+
|
40
|
+
SETTINGS = {
|
41
|
+
type: :settings,
|
42
|
+
stream: 0,
|
43
|
+
payload: {
|
44
|
+
settings_max_concurrent_streams: 10,
|
45
|
+
settings_flow_control_options: 1
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
PUSH_PROMISE = {
|
50
|
+
type: :push_promise,
|
51
|
+
flags: [:end_push_promise],
|
52
|
+
stream: 1,
|
53
|
+
promise_stream: 2,
|
54
|
+
payload: 'headers'
|
55
|
+
}
|
56
|
+
|
57
|
+
PING = {
|
58
|
+
stream: 0,
|
59
|
+
type: :ping,
|
60
|
+
payload: '12345678'
|
61
|
+
}
|
62
|
+
|
63
|
+
PONG = {
|
64
|
+
stream: 0,
|
65
|
+
type: :ping,
|
66
|
+
flags: [:pong],
|
67
|
+
payload: '12345678'
|
68
|
+
}
|
69
|
+
|
70
|
+
GOAWAY = {
|
71
|
+
type: :goaway,
|
72
|
+
last_stream: 2,
|
73
|
+
error: :no_error,
|
74
|
+
payload: 'debug'
|
75
|
+
}
|
76
|
+
|
77
|
+
WINDOW_UPDATE = {
|
78
|
+
type: :window_update,
|
79
|
+
increment: 10
|
80
|
+
}
|
81
|
+
|
82
|
+
CONTINUATION = {
|
83
|
+
type: :continuation,
|
84
|
+
flags: [:end_headers],
|
85
|
+
payload: '-second-block'
|
86
|
+
}
|
87
|
+
|
88
|
+
FRAME_TYPES = [
|
89
|
+
DATA, HEADERS, PRIORITY, RST_STREAM, SETTINGS, PUSH_PROMISE,
|
90
|
+
PING, GOAWAY, WINDOW_UPDATE, CONTINUATION
|
91
|
+
]
|
92
|
+
|
93
|
+
def set_stream_id(bytes, id)
|
94
|
+
head = bytes.slice!(0,8).unpack("SCCL")
|
95
|
+
head[3] = id
|
96
|
+
|
97
|
+
head.pack("SCCL") + bytes
|
98
|
+
end
|