http-2-next 0.2.3 → 0.4.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.
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP2Next
4
+ # Implementation of header compression for HTTP 2.0 (HPACK) format adapted
5
+ # to efficiently represent HTTP headers in the context of HTTP 2.0.
6
+ #
7
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10
8
+ module Header
9
+ # Header representation as defined by the spec.
10
+ HEADREP = {
11
+ indexed: { prefix: 7, pattern: 0x80 },
12
+ incremental: { prefix: 6, pattern: 0x40 },
13
+ noindex: { prefix: 4, pattern: 0x00 },
14
+ neverindexed: { prefix: 4, pattern: 0x10 },
15
+ changetablesize: { prefix: 5, pattern: 0x20 }
16
+ }.each_value(&:freeze).freeze
17
+
18
+ # Predefined options set for Compressor
19
+ # http://mew.org/~kazu/material/2014-hpack.pdf
20
+ NAIVE = { index: :never, huffman: :never }.freeze
21
+ LINEAR = { index: :all, huffman: :never }.freeze
22
+ STATIC = { index: :static, huffman: :never }.freeze
23
+ SHORTER = { index: :all, huffman: :never }.freeze
24
+ NAIVEH = { index: :never, huffman: :always }.freeze
25
+ LINEARH = { index: :all, huffman: :always }.freeze
26
+ STATICH = { index: :static, huffman: :always }.freeze
27
+ SHORTERH = { index: :all, huffman: :shorter }.freeze
28
+ end
29
+ end
30
+
31
+ require "http/2/next/header/huffman"
32
+ require "http/2/next/header/huffman_statemachine"
33
+ require "http/2/next/header/encoding_context"
34
+ require "http/2/next/header/compressor"
35
+ require "http/2/next/header/decompressor"
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP2Next
4
+ module Header
5
+ # Responsible for encoding header key-value pairs using HPACK algorithm.
6
+ class Compressor
7
+ # @param options [Hash] encoding options
8
+ def initialize(options = {})
9
+ @cc = EncodingContext.new(options)
10
+ end
11
+
12
+ # Set dynamic table size in EncodingContext
13
+ # @param size [Integer] new dynamic table size
14
+ def table_size=(size)
15
+ @cc.table_size = size
16
+ end
17
+
18
+ # Encodes provided value via integer representation.
19
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.1
20
+ #
21
+ # If I < 2^N - 1, encode I on N bits
22
+ # Else
23
+ # encode 2^N - 1 on N bits
24
+ # I = I - (2^N - 1)
25
+ # While I >= 128
26
+ # Encode (I % 128 + 128) on 8 bits
27
+ # I = I / 128
28
+ # encode (I) on 8 bits
29
+ #
30
+ # @param i [Integer] value to encode
31
+ # @param n [Integer] number of available bits
32
+ # @return [String] binary string
33
+ def integer(i, n)
34
+ limit = 2**n - 1
35
+ return [i].pack("C") if i < limit
36
+
37
+ bytes = []
38
+ bytes.push limit unless n.zero?
39
+
40
+ i -= limit
41
+ while i >= 128
42
+ bytes.push((i % 128) + 128)
43
+ i /= 128
44
+ end
45
+
46
+ bytes.push i
47
+ bytes.pack("C*")
48
+ end
49
+
50
+ # Encodes provided value via string literal representation.
51
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.2
52
+ #
53
+ # * The string length, defined as the number of bytes needed to store
54
+ # its UTF-8 representation, is represented as an integer with a seven
55
+ # bits prefix. If the string length is strictly less than 127, it is
56
+ # represented as one byte.
57
+ # * If the bit 7 of the first byte is 1, the string value is represented
58
+ # as a list of Huffman encoded octets
59
+ # (padded with bit 1's until next octet boundary).
60
+ # * If the bit 7 of the first byte is 0, the string value is
61
+ # represented as a list of UTF-8 encoded octets.
62
+ #
63
+ # +@options [:huffman]+ controls whether to use Huffman encoding:
64
+ # :never Do not use Huffman encoding
65
+ # :always Always use Huffman encoding
66
+ # :shorter Use Huffman when the result is strictly shorter
67
+ #
68
+ # @param str [String]
69
+ # @return [String] binary string
70
+ def string(str)
71
+ plain = nil
72
+ huffman = nil
73
+ plain = integer(str.bytesize, 7) << str.dup.force_encoding(Encoding::BINARY) unless @cc.options[:huffman] == :always
74
+ unless @cc.options[:huffman] == :never
75
+ huffman = Huffman.new.encode(str)
76
+ huffman = integer(huffman.bytesize, 7) << huffman
77
+ huffman.setbyte(0, huffman.ord | 0x80)
78
+ end
79
+ case @cc.options[:huffman]
80
+ when :always
81
+ huffman
82
+ when :never
83
+ plain
84
+ else
85
+ huffman.bytesize < plain.bytesize ? huffman : plain
86
+ end
87
+ end
88
+
89
+ # Encodes header command with appropriate header representation.
90
+ #
91
+ # @param h [Hash] header command
92
+ # @param buffer [String]
93
+ # @return [Buffer]
94
+ def header(h, buffer = "".b)
95
+ rep = HEADREP[h[:type]]
96
+
97
+ case h[:type]
98
+ when :indexed
99
+ buffer << integer(h[:name] + 1, rep[:prefix])
100
+ when :changetablesize
101
+ buffer << integer(h[:value], rep[:prefix])
102
+ else
103
+ if h[:name].is_a? Integer
104
+ buffer << integer(h[:name] + 1, rep[:prefix])
105
+ else
106
+ buffer << integer(0, rep[:prefix])
107
+ buffer << string(h[:name])
108
+ end
109
+
110
+ buffer << string(h[:value])
111
+ end
112
+
113
+ # set header representation pattern on first byte
114
+ fb = buffer.ord | rep[:pattern]
115
+ buffer.setbyte(0, fb)
116
+
117
+ buffer
118
+ end
119
+
120
+ # Encodes provided list of HTTP headers.
121
+ #
122
+ # @param headers [Array] +[[name, value], ...]+
123
+ # @return [Buffer]
124
+ def encode(headers)
125
+ buffer = "".b
126
+ pseudo_headers, regular_headers = headers.partition { |f, _| f.start_with? ":" }
127
+ headers = [*pseudo_headers, *regular_headers]
128
+ commands = @cc.encode(headers)
129
+ commands.each do |cmd|
130
+ buffer << header(cmd)
131
+ end
132
+
133
+ buffer
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP2Next
4
+ module Header
5
+ using StringExtensions
6
+ # Responsible for decoding received headers and maintaining compression
7
+ # context of the opposing peer. Decompressor must be initialized with
8
+ # appropriate starting context based on local role: client or server.
9
+ #
10
+ # @example
11
+ # server_role = Decompressor.new(:request)
12
+ # client_role = Decompressor.new(:response)
13
+ class Decompressor
14
+ include Error
15
+
16
+ # @param options [Hash] decoding options. Only :table_size is effective.
17
+ def initialize(options = {})
18
+ @cc = EncodingContext.new(options)
19
+ end
20
+
21
+ # Set dynamic table size in EncodingContext
22
+ # @param size [Integer] new dynamic table size
23
+ def table_size=(size)
24
+ @cc.table_size = size
25
+ end
26
+
27
+ # Decodes integer value from provided buffer.
28
+ #
29
+ # @param buf [String]
30
+ # @param n [Integer] number of available bits
31
+ # @return [Integer]
32
+ def integer(buf, n)
33
+ limit = 2**n - 1
34
+ i = !n.zero? ? (buf.shift_byte & limit) : 0
35
+
36
+ m = 0
37
+ if i == limit
38
+ while (byte = buf.shift_byte)
39
+ i += ((byte & 127) << m)
40
+ m += 7
41
+
42
+ break if (byte & 128).zero?
43
+ end
44
+ end
45
+
46
+ i
47
+ end
48
+
49
+ # Decodes string value from provided buffer.
50
+ #
51
+ # @param buf [String]
52
+ # @return [String] UTF-8 encoded string
53
+ # @raise [CompressionError] when input is malformed
54
+ def string(buf)
55
+ raise CompressionError, "invalid header block fragment" if buf.empty?
56
+
57
+ huffman = (buf.getbyte(0) & 0x80) == 0x80
58
+ len = integer(buf, 7)
59
+ str = buf.read(len)
60
+ raise CompressionError, "string too short" unless str.bytesize == len
61
+
62
+ str = Huffman.new.decode(str) if huffman
63
+ str.force_encoding(Encoding::UTF_8)
64
+ end
65
+
66
+ # Decodes header command from provided buffer.
67
+ #
68
+ # @param buf [Buffer]
69
+ # @return [Hash] command
70
+ def header(buf)
71
+ peek = buf.getbyte(0)
72
+
73
+ header = {}
74
+ header[:type], type = HEADREP.find do |_t, desc|
75
+ mask = (peek >> desc[:prefix]) << desc[:prefix]
76
+ mask == desc[:pattern]
77
+ end
78
+
79
+ raise CompressionError unless header[:type]
80
+
81
+ header[:name] = integer(buf, type[:prefix])
82
+
83
+ case header[:type]
84
+ when :indexed
85
+ raise CompressionError if (header[:name]).zero?
86
+
87
+ header[:name] -= 1
88
+ when :changetablesize
89
+ header[:value] = header[:name]
90
+ else
91
+ if (header[:name]).zero?
92
+ header[:name] = string(buf)
93
+ else
94
+ header[:name] -= 1
95
+ end
96
+ header[:value] = string(buf)
97
+ end
98
+
99
+ header
100
+ end
101
+
102
+ FORBIDDEN_HEADERS = %w[connection te].freeze
103
+
104
+ # Decodes and processes header commands within provided buffer.
105
+ #
106
+ # @param buf [Buffer]
107
+ # @param frame [HTTP2Next::Frame, nil]
108
+ # @return [Array] +[[name, value], ...]
109
+ def decode(buf, frame = nil)
110
+ list = []
111
+ decoding_pseudo_headers = true
112
+ @cc.listen_on_table do
113
+ until buf.empty?
114
+ field, value = @cc.process(header(buf))
115
+ next if field.nil?
116
+
117
+ is_pseudo_header = field.start_with? ":"
118
+ if !decoding_pseudo_headers && is_pseudo_header
119
+ raise ProtocolError, "one or more pseudo headers encountered after regular headers"
120
+ end
121
+
122
+ decoding_pseudo_headers = is_pseudo_header
123
+ raise ProtocolError, "invalid header received: #{field}" if FORBIDDEN_HEADERS.include?(field)
124
+
125
+ if frame
126
+ case field
127
+ when ":method"
128
+ frame[:method] = value
129
+ when "content-length"
130
+ frame[:content_length] = Integer(value)
131
+ when "trailer"
132
+ (frame[:trailer] ||= []) << value
133
+ end
134
+ end
135
+ list << [field, value]
136
+ end
137
+ end
138
+ list
139
+ end
140
+ end
141
+ end
142
+ end
@@ -1,18 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP2Next
4
- # Implementation of header compression for HTTP 2.0 (HPACK) format adapted
5
- # to efficiently represent HTTP headers in the context of HTTP 2.0.
6
- #
7
- # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10
4
+ # To decompress header blocks, a decoder only needs to maintain a
5
+ # dynamic table as a decoding context.
6
+ # No other state information is needed.
8
7
  module Header
