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.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +30 -0
  3. data/.gitignore +17 -0
  4. data/.rubocop.yml +36 -0
  5. data/.ruby-version +1 -0
  6. data/Gemfile +9 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +48 -0
  9. data/Rakefile +8 -0
  10. data/biryani.gemspec +21 -0
  11. data/example/echo.rb +27 -0
  12. data/example/hello_world.rb +22 -0
  13. data/lib/biryani/connection.rb +464 -0
  14. data/lib/biryani/connection_error.rb +17 -0
  15. data/lib/biryani/data_buffer.rb +42 -0
  16. data/lib/biryani/frame/continuation.rb +48 -0
  17. data/lib/biryani/frame/data.rb +70 -0
  18. data/lib/biryani/frame/goaway.rb +44 -0
  19. data/lib/biryani/frame/headers.rb +110 -0
  20. data/lib/biryani/frame/ping.rb +49 -0
  21. data/lib/biryani/frame/priority.rb +44 -0
  22. data/lib/biryani/frame/push_promise.rb +75 -0
  23. data/lib/biryani/frame/rst_stream.rb +40 -0
  24. data/lib/biryani/frame/settings.rb +66 -0
  25. data/lib/biryani/frame/unknown.rb +42 -0
  26. data/lib/biryani/frame/window_update.rb +43 -0
  27. data/lib/biryani/frame.rb +146 -0
  28. data/lib/biryani/hpack/decoder.rb +22 -0
  29. data/lib/biryani/hpack/dynamic_table.rb +65 -0
  30. data/lib/biryani/hpack/encoder.rb +22 -0
  31. data/lib/biryani/hpack/error.rb +12 -0
  32. data/lib/biryani/hpack/field.rb +357 -0
  33. data/lib/biryani/hpack/fields.rb +28 -0
  34. data/lib/biryani/hpack/huffman.rb +309 -0
  35. data/lib/biryani/hpack/integer.rb +66 -0
  36. data/lib/biryani/hpack/option.rb +24 -0
  37. data/lib/biryani/hpack/string.rb +40 -0
  38. data/lib/biryani/hpack.rb +84 -0
  39. data/lib/biryani/http_request.rb +83 -0
  40. data/lib/biryani/http_response.rb +61 -0
  41. data/lib/biryani/server.rb +19 -0
  42. data/lib/biryani/state.rb +224 -0
  43. data/lib/biryani/stream.rb +19 -0
  44. data/lib/biryani/stream_error.rb +17 -0
  45. data/lib/biryani/streams_context.rb +89 -0
  46. data/lib/biryani/version.rb +3 -0
  47. data/lib/biryani/window.rb +29 -0
  48. data/lib/biryani.rb +17 -0
  49. data/spec/connection/close_all_streams_spec.rb +17 -0
  50. data/spec/connection/handle_connection_window_update_spec.rb +16 -0
  51. data/spec/connection/handle_ping_spec.rb +21 -0
  52. data/spec/connection/handle_rst_stream_spec.rb +21 -0
  53. data/spec/connection/handle_settings_spec.rb +31 -0
  54. data/spec/connection/handle_stream_window_update_spec.rb +20 -0
  55. data/spec/connection/read_http2_magic_spec.rb +26 -0
  56. data/spec/connection/remove_closed_streams_spec.rb +51 -0
  57. data/spec/connection/send_spec.rb +114 -0
  58. data/spec/connection/transition_state_send_spec.rb +39 -0
  59. data/spec/connection/unwrap_spec.rb +28 -0
  60. data/spec/data_buffer_spec.rb +135 -0
  61. data/spec/frame/continuation_spec.rb +39 -0
  62. data/spec/frame/data_spec.rb +25 -0
  63. data/spec/frame/goaway_spec.rb +23 -0
  64. data/spec/frame/headers_spec.rb +52 -0
  65. data/spec/frame/ping_spec.rb +22 -0
  66. data/spec/frame/priority_spec.rb +22 -0
  67. data/spec/frame/push_promise_spec.rb +24 -0
  68. data/spec/frame/read_spec.rb +30 -0
  69. data/spec/frame/rst_stream_spec.rb +21 -0
  70. data/spec/frame/settings_spec.rb +23 -0
  71. data/spec/frame/window_update_spec.rb +21 -0
  72. data/spec/hpack/decoder_spec.rb +170 -0
  73. data/spec/hpack/encoder_spec.rb +48 -0
  74. data/spec/hpack/field_spec.rb +42 -0
  75. data/spec/hpack/fields_spec.rb +17 -0
  76. data/spec/hpack/huffman_spec.rb +20 -0
  77. data/spec/hpack/integer_spec.rb +27 -0
  78. data/spec/hpack/string_spec.rb +19 -0
  79. data/spec/http_request_builder_spec.rb +45 -0
  80. data/spec/spec_helper.rb +9 -0
  81. 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