hpack 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ module Hpack
2
+ class IntegerReader
3
+ def read length: 7, input: nil, start_with: 0
4
+ prefix_mask = 0xFF >> (8 - length)
5
+ result = start_with & prefix_mask
6
+
7
+ return result if result != prefix_mask
8
+
9
+ index = 0
10
+ begin
11
+ next_byte = input.readbyte
12
+ result += (next_byte & 0b0111_1111) << (7 * index)
13
+ index += 1
14
+ end until next_byte & 0b1000_0000 == 0
15
+
16
+ result
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,172 @@
1
+ module Hpack
2
+ class LookupTable
3
+ class IndexOutOfBounds < StandardError; end
4
+
5
+ class Entry
6
+ attr_accessor :name
7
+ attr_accessor :value
8
+
9
+ def initialize name, value = ""
10
+ @name = name
11
+ @value = value
12
+ end
13
+
14
+ def size
15
+ 32 + name.length + value.length
16
+ end
17
+
18
+ def to_a
19
+ [name, value]
20
+ end
21
+
22
+ def to_s
23
+ "(s = %4d) %s: %s" % [size, name, value]
24
+ end
25
+ end
26
+
27
+ SETTINGS_HEADER_TABLE_SIZE = 4096
28
+ STATIC_TABLE_SIZE = 61
29
+ STATIC_ENTRIES = [
30
+ [":authority"],
31
+ [":method", "GET"],
32
+ [":method", "POST"],
33
+ [":path", "/"],
34
+ [":path", "/index.html"],
35
+ [":scheme", "http"],
36
+ [":scheme", "https"],
37
+ [":status", "200"],
38
+ [":status", "204"],
39
+ [":status", "206"],
40
+ [":status", "304"],
41
+ [":status", "400"],
42
+ [":status", "404"],
43
+ [":status", "500"],
44
+ ["accept-charset"],
45
+ ["accept-encoding", "gzip, deflate"],
46
+ ["accept-language"],
47
+ ["accept-ranges"],
48
+ ["accept"],
49
+ ["access-control-allow-origin"],
50
+ ["age"],
51
+ ["allow"],
52
+ ["authorization"],
53
+ ["cache-control"],
54
+ ["content-disposition"],
55
+ ["content-encoding"],
56
+ ["content-language"],
57
+ ["content-length"],
58
+ ["content-location"],
59
+ ["content-range"],
60
+ ["content-type"],
61
+ ["cookie"],
62
+ ["date"],
63
+ ["etag"],
64
+ ["expect"],
65
+ ["expires"],
66
+ ["from"],
67
+ ["host"],
68
+ ["if-match"],
69
+ ["if-modified-since"],
70
+ ["if-none-match"],
71
+ ["if-range"],
72
+ ["if-unmodified-since"],
73
+ ["last-modified"],
74
+ ["link"],
75
+ ["location"],
76
+ ["max-forwards"],
77
+ ["proxy-authenticate"],
78
+ ["proxy-authorization"],
79
+ ["range"],
80
+ ["referer"],
81
+ ["refresh"],
82
+ ["retry-after"],
83
+ ["server"],
84
+ ["set-cookie"],
85
+ ["strict-transport-security"],
86
+ ["transfer-encoding"],
87
+ ["user-agent"],
88
+ ["vary"],
89
+ ["via"],
90
+ ["www-authenticate"],
91
+ ]
92
+
93
+ def initialize max_size: SETTINGS_HEADER_TABLE_SIZE
94
+ @max_size = max_size
95
+ @dynamic_entries = []
96
+ end
97
+
98
+ def [] index
99
+ if index <= STATIC_TABLE_SIZE
100
+ Entry.new(*STATIC_ENTRIES[index - 1])
101
+ else
102
+ dynamic_index = index - STATIC_TABLE_SIZE - 1
103
+ size = @dynamic_entries.length
104
+
105
+ if dynamic_index >= size
106
+ raise IndexOutOfBounds, "#{dynamic_index} is greater than dynamic table size #{size}"
107
+ end
108
+
109
+ @dynamic_entries[dynamic_index]
110
+ end
111
+ end
112
+
113
+ # 4.1 Calculating Table Size
114
+ #
115
+ # The size of the dynamic table is the sum of the size of its entries.
116
+ #
117
+ # The size of an entry is the sum of its name's length in octets
118
+ # (as defined in Section 5.2), its value's length in octets (see
119
+ # Section 5.2), plus 32.
120
+ #
121
+ # The size of an entry is calculated using the length of the name
122
+ # and value without any Huffman encoding applied.
123
+ #
124
+ # NOTE: The additional 32 octets account for the overhead
125
+ # associated with an entry. For example, an entry structure using
126
+ # two 64-bit pointers to reference the name and the value of the
127
+ # entry, and two 64-bit integers for counting the number of
128
+ # references to the name and value would have 32 octets of
129
+ # overhead.
130
+ #
131
+ def size
132
+ @dynamic_entries
133
+ .map(&:size)
134
+ .reduce(0, :+)
135
+ end
136
+
137
+ def max_size
138
+ @max_size
139
+ end
140
+
141
+ def max_size= value
142
+ @max_size = value
143
+ evict
144
+ end
145
+
146
+ def << entry
147
+ @dynamic_entries.unshift entry
148
+ evict
149
+ end
150
+
151
+ def to_s
152
+ table = @dynamic_entries
153
+ .each_with_index
154
+ .map { |e, i| "[%4s] %s" % [i + 1, e.to_s] }
155
+ .join "\n"
156
+
157
+ summary = " Table size: #{size}"
158
+
159
+ table + "\n" + summary
160
+ end
161
+
162
+ private
163
+
164
+ def evict
165
+ overflow = size - max_size
166
+ while overflow > 0
167
+ evicted_entry = @dynamic_entries.pop
168
+ overflow -= evicted_entry.size
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,3 @@
1
+ module Hpack
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,312 @@
1
+ RSpec.describe Hpack::Decoder do
2
+ RSpec::Matchers.define :have_dynamic_entry_at_position do |position, header, value|
3
+ match do |actual|
4
+ entry = actual[Hpack::LookupTable::STATIC_TABLE_SIZE + position]
5
+ expect(entry.name).to eq header
6
+ expect(entry.value).to eq value
7
+ end
8
+
9
+ failure_message do |actual|
10
+ """Expected that
11
+ #{actual}
12
+ would include '#{header}: #{value}' at position #{position}"
13
+ end
14
+ end
15
+
16
+ def input_fixture data
17
+ io = StringIO.new
18
+ io.write [data.gsub(/\s/, "")].pack "H*"
19
+ io.rewind
20
+ io
21
+ end
22
+
23
+ it "properly decodes an example of a literal header field with indexing" do
24
+ io = input_fixture "400a 6375 7374 6f6d 2d6b 6579 0d63 7573 746f 6d2d 6865 6164 6572"
25
+
26
+ expect { |b| subject.decode io, &b }
27
+ .to yield_with_args("custom-key", "custom-header", anything)
28
+ expect(subject.lookup_table.size).to eq 55
29
+ expect(subject.lookup_table)
30
+ .to have_dynamic_entry_at_position(1, "custom-key", "custom-header")
31
+ end
32
+
33
+ it "properly decodes an example of a literal header field without indexing" do
34
+ io = input_fixture "040c 2f73 616d 706c 652f 7061 7468"
35
+
36
+ expect { |b| subject.decode io, &b }
37
+ .to yield_with_args(":path", "/sample/path", anything)
38
+ expect(subject.lookup_table.size).to eq 0
39
+ end
40
+
41
+ it "properly decodes an example of a never indexed literal header field" do
42
+ io = input_fixture "1008 7061 7373 776f 7264 0673 6563 7265 74"
43
+
44
+ expect { |b| subject.decode io, &b }
45
+ .to yield_with_args("password", "secret", anything)
46
+ expect(subject.lookup_table.size).to eq 0
47
+ end
48
+
49
+ it "properly decodes an example of an indexed header field" do
50
+ io = input_fixture "82"
51
+
52
+ expect { |b| subject.decode io, &b }
53
+ .to yield_with_args(":method", "GET", anything)
54
+ expect(subject.lookup_table.size).to eq 0
55
+ end
56
+
57
+ it "properly decodes a sequence of requests without huffman coding" do
58
+ io = input_fixture """ 8286 8441 0f77 7777 2e65 7861 6d70 6c65
59
+ 2e63 6f6d """
60
+
61
+ expect { |b| subject.decode io, &b }
62
+ .to yield_successive_args(
63
+ [":method", "GET", anything],
64
+ [":scheme", "http", anything],
65
+ [":path", "/", anything],
66
+ [":authority", "www.example.com", anything]
67
+ )
68
+ expect(subject.lookup_table.size).to eq 57
69
+ expect(subject.lookup_table)
70
+ .to have_dynamic_entry_at_position(1, ":authority", "www.example.com")
71
+
72
+ io = input_fixture """ 8286 84be 5808 6e6f 2d63 6163 6865 """
73
+
74
+ expect { |b| subject.decode io, &b }
75
+ .to yield_successive_args(
76
+ [":method", "GET", anything],
77
+ [":scheme", "http", anything],
78
+ [":path", "/", anything],
79
+ [":authority", "www.example.com", anything],
80
+ ["cache-control", "no-cache", anything]
81
+ )
82
+ expect(subject.lookup_table.size).to eq 110
83
+
84
+ expect(subject.lookup_table)
85
+ .to have_dynamic_entry_at_position(1, "cache-control", "no-cache")
86
+ expect(subject.lookup_table)
87
+ .to have_dynamic_entry_at_position(2, ":authority", "www.example.com")
88
+
89
+ io = input_fixture """ 8287 85bf 400a 6375 7374 6f6d 2d6b 6579
90
+ 0c63 7573 746f 6d2d 7661 6c75 65 """
91
+
92
+ expect { |b| subject.decode io, &b }
93
+ .to yield_successive_args(
94
+ [":method", "GET", anything],
95
+ [":scheme", "https", anything],
96
+ [":path", "/index.html", anything],
97
+ [":authority", "www.example.com", anything],
98
+ ["custom-key", "custom-value", anything]
99
+ )
100
+ expect(subject.lookup_table.size).to eq 164
101
+
102
+ expect(subject.lookup_table)
103
+ .to have_dynamic_entry_at_position(1, "custom-key", "custom-value")
104
+ expect(subject.lookup_table)
105
+ .to have_dynamic_entry_at_position(2, "cache-control", "no-cache")
106
+ expect(subject.lookup_table)
107
+ .to have_dynamic_entry_at_position(3, ":authority", "www.example.com")
108
+ end
109
+
110
+ it "properly decodes a sequence of requests with huffman coding" do
111
+ io = input_fixture """ 8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4
112
+ ff """
113
+
114
+ expect { |b| subject.decode io, &b }
115
+ .to yield_successive_args(
116
+ [":method", "GET", anything],
117
+ [":scheme", "http", anything],
118
+ [":path", "/", anything],
119
+ [":authority", "www.example.com", anything]
120
+ )
121
+ expect(subject.lookup_table.size).to eq 57
122
+
123
+ expect(subject.lookup_table)
124
+ .to have_dynamic_entry_at_position(1, ":authority", "www.example.com")
125
+
126
+ io = input_fixture """ 8286 84be 5886 a8eb 1064 9cbf """
127
+
128
+ expect { |b| subject.decode io, &b }
129
+ .to yield_successive_args(
130
+ [":method", "GET", anything],
131
+ [":scheme", "http", anything],
132
+ [":path", "/", anything],
133
+ [":authority", "www.example.com", anything],
134
+ ["cache-control", "no-cache", anything]
135
+ )
136
+ expect(subject.lookup_table.size).to eq 110
137
+
138
+ expect(subject.lookup_table)
139
+ .to have_dynamic_entry_at_position(1, "cache-control", "no-cache")
140
+ expect(subject.lookup_table)
141
+ .to have_dynamic_entry_at_position(2, ":authority", "www.example.com")
142
+
143
+ entry = subject.lookup_table[Hpack::LookupTable::STATIC_TABLE_SIZE + 1]
144
+ expect(entry.name).to eq "cache-control"
145
+ expect(entry.value).to eq "no-cache"
146
+
147
+ io = input_fixture """ 8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925
148
+ a849 e95b b8e8 b4bf """
149
+
150
+ expect { |b| subject.decode io, &b }
151
+ .to yield_successive_args(
152
+ [":method", "GET", anything],
153
+ [":scheme", "https", anything],
154
+ [":path", "/index.html", anything],
155
+ [":authority", "www.example.com", anything],
156
+ ["custom-key", "custom-value", anything]
157
+ )
158
+ expect(subject.lookup_table.size).to eq 164
159
+
160
+ expect(subject.lookup_table)
161
+ .to have_dynamic_entry_at_position(1, "custom-key", "custom-value")
162
+ expect(subject.lookup_table)
163
+ .to have_dynamic_entry_at_position(2, "cache-control", "no-cache")
164
+ expect(subject.lookup_table)
165
+ .to have_dynamic_entry_at_position(3, ":authority", "www.example.com")
166
+ end
167
+
168
+ it "properly decodes a sequence of responses without huffman coding" do
169
+ subject.lookup_table.max_size = 256
170
+
171
+ io = input_fixture """ 4803 3330 3258 0770 7269 7661 7465 611d
172
+ 4d6f 6e2c 2032 3120 4f63 7420 3230 3133
173
+ 2032 303a 3133 3a32 3120 474d 546e 1768
174
+ 7474 7073 3a2f 2f77 7777 2e65 7861 6d70
175
+ 6c65 2e63 6f6d """
176
+
177
+ expect { |b| subject.decode io, &b }
178
+ .to yield_successive_args(
179
+ [":status", "302", anything],
180
+ ["cache-control", "private", anything],
181
+ ["date", "Mon, 21 Oct 2013 20:13:21 GMT", anything],
182
+ ["location", "https://www.example.com", anything]
183
+ )
184
+ expect(subject.lookup_table.size).to eq 222
185
+
186
+ expect(subject.lookup_table)
187
+ .to have_dynamic_entry_at_position(1, "location", "https://www.example.com")
188
+ expect(subject.lookup_table)
189
+ .to have_dynamic_entry_at_position(2, "date", "Mon, 21 Oct 2013 20:13:21 GMT")
190
+ expect(subject.lookup_table)
191
+ .to have_dynamic_entry_at_position(3, "cache-control", "private")
192
+ expect(subject.lookup_table)
193
+ .to have_dynamic_entry_at_position(4, ":status", "302")
194
+
195
+ io = input_fixture """ 4803 3330 37c1 c0bf """
196
+
197
+ expect { |b| subject.decode io, &b }
198
+ .to yield_successive_args(
199
+ [":status", "307", anything],
200
+ ["cache-control", "private", anything],
201
+ ["date", "Mon, 21 Oct 2013 20:13:21 GMT", anything],
202
+ ["location", "https://www.example.com", anything]
203
+ )
204
+ expect(subject.lookup_table.size).to eq 222
205
+
206
+ expect(subject.lookup_table)
207
+ .to have_dynamic_entry_at_position(1, ":status", "307")
208
+ expect(subject.lookup_table)
209
+ .to have_dynamic_entry_at_position(2, "location", "https://www.example.com")
210
+ expect(subject.lookup_table)
211
+ .to have_dynamic_entry_at_position(3, "date", "Mon, 21 Oct 2013 20:13:21 GMT")
212
+ expect(subject.lookup_table)
213
+ .to have_dynamic_entry_at_position(4, "cache-control", "private")
214
+
215
+ io = input_fixture """ 88c1 611d 4d6f 6e2c 2032 3120 4f63 7420
216
+ 3230 3133 2032 303a 3133 3a32 3220 474d
217
+ 54c0 5a04 677a 6970 7738 666f 6f3d 4153
218
+ 444a 4b48 514b 425a 584f 5157 454f 5049
219
+ 5541 5851 5745 4f49 553b 206d 6178 2d61
220
+ 6765 3d33 3630 303b 2076 6572 7369 6f6e
221
+ 3d31 """
222
+
223
+ expect { |b| subject.decode io, &b }
224
+ .to yield_successive_args(
225
+ [":status", "200", anything],
226
+ ["cache-control", "private", anything],
227
+ ["date", "Mon, 21 Oct 2013 20:13:22 GMT", anything],
228
+ ["location", "https://www.example.com", anything],
229
+ ["content-encoding", "gzip", anything],
230
+ ["set-cookie", "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1", anything]
231
+ )
232
+ expect(subject.lookup_table.size).to eq 215
233
+
234
+ expect(subject.lookup_table)
235
+ .to have_dynamic_entry_at_position(1, "set-cookie", "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1")
236
+ expect(subject.lookup_table)
237
+ .to have_dynamic_entry_at_position(2, "content-encoding", "gzip")
238
+ expect(subject.lookup_table)
239
+ .to have_dynamic_entry_at_position(3, "date", "Mon, 21 Oct 2013 20:13:22 GMT")
240
+ end
241
+
242
+ it "properly decodes a sequence of responses with huffman coding" do
243
+ subject.lookup_table.max_size = 256
244
+
245
+ io = input_fixture """ 4882 6402 5885 aec3 771a 4b61 96d0 7abe
246
+ 9410 54d4 44a8 2005 9504 0b81 66e0 82a6
247
+ 2d1b ff6e 919d 29ad 1718 63c7 8f0b 97c8
248
+ e9ae 82ae 43d3 """
249
+
250
+ expect { |b| subject.decode io, &b }
251
+ .to yield_successive_args(
252
+ [":status", "302", anything],
253
+ ["cache-control", "private", anything],
254
+ ["date", "Mon, 21 Oct 2013 20:13:21 GMT", anything],
255
+ ["location", "https://www.example.com", anything]
256
+ )
257
+ expect(subject.lookup_table.size).to eq 222
258
+
259
+ expect(subject.lookup_table)
260
+ .to have_dynamic_entry_at_position(1, "location", "https://www.example.com")
261
+ expect(subject.lookup_table)
262
+ .to have_dynamic_entry_at_position(2, "date", "Mon, 21 Oct 2013 20:13:21 GMT")
263
+ expect(subject.lookup_table)
264
+ .to have_dynamic_entry_at_position(3, "cache-control", "private")
265
+ expect(subject.lookup_table)
266
+ .to have_dynamic_entry_at_position(4, ":status", "302")
267
+
268
+ io = input_fixture """ 4883 640e ffc1 c0bf """
269
+
270
+ expect { |b| subject.decode io, &b }
271
+ .to yield_successive_args(
272
+ [":status", "307", anything],
273
+ ["cache-control", "private", anything],
274
+ ["date", "Mon, 21 Oct 2013 20:13:21 GMT", anything],
275
+ ["location", "https://www.example.com", anything]
276
+ )
277
+ expect(subject.lookup_table.size).to eq 222
278
+
279
+ expect(subject.lookup_table)
280
+ .to have_dynamic_entry_at_position(1, ":status", "307")
281
+ expect(subject.lookup_table)
282
+ .to have_dynamic_entry_at_position(2, "location", "https://www.example.com")
283
+ expect(subject.lookup_table)
284
+ .to have_dynamic_entry_at_position(3, "date", "Mon, 21 Oct 2013 20:13:21 GMT")
285
+ expect(subject.lookup_table)
286
+ .to have_dynamic_entry_at_position(4, "cache-control", "private")
287
+
288
+ io = input_fixture """ 88c1 6196 d07a be94 1054 d444 a820 0595
289
+ 040b 8166 e084 a62d 1bff c05a 839b d9ab
290
+ 77ad 94e7 821d d7f2 e6c7 b335 dfdf cd5b
291
+ 3960 d5af 2708 7f36 72c1 ab27 0fb5 291f
292
+ 9587 3160 65c0 03ed 4ee5 b106 3d50 07 """
293
+
294
+ expect { |b| subject.decode io, &b }
295
+ .to yield_successive_args(
296
+ [":status", "200", anything],
297
+ ["cache-control", "private", anything],
298
+ ["date", "Mon, 21 Oct 2013 20:13:22 GMT", anything],
299
+ ["location", "https://www.example.com", anything],
300
+ ["content-encoding", "gzip", anything],
301
+ ["set-cookie", "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1", anything]
302
+ )
303
+ expect(subject.lookup_table.size).to eq 215
304
+
305
+ expect(subject.lookup_table)
306
+ .to have_dynamic_entry_at_position(1, "set-cookie", "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1")
307
+ expect(subject.lookup_table)
308
+ .to have_dynamic_entry_at_position(2, "content-encoding", "gzip")
309
+ expect(subject.lookup_table)
310
+ .to have_dynamic_entry_at_position(3, "date", "Mon, 21 Oct 2013 20:13:22 GMT")
311
+ end
312
+ end