http-2 0.6.1 → 0.6.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -19,6 +19,4 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_development_dependency "bundler", "~> 1.3"
22
- spec.add_development_dependency "rake"
23
- spec.add_development_dependency "rspec"
24
22
  end
@@ -6,4 +6,6 @@ require "http/2/flow_buffer"
6
6
  require "http/2/compressor"
7
7
  require "http/2/framer"
8
8
  require "http/2/connection"
9
+ require "http/2/client"
10
+ require "http/2/server"
9
11
  require "http/2/stream"
@@ -4,17 +4,34 @@ module HTTP2
4
4
  #
5
5
  class Buffer < String
6
6
 
7
+ UINT32 = "N"
8
+ BINARY = "binary"
9
+ private_constant :UINT32, :BINARY
10
+
7
11
  # Forces binary encoding on the string
8
- def initialize(*args)
9
- force_encoding('binary')
10
- super(*args)
12
+ def initialize(data = '')
13
+ super(data.force_encoding(BINARY))
11
14
  end
12
15
 
13
16
  # Emulate StringIO#read: slice first n bytes from the buffer.
14
17
  #
15
18
  # @param n [Integer] number of bytes to slice from the buffer
16
19
  def read(n)
17
- slice!(0,n)
20
+ Buffer.new(slice!(0,n))
21
+ end
22
+
23
+ # Alias getbyte to readbyte
24
+ alias :readbyte :getbyte
25
+
26
+ # Emulate StringIO#getbyte: slice first byte from buffer.
27
+ def getbyte
28
+ read(1).ord
29
+ end
30
+
31
+ # Slice unsigned 32-bit integer from buffer.
32
+ # @return [Integer]
33
+ def read_uint32
34
+ read(4).unpack(UINT32).first
18
35
  end
19
36
  end
20
37
 
@@ -0,0 +1,50 @@
1
+ module HTTP2
2
+
3
+ # HTTP 2.0 client connection class that implements appropriate header
4
+ # compression / decompression algorithms and stream management logic.
5
+ #
6
+ # Your code is responsible for driving the client object, which in turn
7
+ # performs all of the necessary HTTP 2.0 encoding / decoding, state
8
+ # management, and the rest. A simple example:
9
+ #
10
+ # @example
11
+ # socket = YourTransport.new
12
+ #
13
+ # conn = HTTP2::Client.new
14
+ # conn.on(:frame) {|bytes| socket << bytes }
15
+ #
16
+ # while bytes = socket.read
17
+ # conn << bytes
18
+ # end
19
+ #
20
+ class Client < Connection
21
+
22
+ # Initialize new HTTP 2.0 client object.
23
+ def initialize(*args)
24
+ @stream_id = 1
25
+ @state = :connection_header
26
+ @compressor = Header::Compressor.new(:request)
27
+ @decompressor = Header::Decompressor.new(:response)
28
+
29
+ super
30
+ end
31
+
32
+ # Send an outgoing frame. Connection and stream flow control is managed
33
+ # by Connection class.
34
+ #
35
+ # @see Connection
36
+ # @note Client will emit the connection header as the first 24 bytes
37
+ # @param frame [Hash]
38
+ def send(frame)
39
+ if @state == :connection_header
40
+ emit(:frame, CONNECTION_HEADER)
41
+ @state = :connected
42
+
43
+ settings(stream_limit: @stream_limit, window_limit: @window_limit)
44
+ end
45
+
46
+ super(frame)
47
+ end
48
+ end
49
+
50
+ end
@@ -1,5 +1,3 @@
1
- require "stringio"
2
-
3
1
  module HTTP2
4
2
 
5
3
  # Implementation of header compression for HTTP 2.0 (HPACK) format adapted
@@ -12,97 +10,84 @@ module HTTP2
12
10
  # encoding context: an encoding context contains a header table and a
13
11
  # reference set - there is one encoding context for each direction.
14
12
  #
15
- class CompressionContext
13
+ class EncodingContext
16
14
  include Error
17
15
 
18
16
  # TODO: replace StringIO with Buffer...
19
17
 
20
18
  # Default request working set as defined by the spec.
21
19
  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
