http-2-next 0.2.4 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,144 @@
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 ":status"
128
+ frame[:status] = Integer(value)
129
+ when ":method"
130
+ frame[:method] = value
131
+ when "content-length"
132
+ frame[:content_length] = Integer(value)
133
+ when "trailer"
134
+ (frame[:trailer] ||= []) << value
135
+ end
136
+ end
137
+ list << [field, value]
138
+ end
139
+ end
140
+ list
141
+ end
142
+ end
143
+ end
144
+ 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