biryani 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +30 -0
- data/.gitignore +17 -0
- data/.rubocop.yml +36 -0
- data/.ruby-version +1 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +48 -0
- data/Rakefile +8 -0
- data/biryani.gemspec +21 -0
- data/example/echo.rb +27 -0
- data/example/hello_world.rb +22 -0
- data/lib/biryani/connection.rb +464 -0
- data/lib/biryani/connection_error.rb +17 -0
- data/lib/biryani/data_buffer.rb +42 -0
- data/lib/biryani/frame/continuation.rb +48 -0
- data/lib/biryani/frame/data.rb +70 -0
- data/lib/biryani/frame/goaway.rb +44 -0
- data/lib/biryani/frame/headers.rb +110 -0
- data/lib/biryani/frame/ping.rb +49 -0
- data/lib/biryani/frame/priority.rb +44 -0
- data/lib/biryani/frame/push_promise.rb +75 -0
- data/lib/biryani/frame/rst_stream.rb +40 -0
- data/lib/biryani/frame/settings.rb +66 -0
- data/lib/biryani/frame/unknown.rb +42 -0
- data/lib/biryani/frame/window_update.rb +43 -0
- data/lib/biryani/frame.rb +146 -0
- data/lib/biryani/hpack/decoder.rb +22 -0
- data/lib/biryani/hpack/dynamic_table.rb +65 -0
- data/lib/biryani/hpack/encoder.rb +22 -0
- data/lib/biryani/hpack/error.rb +12 -0
- data/lib/biryani/hpack/field.rb +357 -0
- data/lib/biryani/hpack/fields.rb +28 -0
- data/lib/biryani/hpack/huffman.rb +309 -0
- data/lib/biryani/hpack/integer.rb +66 -0
- data/lib/biryani/hpack/option.rb +24 -0
- data/lib/biryani/hpack/string.rb +40 -0
- data/lib/biryani/hpack.rb +84 -0
- data/lib/biryani/http_request.rb +83 -0
- data/lib/biryani/http_response.rb +61 -0
- data/lib/biryani/server.rb +19 -0
- data/lib/biryani/state.rb +224 -0
- data/lib/biryani/stream.rb +19 -0
- data/lib/biryani/stream_error.rb +17 -0
- data/lib/biryani/streams_context.rb +89 -0
- data/lib/biryani/version.rb +3 -0
- data/lib/biryani/window.rb +29 -0
- data/lib/biryani.rb +17 -0
- data/spec/connection/close_all_streams_spec.rb +17 -0
- data/spec/connection/handle_connection_window_update_spec.rb +16 -0
- data/spec/connection/handle_ping_spec.rb +21 -0
- data/spec/connection/handle_rst_stream_spec.rb +21 -0
- data/spec/connection/handle_settings_spec.rb +31 -0
- data/spec/connection/handle_stream_window_update_spec.rb +20 -0
- data/spec/connection/read_http2_magic_spec.rb +26 -0
- data/spec/connection/remove_closed_streams_spec.rb +51 -0
- data/spec/connection/send_spec.rb +114 -0
- data/spec/connection/transition_state_send_spec.rb +39 -0
- data/spec/connection/unwrap_spec.rb +28 -0
- data/spec/data_buffer_spec.rb +135 -0
- data/spec/frame/continuation_spec.rb +39 -0
- data/spec/frame/data_spec.rb +25 -0
- data/spec/frame/goaway_spec.rb +23 -0
- data/spec/frame/headers_spec.rb +52 -0
- data/spec/frame/ping_spec.rb +22 -0
- data/spec/frame/priority_spec.rb +22 -0
- data/spec/frame/push_promise_spec.rb +24 -0
- data/spec/frame/read_spec.rb +30 -0
- data/spec/frame/rst_stream_spec.rb +21 -0
- data/spec/frame/settings_spec.rb +23 -0
- data/spec/frame/window_update_spec.rb +21 -0
- data/spec/hpack/decoder_spec.rb +170 -0
- data/spec/hpack/encoder_spec.rb +48 -0
- data/spec/hpack/field_spec.rb +42 -0
- data/spec/hpack/fields_spec.rb +17 -0
- data/spec/hpack/huffman_spec.rb +20 -0
- data/spec/hpack/integer_spec.rb +27 -0
- data/spec/hpack/string_spec.rb +19 -0
- data/spec/http_request_builder_spec.rb +45 -0
- data/spec/spec_helper.rb +9 -0
- metadata +165 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
module Biryani
|
|
2
|
+
module HPACK
|
|
3
|
+
# rubocop: disable Metrics/ModuleLength
|
|
4
|
+
module Huffman
|
|
5
|
+
# https://datatracker.ietf.org/doc/html/rfc7541#appendix-B
|
|
6
|
+
ENCODE_TABLE = %w[
|
|
7
|
+
1111111111000
|
|
8
|
+
11111111111111111011000
|
|
9
|
+
1111111111111111111111100010
|
|
10
|
+
1111111111111111111111100011
|
|
11
|
+
1111111111111111111111100100
|
|
12
|
+
1111111111111111111111100101
|
|
13
|
+
1111111111111111111111100110
|
|
14
|
+
1111111111111111111111100111
|
|
15
|
+
1111111111111111111111101000
|
|
16
|
+
111111111111111111101010
|
|
17
|
+
111111111111111111111111111100
|
|
18
|
+
1111111111111111111111101001
|
|
19
|
+
1111111111111111111111101010
|
|
20
|
+
111111111111111111111111111101
|
|
21
|
+
1111111111111111111111101011
|
|
22
|
+
1111111111111111111111101100
|
|
23
|
+
1111111111111111111111101101
|
|
24
|
+
1111111111111111111111101110
|
|
25
|
+
1111111111111111111111101111
|
|
26
|
+
1111111111111111111111110000
|
|
27
|
+
1111111111111111111111110001
|
|
28
|
+
1111111111111111111111110010
|
|
29
|
+
111111111111111111111111111110
|
|
30
|
+
1111111111111111111111110011
|
|
31
|
+
1111111111111111111111110100
|
|
32
|
+
1111111111111111111111110101
|
|
33
|
+
1111111111111111111111110110
|
|
34
|
+
1111111111111111111111110111
|
|
35
|
+
1111111111111111111111111000
|
|
36
|
+
1111111111111111111111111001
|
|
37
|
+
1111111111111111111111111010
|
|
38
|
+
1111111111111111111111111011
|
|
39
|
+
010100
|
|
40
|
+
1111111000
|
|
41
|
+
1111111001
|
|
42
|
+
111111111010
|
|
43
|
+
1111111111001
|
|
44
|
+
010101
|
|
45
|
+
11111000
|
|
46
|
+
11111111010
|
|
47
|
+
1111111010
|
|
48
|
+
1111111011
|
|
49
|
+
11111001
|
|
50
|
+
11111111011
|
|
51
|
+
11111010
|
|
52
|
+
010110
|
|
53
|
+
010111
|
|
54
|
+
011000
|
|
55
|
+
00000
|
|
56
|
+
00001
|
|
57
|
+
00010
|
|
58
|
+
011001
|
|
59
|
+
011010
|
|
60
|
+
011011
|
|
61
|
+
011100
|
|
62
|
+
011101
|
|
63
|
+
011110
|
|
64
|
+
011111
|
|
65
|
+
1011100
|
|
66
|
+
11111011
|
|
67
|
+
111111111111100
|
|
68
|
+
100000
|
|
69
|
+
111111111011
|
|
70
|
+
1111111100
|
|
71
|
+
1111111111010
|
|
72
|
+
100001
|
|
73
|
+
1011101
|
|
74
|
+
1011110
|
|
75
|
+
1011111
|
|
76
|
+
1100000
|
|
77
|
+
1100001
|
|
78
|
+
1100010
|
|
79
|
+
1100011
|
|
80
|
+
1100100
|
|
81
|
+
1100101
|
|
82
|
+
1100110
|
|
83
|
+
1100111
|
|
84
|
+
1101000
|
|
85
|
+
1101001
|
|
86
|
+
1101010
|
|
87
|
+
1101011
|
|
88
|
+
1101100
|
|
89
|
+
1101101
|
|
90
|
+
1101110
|
|
91
|
+
1101111
|
|
92
|
+
1110000
|
|
93
|
+
1110001
|
|
94
|
+
1110010
|
|
95
|
+
11111100
|
|
96
|
+
1110011
|
|
97
|
+
11111101
|
|
98
|
+
1111111111011
|
|
99
|
+
1111111111111110000
|
|
100
|
+
1111111111100
|
|
101
|
+
11111111111100
|
|
102
|
+
100010
|
|
103
|
+
111111111111101
|
|
104
|
+
00011
|
|
105
|
+
100011
|
|
106
|
+
00100
|
|
107
|
+
100100
|
|
108
|
+
00101
|
|
109
|
+
100101
|
|
110
|
+
100110
|
|
111
|
+
100111
|
|
112
|
+
00110
|
|
113
|
+
1110100
|
|
114
|
+
1110101
|
|
115
|
+
101000
|
|
116
|
+
101001
|
|
117
|
+
101010
|
|
118
|
+
00111
|
|
119
|
+
101011
|
|
120
|
+
1110110
|
|
121
|
+
101100
|
|
122
|
+
01000
|
|
123
|
+
01001
|
|
124
|
+
101101
|
|
125
|
+
1110111
|
|
126
|
+
1111000
|
|
127
|
+
1111001
|
|
128
|
+
1111010
|
|
129
|
+
1111011
|
|
130
|
+
111111111111110
|
|
131
|
+
11111111100
|
|
132
|
+
11111111111101
|
|
133
|
+
1111111111101
|
|
134
|
+
1111111111111111111111111100
|
|
135
|
+
11111111111111100110
|
|
136
|
+
1111111111111111010010
|
|
137
|
+
11111111111111100111
|
|
138
|
+
11111111111111101000
|
|
139
|
+
1111111111111111010011
|
|
140
|
+
1111111111111111010100
|
|
141
|
+
1111111111111111010101
|
|
142
|
+
11111111111111111011001
|
|
143
|
+
1111111111111111010110
|
|
144
|
+
11111111111111111011010
|
|
145
|
+
11111111111111111011011
|
|
146
|
+
11111111111111111011100
|
|
147
|
+
11111111111111111011101
|
|
148
|
+
11111111111111111011110
|
|
149
|
+
111111111111111111101011
|
|
150
|
+
11111111111111111011111
|
|
151
|
+
111111111111111111101100
|
|
152
|
+
111111111111111111101101
|
|
153
|
+
1111111111111111010111
|
|
154
|
+
11111111111111111100000
|
|
155
|
+
111111111111111111101110
|
|
156
|
+
11111111111111111100001
|
|
157
|
+
11111111111111111100010
|
|
158
|
+
11111111111111111100011
|
|
159
|
+
11111111111111111100100
|
|
160
|
+
111111111111111011100
|
|
161
|
+
1111111111111111011000
|
|
162
|
+
11111111111111111100101
|
|
163
|
+
1111111111111111011001
|
|
164
|
+
11111111111111111100110
|
|
165
|
+
11111111111111111100111
|
|
166
|
+
111111111111111111101111
|
|
167
|
+
1111111111111111011010
|
|
168
|
+
111111111111111011101
|
|
169
|
+
11111111111111101001
|
|
170
|
+
1111111111111111011011
|
|
171
|
+
1111111111111111011100
|
|
172
|
+
11111111111111111101000
|
|
173
|
+
11111111111111111101001
|
|
174
|
+
111111111111111011110
|
|
175
|
+
11111111111111111101010
|
|
176
|
+
1111111111111111011101
|
|
177
|
+
1111111111111111011110
|
|
178
|
+
111111111111111111110000
|
|
179
|
+
111111111111111011111
|
|
180
|
+
1111111111111111011111
|
|
181
|
+
11111111111111111101011
|
|
182
|
+
11111111111111111101100
|
|
183
|
+
111111111111111100000
|
|
184
|
+
111111111111111100001
|
|
185
|
+
1111111111111111100000
|
|
186
|
+
111111111111111100010
|
|
187
|
+
11111111111111111101101
|
|
188
|
+
1111111111111111100001
|
|
189
|
+
11111111111111111101110
|
|
190
|
+
11111111111111111101111
|
|
191
|
+
11111111111111101010
|
|
192
|
+
1111111111111111100010
|
|
193
|
+
1111111111111111100011
|
|
194
|
+
1111111111111111100100
|
|
195
|
+
11111111111111111110000
|
|
196
|
+
1111111111111111100101
|
|
197
|
+
1111111111111111100110
|
|
198
|
+
11111111111111111110001
|
|
199
|
+
11111111111111111111100000
|
|
200
|
+
11111111111111111111100001
|
|
201
|
+
11111111111111101011
|
|
202
|
+
1111111111111110001
|
|
203
|
+
1111111111111111100111
|
|
204
|
+
11111111111111111110010
|
|
205
|
+
1111111111111111101000
|
|
206
|
+
1111111111111111111101100
|
|
207
|
+
11111111111111111111100010
|
|
208
|
+
11111111111111111111100011
|
|
209
|
+
11111111111111111111100100
|
|
210
|
+
111111111111111111111011110
|
|
211
|
+
111111111111111111111011111
|
|
212
|
+
11111111111111111111100101
|
|
213
|
+
111111111111111111110001
|
|
214
|
+
1111111111111111111101101
|
|
215
|
+
1111111111111110010
|
|
216
|
+
111111111111111100011
|
|
217
|
+
11111111111111111111100110
|
|
218
|
+
111111111111111111111100000
|
|
219
|
+
111111111111111111111100001
|
|
220
|
+
11111111111111111111100111
|
|
221
|
+
111111111111111111111100010
|
|
222
|
+
111111111111111111110010
|
|
223
|
+
111111111111111100100
|
|
224
|
+
111111111111111100101
|
|
225
|
+
11111111111111111111101000
|
|
226
|
+
11111111111111111111101001
|
|
227
|
+
1111111111111111111111111101
|
|
228
|
+
111111111111111111111100011
|
|
229
|
+
111111111111111111111100100
|
|
230
|
+
111111111111111111111100101
|
|
231
|
+
11111111111111101100
|
|
232
|
+
111111111111111111110011
|
|
233
|
+
11111111111111101101
|
|
234
|
+
111111111111111100110
|
|
235
|
+
1111111111111111101001
|
|
236
|
+
111111111111111100111
|
|
237
|
+
111111111111111101000
|
|
238
|
+
11111111111111111110011
|
|
239
|
+
1111111111111111101010
|
|
240
|
+
1111111111111111101011
|
|
241
|
+
1111111111111111111101110
|
|
242
|
+
1111111111111111111101111
|
|
243
|
+
111111111111111111110100
|
|
244
|
+
111111111111111111110101
|
|
245
|
+
11111111111111111111101010
|
|
246
|
+
11111111111111111110100
|
|
247
|
+
11111111111111111111101011
|
|
248
|
+
111111111111111111111100110
|
|
249
|
+
11111111111111111111101100
|
|
250
|
+
11111111111111111111101101
|
|
251
|
+
111111111111111111111100111
|
|
252
|
+
111111111111111111111101000
|
|
253
|
+
111111111111111111111101001
|
|
254
|
+
111111111111111111111101010
|
|
255
|
+
111111111111111111111101011
|
|
256
|
+
1111111111111111111111111110
|
|
257
|
+
111111111111111111111101100
|
|
258
|
+
111111111111111111111101101
|
|
259
|
+
111111111111111111111101110
|
|
260
|
+
111111111111111111111101111
|
|
261
|
+
111111111111111111111110000
|
|
262
|
+
11111111111111111111101110
|
|
263
|
+
111111111111111111111111111111
|
|
264
|
+
].freeze
|
|
265
|
+
DECODE_TABLE = ENCODE_TABLE.each_with_index.to_h.freeze
|
|
266
|
+
EOS = '1' * 30
|
|
267
|
+
|
|
268
|
+
private_constant :ENCODE_TABLE, :DECODE_TABLE, :EOS
|
|
269
|
+
Ractor.make_shareable(ENCODE_TABLE)
|
|
270
|
+
Ractor.make_shareable(DECODE_TABLE)
|
|
271
|
+
Ractor.make_shareable(EOS)
|
|
272
|
+
|
|
273
|
+
# @param s [String]
|
|
274
|
+
#
|
|
275
|
+
# @return [String]
|
|
276
|
+
def self.encode(s)
|
|
277
|
+
bits = s.bytes.map { |sym| ENCODE_TABLE[sym] }.join
|
|
278
|
+
bits << '1' * ((8 - bits.bytesize) % 8)
|
|
279
|
+
[bits].pack('B*')
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
Accumulator = Struct.new(:s, :buf)
|
|
283
|
+
|
|
284
|
+
# @param encoded [String]
|
|
285
|
+
#
|
|
286
|
+
# @raise [HuffmanDecodeError]
|
|
287
|
+
#
|
|
288
|
+
# @return [String]
|
|
289
|
+
def self.decode(encoded)
|
|
290
|
+
bits = encoded.unpack1('B*').chars
|
|
291
|
+
res = bits.each_with_object(Accumulator.new(s: '', buf: '')) do |bit, acc|
|
|
292
|
+
acc.buf << bit
|
|
293
|
+
raise Error::HuffmanDecodeError if acc.buf == EOS
|
|
294
|
+
|
|
295
|
+
if (chr = DECODE_TABLE[acc.buf])
|
|
296
|
+
acc.s << chr
|
|
297
|
+
acc.buf.clear
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
acc
|
|
301
|
+
end
|
|
302
|
+
raise Error::HuffmanDecodeError if res.buf.chars.any? { |c| c != '1' } || res.buf.bytesize >= 8
|
|
303
|
+
|
|
304
|
+
res.s
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
# rubocop: enable Metrics/ModuleLength
|
|
308
|
+
end
|
|
309
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module Biryani
|
|
2
|
+
module HPACK
|
|
3
|
+
module Integer
|
|
4
|
+
# if I < 2^N - 1, encode I on N bits
|
|
5
|
+
# else
|
|
6
|
+
# encode (2^N - 1) on N bits
|
|
7
|
+
# I = I - (2^N - 1)
|
|
8
|
+
# while I >= 128
|
|
9
|
+
# encode (I % 128 + 128) on 8 bits
|
|
10
|
+
# I = I / 128
|
|
11
|
+
# encode I on 8 bits
|
|
12
|
+
# decode I from the next N bits
|
|
13
|
+
# if I < 2^N - 1, return I
|
|
14
|
+
# else
|
|
15
|
+
# M = 0
|
|
16
|
+
# repeat
|
|
17
|
+
# B = next octet
|
|
18
|
+
# I = I + (B & 127) * 2^M
|
|
19
|
+
# M = M + 7
|
|
20
|
+
# while B & 128 == 128
|
|
21
|
+
# return I
|
|
22
|
+
# https://datatracker.ietf.org/doc/html/rfc7541#section-5.1
|
|
23
|
+
#
|
|
24
|
+
# @param i [Integer]
|
|
25
|
+
# @param n [Integer]
|
|
26
|
+
# @param mask [Integer]
|
|
27
|
+
#
|
|
28
|
+
# @return [Array]
|
|
29
|
+
def self.encode(i, n, mask)
|
|
30
|
+
limit = (1 << n) - 1
|
|
31
|
+
return [i | mask].pack('C*') if i < limit
|
|
32
|
+
|
|
33
|
+
bytes = [limit | mask]
|
|
34
|
+
i -= limit
|
|
35
|
+
while i > 128
|
|
36
|
+
bytes << i % 128 + 128
|
|
37
|
+
i /= 128
|
|
38
|
+
end
|
|
39
|
+
bytes << i
|
|
40
|
+
bytes.pack('C*')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @param s [String]
|
|
44
|
+
# @param cursor [Integer]
|
|
45
|
+
# @param n [Integer]
|
|
46
|
+
#
|
|
47
|
+
# @return [Integer]
|
|
48
|
+
# @return [Integer]
|
|
49
|
+
def self.decode(s, n, cursor)
|
|
50
|
+
limit = (1 << n) - 1
|
|
51
|
+
h = s.getbyte(cursor)
|
|
52
|
+
return [h & limit, cursor + 1] if (h & limit) != limit
|
|
53
|
+
|
|
54
|
+
res = limit
|
|
55
|
+
s[cursor + 1..].each_byte.each_with_index.each do |byte, i|
|
|
56
|
+
res += (byte & 127) * 2**(i * 7)
|
|
57
|
+
cursor += 1
|
|
58
|
+
|
|
59
|
+
break if (byte & 128).zero?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
[res, cursor + 1]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Biryani
|
|
2
|
+
module HPACK
|
|
3
|
+
class Some
|
|
4
|
+
attr_reader :index, :value
|
|
5
|
+
|
|
6
|
+
def initialize(index, value)
|
|
7
|
+
@index = index
|
|
8
|
+
@value = value
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def deconstruct
|
|
12
|
+
[@index, @value]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class None
|
|
17
|
+
def initialize; end
|
|
18
|
+
|
|
19
|
+
def deconstruct
|
|
20
|
+
[]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Biryani
|
|
2
|
+
module HPACK
|
|
3
|
+
module String
|
|
4
|
+
# 0 1 2 3 4 5 6 7
|
|
5
|
+
# +---+---+---+---+---+---+---+---+
|
|
6
|
+
# | H | String Length (7+) |
|
|
7
|
+
# +---+---------------------------+
|
|
8
|
+
# | String Data (Length octets) |
|
|
9
|
+
# +-------------------------------+
|
|
10
|
+
# https://datatracker.ietf.org/doc/html/rfc7541#section-5.2
|
|
11
|
+
#
|
|
12
|
+
# @param s [String]
|
|
13
|
+
#
|
|
14
|
+
# @return [String]
|
|
15
|
+
def self.encode(s)
|
|
16
|
+
res = Huffman.encode(s)
|
|
17
|
+
mask = 0b10000000
|
|
18
|
+
if res.bytesize > s.bytesize
|
|
19
|
+
res = s
|
|
20
|
+
mask = 0b00000000
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Integer.encode(res.bytesize, 7, mask) + res
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param s [String]
|
|
27
|
+
# @param cursor [Integer]
|
|
28
|
+
#
|
|
29
|
+
# @return [String]
|
|
30
|
+
# @return [Integer]
|
|
31
|
+
def self.decode(s, cursor)
|
|
32
|
+
h = (s.getbyte(cursor) & 0b10000000).positive?
|
|
33
|
+
len, c = Integer.decode(s, 7, cursor)
|
|
34
|
+
return [Huffman.decode(s[c...c + len]), c + len] if h
|
|
35
|
+
|
|
36
|
+
[s[c...c + len], c + len]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module Biryani
|
|
2
|
+
module HPACK
|
|
3
|
+
# https://datatracker.ietf.org/doc/html/rfc7541#appendix-A
|
|
4
|
+
STATIC_TABLE = [
|
|
5
|
+
[':authority', ''],
|
|
6
|
+
[':method', 'GET'],
|
|
7
|
+
[':method', 'POST'],
|
|
8
|
+
[':path', '/'],
|
|
9
|
+
[':path', '/index.html'],
|
|
10
|
+
[':scheme', 'http'],
|
|
11
|
+
[':scheme', 'https'],
|
|
12
|
+
[':status', '200'],
|
|
13
|
+
[':status', '204'],
|
|
14
|
+
[':status', '206'],
|
|
15
|
+
[':status', '304'],
|
|
16
|
+
[':status', '400'],
|
|
17
|
+
[':status', '404'],
|
|
18
|
+
[':status', '500'],
|
|
19
|
+
['accept-charset', ''],
|
|
20
|
+
['accept-encoding', 'gzip, deflate'],
|
|
21
|
+
['accept-language', ''],
|
|
22
|
+
['accept-ranges', ''],
|
|
23
|
+
['accept', ''],
|
|
24
|
+
['access-control-allow-origin', ''],
|
|
25
|
+
['age', ''],
|
|
26
|
+
['allow', ''],
|
|
27
|
+
['authorization', ''],
|
|
28
|
+
['cache-control', ''],
|
|
29
|
+
['content-disposition', ''],
|
|
30
|
+
['content-encoding', ''],
|
|
31
|
+
['content-language', ''],
|
|
32
|
+
['content-length', ''],
|
|
33
|
+
['content-location', ''],
|
|
34
|
+
['content-range', ''],
|
|
35
|
+
['content-type', ''],
|
|
36
|
+
['cookie', ''],
|
|
37
|
+
['date', ''],
|
|
38
|
+
['etag', ''],
|
|
39
|
+
['expect', ''],
|
|
40
|
+
['expires', ''],
|
|
41
|
+
['from', ''],
|
|
42
|
+
['host', ''],
|
|
43
|
+
['if-match', ''],
|
|
44
|
+
['if-modified-since', ''],
|
|
45
|
+
['if-none-match', ''],
|
|
46
|
+
['if-range', ''],
|
|
47
|
+
['if-unmodified-since', ''],
|
|
48
|
+
['last-modified', ''],
|
|
49
|
+
['link', ''],
|
|
50
|
+
['location', ''],
|
|
51
|
+
['max-forwards', ''],
|
|
52
|
+
['proxy-authenticate', ''],
|
|
53
|
+
['proxy-authorization', ''],
|
|
54
|
+
['range', ''],
|
|
55
|
+
['referer', ''],
|
|
56
|
+
['refresh', ''],
|
|
57
|
+
['retry-after', ''],
|
|
58
|
+
['server', ''],
|
|
59
|
+
['set-cookie', ''],
|
|
60
|
+
['strict-transport-security', ''],
|
|
61
|
+
['transfer-encoding', ''],
|
|
62
|
+
['user-agent', ''],
|
|
63
|
+
['vary', ''],
|
|
64
|
+
['via', ''],
|
|
65
|
+
['www-authenticate', '']
|
|
66
|
+
].freeze
|
|
67
|
+
STATIC_TABLE_SIZE = STATIC_TABLE.length
|
|
68
|
+
|
|
69
|
+
private_constant :STATIC_TABLE, :STATIC_TABLE_SIZE
|
|
70
|
+
Ractor.make_shareable(STATIC_TABLE)
|
|
71
|
+
Ractor.make_shareable(STATIC_TABLE_SIZE)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
require_relative 'hpack/decoder'
|
|
76
|
+
require_relative 'hpack/dynamic_table'
|
|
77
|
+
require_relative 'hpack/encoder'
|
|
78
|
+
require_relative 'hpack/error'
|
|
79
|
+
require_relative 'hpack/field'
|
|
80
|
+
require_relative 'hpack/fields'
|
|
81
|
+
require_relative 'hpack/huffman'
|
|
82
|
+
require_relative 'hpack/integer'
|
|
83
|
+
require_relative 'hpack/option'
|
|
84
|
+
require_relative 'hpack/string'
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
module Biryani
|
|
2
|
+
class HTTPRequest
|
|
3
|
+
attr_accessor :method, :uri, :fields, :content
|
|
4
|
+
|
|
5
|
+
# @param method [String]
|
|
6
|
+
# @param uri [URI]
|
|
7
|
+
# @param fields [Hash<String, String>]
|
|
8
|
+
# @param content [String]
|
|
9
|
+
def initialize(method, uri, fields, content)
|
|
10
|
+
@method = method
|
|
11
|
+
@uri = uri
|
|
12
|
+
@fields = fields
|
|
13
|
+
@content = content
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class HTTPRequestBuilder
|
|
18
|
+
PSEUDO_HEADER_FIELDS = [':authority', ':method', ':path', ':scheme'].freeze
|
|
19
|
+
Ractor.make_shareable(PSEUDO_HEADER_FIELDS)
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
@h = {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param name [String]
|
|
26
|
+
# @param value [String]
|
|
27
|
+
#
|
|
28
|
+
# @return [nil, ConnectioError]
|
|
29
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
|
30
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
|
31
|
+
def field(name, value)
|
|
32
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'field name has uppercase letter') if name.downcase != name
|
|
33
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'unknown pseudo-header field name') if name[0] == ':' && !PSEUDO_HEADER_FIELDS.include?(name)
|
|
34
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'appear pseudo-header fields after regular fields') if name[0] == ':' && @h.any? { |name_, _| name_[0] != ':' }
|
|
35
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'duplicated pseudo-header fields') if PSEUDO_HEADER_FIELDS.include?(name) && @h.key?(name)
|
|
36
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, "invalid `#{name}` field") if PSEUDO_HEADER_FIELDS.include?(name) && value.empty?
|
|
37
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'connection-specific field is forbidden') if name == 'connection'
|
|
38
|
+
|
|
39
|
+
# TODO: trailers
|
|
40
|
+
|
|
41
|
+
if name == 'cookie' && @h.key?('cookie')
|
|
42
|
+
@h[name] += "; #{value}"
|
|
43
|
+
else
|
|
44
|
+
@h[name] = value
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
|
50
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
|
51
|
+
|
|
52
|
+
# @param arr [Array]
|
|
53
|
+
#
|
|
54
|
+
# @return [nil, ConnectioError]
|
|
55
|
+
def fields(arr)
|
|
56
|
+
arr.each do |name, value|
|
|
57
|
+
err = field(name, value)
|
|
58
|
+
return err unless err.nil?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param s [String]
|
|
65
|
+
#
|
|
66
|
+
# @return [HTTPRequest]
|
|
67
|
+
def build(s)
|
|
68
|
+
self.class.http_request(@h, s)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @param fields [Hash<String, String>]
|
|
72
|
+
# @param s [String]
|
|
73
|
+
#
|
|
74
|
+
# @return [HTTPRequest, ConnectionError]
|
|
75
|
+
def self.http_request(fields, s)
|
|
76
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'missing pseudo-header fields') unless PSEUDO_HEADER_FIELDS.all? { |x| fields.key?(x) }
|
|
77
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid content-length') if fields.key?('content-length') && !s.empty? && s.length != fields['content-length'].to_i
|
|
78
|
+
|
|
79
|
+
uri = URI("#{fields[':scheme']}://#{fields[':authority']}#{fields[':path']}")
|
|
80
|
+
HTTPRequest.new(fields[':method'], uri, fields.reject { |name, _| PSEUDO_HEADER_FIELDS.include?(name) }, s)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Biryani
|
|
2
|
+
class HTTPResponse
|
|
3
|
+
attr_accessor :status, :fields, :content
|
|
4
|
+
|
|
5
|
+
# @param status [Integer]
|
|
6
|
+
# @param fields [Hash]
|
|
7
|
+
# @param content [String]
|
|
8
|
+
def initialize(status, fields, content)
|
|
9
|
+
@status = status
|
|
10
|
+
@fields = fields
|
|
11
|
+
@content = content
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class HTTPResponseParser
|
|
16
|
+
# @param res [HTTPResponse]
|
|
17
|
+
def initialize(res)
|
|
18
|
+
@res = res
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Array]
|
|
22
|
+
def fields
|
|
23
|
+
fields = [[':status', @res.status.to_s]]
|
|
24
|
+
@res.fields.each do |name, value|
|
|
25
|
+
# TODO: validate fields
|
|
26
|
+
fields << [name.to_s.downcase, value.to_s]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
fields
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [String]
|
|
33
|
+
def content
|
|
34
|
+
@res.content
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param stream_id [Integer]
|
|
38
|
+
# @param encoder [Encoder]
|
|
39
|
+
# @param max_frame_size [Integer]
|
|
40
|
+
#
|
|
41
|
+
# @return [Array<Object>] frames
|
|
42
|
+
def parse(stream_id, encoder, max_frame_size)
|
|
43
|
+
fragment = encoder.encode(fields)
|
|
44
|
+
len = (fragment.bytesize + max_frame_size - 1) / max_frame_size
|
|
45
|
+
frames = fragment.gsub(/.{1,#{max_frame_size}}/m).with_index.map do |s, index|
|
|
46
|
+
if index.zero?
|
|
47
|
+
Frame::Headers.new(len < 2, content.empty?, stream_id, nil, nil, s, nil)
|
|
48
|
+
else
|
|
49
|
+
Frame::Continuation.new(index == len - 1, stream_id, s)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
len = (content.bytesize + max_frame_size - 1) / max_frame_size
|
|
54
|
+
frames += content.gsub(/.{1,#{max_frame_size}}/m).with_index.map do |s, index|
|
|
55
|
+
Frame::Data.new(index == len - 1, stream_id, s, nil)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
frames
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Biryani
|
|
2
|
+
class Server
|
|
3
|
+
# @param proc [Proc]
|
|
4
|
+
def initialize(proc)
|
|
5
|
+
@proc = proc
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# @param socket [Socket]
|
|
9
|
+
def run(socket)
|
|
10
|
+
loop do
|
|
11
|
+
Ractor.new(socket.accept, @proc) do |io, proc|
|
|
12
|
+
conn = Connection.new(proc)
|
|
13
|
+
conn.serve(io)
|
|
14
|
+
io&.close
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|