- ];
20
+ [':scheme' , 'http' ],
21
+ [':scheme' , 'https'],
22
+ [':host' , '' ],
23
+ [':path' , '/' ],
24
+ [':method' , 'get' ],
25
+ ['accept' , '' ],
26
+ ['accept-charset' , '' ],
27
+ ['accept-encoding' , '' ],
28
+ ['accept-language' , '' ],
29
+ ['cookie' , '' ],
30
+ ['if-modified-since' , '' ],
31
+ ['user-agent' , '' ],
32
+ ['referer' , '' ],
33
+ ['authorization' , '' ],
34
+ ['allow' , '' ],
35
+ ['cache-control' , '' ],
36
+ ['connection' , '' ],
37
+ ['content-length' , '' ],
38
+ ['content-type' , '' ],
39
+ ['date' , '' ],
40
+ ['expect' , '' ],
41
+ ['from' , '' ],
42
+ ['if-match' , '' ],
43
+ ['if-none-match' , '' ],
44
+ ['if-range' , '' ],
45
+ ['if-unmodified-since', '' ],
46
+ ['max-forwards' , '' ],
47
+ ['proxy-authorization', '' ],
48
+ ['range' , '' ],
49
+ ['via' , '' ]
50
+ ]
61
51
 
62
52
  # Default response working set as defined by the spec.
63
53
  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
- ];
54
+ [':status' , '200'],
55
+ ['age' , '' ],
56
+ ['cache-control' , '' ],
57
+ ['content-length' , '' ],
58
+ ['content-type' , '' ],
59
+ ['date' , '' ],
60
+ ['etag' , '' ],
61
+ ['expires' , '' ],
62
+ ['last-modified' , '' ],
63
+ ['server' , '' ],
64
+ ['set-cookie' , '' ],
65
+ ['vary' , '' ],
66
+ ['via' , '' ],
67
+ ['access-control-allow-origin' , '' ],
68
+ ['accept-ranges' , '' ],
69
+ ['allow' , '' ],
70
+ ['connection' , '' ],
71
+ ['content-disposition' , '' ],
72
+ ['content-encoding' , '' ],
73
+ ['content-language' , '' ],
74
+ ['content-location' , '' ],
75
+ ['content-range' , '' ],
76
+ ['link' , '' ],
77
+ ['location' , '' ],
78
+ ['proxy-authenticate' , '' ],
79
+ ['refresh' , '' ],
80
+ ['retry-after' , '' ],
81
+ ['strict-transport-security' , '' ],
82
+ ['transfer-encoding' , '' ],
83
+ ['www-authenticate' , '' ]
84
+ ]
100
85
 
101
86
  # Current table of header key-value pairs.
102
87
  attr_reader :table
103
88
 
104
- # Current working set of header key-value pairs.
105
- attr_reader :workset
89
+ # Current reference set of header key-value pairs.
90
+ attr_reader :refset
106
91
 
107
92
  # Initializes compression context with appropriate client/server
108
93
  # defaults and maximum size of the header table.
@@ -113,37 +98,50 @@ module HTTP2
113
98
  @type = type
114
99
  @table = (type == :request) ? REQ_DEFAULTS.dup : RESP_DEFAULTS.dup
115
100
  @limit = limit
116
- @workset = []
101
+ @refset = []
117
102
  end
118
103
 
119
104
  # Performs differential coding based on provided command type.
120
- # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-01#section-3.1
105
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#section-3.2
121
106
  #
122
107
  # @param cmd [Hash]
108
+ # @return [Hash] emitted header
123
109
  def process(cmd)
110
+ emit = nil
111
+
124
112
  # indexed representation
125
113
  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]}
114
+ # An indexed representation corresponding to an entry not present
115
+ # in the reference set entails the following actions:
116
+ # - The header corresponding to the entry is emitted.
117
+ # - The entry is added to the reference set.
118
+ #
119
+ # An indexed representation corresponding to an entry present in
120
+ # the reference set entails the following actions:
121
+ # - The entry is removed from the reference set.
122
+ #
123
+ idx = cmd[:name]
124
+ cur = @refset.find_index {|(i,v)| i == idx}
134
125
 
135
126
  if cur
136
- @workset.delete_at(cur)
127
+ @refset.delete_at(cur)
137
128
  else
138
- @workset.push [cmd[:name], @table[cmd[:name]]]
129
+ emit = @table[idx]
130
+ @refset.push [idx, @table[idx]]
139
131
  end
140
132
 
141
133
  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.
134
+ # A literal representation that is not added to the header table
135
+ # entails the following action:
136
+ # - The header is emitted.
137
+ #
138
+ # A literal representation that is added to the header table entails
139
+ # the following actions:
140
+ # - The header is emitted.
141
+ # - The header is added to the header table, at the location
142
+ # defined by the representation.
143
+ # - The new entry is added to the reference set.
144
+ #
147
145
  if cmd[:name].is_a? Integer
148
146
  k,v = @table[cmd[:name]]
149
147
 
@@ -152,42 +150,29 @@ module HTTP2
152
150
  cmd[:name] = k
153
151
  end
154
152
 
155
- newval = [cmd[:name], cmd[:value]]
153
+ emit = [cmd[:name], cmd[:value]]
156
154
 
157
155
  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")
