http-2 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,493 @@
|
|
1
|
+
require "stringio"
|
2
|
+
|
3
|
+
module HTTP2
|
4
|
+
|
5
|
+
# Implementation of header compression for HTTP 2.0 (HPACK) format adapted
|
6
|
+
# to efficiently represent HTTP headers in the context of HTTP 2.0.
|
7
|
+
#
|
8
|
+
# - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression
|
9
|
+
module Header
|
10
|
+
|
11
|
+
# The set of components used to encode or decode a header set form an
|
12
|
+
# encoding context: an encoding context contains a header table and a
|
13
|
+
# reference set - there is one encoding context for each direction.
|
14
|
+
#
|
15
|
+
class CompressionContext
|
16
|
+
include Error
|
17
|
+
|
18
|
+
# TODO: replace StringIO with Buffer...
|
19
|
+
|
20
|
+
# Default request working set as defined by the spec.
|
21
|
+
REQ_DEFAULTS = [
|
22
|
+
[':scheme' ,'http' ],
|
23
|
+
[':scheme' ,'https'],
|
24
|
+
[':host' ,'' ],
|
25
|
+
[':path' ,'/' ],
|
26
|
+
[':method' ,'get' ],
|
27
|
+
['accept' ,'' ],
|
28
|
+
['accept-charset' ,'' ],
|
29
|
+
['accept-encoding' ,'' ],
|
30
|
+
['accept-language' ,'' ],
|
31
|
+
['cookie' ,'' ],
|
32
|
+
['if-modified-since' ,'' ],
|
33
|
+
['keep-alive' ,'' ],
|
34
|
+
['user-agent' ,'' ],
|
35
|
+
['proxy-connection' ,'' ],
|
36
|
+
['referer' ,'' ],
|
37
|
+
['accept-datetime' ,'' ],
|
38
|
+
['authorization' ,'' ],
|
39
|
+
['allow' ,'' ],
|
40
|
+
['cache-control' ,'' ],
|
41
|
+
['connection' ,'' ],
|
42
|
+
['content-length' ,'' ],
|
43
|
+
['content-md5' ,'' ],
|
44
|
+
['content-type' ,'' ],
|
45
|
+
['date' ,'' ],
|
46
|
+
['expect' ,'' ],
|
47
|
+
['from' ,'' ],
|
48
|
+
['if-match' ,'' ],
|
49
|
+
['if-none-match' ,'' ],
|
50
|
+
['if-range' ,'' ],
|
51
|
+
['if-unmodified-since','' ],
|
52
|
+
['max-forwards' ,'' ],
|
53
|
+
['pragma' ,'' ],
|
54
|
+
['proxy-authorization','' ],
|
55
|
+
['range' ,'' ],
|
56
|
+
['te' ,'' ],
|
57
|
+
['upgrade' ,'' ],
|
58
|
+
['via' ,'' ],
|
59
|
+
['warning' ,'' ]
|
60
|
+
];
|
61
|
+
|
62
|
+
# Default response working set as defined by the spec.
|
63
|
+
RESP_DEFAULTS = [
|
64
|
+
[':status' ,'200'],
|
65
|
+
['age' ,'' ],
|
66
|
+
['cache-control' ,'' ],
|
67
|
+
['content-length' ,'' ],
|
68
|
+
['content-type' ,'' ],
|
69
|
+
['date' ,'' ],
|
70
|
+
['etag' ,'' ],
|
71
|
+
['expires' ,'' ],
|
72
|
+
['last-modified' ,'' ],
|
73
|
+
['server' ,'' ],
|
74
|
+
['set-cookie' ,'' ],
|
75
|
+
['vary' ,'' ],
|
76
|
+
['via' ,'' ],
|
77
|
+
['access-control-allow-origin','' ],
|
78
|
+
['accept-ranges' ,'' ],
|
79
|
+
['allow' ,'' ],
|
80
|
+
['connection' ,'' ],
|
81
|
+
['content-disposition' ,'' ],
|
82
|
+
['content-encoding' ,'' ],
|
83
|
+
['content-language' ,'' ],
|
84
|
+
['content-location' ,'' ],
|
85
|
+
['content-md5' ,'' ],
|
86
|
+
['content-range' ,'' ],
|
87
|
+
['link' ,'' ],
|
88
|
+
['location' ,'' ],
|
89
|
+
['p3p' ,'' ],
|
90
|
+
['pragma' ,'' ],
|
91
|
+
['proxy-authenticate' ,'' ],
|
92
|
+
['refresh' ,'' ],
|
93
|
+
['retry-after' ,'' ],
|
94
|
+
['strict-transport-security' ,'' ],
|
95
|
+
['trailer' ,'' ],
|
96
|
+
['transfer-encoding' ,'' ],
|
97
|
+
['warning' ,'' ],
|
98
|
+
['www-authenticate' ,'' ]
|
99
|
+
];
|
100
|
+
|
101
|
+
# Current table of header key-value pairs.
|
102
|
+
attr_reader :table
|
103
|
+
|
104
|
+
# Current working set of header key-value pairs.
|
105
|
+
attr_reader :workset
|
106
|
+
|
107
|
+
# Initializes compression context with appropriate client/server
|
108
|
+
# defaults and maximum size of the header table.
|
109
|
+
#
|
110
|
+
# @param type [Symbol] either :request or :response
|
111
|
+
# @param limit [Integer] maximum header table size in bytes
|
112
|
+
def initialize(type, limit = 4096)
|
113
|
+
@type = type
|
114
|
+
@table = (type == :request) ? REQ_DEFAULTS.dup : RESP_DEFAULTS.dup
|
115
|
+
@limit = limit
|
116
|
+
@workset = []
|
117
|
+
end
|
118
|
+
|
119
|
+
# Performs differential coding based on provided command type.
|
120
|
+
# - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-01#section-3.1
|
121
|
+
#
|
122
|
+
# @param cmd [Hash]
|
123
|
+
def process(cmd)
|
124
|
+
# indexed representation
|
125
|
+
if cmd[:type] == :indexed
|
126
|
+
# For an indexed representation, the decoder checks whether the index
|
127
|
+
# is present in the working set. If true, the corresponding entry is
|
128
|
+
# removed from the working set. If several entries correspond to this
|
129
|
+
# encoded index, all these entries are removed from the working set.
|
130
|
+
# If the index is not present in the working set, it is used to
|
131
|
+
# retrieve the corresponding header from the header table, and a new
|
132
|
+
# entry is added to the working set representing this header.
|
133
|
+
cur = @workset.find_index {|(i,v)| i == cmd[:name]}
|
134
|
+
|
135
|
+
if cur
|
136
|
+
@workset.delete_at(cur)
|
137
|
+
else
|
138
|
+
@workset.push [cmd[:name], @table[cmd[:name]]]
|
139
|
+
end
|
140
|
+
|
141
|
+
else
|
142
|
+
# For a literal representation, a new entry is added to the working
|
143
|
+
# set representing this header. If the literal representation specifies
|
144
|
+
# that the header is to be indexed, the header is added accordingly to
|
145
|
+
# the header table, and its index is included in the entry in the working
|
146
|
+
# set. Otherwise, the entry in the working set contains an undefined index.
|
147
|
+
if cmd[:name].is_a? Integer
|
148
|
+
k,v = @table[cmd[:name]]
|
149
|
+
|
150
|
+
cmd[:index] ||= cmd[:name]
|
151
|
+
cmd[:value] ||= v
|
152
|
+
cmd[:name] = k
|
153
|
+
end
|
154
|
+
|
155
|
+
newval = [cmd[:name], cmd[:value]]
|
156
|
+
|
157
|
+
if cmd[:type] != :noindex
|
158
|
+
size_check cmd
|
159
|
+
|
160
|
+
case cmd[:type]
|
161
|
+
when :incremental
|
162
|
+
cmd[:index] = @table.size
|
163
|
+
when :substitution
|
164
|
+
if @table[cmd[:index]].nil?
|
165
|
+
raise HeaderException.new("invalid index")
|
166
|
+
end
|
167
|
+
when :prepend
|
168
|
+
@table = [newval] + @table
|
169
|
+
end
|
170
|
+
|
171
|
+
@table[cmd[:index]] = newval
|
172
|
+
end
|
173
|
+
|
174
|
+
@workset.push [cmd[:index], newval]
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# First, upon starting the decoding of a new set of headers, the
|
179
|
+
# reference set of headers is interpreted into the working set of
|
180
|
+
# headers: for each header in the reference set, an entry is added to
|
181
|
+
# the working set, containing the header name, its value, and its
|
182
|
+
# current index in the header table.
|
183
|
+
#
|
184
|
+
# @return [Array] current working set
|
185
|
+
def update_sets
|
186
|
+
# new refset is the the workset sans headers not in header table
|
187
|
+
refset = @workset.reject {|(i,h)| !@table.include? h}
|
188
|
+
|
189
|
+
# new workset is the refset with index of each header in header table
|
190
|
+
@workset = refset.collect {|(i,h)| [@table.find_index(h), h]}
|
191
|
+
end
|
192
|
+
|
193
|
+
# Emits best available command to encode provided header.
|
194
|
+
#
|
195
|
+
# @param header [Hash]
|
196
|
+
def addcmd(header)
|
197
|
+
# check if we have an exact match in header table
|
198
|
+
if idx = @table.index(header)
|
199
|
+
if !active? idx
|
200
|
+
return { name: idx, type: :indexed }
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# check if we have a partial match on header name
|
205
|
+
if idx = @table.index {|(k,_)| k == header.first}
|
206
|
+
# default to incremental indexing
|
207
|
+
cmd = { name: idx, value: header.last, type: :incremental}
|
208
|
+
|
209
|
+
# TODO: implement literal without indexing strategy
|
210
|
+
# TODO: implement substitution strategy (if it makes sense)
|
211
|
+
# if default? idx
|
212
|
+
# cmd[:type] = :incremental
|
213
|
+
# else
|
214
|
+
# cmd[:type] = :substitution
|
215
|
+
# cmd[:index] = idx
|
216
|
+
# end
|
217
|
+
|
218
|
+
return cmd
|
219
|
+
end
|
220
|
+
|
221
|
+
return { name: header.first, value: header.last, type: :incremental }
|
222
|
+
end
|
223
|
+
|
224
|
+
# Emits command to remove current index from working set.
|
225
|
+
#
|
226
|
+
# @param idx [Integer]
|
227
|
+
def removecmd(idx)
|
228
|
+
{name: idx, type: :indexed}
|
229
|
+
end
|
230
|
+
|
231
|
+
private
|
232
|
+
|
233
|
+
# Before adding a new entry to the header table or changing an existing
|
234
|
+
# one, a check has to be performed to ensure that the change will not
|
235
|
+
# cause the table to grow in size beyond the SETTINGS_MAX_BUFFER_SIZE
|
236
|
+
# limit. If necessary, one or more items from the beginning of the
|
237
|
+
# table are removed until there is enough free space available to make
|
238
|
+
# the modification. Dropping an entry from the beginning of the table
|
239
|
+
# causes the index positions of the remaining entries in the table to
|
240
|
+
# be decremented by 1.
|
241
|
+
#
|
242
|
+
# @param cmd [Hash]
|
243
|
+
def size_check(cmd)
|
244
|
+
cursize = @table.join.bytesize + @table.size * 32
|
245
|
+
cmdsize = cmd[:name].bytesize + cmd[:value].bytesize + 32
|
246
|
+
|
247
|
+
cur = 0
|
248
|
+
while (cursize + cmdsize) > @limit do
|
249
|
+
e = @table.shift
|
250
|
+
|
251
|
+
# When using substitution indexing, it is possible that the existing
|
252
|
+
# item being replaced might be one of the items removed when performing
|
253
|
+
# the necessary size adjustment. In such cases, the substituted value
|
254
|
+
# being added to the header table is inserted at the beginning of the
|
255
|
+
# header table (at index position #0) and the index positions of the
|
256
|
+
# other remaining entries in the table are incremented by 1.
|
257
|
+
if cmd[:type] == :substitution && cur == cmd[:index]
|
258
|
+
cmd[:type] = :prepend
|
259
|
+
end
|
260
|
+
|
261
|
+
cursize -= (e.join.bytesize + 32)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def active?(idx)
|
266
|
+
!@workset.find {|i,_| i == idx }.nil?
|
267
|
+
end
|
268
|
+
|
269
|
+
def default?(idx)
|
270
|
+
t = (@type == :request) ? REQ_DEFAULTS : RESP_DEFAULTS
|
271
|
+
idx < t.size
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
# Header representation as defined by the spec.
|
276
|
+
HEADREP = {
|
277
|
+
indexed: {prefix: 7, pattern: 0x80},
|
278
|
+
noindex: {prefix: 5, pattern: 0x60},
|
279
|
+
incremental: {prefix: 5, pattern: 0x40},
|
280
|
+
substitution: {prefix: 6, pattern: 0x00}
|
281
|
+
}
|
282
|
+
|
283
|
+
# Responsible for encoding header key-value pairs using HPACK algorithm.
|
284
|
+
# Compressor must be initialized with appropriate starting context based
|
285
|
+
# on local role: client or server.
|
286
|
+
#
|
287
|
+
# @example
|
288
|
+
# client_role = Compressor.new(:request)
|
289
|
+
# server_role = Compressor.new(:response)
|
290
|
+
class Compressor
|
291
|
+
def initialize(type)
|
292
|
+
@cc = CompressionContext.new(type)
|
293
|
+
end
|
294
|
+
|
295
|
+
# Encodes provided value via integer representation.
|
296
|
+
# - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-01#section-4.2.1
|
297
|
+
#
|
298
|
+
# If I < 2^N - 1, encode I on N bits
|
299
|
+
# Else, encode 2^N - 1 on N bits and do the following steps:
|
300
|
+
# Set I to (I - (2^N - 1)) and Q to 1
|
301
|
+
# While Q > 0
|
302
|
+
# Compute Q and R, quotient and remainder of I divided by 2^7
|
303
|
+
# If Q is strictly greater than 0, write one 1 bit; otherwise, write one 0 bit
|
304
|
+
# Encode R on the next 7 bits
|
305
|
+
# I = Q
|
306
|
+
#
|
307
|
+
# @param i [Integer] value to encode
|
308
|
+
# @param n [Integer] number of available bits
|
309
|
+
# @return [String] binary string
|
310
|
+
def integer(i, n)
|
311
|
+
limit = 2**n - 1
|
312
|
+
return [i].pack('C') if (i < limit)
|
313
|
+
|
314
|
+
bytes = []
|
315
|
+
bytes.push limit if !n.zero?
|
316
|
+
|
317
|
+
i -= limit
|
318
|
+
q = 1
|
319
|
+
|
320
|
+
while (q > 0) do
|
321
|
+
q, r = i.divmod(128)
|
322
|
+
r += 128 if (q > 0)
|
323
|
+
i = q
|
324
|
+
|
325
|
+
bytes.push(r)
|
326
|
+
end
|
327
|
+
|
328
|
+
bytes.pack('C*')
|
329
|
+
end
|
330
|
+
|
331
|
+
# Encodes provided value via string literal representation.
|
332
|
+
# - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-01#section-4.2.2
|
333
|
+
#
|
334
|
+
# * The string length, defined as the number of bytes needed to store
|
335
|
+
# its UTF-8 representation, is represented as an integer with a zero
|
336
|
+
# bits prefix. If the string length is strictly less than 128, it is
|
337
|
+
# represented as one byte.
|
338
|
+
# * The string value represented as a list of UTF-8 character
|
339
|
+
#
|
340
|
+
# @param str [String]
|
341
|
+
# @return [String] binary string
|
342
|
+
def string(str)
|
343
|
+
integer(str.bytesize, 0) + str.dup.force_encoding('binary')
|
344
|
+
end
|
345
|
+
|
346
|
+
# Encodes header command with appropriate header representation.
|
347
|
+
# - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-01#section-4.3
|
348
|
+
#
|
349
|
+
# @param h [Hash] header command
|
350
|
+
# @param buffer [String]
|
351
|
+
def header(h, buffer = "")
|
352
|
+
rep = HEADREP[h[:type]]
|
353
|
+
|
354
|
+
if h[:type] == :indexed
|
355
|
+
buffer << integer(h[:name], rep[:prefix])
|
356
|
+
|
357
|
+
else
|
358
|
+
if h[:name].is_a? Integer
|
359
|
+
buffer << integer(h[:name]+1, rep[:prefix])
|
360
|
+
else
|
361
|
+
buffer << integer(0, rep[:prefix])
|
362
|
+
buffer << string(h[:name])
|
363
|
+
end
|
364
|
+
|
365
|
+
if h[:type] == :substitution
|
366
|
+
buffer << integer(h[:index], 0)
|
367
|
+
end
|
368
|
+
|
369
|
+
if h[:value].is_a? Integer
|
370
|
+
buffer << integer(h[:value], 0)
|
371
|
+
else
|
372
|
+
buffer << string(h[:value])
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
# set header representation pattern on first byte
|
377
|
+
fb = buffer[0].unpack("C").first | rep[:pattern]
|
378
|
+
buffer.setbyte(0, fb)
|
379
|
+
|
380
|
+
buffer
|
381
|
+
end
|
382
|
+
|
383
|
+
# Encodes provided list of HTTP headers.
|
384
|
+
#
|
385
|
+
# @param headers [Hash]
|
386
|
+
# @return [String] binary string
|
387
|
+
def encode(headers)
|
388
|
+
commands = []
|
389
|
+
@cc.update_sets
|
390
|
+
|
391
|
+
# Remove missing headers from the working set
|
392
|
+
@cc.workset.each do |idx, (wk,wv)|
|
393
|
+
if headers.find {|(hk,hv)| hk == wk && hv == wv }.nil?
|
394
|
+
commands.push @cc.removecmd idx
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
# Add missing headers to the working set
|
399
|
+
headers.each do |(hk,hv)|
|
400
|
+
if @cc.workset.find {|i,(wk,wv)| hk == wk && hv == wv}.nil?
|
401
|
+
commands.push @cc.addcmd [hk, hv]
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
commands.map do |cmd|
|
406
|
+
@cc.process cmd.dup
|
407
|
+
header cmd
|
408
|
+
end.join
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
# Responsible for decoding received headers and maintaining compression
|
413
|
+
# context of the opposing peer. Decompressor must be initialized with
|
414
|
+
# appropriate starting context based on local role: client or server.
|
415
|
+
#
|
416
|
+
# @example
|
417
|
+
# server_role = Decompressor.new(:request)
|
418
|
+
# client_role = Decompressor.new(:response)
|
419
|
+
class Decompressor
|
420
|
+
def initialize(type)
|
421
|
+
@cc = CompressionContext.new(type)
|
422
|
+
end
|
423
|
+
|
424
|
+
# Decodes integer value from provided buffer.
|
425
|
+
#
|
426
|
+
# @param buf [String]
|
427
|
+
# @param n [Integer] number of available bits
|
428
|
+
def integer(buf, n)
|
429
|
+
limit = 2**n - 1
|
430
|
+
i = !n.zero? ? (buf.getbyte & limit) : 0
|
431
|
+
|
432
|
+
m = 0
|
433
|
+
buf.each_byte do |byte|
|
434
|
+
i += ((byte & 127) << m)
|
435
|
+
m += 7
|
436
|
+
|
437
|
+
break if (byte & 128).zero?
|
438
|
+
end if (i == limit)
|
439
|
+
|
440
|
+
i
|
441
|
+
end
|
442
|
+
|
443
|
+
# Decodes string value from provided buffer.
|
444
|
+
#
|
445
|
+
# @param buf [String]
|
446
|
+
# @return [String] UTF-8 encoded string
|
447
|
+
def string(buf)
|
448
|
+
buf.read(integer(buf, 0)).force_encoding('utf-8')
|
449
|
+
end
|
450
|
+
|
451
|
+
# Decodes header command from provided buffer.
|
452
|
+
#
|
453
|
+
# @param buf [String]
|
454
|
+
def header(buf)
|
455
|
+
peek = buf.getbyte
|
456
|
+
buf.seek(-1, IO::SEEK_CUR)
|
457
|
+
|
458
|
+
header = {}
|
459
|
+
header[:type], type = HEADREP.select do |t, desc|
|
460
|
+
mask = (peek >> desc[:prefix]) << desc[:prefix]
|
461
|
+
mask == desc[:pattern]
|
462
|
+
end.first
|
463
|
+
|
464
|
+
header[:name] = integer(buf, type[:prefix])
|
465
|
+
if header[:type] != :indexed
|
466
|
+
header[:name] -= 1
|
467
|
+
|
468
|
+
if header[:name] == -1
|
469
|
+
header[:name] = string(buf)
|
470
|
+
end
|
471
|
+
|
472
|
+
if header[:type] == :substitution
|
473
|
+
header[:index] = integer(buf, 0)
|
474
|
+
end
|
475
|
+
|
476
|
+
header[:value] = string(buf)
|
477
|
+
end
|
478
|
+
|
479
|
+
header
|
480
|
+
end
|
481
|
+
|
482
|
+
# Decodes and processes header commands within provided buffer.
|
483
|
+
#
|
484
|
+
# @param buf [String]
|
485
|
+
def decode(buf)
|
486
|
+
@cc.update_sets
|
487
|
+
@cc.process(header(buf)) while !buf.eof?
|
488
|
+
@cc.workset.map {|i,header| header}
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
end
|
493
|
+
end
|