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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: debc6f11031e879547dd45605849500e45fd1142
4
- data.tar.gz: 61e60a0b2b831553a9ecca42b069c032afe12e30
3
+ metadata.gz: c7bbedd001cf6de0ec84caef4ccd0e4e818315d3
4
+ data.tar.gz: c1129e49ae319b1cf31151f4e75bf16f0a108e0a
5
5
  SHA512:
6
- metadata.gz: b7c36dec0419a45e75f8664e6ac4ede157c49057d51bd7f25f795139313048978e4c053e49d90f0140731ec36b2a7df68fd075c50bb1f9cf98169b3192571ddd
7
- data.tar.gz: 9ab31eb78ec61a37139efeba7185a87cd742588f30a456b35bbeb8935e50487cd948be09e9fed68ba1510ef91db88916815cc30ab41941634b567be59b56099c
6
+ metadata.gz: 6c68277cdbc49a1fd695c177e1015334946e4cbba9e93cc2b7b93572b45ab89c61e92ed2638f485c1eafaa2909f1d8a8d0c3ae352743697176b37bf1b27e8be3
7
+ data.tar.gz: 3357aafb0da7559f22230e8503bd025b4c993ce0fc1dbcc40d078b6b6d0bb5b98b831f40b0d9f53c0358b42939dc7697b72b319ce183975eaaea6e95c092b96a
@@ -0,0 +1 @@
1
+ service_name: travis-ci
@@ -0,0 +1,3 @@
1
+ [submodule "spec/hpack-test-case"]
2
+ path = spec/hpack-test-case
3
+ url = https://github.com/http2jp/hpack-test-case
data/Gemfile CHANGED
@@ -6,6 +6,11 @@ gem 'yard'
6
6
  group :test do
7
7
  gem 'coveralls', :require => false
8
8
  gem 'rspec'
9
+ gem 'rspec-autotest'
10
+ gem 'autotest-standalone'
11
+ gem 'autotest-growl'
12
+ gem 'pry'
13
+ gem 'pry-byebug'
9
14
  end
10
15
 
11
16
  gemspec