156
+ if size_check(cmd)
157
+
158
+ case cmd[:type]
159
+ when :incremental
160
+ cmd[:index] = @table.size
161
+ when :substitution
162
+ if @table[cmd[:index]].nil?
163
+ raise HeaderException.new("invalid index")
164
+ end
165
+ when :prepend
166
+ @table = [emit] + @table
166
167
  end
167
- when :prepend
168
- @table = [newval] + @table
169
- end
170
168
 
171
- @table[cmd[:index]] = newval
169
+ @table[cmd[:index]] = emit
170
+ @refset.push [cmd[:index], emit]
171
+ end
172
172
  end
173
-
174
- @workset.push [cmd[:index], newval]
175
173
  end
176
- end
177
174
 
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]}
175
+ emit
191
176
  end
192
177
 
193
178
  # Emits best available command to encode provided header.
@@ -230,40 +215,55 @@ module HTTP2
230
215
 
231
216
  private
232
217
 
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.
218
+ # Before doing such a modification, it has to be ensured that the header
219
+ # table size will stay lower than or equal to the
220
+ # SETTINGS_HEADER_TABLE_SIZE limit. To achieve this, repeatedly, the
221
+ # first entry of the header table is removed, until enough space is
222
+ # available for the modification.
223
+ #
224
+ # A consequence of removing one or more entries at the beginning of the
225
+ # header table is that the remaining entries are renumbered. The first
226
+ # entry of the header table is always associated to the index 0.
241
227
  #
242
228
  # @param cmd [Hash]
229
+ # @return [Boolean]
243
230
  def size_check(cmd)
244
231
  cursize = @table.join.bytesize + @table.size * 32
245
232
  cmdsize = cmd[:name].bytesize + cmd[:value].bytesize + 32
246
233
 
234
+ # The addition of a new entry with a size greater than the
235
+ # SETTINGS_HEADER_TABLE_SIZE limit causes all the entries from the
236
+ # header table to be dropped and the new entry not to be added to the
237
+ # header table. The replacement of an existing entry with a new entry
238
+ # with a size greater than the SETTINGS_HEADER_TABLE_SIZE has the same
239
+ # consequences.
240
+ if cmdsize > @limit
241
+ @table.clear
242
+ return false
243
+ end
244
+
247
245
  cur = 0
248
246
  while (cursize + cmdsize) > @limit do
249
247
  e = @table.shift
250
248
 
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.
249
+ # When the modification of the header table is the replacement of an
250
+ # existing entry, the replaced entry is the one indicated in the
251
+ # literal representation before any entry is removed from the header
252
+ # table. If the entry to be replaced is removed from the header table
253
+ # when performing the size adjustment, the replacement entry is
254
+ # inserted at the beginning of the header table.
257
255
  if cmd[:type] == :substitution && cur == cmd[:index]
258
256
  cmd[:type] = :prepend
259
257
  end
260
258
 
261
259
  cursize -= (e.join.bytesize + 32)
262
260
  end
261
+
262
+ return true
263
263
  end
264
264
 
265
265
  def active?(idx)
266
- !@workset.find {|i,_| i == idx }.nil?
266
+ !@refset.find {|i,_| i == idx }.nil?
267
267
  end
268
268
 
269
269
  def default?(idx)
@@ -289,20 +289,20 @@ module HTTP2
289
289
  # server_role = Compressor.new(:response)
290
290
  class Compressor
291
291
  def initialize(type)
292
- @cc = CompressionContext.new(type)
292
+ @cc = EncodingContext.new(type)
293
293
  end
294
294
 
295
295
  # Encodes provided value via integer representation.
296
- # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-01#section-4.2.1
296
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#section-4.1.1
297
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
298
+ # If I < 2^N - 1, encode I on N bits
299
+ # Else
300
+ # encode 2^N - 1 on N bits
301
+ # I = I - (2^N - 1)
302
+ # While I >= 128
303
+ # Encode (I % 128 + 128) on 8 bits
304
+ # I = I / 128
305
+ # encode (I) on 8 bits
306
306
  #
307
307
  # @param i [Integer] value to encode
308
308
  # @param n [Integer] number of available bits
@@ -315,21 +315,17 @@ module HTTP2
315
315
  bytes.push limit if !n.zero?
316
316
 
317
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)
318
+ while (i >= 128) do
319
+ bytes.push((i % 128) + 128)
320
+ i = i / 128
326
321
  end
327
322
 
323
+ bytes.push i
328
324
  bytes.pack('C*')
329
325
  end
330
326
 
331
327
  # Encodes provided value via string literal representation.
332
- # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-01#section-4.2.2
328
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#section-4.1.3
333
329
  #
334
330
  # * The string length, defined as the number of bytes needed to store
335
331
  # its UTF-8 representation, is represented as an integer with a zero
