http-2-next 0.2.6 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP2Next
4
+ module Header
5
+ # Responsible for decoding received headers and maintaining compression
6
+ # context of the opposing peer. Decompressor must be initialized with
7
+ # appropriate starting context based on local role: client or server.
8
+ #
9
+ # @example
10
+ # server_role = Decompressor.new(:request)
11
+ # client_role = Decompressor.new(:response)
12
+ class Decompressor
13
+ include Error
14
+
15
+ # @param options [Hash] decoding options. Only :table_size is effective.
16
+ def initialize(options = {})
17
+ @cc = EncodingContext.new(options)
18
+ end
19
+
20
+ # Set dynamic table size in EncodingContext
21
+ # @param size [Integer] new dynamic table size
22
+ def table_size=(size)
23
+ @cc.table_size = size
24
+ end
25
+
26
+ # Decodes integer value from provided buffer.
27
+ #
28
+ # @param buf [String]
29
+ # @param n [Integer] number of available bits
30
+ # @return [Integer]
31
+ def integer(buf, n)
32
+ limit = 2**n - 1
33
+ i = !n.zero? ? (buf.getbyte & limit) : 0
34
+
35
+ m = 0
36
+ if i == limit
37
+ while (byte = buf.getbyte)
38
+ i += ((byte & 127) << m)
39
+ m += 7
40
+
41
+ break if (byte & 128).zero?
42
+ end
43
+ end
44
+
45
+ i
46
+ end
47
+
48
+ # Decodes string value from provided buffer.
49
+ #
50
+ # @param buf [String]
51
+ # @return [String] UTF-8 encoded string
52
+ # @raise [CompressionError] when input is malformed
53
+ def string(buf)
54
+ raise CompressionError, "invalid header block fragment" if buf.empty?
55
+
56
+ huffman = (buf.readbyte(0) & 0x80) == 0x80
57
+ len = integer(buf, 7)
58
+ str = buf.read(len)
59
+ raise CompressionError, "string too short" unless str.bytesize == len
60
+
61
+ str = Huffman.new.decode(Buffer.new(str)) if huffman
62
+ str.force_encoding(Encoding::UTF_8)
63
+ end
64
+
65
+ # Decodes header command from provided buffer.
66
+ #
67
+ # @param buf [Buffer]
68
+ # @return [Hash] command
69
+ def header(buf)
70
+ peek = buf.readbyte(0)
71
+
72
+ header = {}
73
+ header[:type], type = HEADREP.find do |_t, desc|
74
+ mask = (peek >> desc[:prefix]) << desc[:prefix]
75
+ mask == desc[:pattern]
76
+ end
77
+
78
+ raise CompressionError unless header[:type]
79
+
80
+ header[:name] = integer(buf, type[:prefix])
81
+
82
+ case header[:type]
83
+ when :indexed
84
+ raise CompressionError if (header[:name]).zero?
85
+
86
+ header[:name] -= 1
87
+ when :changetablesize
88
+ header[:value] = header[:name]
89
+ else
90
+ if (header[:name]).zero?
91
+ header[:name] = string(buf)
92
+ else
93
+ header[:name] -= 1
94
+ end
95
+ header[:value] = string(buf)
96
+ end
97
+
98
+ header
99
+ end
100
+
101
+ FORBIDDEN_HEADERS = %w[connection te].freeze
102
+
103
+ # Decodes and processes header commands within provided buffer.
104
+ #
105
+ # @param buf [Buffer]
106
+ # @param frame [HTTP2Next::Frame, nil]
107
+ # @return [Array] +[[name, value], ...]
108
+ def decode(buf, frame = nil)
109
+ list = []
110
+ decoding_pseudo_headers = true
111
+ @cc.listen_on_table do
112
+ until buf.empty?
113
+ field, value = @cc.process(header(buf))
114
+ next if field.nil?
115
+
116
+ is_pseudo_header = field.start_with? ":"
117
+ if !decoding_pseudo_headers && is_pseudo_header
118
+ raise ProtocolError, "one or more pseudo headers encountered after regular headers"
119
+ end
120
+
121
+ decoding_pseudo_headers = is_pseudo_header
122
+ raise ProtocolError, "invalid header received: #{field}" if FORBIDDEN_HEADERS.include?(field)
123
+
124
+ if frame
125
+ case field
126
+ when ":method"
127
+ frame[:method] = value
128
+ when "content-length"
129
+ frame[:content_length] = Integer(value)
130
+ when "trailer"
131
+ (frame[:trailer] ||= []) << value
132
+ end
133
+ end
134
+ list << [field, value]
135
+ end
136
+ end
137
+ list
138
+ end
139
+ end
140
+ end
141
+ 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