9
- # To decompress header blocks, a decoder only needs to maintain a
10
- # dynamic table as a decoding context.
11
- # No other state information is needed.
12
8
  class EncodingContext
13
9
  include Error
14
10
 
15
- using Extensions
11
+ using RegexpExtensions
16
12
 
17
13
  UPPER = /[[:upper:]]/.freeze
18
14
 
@@ -185,14 +181,15 @@ module HTTP2Next
185
181
  # o The header field is added to the decoded header list.
186
182
  # o The header field is inserted at the beginning of the dynamic table.
187
183
 
188
- if cmd[:name].is_a? Integer
184
+ case cmd[:name]
185
+ when Integer
189
186
  k, v = dereference(cmd[:name])
190
187
 
191
188
  cmd = cmd.dup
192
189
  cmd[:index] ||= cmd[:name]
193
190
  cmd[:value] ||= v
194
191
  cmd[:name] = k
195
- elsif UPPER.match?(cmd[:name])
192
+ when UPPER
196
193
  raise ProtocolError, "Invalid uppercase key: #{cmd[:name]}"
197
194
  end
198
195
 
@@ -329,293 +326,5 @@ module HTTP2Next
329
326
  cmdsize <= @limit
330
327
  end
331
328
  end
332
-
333
- # Header representation as defined by the spec.
334
- HEADREP = {
335
- indexed: { prefix: 7, pattern: 0x80 },
336
- incremental: { prefix: 6, pattern: 0x40 },
337
- noindex: { prefix: 4, pattern: 0x00 },
338
- neverindexed: { prefix: 4, pattern: 0x10 },
339
- changetablesize: { prefix: 5, pattern: 0x20 }
340
- }.each_value(&:freeze).freeze
341
-
342
- # Predefined options set for Compressor
343
- # http://mew.org/~kazu/material/2014-hpack.pdf
344
- NAIVE = { index: :never, huffman: :never }.freeze
345
- LINEAR = { index: :all, huffman: :never }.freeze
346
- STATIC = { index: :static, huffman: :never }.freeze
347
- SHORTER = { index: :all, huffman: :never }.freeze
348
- NAIVEH = { index: :never, huffman: :always }.freeze
349
- LINEARH = { index: :all, huffman: :always }.freeze
350
- STATICH = { index: :static, huffman: :always }.freeze
351
- SHORTERH = { index: :all, huffman: :shorter }.freeze
352
-
353
- # Responsible for encoding header key-value pairs using HPACK algorithm.
354
- class Compressor
355
- # @param options [Hash] encoding options
356
- def initialize(options = {})
357
- @cc = EncodingContext.new(options)
358
- end
359
-
360
- # Set dynamic table size in EncodingContext
361
- # @param size [Integer] new dynamic table size
362
- def table_size=(size)
363
- @cc.table_size = size
364
- end
365
-
366
- # Encodes provided value via integer representation.
367
- # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.1
368
- #
369
- # If I < 2^N - 1, encode I on N bits
370
- # Else
371
- # encode 2^N - 1 on N bits
372
- # I = I - (2^N - 1)
373
- # While I >= 128
374
- # Encode (I % 128 + 128) on 8 bits
375
- # I = I / 128
376
- # encode (I) on 8 bits
377
- #
378
- # @param i [Integer] value to encode
379
- # @param n [Integer] number of available bits
380
- # @return [String] binary string
381
- def integer(i, n)
382
- limit = 2**n - 1
383
- return [i].pack("C") if i < limit
384
-
385
- bytes = []
386
- bytes.push limit unless n.zero?
387
-
388
- i -= limit
389
- while i >= 128
390
- bytes.push((i % 128) + 128)
391
- i /= 128
392
- end
393
-
394
- bytes.push i
395
- bytes.pack("C*")
396
- end
397
-
398
- # Encodes provided value via string literal representation.
399
- # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.2
400
- #
401
- # * The string length, defined as the number of bytes needed to store
402
- # its UTF-8 representation, is represented as an integer with a seven
403
- # bits prefix. If the string length is strictly less than 127, it is
404
- # represented as one byte.
405
- # * If the bit 7 of the first byte is 1, the string value is represented
406
- # as a list of Huffman encoded octets
407
- # (padded with bit 1's until next octet boundary).
408
- # * If the bit 7 of the first byte is 0, the string value is
409
- # represented as a list of UTF-8 encoded octets.
410
- #
411
- # +@options [:huffman]+ controls whether to use Huffman encoding:
412
- # :never Do not use Huffman encoding
413
- # :always Always use Huffman encoding
414
- # :shorter Use Huffman when the result is strictly shorter
415
- #
416
- # @param str [String]
417
- # @return [String] binary string
418
- def string(str)
419
- plain = nil
420
- huffman = nil
421
- plain = integer(str.bytesize, 7) << str.dup.force_encoding(Encoding::BINARY) unless @cc.options[:huffman] == :always
422
- unless @cc.options[:huffman] == :never
423
- huffman = Huffman.new.encode(str)
424
- huffman = integer(huffman.bytesize, 7) << huffman
425
- huffman.setbyte(0, huffman.ord | 0x80)
426
- end
427
- case @cc.options[:huffman]
428
- when :always
429
- huffman
430
- when :never
431
- plain
432
- else
433
- huffman.bytesize < plain.bytesize ? huffman : plain
434
- end
435
- end
436
-
437
- # Encodes header command with appropriate header representation.
438
- #
439
- # @param h [Hash] header command
440
- # @param buffer [String]
441
- # @return [Buffer]
442
- def header(h, buffer = Buffer.new)
443
- rep = HEADREP[h[:type]]
444
-
445
- case h[:type]
446
- when :indexed
447
- buffer << integer(h[:name] + 1, rep[:prefix])
448
- when :changetablesize
449
- buffer << integer(h[:value], rep[:prefix])
450
- else
451
- if h[:name].is_a? Integer
452
- buffer << integer(h[:name] + 1, rep[:prefix])
453
- else
454
- buffer << integer(0, rep[:prefix])
455
- buffer << string(h[:name])
456
- end
457
-
458
- buffer << string(h[:value])
459
- end
460
-
461
- # set header representation pattern on first byte
462
- fb = buffer.ord | rep[:pattern]
463
- buffer.setbyte(0, fb)
464
-
465
- buffer
466
- end
467
-
468
- # Encodes provided list of HTTP headers.
469
- #
470
- # @param headers [Array] +[[name, value], ...]+
471
- # @return [Buffer]
472
- def encode(headers)
473
- buffer = Buffer.new
474
- pseudo_headers, regular_headers = headers.partition { |f, _| f.start_with? ":" }
475
- headers = [*pseudo_headers, *regular_headers]
476
- commands = @cc.encode(headers)
477
- commands.each do |cmd|
478
- buffer << header(cmd)
479
- end
480
-
481
- buffer
482
- end
483
- end
484
-
485
- # Responsible for decoding received headers and maintaining compression
486
- # context of the opposing peer. Decompressor must be initialized with
487
- # appropriate starting context based on local role: client or server.
488
- #
489
- # @example
490
- # server_role = Decompressor.new(:request)
491
- # client_role = Decompressor.new(:response)
492
- class Decompressor
493
- include Error
494
-
495
- # @param options [Hash] decoding options. Only :table_size is effective.
496
- def initialize(options = {})
497
- @cc = EncodingContext.new(options)
498
- end
499
-
500
- # Set dynamic table size in EncodingContext
501
- # @param size [Integer] new dynamic table size
502
- def table_size=(size)
503
- @cc.table_size = size
504
- end
505
-
506
- # Decodes integer value from provided buffer.
507
- #
508
- # @param buf [String]
509
- # @param n [Integer] number of available bits
510
- # @return [Integer]
511
- def integer(buf, n)
512
- limit = 2**n - 1
513
- i = !n.zero? ? (buf.getbyte & limit) : 0
514
-
515
- m = 0
516
- if i == limit
517
- while (byte = buf.getbyte)
518
- i += ((byte & 127) << m)
519
- m += 7
520
-
521
- break if (byte & 128).zero?
522
- end
523
- end
524
-
525
- i
526
- end
527
-
528
- # Decodes string value from provided buffer.
529
- #
530
- # @param buf [String]
531
- # @return [String] UTF-8 encoded string
532
- # @raise [CompressionError] when input is malformed
533
- def string(buf)
534
- raise CompressionError, "invalid header block fragment" if buf.empty?
535
-
536
- huffman = (buf.readbyte(0) & 0x80) == 0x80
537
- len = integer(buf, 7)
538
- str = buf.read(len)
539
- raise CompressionError, "string too short" unless str.bytesize == len
540
-
541
- str = Huffman.new.decode(Buffer.new(str)) if huffman
542
- str.force_encoding(Encoding::UTF_8)
543
- end
544
-
545
- # Decodes header command from provided buffer.
546
- #
547
- # @param buf [Buffer]
548
- # @return [Hash] command
549
- def header(buf)
550
- peek = buf.readbyte(0)
551
-
552
- header = {}
553
- header[:type], type = HEADREP.find do |_t, desc|
554
- mask = (peek >> desc[:prefix]) << desc[:prefix]
555
- mask == desc[:pattern]
556
- end
557
-
558
- raise CompressionError unless header[:type]
559
-
560
- header[:name] = integer(buf, type[:prefix])
561
-
562
- case header[:type]
563
- when :indexed
564
- raise CompressionError if (header[:name]).zero?
565
-
566
- header[:name] -= 1
567
- when :changetablesize
568
- header[:value] = header[:name]
569
- else
570
- if (header[:name]).zero?
571
- header[:name] = string(buf)
572
- else
573
- header[:name] -= 1
574
- end
575
- header[:value] = string(buf)
576
- end
577
-
578
- header
579
- end
580
-
581
- FORBIDDEN_HEADERS = %w[connection te].freeze
582
-
583
- # Decodes and processes header commands within provided buffer.
584
- #
585
- # @param buf [Buffer]
586
- # @param frame [HTTP2Next::Frame, nil]
587
- # @return [Array] +[[name, value], ...]
588
- def decode(buf, frame = nil)
589
- list = []
590
- decoding_pseudo_headers = true
591
- @cc.listen_on_table do
592
- until buf.empty?
593
- field, value = @cc.process(header(buf))
594
- next if field.nil?
595
-
596
- is_pseudo_header = field.start_with? ":"
597
- if !decoding_pseudo_headers && is_pseudo_header
598
- raise ProtocolError, "one or more pseudo headers encountered after regular headers"
599
- end
600
-
601
- decoding_pseudo_headers = is_pseudo_header
602
- raise ProtocolError, "invalid header received: #{field}" if FORBIDDEN_HEADERS.include?(field)
603
-
604
- if frame
605
- case field
606
- when ":method"
607
- frame[:method] = value
608
- when "content-length"
609
- frame[:content_length] = Integer(value)
610
- when "trailer"
611
- (frame[:trailer] ||= []) << value
612
- end
613
- end
614
- list << [field, value]
615
- end
616
- end
617
- list
618
- end
619
- end
620
329
  end
621
330
  end