data/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  [![Gem Version](https://badge.fury.io/rb/http-2.png)](http://rubygems.org/gems/http-2)
4
4
  [![Build Status](https://travis-ci.org/igrigorik/http-2.png?branch=master)](https://travis-ci.org/igrigorik/http-2)
5
5
  [![Coverage Status](https://coveralls.io/repos/igrigorik/http-2/badge.png)](https://coveralls.io/r/igrigorik/http-2)
6
+ [![Analytics](https://ga-beacon.appspot.com/UA-71196-10/http-2/readme)](https://github.com/igrigorik/ga-beacon)
6
7
 
7
8
  Pure ruby, framework and transport agnostic implementation of [HTTP 2.0 protocol](http://tools.ietf.org/html/draft-ietf-httpbis-http2) with support for:
8
9
 
@@ -15,8 +16,8 @@ Pure ruby, framework and transport agnostic implementation of [HTTP 2.0 protocol
15
16
 
16
17
  Current implementation (see [HPBN chapter for HTTP 2.0 overview](http://chimera.labs.oreilly.com/books/1230000000545/ch12.html)), is based on:
17
18
 
18
- * [draft-ietf-httpbis-http2-06](http://tools.ietf.org/html/draft-ietf-httpbis-http2-06)
19
- * [draft-ietf-httpbis-header-compression-03](http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03)
19
+ * [draft-ietf-httpbis-http2-14](http://tools.ietf.org/html/draft-ietf-httpbis-http2-14)
20
+ * [draft-ietf-httpbis-header-compression-09](http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-09)
20
21
 
21
22
  _Note: the underlying specifications are still evolving, expect APIs to change and evolve also..._
22
23
 
@@ -43,7 +44,7 @@ while bytes = socket.read
43
44
  end
44
45
  ```
45
46
 
46
- Checkout provided [client](https://github.com/igrigorik/http-2/blob/master/example/client.rb) and [server]((https://github.com/igrigorik/http-2/blob/master/example/server.rb) implementations for basic examples.
47
+ Checkout provided [client](https://github.com/igrigorik/http-2/blob/master/example/client.rb) and [server](https://github.com/igrigorik/http-2/blob/master/example/server.rb) implementations for basic examples.
47
48
 
48
49
 
49
50
  ### Connection lifecycle management
@@ -283,4 +284,4 @@ client.settings(streams: 0) # setting max limit to 0 disables server push
283
284
 
284
285
  ### License
285
286
 
286
- (MIT License) - Copyright (c) 2013 Ilya Grigorik
287
+ (MIT License) - Copyright (c) 2013 Ilya Grigorik ![GA](https://www.google-analytics.com/__utm.gif?utmac=UA-71196-9&utmhn=github.com&utmdt=HTTP2&utmp=/http-2/readme)
data/Rakefile CHANGED
@@ -1,5 +1,6 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rspec/core/rake_task"
3
+ require_relative "lib/tasks/generate_huffman_table"
3
4
 
4
5
  desc "Run all RSpec tests"
5
6
  RSpec::Core::RakeTask.new(:spec)
@@ -39,10 +39,16 @@ end
39
39
 
40
40
  conn = HTTP2::Client.new
41
41
  conn.on(:frame) do |bytes|
42
- puts "Sending bytes: #{bytes.inspect}"
42
+ # puts "Sending bytes: #{bytes.unpack("H*").first}"
43
43
  sock.print bytes
44
44
  sock.flush
45
45
  end
46
+ conn.on(:frame_sent) do |frame|
47
+ puts "Sent frame: #{frame.inspect}"
48
+ end
49
+ conn.on(:frame_received) do |frame|
50
+ puts "Received frame: #{frame.inspect}"
51
+ end
46
52
 
47
53
  stream = conn.new_stream
48
54
  log = Logger.new(stream.id)
@@ -57,6 +63,10 @@ conn.on(:promise) do |promise|
57
63
  end
58
64
  end
59
65
 
66
+ conn.on(:altsvc) do |f|
67
+ log.info "received ALTSVC #{f}"
68
+ end
69
+
60
70
  stream.on(:close) do
61
71
  log.info "stream closed"
62
72
  sock.close
@@ -75,16 +85,21 @@ stream.on(:data) do |d|
75
85
  log.info "response data chunk: <<#{d}>>"
76
86
  end
77
87
 
88
+ stream.on(:altsvc) do |f|
89
+ log.info "received ALTSVC #{f}"
90
+ end
91
+
92
+
78
93
  head = {
79
94
  ":scheme" => uri.scheme,
80
- ":method" => (options[:payload].nil? ? "get" : "post"),
81
- ":host" => [uri.host, uri.port].join(':'),
95
+ ":method" => (options[:payload].nil? ? "GET" : "POST"),
96
+ ":authority" => [uri.host, uri.port].join(':'),
82
97
  ":path" => uri.path,
83
98
  "accept" => "*/*"
84
99
  }
85
100
 
86
101
  puts "Sending HTTP 2.0 request"
87
- if head[":method"] == "get"
102
+ if head[":method"] == "GET"
88
103
  stream.headers(head, end_stream: true)
89
104
  else
90
105
  stream.headers(head, end_stream: false)
@@ -93,7 +108,7 @@ end
93
108
 
94
109
  while !sock.closed? && !sock.eof?
95
110
  data = sock.read_nonblock(1024)
96
- # puts "Received bytes: #{data.inspect}"
111
+ # puts "Received bytes: #{data.unpack("H*").first}"
97
112
 
98
113
  begin
99
114
  conn << data
@@ -6,7 +6,7 @@ require 'openssl'
6
6
  require 'http/2'
7
7
  require 'uri'
8
8
 
9
- DRAFT = 'HTTP-draft-06/2.0'
9
+ DRAFT = 'h2-14'
10
10
 
11
11
  class Logger
12
12
  def initialize(id)
@@ -1,24 +1,23 @@
1
1
  -----BEGIN CERTIFICATE-----
2
- MIID9DCCAtygAwIBAgIJAKIwkCCNJr2mMA0GCSqGSIb3DQEBBQUAMFkxCzAJBgNV
3
- BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
4
- aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xMjA2MjQxODE4
5
- MTZaFw0xMzA2MjQxODE4MTZaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21l
6
- LVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV
7
- BAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOTQ
8
- eHgUpZnggH2RwBeghuf86/7wNeO4DIJacLrmCynA871ioOvxzPx5jPTV6dkkitMT
9
- lNroneNa/BbRHHOVUvCtuJyKXEiFyWs/jCYAZ335uMYtDK2FAHRvNgXMM1YZTpbM
10
- CSIteVsWIAuNFAWwQS8Bd9oACUS1P69eNbA1BBOqnaYoZT1D+JD4bp9kpBYqXJtf
11
- wOgHGzPOVatqzJDgOqSwYVz1ZETVopv+TMBght938iVoow2cHnJ2Zj/n/jBkG8E9
12
- FNlmc67WqQkJsS16E9sEntJOSNldBRsjMFux4E+g7wzyn9tFJ0nYJ/rklWa24BN7
13
- Vg9diYljQNLLSMv8/tcCAwEAAaOBvjCBuzAdBgNVHQ4EFgQUqBtvGKKXRaXzFV16
14
- W1uMzroa8SkwgYsGA1UdIwSBgzCBgIAUqBtvGKKXRaXzFV16W1uMzroa8SmhXaRb
15
- MFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJ
16
- bnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWxvY2FsaG9zdIIJAKIw
17
- kCCNJr2mMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBALji7DAGBRU9
18
- x6/zw7EcEDlutGv6qp1VasJzSexVCZAr5c7vSDbdM+wlAxvwBguoFNLvrIvBxUMN
19
- pf/lvig2Q++lGTiqu5VodnmJXnqze7PO2Rdq9Yqgkva7qVFuGrYwGTXfiuz5LqGV
20
- BWz/i1i8EwJmO+p0yLk+r1hjBaeAqfbZtt1ACeZgu/8zaI9ypb2vpwOyW5TUJ8hk
21
- S+ZAX1Pd5OMqerD6DTfD/McHkWnC3ilcW4ZQIIJABKGxPc7k9Gyod9PZIPJS/ER7
22
- 7koJ84u8esvnaP3DGHIuWngMpQcgMzJbD5bNX+C0DoaxkWre8u7afCW68a58RP19
23
- LTlr/GlI51c=
2
+ MIID1zCCAr+gAwIBAgIJANjbVITTVqaAMA0GCSqGSIb3DQEBBQUAMFAxCzAJBgNV
3
+ BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMRgwFgYDVQQKEw9TUERZIFByb3h5
4
+ IERlbW8xEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xNDEwMTQwNDUwMTJaFw0xNTEw
5
+ MTQwNDUwMTJaMFAxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMRgw
6
+ FgYDVQQKEw9TUERZIFByb3h5IERlbW8xEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIw
7
+ DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMxv7mqzNMVVFoBjqOSUy2cNM9c6
8
+ 6gTgVLr9ssoLU0TC4biY1B/KoD7G9Ive6PwfdpipgGY+tuPHfEzBCCHD7exER1NL
9
+ npWauo6Lwh3wOjuo5Er6klgBGFuHYx8jJ2jBwCFvTcG2zJRedU/Pby6Fa27X6acw
10
+ faAtReG5YOHs8YRmg4ErWqfRucoM3zj8vvMWnushMhYQxo1EVLJ2EvvbHEkip4ap
11
+ pro+2Ql0KY4XT3EoMTRHICbolK/uQYoe0musKnwCGPg2NL6e27uvi47G7GrIpcf3
12
+ HN4HZMoOzJ8ti7IIEkF0fVTgQEVkluInfned69WCwxecMQZs5sdBuwE3Kh0CAwEA
13
+ AaOBszCBsDAdBgNVHQ4EFgQU86bqiYciIYDN+KAPlnJL6tSbH6IwgYAGA1UdIwR5
14
+ MHeAFPOm6omHIiGAzfigD5ZyS+rUmx+ioVSkUjBQMQswCQYDVQQGEwJBVTETMBEG
15
+ A1UECBMKU29tZS1TdGF0ZTEYMBYGA1UEChMPU1BEWSBQcm94eSBEZW1vMRIwEAYD
16
+ VQQDEwlsb2NhbGhvc3SCCQDY21SE01amgDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3
17
+ DQEBBQUAA4IBAQCkcr0DLPCbP5l9G0YI/XKVsUW9fXcTvge6Eko0R8qAkzTcsZQv
18
+ DbKcIM3z52QguCuJ9k63X4p174FKq7+qmieqaifosGKV03pyyxWLMpRooUUVXEBM
19
+ gZaRfp9VG2N4zrRaIklOSkAscnwybv2U3LZhKDlc7Yatsr1/TFkbCnzll514UnTz
20
+ ewjrlzVitUSEkwEGvLhKQuVPM9/3MAm+ztFpx846/GZ2XJSAFQLtHudjMXnFLihA
21
+ 7nGZvE4rudyT70YsKu0BP0KjVZXrxTh81C4kyJu9xo4YuiDCFtvwtjoty0ygbuQN
22
+ a38i0bxFlYFmbWHooNCPUWVy59MOnW9zxaTV
24
23
  -----END CERTIFICATE-----
@@ -1,27 +1,27 @@
1
1
  -----BEGIN RSA PRIVATE KEY-----
2
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- h8yqipY3aJD5xCz96Fv9GWLqDJGXVmDl8+mg8hUofPhSMUnNEO4/UgVeku/5zXvk
21
- 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
- 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
- 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
- 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
- M5nqfiH5QjO1NkKXrqUYAFVdAxcLRPXSAMHK0G61HyWDKyUEwAqWQ4sZGgFrzFUb
26
- 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
27
27
  -----END RSA PRIVATE KEY-----
@@ -31,9 +31,15 @@ loop do
31
31
 
32
32
  conn = HTTP2::Server.new
33
33
  conn.on(:frame) do |bytes|
34
- puts "Writing bytes: #{bytes.inspect}"
34
+ # puts "Writing bytes: #{bytes.unpack("H*").first}"
35
35
  sock.write bytes
36
36
  end
37
+ conn.on(:frame_sent) do |frame|
38
+ puts "Sent frame: #{frame.inspect}"
39
+ end
40
+ conn.on(:frame_received) do |frame|
41
+ puts "Received frame: #{frame.inspect}"
42
+ end
37
43
 
38
44
  conn.on(:stream) do |stream|
39
45
  log = Logger.new(stream.id)
@@ -43,7 +49,7 @@ loop do
43
49
  stream.on(:close) { log.info "stream closed" }
44
50
 
45
51
  stream.on(:headers) do |h|
46
- req = h
52
+ req = Hash[*h.flatten]
47
53
  log.info "request headers: #{h}"
48
54
  end
49
55
 
@@ -56,7 +62,7 @@ loop do
56
62
  log.info "client closed its end of the stream"
57
63
 
58
64
  response = nil
59
- if req[":method"] == "post"
65
+ if req[":method"] == "POST"
60
66
  log.info "Received POST request, payload: #{buffer}"
61
67
  response = "Hello HTTP 2.0! POST payload: #{buffer}"
62
68
  else
@@ -78,6 +84,7 @@ loop do
78
84
 
79
85
  while !sock.closed? && !sock.eof?
80
86
  data = sock.readpartial(1024)
87
+ # puts "Received bytes: #{data.unpack("H*").first}"
81
88
 
82
89
  begin
83
90
  conn << data
@@ -6,7 +6,7 @@ require 'http/2/version'
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "http-2"
8
8
  spec.version = HTTP2::VERSION
9
- spec.authors = ["Ilya Grigorik"]
9
+ spec.authors = ["Ilya Grigorik", "Kaoru Maeda"]
10
10
  spec.email = ["ilya@igvita.com"]
11
11
  spec.description = "Pure-ruby HTTP 2.0 protocol implementation"
12
12
  spec.summary = spec.description
@@ -3,6 +3,8 @@ require "http/2/error"
3
3
  require "http/2/emitter"
4
4
  require "http/2/buffer"
5
5
  require "http/2/flow_buffer"
6
+ require "http/2/huffman"
7
+ require "http/2/huffman_statemachine"
6
8
  require "http/2/compressor"
7
9
  require "http/2/framer"
8
10
  require "http/2/connection"
@@ -20,11 +20,12 @@ module HTTP2
20
20
  class Client < Connection
21
21
 
22
22
  # Initialize new HTTP 2.0 client object.
23
- def initialize(*args)
23
+ def initialize(**settings)
24
24
  @stream_id = 1
25
- @state = :connection_header
26
- @compressor = Header::Compressor.new(:request)
27
- @decompressor = Header::Decompressor.new(:response)
25
+ @state = :waiting_connection_preface
26
+
27
+ @local_role = :client
28
+ @remote_role = :server
28
29
 
29
30
  super
30
31
  end
@@ -33,18 +34,23 @@ module HTTP2
33
34
  # by Connection class.
34
35
  #
35
36
  # @see Connection
36
- # @note Client will emit the connection header as the first 24 bytes
37
37
  # @param frame [Hash]
38
38
  def send(frame)
39
- if @state == :connection_header
40
- emit(:frame, CONNECTION_HEADER)
39
+ send_connection_preface
40
+ super(frame)
41
+ end
42
+
43
+ # Emit the connection preface if not yet
44
+ def send_connection_preface
45
+ if @state == :waiting_connection_preface
41
46
  @state = :connected
47
+ emit(:frame, CONNECTION_PREFACE_MAGIC)
42
48
 
43
- settings(stream_limit: @stream_limit, window_limit: @window_limit)
49
+ payload = @local_settings.select {|k,v| v != SPEC_DEFAULT_CONNECTION_SETTINGS[k]}
50
+ settings(payload)
44
51
  end
45
-
46
- super(frame)
47
52
  end
53
+
48
54
  end
49
55
 
50
56
  end
@@ -3,148 +3,179 @@ module HTTP2
3
3
  # Implementation of header compression for HTTP 2.0 (HPACK) format adapted
4
4
  # to efficiently represent HTTP headers in the context of HTTP 2.0.
5
5
  #
6
- # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression
6
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-09
7
7
  module Header
8
8
 
9
- # The set of components used to encode or decode a header set form an
10
- # encoding context: an encoding context contains a header table and a
11
- # reference set - there is one encoding context for each direction.
12
- #
9
+ BINARY = 'binary'
10
+
11
+ # To decompress header blocks, a decoder only needs to maintain a
12
+ # header table as a decoding context.
13
+ # No other state information is needed.
13
14
  class EncodingContext
14
15
  include Error
15
16
 
16
- # TODO: replace StringIO with Buffer...
17
-
18
- # Default request working set as defined by the spec.
19
- REQ_DEFAULTS = [
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
- ]
51
-
52
- # Default response working set as defined by the spec.
53
- RESP_DEFAULTS = [
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
- ]
17
+ # @private
18
+ # Static table
19
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-09#appendix-B
20
+ STATIC_TABLE = [
21
+ [':authority', '' ],
22
+ [':method', 'GET' ],
23
+ [':method', 'POST' ],
24
+ [':path', '/' ],
25
+ [':path', '/index.html' ],
26
+ [':scheme', 'http' ],
27
+ [':scheme', 'https' ],
28
+ [':status', '200' ],
29
+ [':status', '204' ],
30
+ [':status', '206' ],
31
+ [':status', '304' ],
32
+ [':status', '400' ],
33
+ [':status', '404' ],
34
+ [':status', '500' ],
35
+ ['accept-charset', '' ],
36
+ ['accept-encoding', 'gzip, deflate' ],
37
+ ['accept-language', '' ],
38
+ ['accept-ranges', '' ],
39
+ ['accept', '' ],
40
+ ['access-control-allow-origin', '' ],
41
+ ['age', '' ],
42
+ ['allow', '' ],
43
+ ['authorization', '' ],
44
+ ['cache-control', '' ],
45
+ ['content-disposition', '' ],
46
+ ['content-encoding', '' ],
47
+ ['content-language', '' ],
48
+ ['content-length', '' ],
49
+ ['content-location', '' ],
50
+ ['content-range', '' ],
51
+ ['content-type', '' ],
52
+ ['cookie', '' ],
53
+ ['date', '' ],
54
+ ['etag', '' ],
55
+ ['expect', '' ],
56
+ ['expires', '' ],
57
+ ['from', '' ],
58
+ ['host', '' ],
59
+ ['if-match', '' ],
60
+ ['if-modified-since', '' ],
61
+ ['if-none-match', '' ],
62
+ ['if-range', '' ],
63
+ ['if-unmodified-since', '' ],
64
+ ['last-modified', '' ],
65
+ ['link', '' ],
66
+ ['location', '' ],
67
+ ['max-forwards', '' ],
68
+ ['proxy-authenticate', '' ],
69
+ ['proxy-authorization', '' ],
70
+ ['range', '' ],
71
+ ['referer', '' ],
72
+ ['refresh', '' ],
73
+ ['retry-after', '' ],
74
+ ['server', '' ],
75
+ ['set-cookie', '' ],
76
+ ['strict-transport-security', '' ],
77
+ ['transfer-encoding', '' ],
78
+ ['user-agent', '' ],
79
+ ['vary', '' ],
80
+ ['via', '' ],
81
+ ['www-authenticate', '' ],
82
+ ].freeze
85
83
 
86
84
  # Current table of header key-value pairs.
87
85
  attr_reader :table
88
86
 
89
- # Current reference set of header key-value pairs.
90
- attr_reader :refset
87
+ # Current encoding options
88
+ #
89
+ # :table_size Integer maximum header table size in bytes
90
+ # :huffman Symbol :always, :never, :shorter
91
+ # :index Symbol :all, :static, :never
92
+ attr_reader :options
91
93
 
92
94
  # Initializes compression context with appropriate client/server
93
95
  # defaults and maximum size of the header table.
94
96
  #
95
- # @param type [Symbol] either :request or :response
96
- # @param limit [Integer] maximum header table size in bytes
97
- def initialize(type, limit = 4096)
98
- @type = type
99
- @table = (type == :request) ? REQ_DEFAULTS.dup : RESP_DEFAULTS.dup
100
- @limit = limit
101
- @refset = []
97
+ # @param options [Hash] encoding options
98
+ # :table_size Integer maximum header table size in bytes
99
+ # :huffman Symbol :always, :never, :shorter
100
+ # :index Symbol :all, :static, :never
101
+ def initialize(**options)
102
+ default_options = {
103
+ huffman: :shorter,
104
+ index: :all,
105
+ table_size: 4096,
106
+ }
107
+ @table = []
108
+ @options = default_options.merge(options)
109
+ @limit = @options[:table_size]
102
110
  end
103
111
 
104
- # Performs differential coding based on provided command type.
105
- # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#section-3.2
112
+ # Duplicates current compression context
113
+ # @return [EncodingContext]
114
+ def dup
115
+ other = EncodingContext.new(@options)
116
+ t = @table
117
+ l = @limit
118
+ other.instance_eval {
119
+ @table = t.dup # shallow copy
120
+ @limit = l
121
+ }
122
+ other
123
+ end
124
+
125
+ # Finds an entry in current header table by index.
126
+ # Note that index is zero-based in this module.
106
127
  #
107
- # @param cmd [Hash]
108
- # @return [Hash] emitted header
128
+ # If the index is greater than the last index in the static table,
129
+ # an entry in the header table is dereferenced.
130
+ #
131
+ # If the index is greater than the last header index, an error is raised.
132
+ #
133
+ # @param index [Integer] zero-based index in the header table.
134
+ # @return [Array] +[key, value]+
135
+ def dereference(index)
136
+ # NOTE: index is zero-based in this module.
137
+ STATIC_TABLE[index] or
138
+ @table[index - STATIC_TABLE.size] or
139
+ raise CompressionError.new("Index too large")
140
+ end
141
+
142
+ # Header Block Processing
143
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-09#section-4.1
144
+ #
145
+ # @param cmd [Hash] { type:, name:, value:, index: }
146
+ # @return [Array] +[name, value]+ header field that is added to the decoded header list
109
147
  def process(cmd)
110
148
  emit = nil
111
149
 
112
- # indexed representation
113
- if cmd[:type] == :indexed
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
- #
150
+ case cmd[:type]
151
+ when :changetablesize
152
+ set_table_size(cmd[:value])
153
+
154
+ when :indexed
155
+ # Indexed Representation
156
+ # An _indexed representation_ entails the following actions:
157
+ # o The header field corresponding to the referenced entry in either
158
+ # the static table or header table is added to the decoded header
159
+ # list.
123
160
  idx = cmd[:name]
124
- cur = @refset.find_index {|(i,v)| i == idx}
125
161
 
126
- if cur
127
- @refset.delete_at(cur)
128
- else
129
- emit = @table[idx]
130
- @refset.push [idx, @table[idx]]
131
- end
162
+ k, v = dereference(idx)
163
+ emit = [k, v]
132
164
 
133
- else
134
- # A literal representation that is not added to the header table
165
+ when :incremental, :noindex, :neverindexed
166
+ # A _literal representation_ that is _not added_ to the header table
135
167
  # 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
- #
168
+ # o The header field is added to the decoded header list.
169
+
170
+ # A _literal representation_ that is _added_ to the header table
171
+ # entails the following actions:
172
+ # o The header field is added to the decoded header list.
173
+ # o The header field is inserted at the beginning of the header table.
174
+
145
175
  if cmd[:name].is_a? Integer
146
- k,v = @table[cmd[:name]]
176
+ k, v = dereference(cmd[:name])
147
177
 
178
+ cmd = cmd.dup
148
179
  cmd[:index] ||= cmd[:name]
149
180
  cmd[:value] ||= v
150
181
  cmd[:name] = k
@@ -152,148 +183,167 @@ module HTTP2
152
183
 
153
184
  emit = [cmd[:name], cmd[:value]]
154
185
 
155
- if cmd[:type] != :noindex
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
167
- end
168
-
169
- @table[cmd[:index]] = emit
170
- @refset.push [cmd[:index], emit]
171
- end
186
+ if cmd[:type] == :incremental
187
+ add_to_table(emit)
172
188
  end
189
+
190
+ else
191
+ raise CompressionError.new("Invalid type: #{cmd[:type]}")
173
192
  end
174
193
 
175
194
  emit
176
195
  end
177
196
 
178
- # Emits best available command to encode provided header.
197
+ # Plan header compression according to +@options [:index]+
198
+ # :never Do not use header table or static table reference at all.
199
+ # :static Use static table only.
200
+ # :all Use all of them.
179
201
  #
180
- # @param header [Hash]
202
+ # @param headers [Array] +[[name, value], ...]+
203
+ # @return [Array] array of commands
204
+ def encode(headers)
205
+ commands = []
206
+ # Literals commands are marked with :noindex when index is not used
207
+ noindex = [:static, :never].include?(@options[:index])
208
+ headers.each do |h|
209
+ cmd = addcmd(h)
210
+ if noindex && cmd[:type] == :incremental
211
+ cmd[:type] = :noindex
212
+ end
213
+ commands << cmd
214
+ process(cmd)
215
+ end
216
+ commands
217
+ end
218
+
219
+ # Emits command for a header.
220
+ # Prefer static table over header table.
221
+ # Prefer exact match over name-only match.
222
+ #
223
+ # +@options [:index]+ controls whether to use the header table,
224
+ # static table, or both.
225
+ # :never Do not use header table or static table reference at all.
226
+ # :static Use static table only.
227
+ # :all Use all of them.
228
+ #
229
+ # @param header [Array] +[name, value]+
230
+ # @return [Hash] command
181
231
  def addcmd(header)
182
- # check if we have an exact match in header table
183
- if idx = @table.index(header)
184
- if !active? idx
185
- return { name: idx, type: :indexed }
232
+ exact = nil
233
+ name_only = nil
234
+
235
+ if [:all, :static].include?(@options[:index])
236
+ STATIC_TABLE.each_index do |i|
237
+ if STATIC_TABLE[i] == header
238
+ exact ||= i
239
+ break
240
+ elsif STATIC_TABLE[i].first == header.first
241
+ name_only ||= i
242
+ end
243
+ end
244
+ end
245
+ if [:all].include?(@options[:index]) && !exact
246
+ @table.each_index do |i|
247
+ if @table[i] == header
248
+ exact ||= i + STATIC_TABLE.size
249
+ break
250
+ elsif @table[i].first == header.first
251
+ name_only ||= i + STATIC_TABLE.size
252
+ end
186
253
  end
187
254
  end
188
255
 
189
- # check if we have a partial match on header name
190
- if idx = @table.index {|(k,_)| k == header.first}
191
- # default to incremental indexing
192
- cmd = { name: idx, value: header.last, type: :incremental}
193
-
194
- # TODO: implement literal without indexing strategy
195
- # TODO: implement substitution strategy (if it makes sense)
196
- # if default? idx
197
- # cmd[:type] = :incremental
198
- # else
199
- # cmd[:type] = :substitution
200
- # cmd[:index] = idx
201
- # end
202
-
203
- return cmd
256
+ if exact
257
+ { name: exact, type: :indexed }
258
+ elsif name_only
259
+ { name: name_only, value: header.last, type: :incremental }
260
+ else
261
+ { name: header.first, value: header.last, type: :incremental }
204
262
  end
263
+ end
205
264
 
206
- return { name: header.first, value: header.last, type: :incremental }
265
+ # Alter header table size.
266
+ # When the size is reduced, some headers might be evicted.
267
+ def set_table_size(size)
268
+ @limit = size
269
+ size_check(nil)
207
270
  end
208
271
 
209
- # Emits command to remove current index from working set.
210
- #
211
- # @param idx [Integer]
212
- def removecmd(idx)
213
- {name: idx, type: :indexed}
272
+ # Returns current table size in octets
273
+ # @return [Integer]
274
+ def current_table_size
275
+ @table.inject(0){|r,(k,v)| r += k.bytesize + v.bytesize + 32 }
214
276
  end
215
277
 
216
278
  private
217
279
 
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.
280
+ # Add a name-value pair to the header table.
281
+ # Older entries might have been evicted so that
282
+ # the new entry fits in the header table.
223
283
  #
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.
284
+ # @param cmd [Array] +[name, value]+
285
+ def add_to_table(cmd)
286
+ if size_check(cmd)
287
+ @table.unshift(cmd)
288
+ end
289
+ end
290
+
291
+ # To keep the header table size lower than or equal to @limit,
292
+ # remove one or more entries at the end of the header table.
227
293
  #
228
294
  # @param cmd [Hash]
229
- # @return [Boolean]
295
+ # @return [Boolean] whether +cmd+ fits in the header table.
230
296
  def size_check(cmd)
231
- cursize = @table.join.bytesize + @table.size * 32
232
- cmdsize = cmd[:name].bytesize + cmd[:value].bytesize + 32
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
-
245
- cur = 0
246
- while (cursize + cmdsize) > @limit do
247
- e = @table.shift
248
-
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.
255
- if cmd[:type] == :substitution && cur == cmd[:index]
256
- cmd[:type] = :prepend
257
- end
258
-
259
- cursize -= (e.join.bytesize + 32)
260
- end
297
+ cursize = current_table_size
298
+ cmdsize = cmd.nil? ? 0 : cmd[0].bytesize + cmd[1].bytesize + 32
261
299
 
262
- return true
263
- end
300
+ while cursize + cmdsize > @limit do
301
+ break if @table.empty?
264
302
 
265
- def active?(idx)
266
- !@refset.find {|i,_| i == idx }.nil?
267
- end
303
+ last_index = @table.size - 1
304
+ e = @table.pop
305
+ cursize -= e[0].bytesize + e[1].bytesize + 32
306
+ end
268
307
 
269
- def default?(idx)
270
- t = (@type == :request) ? REQ_DEFAULTS : RESP_DEFAULTS
271
- idx < t.size
308
+ return cmdsize <= @limit
272
309
  end
273
310
  end
274
311
 
275
312
  # Header representation as defined by the spec.
276
313
  HEADREP = {
277
314
  indexed: {prefix: 7, pattern: 0x80},
278
- noindex: {prefix: 5, pattern: 0x60},
279
- incremental: {prefix: 5, pattern: 0x40},
280
- substitution: {prefix: 6, pattern: 0x00}
315
+ incremental: {prefix: 6, pattern: 0x40},
316
+ noindex: {prefix: 4, pattern: 0x00},
317
+ neverindexed: {prefix: 4, pattern: 0x10},
318
+ changetablesize: {prefix: 5, pattern: 0x20},
281
319
  }
282
320
 
321
+ # Predefined options set for Compressor
322
+ # http://mew.org/~kazu/material/2014-hpack.pdf
323
+ NAIVE = { index: :never, huffman: :never }.freeze
324
+ LINEAR = { index: :all, huffman: :never }.freeze
325
+ STATIC = { index: :static, huffman: :never }.freeze
326
+ SHORTER = { index: :all, huffman: :never }.freeze
327
+ NAIVEH = { index: :never, huffman: :always }.freeze
328
+ LINEARH = { index: :all, huffman: :always }.freeze
329
+ STATICH = { index: :static, huffman: :always }.freeze
330
+ SHORTERH = { index: :all, huffman: :shorter }.freeze
331
+
283
332
  # Responsible for encoding header key-value pairs using HPACK algorithm.
284
- # Compressor must be initialized with appropriate starting context based
285
- # on local role: client or server.
286
- #
287
- # @example
288
- # client_role = Compressor.new(:request)
289
- # server_role = Compressor.new(:response)
290
333
  class Compressor
291
- def initialize(type)
292
- @cc = EncodingContext.new(type)
334
+ # @param options [Hash] encoding options
335
+ def initialize(**options)
336
+ @cc = EncodingContext.new(options)
337
+ end
338
+
339
+ # Set header table size in EncodingContext
340
+ # @param size [Integer] new header table size
341
+ def set_table_size(size)
342
+ @cc.set_table_size(size)
293
343
  end
294
344
 
295
345
  # Encodes provided value via integer representation.
296
- # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#section-4.1.1
346
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-09#section-6.1
297
347
  #
298
348
  # If I < 2^N - 1, encode I on N bits
299
349
  # Else
@@ -325,31 +375,58 @@ module HTTP2
325
375
  end
326
376
 
327
377
  # Encodes provided value via string literal representation.
328
- # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#section-4.1.3
378
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-09#section-6.2
329
379
  #
330
380
  # * The string length, defined as the number of bytes needed to store
331
- # its UTF-8 representation, is represented as an integer with a zero
332
- # bits prefix. If the string length is strictly less than 128, it is
381
+ # its UTF-8 representation, is represented as an integer with a seven
382
+ # bits prefix. If the string length is strictly less than 127, it is
333
383
  # represented as one byte.
334
- # * The string value represented as a list of UTF-8 character
384
+ # * If the bit 7 of the first byte is 1, the string value is represented
385
+ # as a list of Huffman encoded octets
386
+ # (padded with bit 1's until next octet boundary).
387
+ # * If the bit 7 of the first byte is 0, the string value is
388
+ # represented as a list of UTF-8 encoded octets.
389
+ #
390
+ # +@options [:huffman]+ controls whether to use Huffman encoding:
391
+ # :never Do not use Huffman encoding
392
+ # :always Always use Huffman encoding
393
+ # :shorter Use Huffman when the result is strictly shorter
335
394
  #
336
395
  # @param str [String]
337
396
  # @return [String] binary string
338
397
  def string(str)
339
- integer(str.bytesize, 0) << str.dup.force_encoding('binary')
398
+ plain, huffman = nil, nil
399
+ unless @cc.options[:huffman] == :always
400
+ plain = integer(str.bytesize, 7) << str.dup.force_encoding(BINARY)
401
+ end
402
+ unless @cc.options[:huffman] == :never
403
+ huffman = Huffman.new.encode(str)
404
+ huffman = integer(huffman.bytesize, 7) << huffman
405
+ huffman.setbyte(0, huffman.ord | 0x80)
406
+ end
407
+ case @cc.options[:huffman]
408
+ when :always
409
+ huffman
410
+ when :never
411
+ plain
412
+ else
413
+ huffman.bytesize < plain.bytesize ? huffman : plain
414
+ end
340
415
  end
341
416
 
342
417
  # Encodes header command with appropriate header representation.
343
- # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#section-4
344
418
  #
345
419
  # @param h [Hash] header command
346
420
  # @param buffer [String]
421
+ # @return [Buffer]
347
422
  def header(h, buffer = Buffer.new)
348
423
  rep = HEADREP[h[:type]]
349
424
 
350
- if h[:type] == :indexed
351
- buffer << integer(h[:name], rep[:prefix])
352
-
425
+ case h[:type]
426
+ when :indexed
427
+ buffer << integer(h[:name]+1, rep[:prefix])
428
+ when :changetablesize
429
+ buffer << integer(h[:value], rep[:prefix])
353
430
  else
354
431
  if h[:name].is_a? Integer
355
432
  buffer << integer(h[:name]+1, rep[:prefix])
@@ -358,19 +435,11 @@ module HTTP2
358
435
  buffer << string(h[:name])
359
436
  end
360
437
 
361
- if h[:type] == :substitution
362
- buffer << integer(h[:index], 0)
363
- end
364
-
365
- if h[:value].is_a? Integer
366
- buffer << integer(h[:value], 0)
367
- else
368
- buffer << string(h[:value])
369
- end
438
+ buffer << string(h[:value])
370
439
  end
371
440
 
372
441
  # set header representation pattern on first byte
373
- fb = buffer[0].unpack("C").first | rep[:pattern]
442
+ fb = buffer.ord | rep[:pattern]
374
443
  buffer.setbyte(0, fb)
375
444
 
376
445
  buffer
@@ -378,32 +447,17 @@ module HTTP2
378
447
 
379
448
  # Encodes provided list of HTTP headers.
380
449
  #
381
- # @param headers [Hash]
450
+ # @param headers [Array] +[[name, value], ...]+
382
451
  # @return [Buffer]
383
452
  def encode(headers)
384
453
  buffer = Buffer.new
385
- commands = []
386
454
 
387
455
  # Literal header names MUST be translated to lowercase before
388
456
  # 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
- if headers.find {|(hk,hv)| hk == wk && hv == wv }.nil?
394
- commands.push @cc.removecmd idx
395
- end
396
- end
397
-
398
- # Generate add commands for new headers
399
- headers.each do |(hk,hv)|
400
- if @cc.refset.find {|i,(wk,wv)| hk == wk && hv == wv}.nil?
401
- commands.push @cc.addcmd [hk, hv]
402
- end
403
- end
457
+ headers.map! {|hk,hv| [hk.downcase, hv] }
404
458
 
459
+ commands = @cc.encode(headers)
405
460
  commands.each do |cmd|
406
- @cc.process cmd.dup
407
461
  buffer << header(cmd)
408
462
  end
409
463
 
@@ -419,14 +473,22 @@ module HTTP2
419
473
  # server_role = Decompressor.new(:request)
420
474
  # client_role = Decompressor.new(:response)
421
475
  class Decompressor
422
- def initialize(type)
423
- @cc = EncodingContext.new(type)
476
+ # @param options [Hash] decoding options. Only :table_size is effective.
477
+ def initialize(**options)
478
+ @cc = EncodingContext.new(options)
479
+ end
480
+
481
+ # Set header table size in EncodingContext
482
+ # @param size [Integer] new header table size
483
+ def set_table_size(size)
484
+ @cc.set_table_size(size)
424
485
  end
425
486
 
426
487
  # Decodes integer value from provided buffer.
427
488
  #
428
489
  # @param buf [String]
429
490
  # @param n [Integer] number of available bits
491
+ # @return [Integer]
430
492
  def integer(buf, n)
431
493
  limit = 2**n - 1
432
494
  i = !n.zero? ? (buf.getbyte & limit) : 0
@@ -446,13 +508,21 @@ module HTTP2
446
508
  #
447
509
  # @param buf [String]
448
510
  # @return [String] UTF-8 encoded string
511
+ # @raise [CompressionError] when input is malformed
449
512
  def string(buf)
450
- buf.read(integer(buf, 0)).force_encoding('utf-8')
513
+ huffman = (buf.readbyte(0) & 0x80) == 0x80
514
+ len = integer(buf, 7)
515
+ str = buf.read(len)
516
+ str.bytesize == len or raise CompressionError.new("string too short")
517
+ huffman and str = Huffman.new.decode(Buffer.new(str))
518
+ str = str.force_encoding('utf-8')
519
+ str
451
520
  end
452
521
 
453
522
  # Decodes header command from provided buffer.
454
523
  #
455
524
  # @param buf [Buffer]
525
+ # @return [Hash] command
456
526
  def header(buf)
457
527
  peek = buf.readbyte(0)
458
528
 
@@ -462,18 +532,22 @@ module HTTP2
462
532
  mask == desc[:pattern]
463
533
  end.first
464
534
 
535
+ header[:type] or raise CompressionError
536
+
465
537
  header[:name] = integer(buf, type[:prefix])
466
- if header[:type] != :indexed
467
- header[:name] -= 1
468
538
 
469
- if header[:name] == -1
539
+ case header[:type]
540
+ when :indexed
541
+ header[:name] == 0 and raise CompressionError.new
542
+ header[:name] -= 1
543
+ when :changetablesize
544
+ header[:value] = header[:name]
545
+ else
546
+ if header[:name] == 0
470
547
  header[:name] = string(buf)
548
+ else
549
+ header[:name] -= 1
471
550
  end
472
-
473
- if header[:type] == :substitution
474
- header[:index] = integer(buf, 0)
475
- end
476
-
477
551
  header[:value] = string(buf)
478
552
  end
479
553
 
@@ -482,26 +556,12 @@ module HTTP2
482
556
 
483
557
  # Decodes and processes header commands within provided buffer.
484
558
  #
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
559
  # @param buf [Buffer]
496
- # @return [Array] set of HTTP headers
560
+ # @return [Array] +[[name, value], ...]+
497
561
  def decode(buf)
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
562
+ list = []
563
+ list << @cc.process(header(buf)) while !buf.empty?
564
+ list.compact
505
565
  end
506
566
  end
507
567