http-2 0.6.3 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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(*args)
25
+ def initialize(**settings)
26
26
  @stream_id = 2
27
- @state = :new
28
- @compressor = Header::Compressor.new(:response)
29
- @decompressor = Header::Decompressor.new(:request)
27
+ @state = :waiting_magic
28
+
29
+ @local_role = :server
30
+ @remote_role = :client
30
31
 
31
32
  super
32
33
  end
@@ -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 :priority
52
+ attr_reader :weight
53
+ attr_reader :dependency
53
54
 
54
55
  # Size of current stream flow control window.
55
- attr_reader :window
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 priority [Integer]
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, priority, window, parent = nil)
71
- @id = id
72
- @priority = priority
73
- @window = window
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| @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
- if frame[:payload].is_a? Array
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
- @priority = frame[:priority]
100
- emit(:priority, @priority)
105
+ process_priority(frame)
101
106
  when :window_update
102
- @window += frame[:increment]
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
- @priority = frame[:priority] if frame[:type] == :priority
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, end_push_promise: true, &block)
150
+ def promise(headers, end_headers: true, &block)
144
151
  raise Exception.new("must provide callback") if !block_given?
145
152
 
146
- flags = end_push_promise ? [:end_push_promise] : []
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 p [Integer] new stream priority value
154
- def reprioritize(p)
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, priority: p})
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
- while payload.bytesize > MAX_FRAME_SIZE do
168
- chunk = payload.slice!(0, MAX_FRAME_SIZE)
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
- if frame[:type] == :rst_stream
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 :window_update, :priority
358
- frame[:igore] = true
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
- case @closed
415
- when :remote_rst, :remote_closed
416
- stream_error(:stream_closed) if !(frame[:type] == :rst_stream)
417
- when :local_rst, :local_closed
418
- frame[:ignore] = true
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
@@ -1,3 +1,3 @@
1
1
  module HTTP2
2
- VERSION = "0.6.3"
2
+ VERSION = "0.7.0"
3
3
  end
@@ -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
@@ -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 CONNECTION_HEADER
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(streams: 200)
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].should eq 200
33
+ frame[:payload].should include([:settings_max_concurrent_streams, 200])
34
34
  end
35
35
  end
36
36
 
@@ -2,8 +2,8 @@ require "helper"
2
2
 
3
3
  describe HTTP2::Header do
4
4
 
5
- let(:c) { Compressor.new :request }
6
- let(:d) { Decompressor.new :response }
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
- it "should handle ascii codepoints" do
37
- ascii = "abcdefghij"
38
- str = c.string(ascii)
39
-
40
- buf = Buffer.new(str+"trailer")
41
- d.string(buf).should eq ascii
42
- end
43
-
44
- it "should handle utf-8 codepoints" do
45
- utf8 = "éáűőúöüó€"
46
- str = c.string(utf8)
47
-
48
- buf = Buffer.new(str+"trailer")
49
- d.string(buf).should eq utf8
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
- it "should handle long utf-8 strings" do
53
- utf8 = "éáűőúöüó€"*100
54
- str = c.string(utf8)
55
-
56
- buf = Buffer.new(str+"trailer")
57
- d.string(buf).should eq utf8
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
- d.header(c.header(h)).should eq h
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
- d.header(c.header(h)).should eq h
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
- d.header(c.header(h)).should eq h
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
- d.header(c.header(h)).should eq h
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
- d.header(c.header(h)).should eq h
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 w/ substitution indexing" do
124
+ context "literal never indexed" do
93
125
  it "should handle indexed header" do
94
- h = {name: 1, value: "my-value", index: 10, type: :substitution}
95
- d.header(c.header(h)).should eq h
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-new", value: "my-value", index: 10, type: :substitution}
100
- d.header(c.header(h)).should eq h
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 "differential coding" do
106
- context "shared compression context" do
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
- @cc.process({name: 0, type: :indexed})
128
- @cc.refset.should be_empty
129
- end
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
- context "no indexing" do
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: 3, value: "/path", type: :noindex})
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: :noindex})
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
- context "incremental indexing" do
169
- it "should process literal header with literal value" do
170
- original_table = @cc.table.dup
173
+ context "incremental indexing" do
174
+ it "should process indexed header with literal value" do
175
+ original_table = @cc.table.dup
171
176
 
172
- @cc.process({name: "x-custom", value: "random", type: :incremental})
173
- @cc.refset.first.should eq [original_table.size, ["x-custom", "random"]]
174
- (@cc.table - original_table).should eq [["x-custom", "random"]]
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
- context "substitution indexing" do
179
- it "should process literal header with literal value" do
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
- @cc.refset.first.should eq [idx, ["x-custom", "random"]]
189
- (@cc.table - original_table).should eq [["x-custom", "random"]]
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
- context "size bounds" do
204
- it "should drop headers from beginning of table" do
205
- cc = EncodingContext.new(:request, 2048)
206
- original_table = cc.table.dup
207
- original_size = original_table.join.bytesize +
208
- original_table.size * 32
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
- it "should clear table if entry exceeds table size" do
238
- cc = EncodingContext.new(:request, 2048)
198
+ original_table = cc.table.dup
199
+ original_size = original_table.join.bytesize +
200
+ original_table.size * 32
239
201
 
