http-2 0.6.3 → 0.7.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.
- checksums.yaml +4 -4
- data/.coveralls.yml +1 -0
- data/.gitmodules +3 -0
- data/Gemfile +5 -0
- data/README.md +5 -4
- data/Rakefile +1 -0
- data/example/client.rb +20 -5
- data/example/helper.rb +1 -1
- data/example/keys/mycert.pem +21 -22
- data/example/keys/mykey.pem +25 -25
- data/example/server.rb +10 -3
- data/http-2.gemspec +1 -1
- data/lib/http/2.rb +2 -0
- data/lib/http/2/client.rb +16 -10
- data/lib/http/2/compressor.rb +346 -286
- data/lib/http/2/connection.rb +254 -95
- data/lib/http/2/error.rb +0 -6
- data/lib/http/2/flow_buffer.rb +12 -10
- data/lib/http/2/framer.rb +203 -57
- data/lib/http/2/huffman.rb +332 -0
- data/lib/http/2/huffman_statemachine.rb +272 -0
- data/lib/http/2/server.rb +5 -4
- data/lib/http/2/stream.rb +72 -35
- data/lib/http/2/version.rb +1 -1
- data/lib/tasks/generate_huffman_table.rb +160 -0
- data/spec/client_spec.rb +3 -3
- data/spec/compressor_spec.rb +422 -281
- data/spec/connection_spec.rb +236 -56
- data/spec/framer_spec.rb +213 -45
- data/spec/helper.rb +42 -15
- data/spec/hpack_test_spec.rb +83 -0
- data/spec/huffman_spec.rb +68 -0
- data/spec/server_spec.rb +7 -6
- data/spec/stream_spec.rb +81 -54
- metadata +21 -11
data/lib/http/2/server.rb
CHANGED
@@ -22,11 +22,12 @@ module HTTP2
|
|
22
22
|
class Server < Connection
|
23
23
|
|
24
24
|
# Initialize new HTTP 2.0 server object.
|
25
|
-
def initialize(
|
25
|
+
def initialize(**settings)
|
26
26
|
@stream_id = 2
|
27
|
-
@state = :
|
28
|
-
|
29
|
-
@
|
27
|
+
@state = :waiting_magic
|
28
|
+
|
29
|
+
@local_role = :server
|
30
|
+
@remote_role = :client
|
30
31
|
|
31
32
|
super
|
32
33
|
end
|
data/lib/http/2/stream.rb
CHANGED
@@ -24,7 +24,7 @@ module HTTP2
|
|
24
24
|
# | | ,-------|:active |-------. | |
|
25
25
|
# | | H / ES | | ES \ H | |
|
26
26
|
# | v v +--------+ v v |
|
27
|
-
# | +-----------+ |
|
27
|
+
# | +-----------+ | +-----------+ |
|
28
28
|
# | |:half_close| | |:half_close| |
|
29
29
|
# | | (remote) | | | (local) | |
|
30
30
|
# | +-----------+ | +-----------+ |
|
@@ -49,10 +49,13 @@ module HTTP2
|
|
49
49
|
attr_reader :parent
|
50
50
|
|
51
51
|
# Stream priority as set by initiator.
|
52
|
-
attr_reader :
|
52
|
+
attr_reader :weight
|
53
|
+
attr_reader :dependency
|
53
54
|
|
54
55
|
# Size of current stream flow control window.
|
55
|
-
attr_reader :
|
56
|
+
attr_reader :local_window
|
57
|
+
attr_reader :remote_window
|
58
|
+
alias :window :local_window
|
56
59
|
|
57
60
|
# Reason why connection was closed.
|
58
61
|
attr_reader :closed
|
@@ -64,20 +67,26 @@ module HTTP2
|
|
64
67
|
# will emit new stream objects, when new stream frames are received.
|
65
68
|
#
|
66
69
|
# @param id [Integer]
|
67
|
-
# @param
|
70
|
+
# @param weight [Integer]
|
71
|
+
# @param dependency [Integer]
|
72
|
+
# @param exclusive [Boolean]
|
68
73
|
# @param window [Integer]
|
69
74
|
# @param parent [Stream]
|
70
|
-
def initialize(id,
|
71
|
-
@
|
72
|
-
@
|
73
|
-
@
|
75
|
+
def initialize(connection: nil, id: nil, weight: 16, dependency: 0, exclusive: false, parent: nil)
|
76
|
+
@connection = connection or raise ArgumentError.new("missing mandatory argument connection")
|
77
|
+
@id = id or raise ArgumentError.new("missing mandatory argument id")
|
78
|
+
@weight = weight
|
79
|
+
@dependency = dependency
|
80
|
+
process_priority({weight: weight, stream_dependency: dependency, exclusive: exclusive})
|
81
|
+
@remote_window = connection.remote_settings[:settings_initial_window_size]
|
74
82
|
@parent = parent
|
75
83
|
@state = :idle
|
76
84
|
@error = false
|
77
85
|
@closed = false
|
78
86
|
@send_buffer = []
|
79
87
|
|
80
|
-
on(:window) { |v| @
|
88
|
+
on(:window) { |v| @remote_window = v }
|
89
|
+
on(:local_window) { |v| @local_window = v }
|
81
90
|
end
|
82
91
|
|
83
92
|
# Processes incoming HTTP 2.0 frames. The frames must be decoded upstream.
|
@@ -88,19 +97,17 @@ module HTTP2
|
|
88
97
|
|
89
98
|
case frame[:type]
|
90
99
|
when :data
|
100
|
+
# TODO: when receiving DATA, keep track of local_window.
|
91
101
|
emit(:data, frame[:payload]) if !frame[:ignore]
|
92
102
|
when :headers, :push_promise
|
93
|
-
|
94
|
-
emit(:headers, Hash[*frame[:payload].flatten]) if !frame[:ignore]
|
95
|
-
else
|
96
|
-
emit(:headers, frame[:payload]) if !frame[:ignore]
|
97
|
-
end
|
103
|
+
emit(:headers, frame[:payload]) if !frame[:ignore]
|
98
104
|
when :priority
|
99
|
-
|
100
|
-
emit(:priority, @priority)
|
105
|
+
process_priority(frame)
|
101
106
|
when :window_update
|
102
|
-
@
|
107
|
+
@remote_window += frame[:increment]
|
103
108
|
send_data
|
109
|
+
when :altsvc, :blocked
|
110
|
+
emit(frame[:type], frame)
|
104
111
|
end
|
105
112
|
|
106
113
|
complete_transition(frame)
|
@@ -116,7 +123,7 @@ module HTTP2
|
|
116
123
|
transition(frame, true)
|
117
124
|
frame[:stream] ||= @id
|
118
125
|
|
119
|
-
|
126
|
+
process_priority(frame) if frame[:type] == :priority
|
120
127
|
|
121
128
|
if frame[:type] == :data
|
122
129
|
send_data(frame)
|
@@ -129,7 +136,7 @@ module HTTP2
|
|
129
136
|
|
130
137
|
# Sends a HEADERS frame containing HTTP response headers.
|
131
138
|
#
|
132
|
-
# @param headers [Hash]
|
139
|
+
# @param headers [Array or Hash] Array of key-value pairs or Hash
|
133
140
|
# @param end_headers [Boolean] indicates that no more headers will be sent
|
134
141
|
# @param end_stream [Boolean] indicates that no payload will be sent
|
135
142
|
def headers(headers, end_headers: true, end_stream: false)
|
@@ -140,20 +147,21 @@ module HTTP2
|
|
140
147
|
send({type: :headers, flags: flags, payload: headers.to_a})
|
141
148
|
end
|
142
149
|
|
143
|
-
def promise(headers,
|
150
|
+
def promise(headers, end_headers: true, &block)
|
144
151
|
raise Exception.new("must provide callback") if !block_given?
|
145
152
|
|
146
|
-
flags =
|
153
|
+
flags = end_headers ? [:end_headers] : []
|
147
154
|
emit(:promise, self, headers, flags, &block)
|
148
155
|
end
|
149
156
|
|
150
157
|
# Sends a PRIORITY frame with new stream priority value (can only be
|
151
158
|
# performed by the client).
|
152
159
|
#
|
153
|
-
# @param
|
154
|
-
|
160
|
+
# @param weight [Integer] new stream weight value
|
161
|
+
# @param dependency [Integer] new stream dependency stream
|
162
|
+
def reprioritize(weight: 16, dependency: 0, exclusive: false)
|
155
163
|
stream_error if @id.even?
|
156
|
-
send({type: :priority,
|
164
|
+
send({type: :priority, weight: weight, stream_dependency: dependency, exclusive: exclusive})
|
157
165
|
end
|
158
166
|
|
159
167
|
# Sends DATA frame containing response payload.
|
@@ -164,8 +172,11 @@ module HTTP2
|
|
164
172
|
flags = []
|
165
173
|
flags << :end_stream if end_stream
|
166
174
|
|
167
|
-
|
168
|
-
|
175
|
+
# Split data according to each frame is smaller enough
|
176
|
+
# TODO: consider padding?
|
177
|
+
max_size = @connection.remote_settings[:settings_max_frame_size]
|
178
|
+
while payload.bytesize > max_size do
|
179
|
+
chunk = payload.slice!(0, max_size)
|
169
180
|
send({type: :data, payload: chunk})
|
170
181
|
end
|
171
182
|
|
@@ -344,8 +355,11 @@ module HTTP2
|
|
344
355
|
# frame bearing the END_STREAM flag is sent.
|
345
356
|
when :half_closed_local
|
346
357
|
if sending
|
347
|
-
|
358
|
+
case frame[:type]
|
359
|
+
when :rst_stream
|
348
360
|
event(:local_rst)
|
361
|
+
when :priority
|
362
|
+
process_priority(frame)
|
349
363
|
else
|
350
364
|
stream_error
|
351
365
|
end
|
@@ -354,8 +368,10 @@ module HTTP2
|
|
354
368
|
when :data, :headers, :continuation
|
355
369
|
event(:remote_closed) if end_stream?(frame)
|
356
370
|
when :rst_stream then event(:remote_rst)
|
357
|
-
when :
|
358
|
-
frame
|
371
|
+
when :priority
|
372
|
+
process_priority(frame)
|
373
|
+
when :window_update
|
374
|
+
frame[:ignore] = true
|
359
375
|
end
|
360
376
|
end
|
361
377
|
|
@@ -380,6 +396,8 @@ module HTTP2
|
|
380
396
|
case frame[:type]
|
381
397
|
when :rst_stream then event(:remote_rst)
|
382
398
|
when :window_update then frame[:ignore] = true
|
399
|
+
when :priority
|
400
|
+
process_priority(frame)
|
383
401
|
else stream_error(:stream_closed); end
|
384
402
|
end
|
385
403
|
|
@@ -407,15 +425,21 @@ module HTTP2
|
|
407
425
|
if sending
|
408
426
|
case frame[:type]
|
409
427
|
when :rst_stream then # ignore
|
428
|
+
when :priority then
|
429
|
+
process_priority(frame)
|
410
430
|
else
|
411
431
|
stream_error(:stream_closed) if !(frame[:type] == :rst_stream)
|
412
432
|
end
|
413
433
|
else
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
434
|
+
if frame[:type] == :priority
|
435
|
+
process_priority(frame)
|
436
|
+
else
|
437
|
+
case @closed
|
438
|
+
when :remote_rst, :remote_closed
|
439
|
+
stream_error(:stream_closed) if !(frame[:type] == :rst_stream)
|
440
|
+
when :local_rst, :local_closed
|
441
|
+
frame[:ignore] = true
|
442
|
+
end
|
419
443
|
end
|
420
444
|
end
|
421
445
|
end
|
@@ -455,6 +479,20 @@ module HTTP2
|
|
455
479
|
end
|
456
480
|
end
|
457
481
|
|
482
|
+
def process_priority(frame)
|
483
|
+
@weight = frame[:weight]
|
484
|
+
@dependency = frame[:stream_dependency]
|
485
|
+
emit(:priority,
|
486
|
+
weight: frame[:weight],
|
487
|
+
dependency: frame[:stream_dependency],
|
488
|
+
exclusive: frame[:exclusive])
|
489
|
+
# TODO: implement dependency tree housekeeping
|
490
|
+
# Latest draft defines a fairly complex priority control.
|
491
|
+
# See https://tools.ietf.org/html/draft-ietf-httpbis-http2-14#section-5.3
|
492
|
+
# We currently have no prioritization among streams.
|
493
|
+
# We should add code here.
|
494
|
+
end
|
495
|
+
|
458
496
|
def end_stream?(frame)
|
459
497
|
case frame[:type]
|
460
498
|
when :data, :headers, :continuation
|
@@ -469,6 +507,5 @@ module HTTP2
|
|
469
507
|
klass = error.to_s.split('_').map(&:capitalize).join
|
470
508
|
raise Error.const_get(klass).new(msg)
|
471
509
|
end
|
472
|
-
|
473
510
|
end
|
474
511
|
end
|
data/lib/http/2/version.rb
CHANGED
@@ -0,0 +1,160 @@
|
|
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
|
18
|
+
def initialize(depth)
|
19
|
+
@next = [nil, nil]
|
20
|
+
@id = @@id
|
21
|
+
@@id += 1
|
22
|
+
@final = false
|
23
|
+
@depth = depth
|
24
|
+
end
|
25
|
+
def add(code, len, chr)
|
26
|
+
chr == EOS && @depth <= 7 and self.final = true
|
27
|
+
if len == 0
|
28
|
+
@emit = chr
|
29
|
+
else
|
30
|
+
bit = (code & (1 << (len - 1))) == 0 ? 0 : 1
|
31
|
+
node = @next[bit] ||= Node.new(@depth + 1)
|
32
|
+
node.add(code, len - 1, chr)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Transition
|
37
|
+
attr_accessor :emit, :node
|
38
|
+
def initialize(emit, node)
|
39
|
+
@emit = emit
|
40
|
+
@node = node
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.generate_tree
|
45
|
+
@root = new(0)
|
46
|
+
HTTP2::Header::Huffman::CODES.each_with_index do |c, chr|
|
47
|
+
code, len = c
|
48
|
+
@root.add(code, len, chr)
|
49
|
+
end
|
50
|
+
puts "#{@@id} nodes"
|
51
|
+
@root
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.generate_machine
|
55
|
+
generate_tree
|
56
|
+
togo = Set[@root]
|
57
|
+
@states = Set[@root]
|
58
|
+
|
59
|
+
until togo.empty?
|
60
|
+
node = togo.first
|
61
|
+
togo.delete(node)
|
62
|
+
|
63
|
+
next if node.transitions
|
64
|
+
node.transitions = Array[1 << BITS_AT_ONCE]
|
65
|
+
|
66
|
+
(1 << BITS_AT_ONCE).times do |input|
|
67
|
+
n = node
|
68
|
+
emit = ''
|
69
|
+
(BITS_AT_ONCE - 1).downto(0) do |i|
|
70
|
+
bit = (input & (1 << i)) == 0 ? 0 : 1
|
71
|
+
n = n.next[bit]
|
72
|
+
if n.emit
|
73
|
+
if n.emit == EOS
|
74
|
+
emit = EOS # cause error on decoding
|
75
|
+
else
|
76
|
+
emit << n.emit.chr('binary') unless emit == EOS
|
77
|
+
end
|
78
|
+
n = @root
|
79
|
+
end
|
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
|
+
(1 << BITS_AT_ONCE).times do |t|
|
124
|
+
emit = n.transitions[t].emit
|
125
|
+
emit == EOS or emit = emit.dup.force_encoding('binary')
|
126
|
+
f.print %Q/[#{emit == '' ? "nil" : emit.inspect},#{state_id[n.transitions[t].node]}],/
|
127
|
+
end
|
128
|
+
f.print "],\n"
|
129
|
+
end
|
130
|
+
f.print <<TAILER
|
131
|
+
]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
TAILER
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.root
|
140
|
+
@root
|
141
|
+
end
|
142
|
+
|
143
|
+
# Test decoder
|
144
|
+
def self.decode(input)
|
145
|
+
emit = ''
|
146
|
+
n = root
|
147
|
+
nibbles = input.unpack("C*").flat_map{|b| [((b & 0xf0) >> 4), b & 0xf]}
|
148
|
+
until nibbles.empty?
|
149
|
+
nb = nibbles.shift
|
150
|
+
t = n.transitions[nb]
|
151
|
+
emit << t.emit
|
152
|
+
n = t.node
|
153
|
+
end
|
154
|
+
unless n.final && nibbles.all?{|x| x == 0xf}
|
155
|
+
puts "len = #{emit.size} n.final = #{n.final} nibbles = #{nibbles}"
|
156
|
+
end
|
157
|
+
emit
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
data/spec/client_spec.rb
CHANGED
@@ -17,20 +17,20 @@ describe HTTP2::Client do
|
|
17
17
|
@client.on(:frame) { |bytes| frames << bytes }
|
18
18
|
@client.ping("12345678")
|
19
19
|
|
20
|
-
frames[0].should eq
|
20
|
+
frames[0].should eq CONNECTION_PREFACE_MAGIC
|
21
21
|
f.parse(frames[1])[:type].should eq :settings
|
22
22
|
end
|
23
23
|
|
24
24
|
it "should initialize client with custom connection settings" do
|
25
25
|
frames = []
|
26
26
|
|
27
|
-
@client = Client.new(
|
27
|
+
@client = Client.new(:settings_max_concurrent_streams => 200)
|
28
28
|
@client.on(:frame) { |bytes| frames << bytes }
|
29
29
|
@client.ping("12345678")
|
30
30
|
|
31
31
|
frame = f.parse(frames[1])
|
32
32
|
frame[:type].should eq :settings
|
33
|
-
frame[:payload][:settings_max_concurrent_streams
|
33
|
+
frame[:payload].should include([:settings_max_concurrent_streams, 200])
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
data/spec/compressor_spec.rb
CHANGED
@@ -2,8 +2,8 @@ require "helper"
|
|
2
2
|
|
3
3
|
describe HTTP2::Header do
|
4
4
|
|
5
|
-
let(:c) { Compressor.new
|
6
|
-
let(:d) { Decompressor.new
|
5
|
+
let(:c) { Compressor.new }
|
6
|
+
let(:d) { Decompressor.new }
|
7
7
|
|
8
8
|
context "literal representation" do
|
9
9
|
context "integer" do
|
@@ -33,28 +33,39 @@ describe HTTP2::Header do
|
|
33
33
|
end
|
34
34
|
|
35
35
|
context "string" do
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
36
|
+
[ ['with huffman', :always, 0x80 ],
|
37
|
+
['without huffman', :never, 0] ].each do |desc, option, msb|
|
38
|
+
let (:trailer) { "trailer" }
|
39
|
+
|
40
|
+
[
|
41
|
+
['ascii codepoints', 'abcdefghij'],
|
42
|
+
['utf-8 codepoints', 'éáűőúöüó€'],
|
43
|
+
['long utf-8 strings', 'éáűőúöüó€'*100],
|
44
|
+
].each do |datatype, plain|
|
45
|
+
it "should handle #{datatype} #{desc}" do
|
46
|
+
# NOTE: don't put this new in before{} because of test case shuffling
|
47
|
+
@c = Compressor.new(huffman: option)
|
48
|
+
str = @c.string(plain)
|
49
|
+
(str.getbyte(0) & 0x80).should eq msb
|
50
|
+
|
51
|
+
buf = Buffer.new(str + trailer)
|
52
|
+
d.string(buf).should eq plain
|
53
|
+
buf.should eq trailer
|
54
|
+
end
|
55
|
+
end
|
50
56
|
end
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
57
|
+
context "choosing shorter representation" do
|
58
|
+
[ ['日本語', :plain],
|
59
|
+
['200', :huffman],
|
60
|
+
['xq', :plain], # prefer plain if equal size
|
61
|
+
].each do |string, choice|
|
62
|
+
before { @c = Compressor.new(huffman: :shorter) }
|
63
|
+
|
64
|
+
it "should return #{choice} representation" do
|
65
|
+
wire = @c.string(string)
|
66
|
+
(wire.getbyte(0) & 0x80).should eq (choice == :plain ? 0 : 0x80)
|
67
|
+
end
|
68
|
+
end
|
58
69
|
end
|
59
70
|
end
|
60
71
|
end
|
@@ -62,332 +73,462 @@ describe HTTP2::Header do
|
|
62
73
|
context "header representation" do
|
63
74
|
it "should handle indexed representation" do
|
64
75
|
h = {name: 10, type: :indexed}
|
65
|
-
|
76
|
+
wire = c.header(h)
|
77
|
+
(wire.readbyte(0) & 0x80).should eq 0x80
|
78
|
+
(wire.readbyte(0) & 0x7f).should eq h[:name] + 1
|
79
|
+
d.header(wire).should eq h
|
80
|
+
end
|
81
|
+
it "should raise when decoding indexed representation with index zero" do
|
82
|
+
h = {name: 10, type: :indexed}
|
83
|
+
wire = c.header(h)
|
84
|
+
wire[0] = 0x80.chr('binary')
|
85
|
+
expect { d.header(wire) }.to raise_error CompressionError
|
66
86
|
end
|
67
87
|
|
68
88
|
context "literal w/o indexing representation" do
|
69
89
|
it "should handle indexed header" do
|
70
90
|
h = {name: 10, value: "my-value", type: :noindex}
|
71
|
-
|
91
|
+
wire = c.header(h)
|
92
|
+
(wire.readbyte(0) & 0xf0).should eq 0x0
|
93
|
+
(wire.readbyte(0) & 0x0f).should eq h[:name] + 1
|
94
|
+
d.header(wire).should eq h
|
72
95
|
end
|
73
96
|
|
74
97
|
it "should handle literal header" do
|
75
98
|
h = {name: "x-custom", value: "my-value", type: :noindex}
|
76
|
-
|
99
|
+
wire = c.header(h)
|
100
|
+
(wire.readbyte(0) & 0xf0).should eq 0x0
|
101
|
+
(wire.readbyte(0) & 0x0f).should eq 0
|
102
|
+
d.header(wire).should eq h
|
77
103
|
end
|
78
104
|
end
|
79
105
|
|
80
106
|
context "literal w/ incremental indexing" do
|
81
107
|
it "should handle indexed header" do
|
82
108
|
h = {name: 10, value: "my-value", type: :incremental}
|
83
|
-
|
109
|
+
wire = c.header(h)
|
110
|
+
(wire.readbyte(0) & 0xc0).should eq 0x40
|
111
|
+
(wire.readbyte(0) & 0x3f).should eq h[:name] + 1
|
112
|
+
d.header(wire).should eq h
|
84
113
|
end
|
85
114
|
|
86
115
|
it "should handle literal header" do
|
87
116
|
h = {name: "x-custom", value: "my-value", type: :incremental}
|
88
|
-
|
117
|
+
wire = c.header(h)
|
118
|
+
(wire.readbyte(0) & 0xc0).should eq 0x40
|
119
|
+
(wire.readbyte(0) & 0x3f).should eq 0
|
120
|
+
d.header(wire).should eq h
|
89
121
|
end
|
90
122
|
end
|
91
123
|
|
92
|
-
context "literal
|
124
|
+
context "literal never indexed" do
|
93
125
|
it "should handle indexed header" do
|
94
|
-
h = {name:
|
95
|
-
|
126
|
+
h = {name: 10, value: "my-value", type: :neverindexed}
|
127
|
+
wire = c.header(h)
|
128
|
+
(wire.readbyte(0) & 0xf0).should eq 0x10
|
129
|
+
(wire.readbyte(0) & 0x0f).should eq h[:name] + 1
|
130
|
+
d.header(wire).should eq h
|
96
131
|
end
|
97
132
|
|
98
133
|
it "should handle literal header" do
|
99
|
-
h = {name: "x-
|
100
|
-
|
134
|
+
h = {name: "x-custom", value: "my-value", type: :neverindexed}
|
135
|
+
wire = c.header(h)
|
136
|
+
(wire.readbyte(0) & 0xf0).should eq 0x10
|
137
|
+
(wire.readbyte(0) & 0x0f).should eq 0
|
138
|
+
d.header(wire).should eq h
|
101
139
|
end
|
102
140
|
end
|
103
141
|
end
|
104
142
|
|
105
|
-
context "
|
106
|
-
|
107
|
-
before(:each) { @cc = EncodingContext.new(:request) }
|
108
|
-
|
109
|
-
it "should be initialized with pre-defined headers" do
|
110
|
-
cc = EncodingContext.new(:request)
|
111
|
-
cc.table.size.should eq 30
|
112
|
-
|
113
|
-
cc = EncodingContext.new(:response)
|
114
|
-
cc.table.size.should eq 30
|
115
|
-
end
|
116
|
-
|
117
|
-
it "should be initialized with empty working set" do
|
118
|
-
@cc.refset.should be_empty
|
119
|
-
end
|
120
|
-
|
121
|
-
it "should update working set based on prior state" do
|
122
|
-
@cc.refset.should be_empty
|
123
|
-
|
124
|
-
@cc.process({name: 0, type: :indexed})
|
125
|
-
@cc.refset.should eq [[0, [":scheme", "http"]]]
|
143
|
+
context "shared compression context" do
|
144
|
+
before(:each) { @cc = EncodingContext.new }
|
126
145
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
context "processing" do
|
132
|
-
it "should toggle index representation headers in working set" do
|
133
|
-
@cc.process({name: 0, type: :indexed})
|
134
|
-
@cc.refset.first.should eq [0, [":scheme", "http"]]
|
135
|
-
|
136
|
-
@cc.process({name: 0, type: :indexed})
|
137
|
-
@cc.refset.should be_empty
|
138
|
-
end
|
146
|
+
it "should be initialized with empty headers" do
|
147
|
+
cc = EncodingContext.new
|
148
|
+
cc.table.should be_empty
|
149
|
+
end
|
139
150
|
|
140
|
-
|
151
|
+
context "processing" do
|
152
|
+
[ ["no indexing", :noindex],
|
153
|
+
["never indexed", :neverindexed]].each do |desc, type|
|
154
|
+
context "#{desc}" do
|
141
155
|
it "should process indexed header with literal value" do
|
142
|
-
original_table = @cc.table
|
156
|
+
original_table = @cc.table.dup
|
143
157
|
|
144
|
-
emit = @cc.process({name:
|
158
|
+
emit = @cc.process({name: 4, value: "/path", type: type})
|
145
159
|
emit.should eq [":path", "/path"]
|
146
|
-
@cc.refset.should be_empty
|
147
|
-
@cc.table.should eq original_table
|
148
|
-
end
|
149
|
-
|
150
|
-
it "should process indexed header with default value" do
|
151
|
-
original_table = @cc.table
|
152
|
-
|
153
|
-
emit = @cc.process({name: 3, type: :noindex})
|
154
|
-
emit.should eq [":path", "/"]
|
155
160
|
@cc.table.should eq original_table
|
156
161
|
end
|
157
162
|
|
158
163
|
it "should process literal header with literal value" do
|
159
|
-
original_table = @cc.table
|
164
|
+
original_table = @cc.table.dup
|
160
165
|
|
161
|
-
emit = @cc.process({name: "x-custom", value: "random", type:
|
166
|
+
emit = @cc.process({name: "x-custom", value: "random", type: type})
|
162
167
|
emit.should eq ["x-custom", "random"]
|
163
|
-
@cc.refset.should be_empty
|
164
168
|
@cc.table.should eq original_table
|
165
169
|
end
|
166
170
|
end
|
171
|
+
end
|
167
172
|
|
168
|
-
|
169
|
-
|
170
|
-
|
173
|
+
context "incremental indexing" do
|
174
|
+
it "should process indexed header with literal value" do
|
175
|
+
original_table = @cc.table.dup
|
171
176
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
end
|
177
|
+
emit = @cc.process({name: 4, value: "/path", type: :incremental})
|
178
|
+
emit.should eq [":path", "/path"]
|
179
|
+
(@cc.table - original_table).should eq [[":path", "/path"]]
|
176
180
|
end
|
177
181
|
|
178
|
-
|
179
|
-
|
180
|
-
original_table = @cc.table.dup
|
181
|
-
idx = original_table.size-1
|
182
|
-
|
183
|
-
@cc.process({
|
184
|
-
name: "x-custom", value: "random",
|
185
|
-
index: idx, type: :substitution
|
186
|
-
})
|
182
|
+
it "should process literal header with literal value" do
|
183
|
+
original_table = @cc.table.dup
|
187
184
|
|
188
|
-
|
189
|
-
|
190
|
-
(original_table - @cc.table).should eq [["via", ""]]
|
191
|
-
end
|
192
|
-
|
193
|
-
it "should raise error on invalid substitution index" do
|
194
|
-
lambda {
|
195
|
-
@cc.process({
|
196
|
-
name: "x-custom", value: "random",
|
197
|
-
index: 1000, type: :substitution
|
198
|
-
})
|
199
|
-
}.should raise_error(HeaderException)
|
200
|
-
end
|
185
|
+
@cc.process({name: "x-custom", value: "random", type: :incremental})
|
186
|
+
(@cc.table - original_table).should eq [["x-custom", "random"]]
|
201
187
|
end
|
188
|
+
end
|
202
189
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
cc.process({
|
211
|
-
name: "x-custom",
|
212
|
-
value: "a" * (2048 - original_size),
|
213
|
-
type: :incremental
|
214
|
-
})
|
215
|
-
|
216
|
-
cc.table.last[0].should eq "x-custom"
|
217
|
-
cc.table.size.should eq original_table.size
|
218
|
-
end
|
219
|
-
|
220
|
-
it "should prepend on dropped substitution index" do
|
221
|
-
cc = EncodingContext.new(:request, 2048)
|
222
|
-
original_table = cc.table.dup
|
223
|
-
original_size = original_table.join.bytesize +
|
224
|
-
original_table.size * 32
|
225
|
-
|
226
|
-
cc.process({
|
227
|
-
name: "x-custom",
|
228
|
-
value: "a" * (2048 - original_size),
|
229
|
-
index: 0, type: :substitution
|
230
|
-
})
|
231
|
-
|
232
|
-
cc.table[0][0].should eq "x-custom"
|
233
|
-
cc.table[1][0].should eq ":scheme"
|
190
|
+
context "size bounds" do
|
191
|
+
it "should drop headers from end of table" do
|
192
|
+
cc = EncodingContext.new(table_size: 2048)
|
193
|
+
cc.instance_eval do
|
194
|
+
add_to_table(["test1", "1" * 1024])
|
195
|
+
add_to_table(["test2", "2" * 500])
|
234
196
|
end
|
235
|
-
end
|
236
197
|
|
237
|
-
|
238
|
-
|
198
|
+
original_table = cc.table.dup
|
199
|
+
original_size = original_table.join.bytesize +
|
200
|
+
original_table.size * 32
|
239
201
|
|
240
|
-
|
241
|
-
|
202
|
+
cc.process({
|
203
|
+
name: "x-custom",
|
204
|
+
value: "a" * (2048 - original_size),
|
205
|
+
type: :incremental
|
206
|
+
})
|
242
207
|
|
243
|
-
cc.
|
244
|
-
cc.
|
245
|
-
cc.table.should be_empty
|
246
|
-
|
247
|
-
cc.process(h)
|
248
|
-
cc.process(e.merge({type: :incremental}))
|
249
|
-
cc.table.should be_empty
|
208
|
+
cc.table.first[0].should eq "x-custom"
|
209
|
+
cc.table.size.should eq original_table.size # number of entries
|
250
210
|
end
|
251
211
|
end
|
252
|
-
end
|
253
212
|
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
{name: 11, value: "my-user-agent"},
|
261
|
-
{name: "mynewheader", value: "first"}
|
262
|
-
]
|
213
|
+
it "should clear table if entry exceeds table size" do
|
214
|
+
cc = EncodingContext.new(table_size: 2048)
|
215
|
+
cc.instance_eval do
|
216
|
+
add_to_table(["test1", "1" * 1024])
|
217
|
+
add_to_table(["test2", "2" * 500])
|
218
|
+
end
|
263
219
|
|
264
|
-
|
220
|
+
h = { name: "x-custom", value: "a", index: 0, type: :incremental }
|
221
|
+
e = { name: "large", value: "a" * 2048, index: 0}
|
265
222
|
|
266
|
-
|
267
|
-
|
268
|
-
|
223
|
+
cc.process(h)
|
224
|
+
cc.process(e.merge({type: :incremental}))
|
225
|
+
cc.table.should be_empty
|
269
226
|
end
|
270
227
|
|
271
|
-
it "should
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
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"]
|
228
|
+
it "should shrink table if set smaller size" do
|
229
|
+
cc = EncodingContext.new(table_size: 2048)
|
230
|
+
cc.instance_eval do
|
231
|
+
add_to_table(["test1", "1" * 1024])
|
232
|
+
add_to_table(["test2", "2" * 500])
|
233
|
+
end
|
234
|
+
|
235
|
+
cc.process({type: :changetablesize, value: 1500})
|
236
|
+
cc.table.size.should be 1
|
237
|
+
cc.table.first[0].should eq 'test2'
|
284
238
|
end
|
285
239
|
end
|
286
240
|
end
|
287
241
|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
242
|
+
spec_examples = [
|
243
|
+
{ title: "D.3. Request Examples without Huffman",
|
244
|
+
type: :request,
|
245
|
+
table_size: 4096,
|
246
|
+
huffman: :never,
|
247
|
+
streams: [
|
248
|
+
{ wire: "8286 8441 0f77 7777 2e65 7861 6d70 6c65
|
249
|
+
2e63 6f6d",
|
250
|
+
emitted: [
|
251
|
+
[":method", "GET"],
|
252
|
+
[":scheme", "http"],
|
253
|
+
[":path", "/"],
|
254
|
+
[":authority", "www.example.com"],
|
255
|
+
],
|
256
|
+
table: [
|
257
|
+
[":authority", "www.example.com"],
|
258
|
+
],
|
259
|
+
table_size: 57,
|
260
|
+
},
|
261
|
+
{ wire: "8286 84be 5808 6e6f 2d63 6163 6865",
|
262
|
+
emitted: [
|
263
|
+
[":method", "GET"],
|
264
|
+
[":scheme", "http"],
|
265
|
+
[":path", "/"],
|
266
|
+
[":authority", "www.example.com"],
|
267
|
+
["cache-control", "no-cache"],
|
268
|
+
],
|
269
|
+
table: [
|
270
|
+
["cache-control", "no-cache"],
|
271
|
+
[":authority", "www.example.com"],
|
272
|
+
],
|
273
|
+
table_size: 110,
|
274
|
+
},
|
275
|
+
{ wire: "8287 85bf 400a 6375 7374 6f6d 2d6b 6579
|
276
|
+
0c63 7573 746f 6d2d 7661 6c75 65",
|
277
|
+
emitted: [
|
278
|
+
[":method", "GET"],
|
279
|
+
[":scheme", "https"],
|
280
|
+
[":path", "/index.html"],
|
281
|
+
[":authority", "www.example.com"],
|
282
|
+
["custom-key", "custom-value"],
|
283
|
+
],
|
284
|
+
table: [
|
285
|
+
["custom-key", "custom-value"],
|
286
|
+
["cache-control", "no-cache"],
|
287
|
+
[":authority", "www.example.com"],
|
288
|
+
],
|
289
|
+
table_size: 164,
|
290
|
+
}
|
291
|
+
],
|
292
|
+
},
|
293
|
+
{ title: "D.4. Request Examples with Huffman",
|
294
|
+
type: :request,
|
295
|
+
table_size: 4096,
|
296
|
+
huffman: :always,
|
297
|
+
streams: [
|
298
|
+
{ wire: "8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4 ff",
|
299
|
+
emitted: [
|
300
|
+
[":method", "GET"],
|
301
|
+
[":scheme", "http"],
|
302
|
+
[":path", "/"],
|
303
|
+
[":authority", "www.example.com"],
|
304
|
+
],
|
305
|
+
table: [
|
306
|
+
[":authority", "www.example.com"],
|
307
|
+
],
|
308
|
+
table_size: 57,
|
309
|
+
},
|
310
|
+
{ wire: "8286 84be 5886 a8eb 1064 9cbf",
|
311
|
+
emitted: [
|
312
|
+
[":method", "GET"],
|
313
|
+
[":scheme", "http"],
|
314
|
+
[":path", "/"],
|
315
|
+
[":authority", "www.example.com"],
|
316
|
+
["cache-control", "no-cache"],
|
317
|
+
],
|
318
|
+
table: [
|
319
|
+
["cache-control", "no-cache"],
|
320
|
+
[":authority", "www.example.com"],
|
321
|
+
],
|
322
|
+
table_size: 110,
|
323
|
+
},
|
324
|
+
{ wire: "8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925
|
325
|
+
a849 e95b b8e8 b4bf",
|
326
|
+
emitted: [
|
327
|
+
[":method", "GET"],
|
328
|
+
[":scheme", "https"],
|
329
|
+
[":path", "/index.html"],
|
330
|
+
[":authority", "www.example.com"],
|
331
|
+
["custom-key", "custom-value"],
|
332
|
+
],
|
333
|
+
table: [
|
334
|
+
["custom-key", "custom-value"],
|
335
|
+
["cache-control", "no-cache"],
|
336
|
+
[":authority", "www.example.com"],
|
337
|
+
],
|
338
|
+
table_size: 164,
|
339
|
+
},
|
340
|
+
],
|
341
|
+
},
|
342
|
+
{ title: "D.5. Response Examples without Huffman",
|
343
|
+
type: :response,
|
344
|
+
table_size: 256,
|
345
|
+
huffman: :never,
|
346
|
+
streams: [
|
347
|
+
{ wire: "4803 3330 3258 0770 7269 7661 7465 611d
|
348
|
+
4d6f 6e2c 2032 3120 4f63 7420 3230 3133
|
349
|
+
2032 303a 3133 3a32 3120 474d 546e 1768
|
350
|
+
7474 7073 3a2f 2f77 7777 2e65 7861 6d70
|
351
|
+
6c65 2e63 6f6d",
|
352
|
+
emitted: [
|
353
|
+
[":status", "302"],
|
354
|
+
["cache-control", "private"],
|
355
|
+
["date", "Mon, 21 Oct 2013 20:13:21 GMT"],
|
356
|
+
["location", "https://www.example.com"],
|
357
|
+
],
|
358
|
+
table: [
|
359
|
+
["location", "https://www.example.com"],
|
360
|
+
["date", "Mon, 21 Oct 2013 20:13:21 GMT"],
|
361
|
+
["cache-control", "private"],
|
362
|
+
[":status", "302"],
|
363
|
+
],
|
364
|
+
table_size: 222,
|
365
|
+
},
|
366
|
+
{ wire: "4803 3330 37c1 c0bf",
|
367
|
+
emitted: [
|
368
|
+
[":status", "307"],
|
369
|
+
["cache-control", "private"],
|
370
|
+
["date", "Mon, 21 Oct 2013 20:13:21 GMT"],
|
371
|
+
["location", "https://www.example.com"],
|
372
|
+
],
|
373
|
+
table: [
|
374
|
+
[":status", "307"],
|
375
|
+
["location", "https://www.example.com"],
|
376
|
+
["date", "Mon, 21 Oct 2013 20:13:21 GMT"],
|
377
|
+
["cache-control", "private"],
|
378
|
+
],
|
379
|
+
table_size: 222,
|
380
|
+
},
|
381
|
+
{ wire: "88c1 611d 4d6f 6e2c 2032 3120 4f63 7420
|
382
|
+
3230 3133 2032 303a 3133 3a32 3220 474d
|
383
|
+
54c0 5a04 677a 6970 7738 666f 6f3d 4153
|
384
|
+
444a 4b48 514b 425a 584f 5157 454f 5049
|
385
|
+
5541 5851 5745 4f49 553b 206d 6178 2d61
|
386
|
+
6765 3d33 3630 303b 2076 6572 7369 6f6e
|
387
|
+
3d31",
|
388
|
+
emitted: [
|
389
|
+
[":status", "200"],
|
390
|
+
["cache-control", "private"],
|
391
|
+
["date", "Mon, 21 Oct 2013 20:13:22 GMT"],
|
392
|
+
["location", "https://www.example.com"],
|
393
|
+
["content-encoding", "gzip"],
|
394
|
+
["set-cookie", "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"],
|
395
|
+
],
|
396
|
+
table: [
|
397
|
+
["set-cookie", "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"],
|
398
|
+
["content-encoding", "gzip"],
|
399
|
+
["date", "Mon, 21 Oct 2013 20:13:22 GMT"],
|
400
|
+
],
|
401
|
+
table_size: 215,
|
402
|
+
},
|
403
|
+
],
|
404
|
+
},
|
405
|
+
{ title: "D.6. Response Examples with Huffman",
|
406
|
+
type: :response,
|
407
|
+
table_size: 256,
|
408
|
+
huffman: :always,
|
409
|
+
streams: [
|
410
|
+
{ wire: "4882 6402 5885 aec3 771a 4b61 96d0 7abe
|
411
|
+
9410 54d4 44a8 2005 9504 0b81 66e0 82a6
|
412
|
+
2d1b ff6e 919d 29ad 1718 63c7 8f0b 97c8
|
413
|
+
e9ae 82ae 43d3",
|
414
|
+
emitted: [
|
415
|
+
[":status", "302"],
|
416
|
+
["cache-control", "private"],
|
417
|
+
["date", "Mon, 21 Oct 2013 20:13:21 GMT"],
|
418
|
+
["location", "https://www.example.com"],
|
419
|
+
],
|
420
|
+
table: [
|
421
|
+
["location", "https://www.example.com"],
|
422
|
+
["date", "Mon, 21 Oct 2013 20:13:21 GMT"],
|
423
|
+
["cache-control", "private"],
|
424
|
+
[":status", "302"],
|
425
|
+
],
|
426
|
+
table_size: 222,
|
427
|
+
},
|
428
|
+
{ wire: "4883 640e ffc1 c0bf",
|
429
|
+
emitted: [
|
430
|
+
[":status", "307"],
|
431
|
+
["cache-control", "private"],
|
432
|
+
["date", "Mon, 21 Oct 2013 20:13:21 GMT"],
|
433
|
+
["location", "https://www.example.com"],
|
434
|
+
],
|
435
|
+
table: [
|
436
|
+
[":status", "307"],
|
437
|
+
["location", "https://www.example.com"],
|
438
|
+
["date", "Mon, 21 Oct 2013 20:13:21 GMT"],
|
439
|
+
["cache-control", "private"],
|
440
|
+
],
|
441
|
+
table_size: 222,
|
442
|
+
},
|
443
|
+
{ wire: "88c1 6196 d07a be94 1054 d444 a820 0595
|
444
|
+
040b 8166 e084 a62d 1bff c05a 839b d9ab
|
445
|
+
77ad 94e7 821d d7f2 e6c7 b335 dfdf cd5b
|
446
|
+
3960 d5af 2708 7f36 72c1 ab27 0fb5 291f
|
447
|
+
9587 3160 65c0 03ed 4ee5 b106 3d50 07",
|
448
|
+
emitted: [
|
449
|
+
[":status", "200"],
|
450
|
+
["cache-control", "private"],
|
451
|
+
["date", "Mon, 21 Oct 2013 20:13:22 GMT"],
|
452
|
+
["location", "https://www.example.com"],
|
453
|
+
["content-encoding", "gzip"],
|
454
|
+
["set-cookie", "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"],
|
455
|
+
],
|
456
|
+
table: [
|
457
|
+
["set-cookie", "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"],
|
458
|
+
["content-encoding", "gzip"],
|
459
|
+
["date", "Mon, 21 Oct 2013 20:13:22 GMT"],
|
460
|
+
],
|
461
|
+
table_size: 215,
|
462
|
+
},
|
463
|
+
],
|
464
|
+
},
|
465
|
+
]
|
466
|
+
|
467
|
+
context "decode" do
|
468
|
+
spec_examples.each do |ex|
|
469
|
+
context "spec example #{ex[:title]}" do
|
470
|
+
ex[:streams].size.times do |nth|
|
471
|
+
context "request #{nth+1}" do
|
472
|
+
before { @dc = Decompressor.new(table_size: ex[:table_size]) }
|
473
|
+
before do
|
474
|
+
(0...nth).each do |i|
|
475
|
+
bytes = [ex[:streams][i][:wire].delete(" \n")].pack("H*")
|
476
|
+
@dc.decode(HTTP2::Buffer.new(bytes))
|
477
|
+
end
|
478
|
+
end
|
479
|
+
subject do
|
480
|
+
bytes = [ex[:streams][nth][:wire].delete(" \n")].pack("H*")
|
481
|
+
@emitted = @dc.decode(HTTP2::Buffer.new(bytes))
|
482
|
+
end
|
483
|
+
it "should emit expected headers" do
|
484
|
+
subject
|
485
|
+
# order-perserving compare
|
486
|
+
@emitted.should eq ex[:streams][nth][:emitted]
|
487
|
+
end
|
488
|
+
it "should update header table" do
|
489
|
+
subject
|
490
|
+
@dc.instance_eval{@cc.table}.should eq ex[:streams][nth][:table]
|
491
|
+
end
|
492
|
+
it "should compute header table size" do
|
493
|
+
subject
|
494
|
+
@dc.instance_eval{@cc.current_table_size}.should eq ex[:streams][nth][:table_size]
|
495
|
+
end
|
496
|
+
end
|
497
|
+
end
|
498
|
+
end
|
382
499
|
end
|
500
|
+
end
|
383
501
|
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
502
|
+
context "encode" do
|
503
|
+
spec_examples.each do |ex|
|
504
|
+
context "spec example #{ex[:title]}" do
|
505
|
+
ex[:streams].size.times do |nth|
|
506
|
+
context "request #{nth+1}" do
|
507
|
+
before { @cc = Compressor.new(table_size: ex[:table_size],
|
508
|
+
huffman: ex[:huffman]) }
|
509
|
+
before do
|
510
|
+
(0...nth).each do |i|
|
511
|
+
@cc.encode(ex[:streams][i][:emitted])
|
512
|
+
end
|
513
|
+
end
|
514
|
+
subject do
|
515
|
+
@cc.encode(ex[:streams][nth][:emitted])
|
516
|
+
end
|
517
|
+
it "should emit expected bytes on wire" do
|
518
|
+
subject.unpack("H*").first.should eq ex[:streams][nth][:wire].delete(" \n")
|
519
|
+
end
|
520
|
+
it "should update header table" do
|
521
|
+
subject
|
522
|
+
@cc.instance_eval{@cc.table}.should eq ex[:streams][nth][:table]
|
523
|
+
end
|
524
|
+
it "should compute header table size" do
|
525
|
+
subject
|
526
|
+
@cc.instance_eval{@cc.current_table_size}.should eq ex[:streams][nth][:table_size]
|
527
|
+
end
|
528
|
+
end
|
529
|
+
end
|
530
|
+
end
|
391
531
|
end
|
392
532
|
end
|
533
|
+
|
393
534
|
end
|