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 +4 -4
- data/.coveralls.yml +1 -0
- data/.gitmodules +3 -0
- data/Gemfile +5 -0
- data/README.md +5 -4
- data/Rakefile +1 -0
- data/example/client.rb +20 -5
- data/example/helper.rb +1 -1
- data/example/keys/mycert.pem +21 -22
- data/example/keys/mykey.pem +25 -25
- data/example/server.rb +10 -3
- data/http-2.gemspec +1 -1
- data/lib/http/2.rb +2 -0
- data/lib/http/2/client.rb +16 -10
- data/lib/http/2/compressor.rb +346 -286
- data/lib/http/2/connection.rb +254 -95
- data/lib/http/2/error.rb +0 -6
- data/lib/http/2/flow_buffer.rb +12 -10
- data/lib/http/2/framer.rb +203 -57
- data/lib/http/2/huffman.rb +332 -0
- data/lib/http/2/huffman_statemachine.rb +272 -0
- data/lib/http/2/server.rb +5 -4
- data/lib/http/2/stream.rb +72 -35
- data/lib/http/2/version.rb +1 -1
- data/lib/tasks/generate_huffman_table.rb +160 -0
- data/spec/client_spec.rb +3 -3
- data/spec/compressor_spec.rb +422 -281
- data/spec/connection_spec.rb +236 -56
- data/spec/framer_spec.rb +213 -45
- data/spec/helper.rb +42 -15
- data/spec/hpack_test_spec.rb +83 -0
- data/spec/huffman_spec.rb +68 -0
- data/spec/server_spec.rb +7 -6
- data/spec/stream_spec.rb +81 -54
- metadata +21 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c7bbedd001cf6de0ec84caef4ccd0e4e818315d3
|
4
|
+
data.tar.gz: c1129e49ae319b1cf31151f4e75bf16f0a108e0a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6c68277cdbc49a1fd695c177e1015334946e4cbba9e93cc2b7b93572b45ab89c61e92ed2638f485c1eafaa2909f1d8a8d0c3ae352743697176b37bf1b27e8be3
|
7
|
+
data.tar.gz: 3357aafb0da7559f22230e8503bd025b4c993ce0fc1dbcc40d078b6b6d0bb5b98b831f40b0d9f53c0358b42939dc7697b72b319ce183975eaaea6e95c092b96a
|
data/.coveralls.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
service_name: travis-ci
|
data/.gitmodules
ADDED
data/Gemfile
CHANGED
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-
|
19
|
-
* [draft-ietf-httpbis-header-compression-
|
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](
|
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
data/example/client.rb
CHANGED
@@ -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.
|
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? ? "
|
81
|
-
":
|
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"] == "
|
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.
|
111
|
+
# puts "Received bytes: #{data.unpack("H*").first}"
|
97
112
|
|
98
113
|
begin
|
99
114
|
conn << data
|
data/example/helper.rb
CHANGED
data/example/keys/mycert.pem
CHANGED
@@ -1,24 +1,23 @@
|
|
1
1
|
-----BEGIN CERTIFICATE-----
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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-----
|
data/example/keys/mykey.pem
CHANGED
@@ -1,27 +1,27 @@
|
|
1
1
|
-----BEGIN RSA PRIVATE KEY-----
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
/
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
2
|
+
MIIEowIBAAKCAQEAzG/uarM0xVUWgGOo5JTLZw0z1zrqBOBUuv2yygtTRMLhuJjU
|
3
|
+
H8qgPsb0i97o/B92mKmAZj6248d8TMEIIcPt7ERHU0uelZq6jovCHfA6O6jkSvqS
|
4
|
+
WAEYW4djHyMnaMHAIW9NwbbMlF51T89vLoVrbtfppzB9oC1F4blg4ezxhGaDgSta
|
5
|
+
p9G5ygzfOPy+8xae6yEyFhDGjURUsnYS+9scSSKnhqmmuj7ZCXQpjhdPcSgxNEcg
|
6
|
+
JuiUr+5Bih7Sa6wqfAIY+DY0vp7bu6+Ljsbsasilx/cc3gdkyg7Mny2LsggSQXR9
|
7
|
+
VOBARWSW4id+d53r1YLDF5wxBmzmx0G7ATcqHQIDAQABAoIBACybj85AZBdaxZom
|
8
|
+
JMgbn3ZQ7yrbdAy0Vkim6sgjSHwMeewpjL+TGvwXtWx/qx64Tsxoz9d/f7Cb6odk
|
9
|
+
5z1W3ydajqWiLmw+Ys6PuD+IF2zFIWsq2ZvSQVpXZE17AjJddGrXOoQ2OtV09uv/
|
10
|
+
OydPfW2mNxl//ylgN4tVQ8qIRPq6b1GWWZvjTw4K3jPrlAifobYBBR+BSk446O7F
|
11
|
+
iGvax5lNNCDMN2y+6hlnhlTHuvc0DXQA0XBhWTNYu8BNNrvC3I31RmxdY7Frm7IA
|
12
|
+
RUGy/l2kLHCRCTF8Q0C4ydpE5ZFgpxkWK7p3QEv/gnVAwsOSN/nThdoorWWHTbNl
|
13
|
+
pA5l1RECgYEA/ASaS9mqWWthUkOW51L6c7IIiRPAhrbPnx1mkAtUPeehHn1+G8Qu
|
14
|
+
upUEXslWokhmQ3UAGhrId6FVYsfftNPMNck9mv4ntW7MoZLXZqTiFSqx4pQTjoYg
|
15
|
+
PQ4c/jrQLsmislcKTiVx6kFYFcnI1ayXXEtaby0lri8XsAR5F90OpycCgYEAz6re
|
16
|
+
DR5EZZKx61wyEfuPWE6aACWlEqlbTa8nTMddxnZUZXtzbwRFapGfs+uHCURF0dGj
|
17
|
+
37cl4q8JdGbbYePk9nlOC4RoSw8Zh3fB4yRSZocB4yB047ofpBmt4BigGtgZ5BLZ
|
18
|
+
zqVREgBUI+tFPPHkMmBY4lCaUsCe11SEwyZFzxsCgYEA3nRNonBy/tVbJZtFw9Eq
|
19
|
+
BB/9isolooQRxrjUBIgLh01Dmj9ZprbILKhHIEgGsd7IbfkD6wcDNx3w2e3mGJ7v
|
20
|
+
3fZR69M2R9+Sv3h3rEIU0mxKct8UWDUqldo0W3CcvP/9HgDYttw0rnuZfjoMjhf3
|
21
|
+
z18wZ3xpi1RES3nXTeox+fcCgYBlPxkjrC4Ml4jHBxwiSFOK6keK6s+gWZF6Pnsa
|
22
|
+
o9jEecyL7bRJ2/s8CeOjBKHBkte3hE4xNEn0SwKBDeTHxSRMRrgWRWfTsHjx4yFU
|
23
|
+
bND/y7LP2XMj1Aq5JwvuxhLJA7Mbz1UBuvfbnu1m1b3cCNMI/JBZRpL25ZKLyVkx
|
24
|
+
C+fdIQKBgA+tLeF10zqGGc4269b6nQWplc5E/qnIRK0cfnKb9BtffmA4FbjUpZKj
|
25
|
+
+cGmbtbw7ySkAIKLp4HoJmzkXJageGTSEb/sQIodxMiJCGvvgJmPPnGzU8OiUGAl
|
26
|
+
VmRjuAQ2eCcsUyvrJYgKW9UWskqSe6z5w/Uxo/sZdHlaGljNdKcn
|
27
27
|
-----END RSA PRIVATE KEY-----
|
data/example/server.rb
CHANGED
@@ -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.
|
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"] == "
|
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
|
data/http-2.gemspec
CHANGED
@@ -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
|
data/lib/http/2.rb
CHANGED
data/lib/http/2/client.rb
CHANGED
@@ -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(
|
23
|
+
def initialize(**settings)
|
24
24
|
@stream_id = 1
|
25
|
-
@state = :
|
26
|
-
|
27
|
-
@
|
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
|
-
|
40
|
-
|
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
|
-
|
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
|
data/lib/http/2/compressor.rb
CHANGED
@@ -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
|
-
|
10
|
-
|
11
|
-
#
|
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
|
-
#
|
17
|
-
|
18
|
-
#
|
19
|
-
|
20
|
-
[':
|
21
|
-
[':
|
22
|
-
[':
|
23
|
-
[':path'
|
24
|
-
[':
|
25
|
-
['
|
26
|
-
['
|
27
|
-
['
|
28
|
-
['
|
29
|
-
['
|
30
|
-
['
|
31
|
-
['
|
32
|
-
['
|
33
|
-
['
|
34
|
-
['
|
35
|
-
['
|
36
|
-
['
|
37
|
-
['
|
38
|
-
['
|
39
|
-
['
|
40
|
-
['
|
41
|
-
['
|
42
|
-
['
|
43
|
-
['
|
44
|
-
['
|
45
|
-
['
|
46
|
-
['
|
47
|
-
['
|
48
|
-
['
|
49
|
-
['
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
['
|
55
|
-
['
|
56
|
-
['
|
57
|
-
['
|
58
|
-
['
|
59
|
-
['
|
60
|
-
['
|
61
|
-
['
|
62
|
-
['
|
63
|
-
['
|
64
|
-
['
|
65
|
-
['
|
66
|
-
['
|
67
|
-
['
|
68
|
-
['
|
69
|
-
['
|
70
|
-
['
|
71
|
-
['
|
72
|
-
['
|
73
|
-
['
|
74
|
-
['
|
75
|
-
['
|
76
|
-
['
|
77
|
-
['
|
78
|
-
['
|
79
|
-
['
|
80
|
-
['
|
81
|
-
|
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
|
90
|
-
|
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
|
96
|
-
#
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
-
#
|
105
|
-
#
|
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
|
-
#
|
108
|
-
#
|
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
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
#
|
118
|
-
#
|
119
|
-
#
|
120
|
-
# the
|
121
|
-
#
|
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
|
-
|
127
|
-
|
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
|
-
|
134
|
-
# A
|
165
|
+
when :incremental, :noindex, :neverindexed
|
166
|
+
# A _literal representation_ that is _not added_ to the header table
|
135
167
|
# entails the following action:
|
136
|
-
#
|
137
|
-
|
138
|
-
# A
|
139
|
-
# the following actions:
|
140
|
-
#
|
141
|
-
#
|
142
|
-
|
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 =
|
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]
|
156
|
-
|
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
|
-
#
|
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
|
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
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
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
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
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
|
-
|
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
|
-
#
|
210
|
-
#
|
211
|
-
|
212
|
-
|
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
|
-
#
|
219
|
-
#
|
220
|
-
#
|
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
|
-
#
|
225
|
-
|
226
|
-
|
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 =
|
232
|
-
cmdsize = cmd[
|
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
|
-
|
263
|
-
|
300
|
+
while cursize + cmdsize > @limit do
|
301
|
+
break if @table.empty?
|
264
302
|
|
265
|
-
|
266
|
-
|
267
|
-
|
303
|
+
last_index = @table.size - 1
|
304
|
+
e = @table.pop
|
305
|
+
cursize -= e[0].bytesize + e[1].bytesize + 32
|
306
|
+
end
|
268
307
|
|
269
|
-
|
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
|
-
|
279
|
-
|
280
|
-
|
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
|
-
|
292
|
-
|
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-
|
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-
|
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
|
332
|
-
# bits prefix. If the string length is strictly less than
|
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
|
-
# *
|
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
|
-
|
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
|
-
|
351
|
-
|
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
|
-
|
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
|
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 [
|
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! {|
|
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
|
-
|
423
|
-
|
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.
|
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
|
-
|
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]
|
560
|
+
# @return [Array] +[[name, value], ...]+
|
497
561
|
def decode(buf)
|
498
|
-
|
499
|
-
|
500
|
-
|
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
|
|