240
- h = { name: "x-custom", value: "a", index: 0, type: :incremental }
241
- e = { name: "large", value: "a" * 2048, index: 0}
202
+ cc.process({
203
+ name: "x-custom",
204
+ value: "a" * (2048 - original_size),
205
+ type: :incremental
206
+ })
242
207
 
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
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
- context "integration" do
255
- before (:all) { @cc = EncodingContext.new(:request) }
256
-
257
- it "should match first header set in spec appendix" do
258
- req_headers = [
259
- {name: 3, value: "/my-example/index.html"},
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
- req_headers.each {|h| @cc.process(h.merge({type: :incremental})) }
220
+ h = { name: "x-custom", value: "a", index: 0, type: :incremental }
221
+ e = { name: "large", value: "a" * 2048, index: 0}
265
222
 
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
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 match second header set in spec appendix" do
272
- @cc.process({name: 30, type: :indexed})
273
- @cc.process({name: 31, type: :indexed})
274
- @cc.process({
275
- name: 3, value: "/my-example/resources/script.js",
276
- index: 30, type: :substitution
277
- })
278
- @cc.process({name: 32, value: "second", type: :incremental})
279
-
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"]
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
- context "encode and decode" do
289
- # http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-01#appendix-B
290
-
291
- before (:all) do
292
- @cc = Compressor.new(:request)
293
- @dc = Decompressor.new(:request)
294
- end
295
-
296
- E1_BYTES = [
297
- 0x44, # (literal header with incremental indexing, name index = 3)
298
- 0x16, # (header value string length = 22)
299
- "/my-example/index.html".bytes,
300
- 0x4C, # (literal header with incremental indexing, name index = 11)
301
- 0x0D, # (header value string length = 13)
302
- "my-user-agent".bytes,
303
- 0x40, # (literal header with incremental indexing, new name)
304
- 0x0B, # (header name string length = 11)
305
- "mynewheader".bytes,
306
- 0x05, # (header value string length = 5)
307
- "first".bytes
308
- ].flatten
309
-
310
- E1_HEADERS = [
311
- [":path", "/my-example/index.html"],
312
- ["user-agent", "my-user-agent"],
313
- ["mynewheader", "first"]
314
- ]
315
-
316
- it "should match first header set in spec appendix" do
317
- @cc.encode(E1_HEADERS).bytes.should eq E1_BYTES
318
- end
319
-
320
- it "should decode first header set in spec appendix" do
321
- @dc.decode(Buffer.new(E1_BYTES.pack("C*"))).should eq E1_HEADERS
322
- end
323
-
324
- E2_BYTES = [
325
- 0x9e, # (indexed header, index = 30: removal from reference set)
326
- 0xa0, # (indexed header, index = 32: removal from reference set)
327
- 0x04, # (literal header, substitution indexing, name index = 3)
328
- 0x1e, # (replaced entry index = 30)
329
- 0x1f, # (header value string length = 31)
330
- "/my-example/resources/script.js".bytes,
331
- 0x5f,
332
- 0x02, # (literal header, incremental indexing, name index = 32)
333
- 0x06, # (header value string length = 6)
334
- "second".bytes
335
- ].flatten
336
-
337
- E2_HEADERS = [
338
- [":path", "/my-example/resources/script.js"],
339
- ["user-agent", "my-user-agent"],
340
- ["mynewheader", "second"]
341
- ]
342
-
343
- it "should match second header set in spec appendix" do
344
- # Force incremental indexing, the spec doesn't specify any strategy
345
- # for deciding when to use incremental vs substitution indexing, and
346
- # early implementations defer to incremental by default:
347
- # - https://github.com/sludin/http2-perl/blob/master/lib/HTTP2/Draft/Compress.pm#L157
348
- # - https://github.com/MSOpenTech/http2-katana/blob/master/Shared/SharedProtocol/Compression/HeadersDeltaCompression/CompressionProcessor.cs#L259
349
- # - https://hg.mozilla.org/try/file/9d9a29992e4d/netwerk/protocol/http/Http2CompressionDraft00.cpp#l636
350
- #
351
- e2bytes = E2_BYTES.dup
352
- e2bytes[2] = 0x44 # incremental indexing, name index = 3
353
- e2bytes.delete_at(3) # remove replacement index byte
354
-
355
- @cc.encode(E2_HEADERS).bytes.should eq e2bytes
356
- end
357
-
358
- it "should decode second header set in spec appendix" do
359
- @dc.decode(Buffer.new(E2_BYTES.pack("C*"))).should match_array E2_HEADERS
360
- end
361
-
362
- it "encode-decode should be invariant" do
363
- cc = Compressor.new(:request)
364
- dc = Decompressor.new(:request)
365
-
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
- end
369
-
370
- it "should encode-decode request set of headers" do
371
- cc = Compressor.new(:request)
372
- dc = Decompressor.new(:request)
373
-
374
- req = [
375
- [":method", "get"],
376
- [":host", "localhost"],
377
- [":path", "/resource"],
378
- ["accept", "*/*"]
379
- ]
380
-
381
- dc.decode(cc.encode(req)).should eq req
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
- 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"]]
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