@@ -340,15 +336,15 @@ module HTTP2
340
336
  # @param str [String]
341
337
  # @return [String] binary string
342
338
  def string(str)
343
- integer(str.bytesize, 0) + str.dup.force_encoding('binary')
339
+ integer(str.bytesize, 0) << str.dup.force_encoding('binary')
344
340
  end
345
341
 
346
342
  # Encodes header command with appropriate header representation.
347
- # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-01#section-4.3
343
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#section-4
348
344
  #
349
345
  # @param h [Hash] header command
350
346
  # @param buffer [String]
351
- def header(h, buffer = "")
347
+ def header(h, buffer = Buffer.new)
352
348
  rep = HEADREP[h[:type]]
353
349
 
354
350
  if h[:type] == :indexed
@@ -383,29 +379,35 @@ module HTTP2
383
379
  # Encodes provided list of HTTP headers.
384
380
  #
385
381
  # @param headers [Hash]
386
- # @return [String] binary string
382
+ # @return [Buffer]
387
383
  def encode(headers)
384
+ buffer = Buffer.new
388
385
  commands = []
389
- @cc.update_sets
390
386
 
391
- # Remove missing headers from the working set
392
- @cc.workset.each do |idx, (wk,wv)|
387
+ # Literal header names MUST be translated to lowercase before
388
+ # encoding and transmission.
389
+ headers.map! {|(hk,hv)| [hk.downcase, hv] }
390
+
391
+ # Generate remove commands for missing headers
392
+ @cc.refset.each do |idx, (wk,wv)|
393
393
  if headers.find {|(hk,hv)| hk == wk && hv == wv }.nil?
394
394
  commands.push @cc.removecmd idx
395
395
  end
396
396
  end
397
397
 
398
- # Add missing headers to the working set
398
+ # Generate add commands for new headers
399
399
  headers.each do |(hk,hv)|
400
- if @cc.workset.find {|i,(wk,wv)| hk == wk && hv == wv}.nil?
400
+ if @cc.refset.find {|i,(wk,wv)| hk == wk && hv == wv}.nil?
401
401
  commands.push @cc.addcmd [hk, hv]
402
402
  end
403
403
  end
404
404
 
405
- commands.map do |cmd|
405
+ commands.each do |cmd|
406
406
  @cc.process cmd.dup
407
- header cmd
408
- end.join
407
+ buffer << header(cmd)
408
+ end
409
+
410
+ buffer
409
411
  end
410
412
  end
411
413
 
@@ -418,7 +420,7 @@ module HTTP2
418
420
  # client_role = Decompressor.new(:response)
419
421
  class Decompressor
420
422
  def initialize(type)
421
- @cc = CompressionContext.new(type)
423
+ @cc = EncodingContext.new(type)
422
424
  end
423
425
 
424
426
  # Decodes integer value from provided buffer.
@@ -430,7 +432,7 @@ module HTTP2
430
432
  i = !n.zero? ? (buf.getbyte & limit) : 0
431
433
 
432
434
  m = 0
433
- buf.each_byte do |byte|
435
+ while byte = buf.getbyte do
434
436
  i += ((byte & 127) << m)
435
437
  m += 7
436
438
 
@@ -450,10 +452,9 @@ module HTTP2
450
452
 
451
453
  # Decodes header command from provided buffer.
452
454
  #
453
- # @param buf [String]
455
+ # @param buf [Buffer]
454
456
  def header(buf)
455
- peek = buf.getbyte
456
- buf.seek(-1, IO::SEEK_CUR)
457
+ peek = buf.readbyte(0)
457
458
 
458
459
  header = {}
459
460
  header[:type], type = HEADREP.select do |t, desc|
@@ -481,11 +482,26 @@ module HTTP2
481
482
 
482
483
  # Decodes and processes header commands within provided buffer.
483
484
  #
484
- # @param buf [String]
485
+ # Once all the representations contained in a header block have been
486
+ # processed, the headers that are in common with the previous header
487
+ # set are emitted, during the reference set emission.
488
+ #
489
+ # For the reference set emission, each header contained in the
490
+ # reference set that has not been emitted during the processing of the
491
+ # header block is emitted.
492
+ #
493
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#section-3.2.2
494
+ #
495
+ # @param buf [Buffer]
496
+ # @return [Array] set of HTTP headers
485
497
  def decode(buf)
486
- @cc.update_sets
487
- @cc.process(header(buf)) while !buf.eof?
488
- @cc.workset.map {|i,header| header}
498
+ set = []
499
+ set << @cc.process(header(buf)) while !buf.empty?
500
+ @cc.refset.each do |i,header|
501
+ set << header if !set.include? header
502
+ end
503
+
504
+ set.compact
489
505
  end
490
506
  end
491
507