tipi 0.41 → 0.42

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.
data/lib/tipi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tipi
4
- VERSION = '0.41'
4
+ VERSION = '0.42'
5
5
  end
data/security/http1.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tipi
4
+ HTTP1_LIMITS = {
5
+ max_method_length: 16,
6
+ max_path_length: 4096,
7
+ max_header_key_length: 128,
8
+ max_header_value_length: 2048,
9
+ max_header_count: 256,
10
+ max_chunked_encoding_chunk_size_length: 16,
11
+ }
12
+ end
data/test/helper.rb CHANGED
@@ -14,26 +14,24 @@ require 'polyphony'
14
14
 
15
15
  ::Exception.__disable_sanitized_backtrace__ = true
16
16
 
17
- Minitest::Reporters.use! [
18
- Minitest::Reporters::SpecReporter.new
19
- ]
20
-
21
17
  class MiniTest::Test
22
18
  def setup
23
- # puts "* setup #{self.name}"
24
- if Fiber.current.children.size > 0
25
- puts "Children left: #{Fiber.current.children.inspect}"
26
- exit!
27
- end
19
+ # trace "* setup #{self.name}"
28
20
  Fiber.current.setup_main_fiber
29
21
  Fiber.current.instance_variable_set(:@auto_watcher, nil)
22
+ Thread.current.backend.finalize
30
23
  Thread.current.backend = Polyphony::Backend.new
31
24
  sleep 0
32
25
  end
33
26
 
34
27
  def teardown
35
- # puts "* teardown #{self.name.inspect} Fiber.current: #{Fiber.current.inspect}"
28
+ # trace "* teardown #{self.name}"
36
29
  Fiber.current.shutdown_all_children
30
+ if Fiber.current.children.size > 0
31
+ puts "Children left after #{self.name}: #{Fiber.current.children.inspect}"
32
+ exit!
33
+ end
34
+ Fiber.current.instance_variable_set(:@auto_watcher, nil)
37
35
  rescue => e
38
36
  puts e
39
37
  puts e.backtrace.join("\n")
@@ -47,4 +45,55 @@ module Kernel
47
45
  rescue Exception => e
48
46
  e
49
47
  end
50
- end
48
+
49
+ def trace(*args)
50
+ STDOUT.orig_write(format_trace(args))
51
+ end
52
+
53
+ def format_trace(args)
54
+ if args.first.is_a?(String)
55
+ if args.size > 1
56
+ format("%s: %p\n", args.shift, args)
57
+ else
58
+ format("%s\n", args.first)
59
+ end
60
+ else
61
+ format("%p\n", args.size == 1 ? args.first : args)
62
+ end
63
+ end
64
+ end
65
+
66
+ class IO
67
+ # Creates two mockup sockets for simulating server-client communication
68
+ def self.server_client_mockup
69
+ server_in, client_out = IO.pipe
70
+ client_in, server_out = IO.pipe
71
+
72
+ server_connection = mockup_connection(server_in, server_out, client_out)
73
+ client_connection = mockup_connection(client_in, client_out, server_out)
74
+
75
+ [server_connection, client_connection]
76
+ end
77
+
78
+ def self.mockup_connection(input, output, output2)
79
+ eg(
80
+ __parser_read_method__: ->() { :readpartial },
81
+ read: ->(*args) { input.read(*args) },
82
+ read_loop: ->(*args, &block) { input.read_loop(*args, &block) },
83
+ recv_loop: ->(*args, &block) { input.read_loop(*args, &block) },
84
+ readpartial: ->(*args) { input.readpartial(*args) },
85
+ recv: ->(*args) { input.readpartial(*args) },
86
+ '<<': ->(*args) { output.write(*args) },
87
+ write: ->(*args) { output.write(*args) },
88
+ close: -> { output.close },
89
+ eof?: -> { output2.closed? }
90
+ )
91
+ end
92
+ end
93
+
94
+ module Minitest::Assertions
95
+ def assert_in_range exp_range, act
96
+ msg = message(msg) { "Expected #{mu_pp(act)} to be in range #{mu_pp(exp_range)}" }
97
+ assert exp_range.include?(act), msg
98
+ end
99
+ end
@@ -0,0 +1,586 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+ require 'tipi_ext'
5
+ require_relative '../security/http1.rb'
6
+
7
+ class HTTP1ParserTest < MiniTest::Test
8
+ Error = Tipi::HTTP1Parser::Error
9
+
10
+ def setup
11
+ super
12
+ @i, @o = IO.pipe
13
+ @parser = Tipi::HTTP1Parser.new(@i)
14
+ end
15
+ alias_method :reset_parser, :setup
16
+
17
+ def test_request_line
18
+ msg = "GET / HTTP/1.1\r\n\r\n"
19
+ @o << msg
20
+ headers = @parser.parse_headers
21
+
22
+ assert_equal(
23
+ {
24
+ ':method' => 'get',
25
+ ':path' => '/',
26
+ ':protocol' => 'http/1.1',
27
+ ':rx' => msg.bytesize
28
+ },
29
+ headers
30
+ )
31
+ end
32
+
33
+ def test_request_line_whitespace
34
+ msg = "GET / HTTP/1.1\r\n\r\n"
35
+ @o << msg
36
+ headers = @parser.parse_headers
37
+
38
+ assert_equal(
39
+ {
40
+ ':method' => 'get',
41
+ ':path' => '/',
42
+ ':protocol' => 'http/1.1',
43
+ ':rx' => msg.bytesize
44
+ },
45
+ headers
46
+ )
47
+ end
48
+
49
+ def test_eof
50
+ @o << "GET / HTTP/1.1"
51
+ @o.close
52
+
53
+ assert_nil @parser.parse_headers
54
+ end
55
+
56
+ def test_method_case
57
+ @o << "GET / HTTP/1.1\r\n\r\n"
58
+ headers = @parser.parse_headers
59
+ assert_equal 'get', headers[':method']
60
+
61
+ reset_parser
62
+ @o << "post / HTTP/1.1\r\n\r\n"
63
+ headers = @parser.parse_headers
64
+ assert_equal 'post', headers[':method']
65
+
66
+ reset_parser
67
+ @o << "PoST / HTTP/1.1\r\n\r\n"
68
+ headers = @parser.parse_headers
69
+ assert_equal 'post', headers[':method']
70
+ end
71
+
72
+ def test_bad_method
73
+ @o << " / HTTP/1.1\r\n\r\n"
74
+ @o.close
75
+
76
+ assert_raises(Error) { @parser.parse_headers }
77
+
78
+ max_length = Tipi::HTTP1_LIMITS[:max_method_length]
79
+
80
+ reset_parser
81
+ @o << "#{'a' * max_length} / HTTP/1.1\r\n\r\n"
82
+ assert_equal 'a' * max_length, @parser.parse_headers[':method']
83
+
84
+ reset_parser
85
+ @o << "#{'a' * (max_length + 1)} / HTTP/1.1\r\n\r\n"
86
+ assert_raises(Error) { @parser.parse_headers }
87
+ end
88
+
89
+ def test_path_characters
90
+ @o << "GET /äBçDé¤23~{@€ HTTP/1.1\r\n\r\n"
91
+ headers = @parser.parse_headers
92
+ assert_equal '/äBçDé¤23~{@€', headers[':path']
93
+ end
94
+
95
+ def test_bad_path
96
+ @o << "GET HTTP/1.1\r\n\r\n"
97
+ assert_raises(Error) { @parser.parse_headers }
98
+
99
+ max_length = Tipi::HTTP1_LIMITS[:max_path_length]
100
+
101
+ reset_parser
102
+ @o << "get #{'a' * max_length} HTTP/1.1\r\n\r\n"
103
+ assert_equal 'a' * max_length, @parser.parse_headers[':path']
104
+
105
+ reset_parser
106
+ @o << "get #{'a' * (max_length + 1)} HTTP/1.1\r\n\r\n"
107
+ assert_raises(Error) { @parser.parse_headers }
108
+ end
109
+
110
+ def test_protocol
111
+ @o << "GET / http/1\r\n\r\n"
112
+ headers = @parser.parse_headers
113
+ assert_equal 'http/1', headers[':protocol']
114
+
115
+ reset_parser
116
+ @o << "GET / HTTP/1\r\n\r\n"
117
+ headers = @parser.parse_headers
118
+ assert_equal 'http/1', headers[':protocol']
119
+
120
+ reset_parser
121
+ @o << "GET / HTTP/1.0\r\n\r\n"
122
+ headers = @parser.parse_headers
123
+ assert_equal 'http/1.0', headers[':protocol']
124
+
125
+ @o << "GET / HttP/1.1\r\n\r\n"
126
+ headers = @parser.parse_headers
127
+ assert_equal 'http/1.1', headers[':protocol']
128
+ end
129
+
130
+ def test_bad_protocol
131
+ @o << "GET / blah\r\n\r\n"
132
+ assert_raises(Error) { @parser.parse_headers }
133
+
134
+ reset_parser
135
+ @o << "GET / http\r\n\r\n"
136
+ assert_raises(Error) { @parser.parse_headers }
137
+
138
+ reset_parser
139
+ @o << "GET / http/2\r\n\r\n"
140
+ assert_raises(Error) { @parser.parse_headers }
141
+
142
+ reset_parser
143
+ @o << "GET / http/1.\r\n\r\n"
144
+ assert_raises(Error) { @parser.parse_headers }
145
+
146
+ reset_parser
147
+ @o << "GET / http/a.1\r\n\r\n"
148
+ assert_raises(Error) { @parser.parse_headers }
149
+
150
+ reset_parser
151
+ @o << "GET / http/1.1.1\r\n\r\n"
152
+ assert_raises(Error) { @parser.parse_headers }
153
+ end
154
+
155
+ def test_headers_eof
156
+ @o << "GET / HTTP/1.1\r\na"
157
+ @o.close
158
+
159
+ assert_nil @parser.parse_headers
160
+
161
+ reset_parser
162
+ @o << "GET / HTTP/1.1\r\na:"
163
+ @o.close
164
+
165
+ assert_nil @parser.parse_headers
166
+
167
+ reset_parser
168
+ @o << "GET / HTTP/1.1\r\na: "
169
+ @o.close
170
+
171
+ assert_nil @parser.parse_headers
172
+ end
173
+
174
+ def test_headers
175
+ @o << "GET / HTTP/1.1\r\nFoo: Bar\r\n\r\n"
176
+ headers = @parser.parse_headers
177
+ assert_equal [':method', ':path', ':protocol', 'foo', ':rx'], headers.keys
178
+ assert_equal 'Bar', headers['foo']
179
+
180
+ reset_parser
181
+ @o << "GET / HTTP/1.1\r\nFOO: baR\r\n\r\n"
182
+ headers = @parser.parse_headers
183
+ assert_equal 'baR', headers['foo']
184
+
185
+ reset_parser
186
+ @o << "GET / HTTP/1.1\r\na: bbb\r\nc: ddd\r\n\r\n"
187
+ headers = @parser.parse_headers
188
+ assert_equal 'bbb', headers['a']
189
+ assert_equal 'ddd', headers['c']
190
+ end
191
+
192
+ def test_headers_multiple_values
193
+ @o << "GET / HTTP/1.1\r\nFoo: Bar\r\nfoo: baz\r\n\r\n"
194
+ headers = @parser.parse_headers
195
+ assert_equal ['Bar', 'baz'], headers['foo']
196
+ end
197
+
198
+ def test_bad_headers
199
+ @o << "GET / http/1.1\r\n a: b\r\n\r\n"
200
+ assert_raises(Error) { @parser.parse_headers }
201
+
202
+ reset_parser
203
+ @o << "GET / http/1.1\r\na b\r\n\r\n"
204
+ assert_raises(Error) { @parser.parse_headers }
205
+
206
+ max_key_length = Tipi::HTTP1_LIMITS[:max_header_key_length]
207
+
208
+ reset_parser
209
+ @o << "GET / http/1.1\r\n#{'a' * max_key_length}: b\r\n\r\n"
210
+ headers = @parser.parse_headers
211
+ assert_equal 'b', headers['a' * max_key_length]
212
+
213
+ reset_parser
214
+ @o << "GET / http/1.1\r\n#{'a' * (max_key_length + 1)}: b\r\n\r\n"
215
+ assert_raises(Error) { @parser.parse_headers }
216
+
217
+ max_value_length = Tipi::HTTP1_LIMITS[:max_header_value_length]
218
+
219
+ reset_parser
220
+ @o << "GET / http/1.1\r\nfoo: #{'a' * max_value_length}\r\n\r\n"
221
+ headers = @parser.parse_headers
222
+ assert_equal 'a' * max_value_length, headers['foo']
223
+
224
+ reset_parser
225
+ @o << "GET / http/1.1\r\nfoo: #{'a' * (max_value_length + 1)}\r\n\r\n"
226
+ assert_raises(Error) { @parser.parse_headers }
227
+
228
+ max_header_count = Tipi::HTTP1_LIMITS[:max_header_count]
229
+
230
+ reset_parser
231
+ hdrs = (1..max_header_count).map { |i| "foo#{i}: bar\r\n" }.join
232
+ @o << "GET / http/1.1\r\n#{hdrs}\r\n"
233
+ headers = @parser.parse_headers
234
+ assert_equal (max_header_count + 4), headers.size
235
+
236
+ reset_parser
237
+ hdrs = (1..(max_header_count + 1)).map { |i| "foo#{i}: bar\r\n" }.join
238
+ @o << "GET / http/1.1\r\n#{hdrs}\r\n"
239
+ assert_raises(Error) { @parser.parse_headers }
240
+ end
241
+
242
+ def test_request_without_cr
243
+ msg = "GET /foo HTTP/1.1\nBar: baz\n\n"
244
+ @o << msg
245
+ headers = @parser.parse_headers
246
+ assert_equal({
247
+ ':method' => 'get',
248
+ ':path' => '/foo',
249
+ ':protocol' => 'http/1.1',
250
+ 'bar' => 'baz',
251
+ ':rx' => msg.bytesize
252
+ }, headers)
253
+ end
254
+
255
+ def test_read_body_with_content_length
256
+ 10.times do
257
+ data = ' ' * rand(20..60000)
258
+ msg = "POST /foo HTTP/1.1\r\nContent-Length: #{data.bytesize}\r\n\r\n#{data}"
259
+ spin do
260
+ @o << msg
261
+ end
262
+ headers = @parser.parse_headers
263
+ assert_equal data.bytesize.to_s, headers['content-length']
264
+
265
+ body = @parser.read_body
266
+ assert_equal data, body
267
+ assert_equal msg.bytesize, headers[':rx']
268
+ end
269
+ end
270
+
271
+ def test_read_body_chunk_with_content_length
272
+ data = 'abc' * 20000
273
+ msg = "POST /foo HTTP/1.1\r\nContent-Length: #{data.bytesize}\r\n\r\n#{data}"
274
+ spin { @o << msg }
275
+ headers = @parser.parse_headers
276
+ assert_equal data.bytesize.to_s, headers['content-length']
277
+
278
+ buf = +''
279
+ count = 0
280
+ while (chunk = @parser.read_body_chunk(false))
281
+ count += 1
282
+ buf += chunk
283
+ end
284
+ assert_equal data.bytesize, data.bytesize
285
+ assert_equal data, buf
286
+ assert_in_range 1..3, count
287
+ assert_equal msg.bytesize, headers[':rx']
288
+ end
289
+
290
+ def test_read_body_with_content_length_incomplete
291
+ data = ' ' * rand(20..60000)
292
+ spin do
293
+ @o << "POST /foo HTTP/1.1\r\nContent-Length: #{data.bytesize + 1}\r\n\r\n#{data}"
294
+ @o.close # !!! otherwise the parser will keep waiting
295
+ end
296
+ headers = @parser.parse_headers
297
+
298
+ assert_raises(Tipi::HTTP1Parser::Error) { @parser.read_body }
299
+ end
300
+
301
+ def test_read_body_chunk_with_content_length_incomplete
302
+ data = 'abc' * 50
303
+ @o << "POST /foo HTTP/1.1\r\nContent-Length: #{data.bytesize + 1}\r\n\r\n#{data}"
304
+ @o.close
305
+ headers = @parser.parse_headers
306
+
307
+ assert_raises(Tipi::HTTP1Parser::Error) { @parser.read_body_chunk(false) }
308
+ end
309
+
310
+ def test_read_body_with_chunked_encoding
311
+ chunks = []
312
+ total_sent = 0
313
+ spin do
314
+ msg = "POST /foo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
315
+ @o << msg
316
+ total_sent += msg.bytesize
317
+ rand(8..16).times do |i|
318
+ chunk = i.to_s * rand(200..360000)
319
+ msg = "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
320
+ @o << msg
321
+ chunks << chunk
322
+ total_sent += msg.bytesize
323
+ end
324
+ msg = "0\r\n\r\n"
325
+ @o << msg
326
+ total_sent += msg.bytesize
327
+ end
328
+ headers = @parser.parse_headers
329
+ assert_equal 'chunked', headers['transfer-encoding']
330
+
331
+ body = @parser.read_body
332
+ assert_equal chunks.join, body
333
+ assert_equal total_sent, headers[':rx']
334
+ end
335
+
336
+ def test_read_body_chunk_with_chunked_encoding
337
+ chunks = []
338
+ total_sent = 0
339
+ spin do
340
+ msg = "POST /foo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
341
+ @o << msg
342
+ total_sent += msg.bytesize
343
+ rand(8..16).times do |i|
344
+ chunk = i.to_s * rand(40000..360000)
345
+ msg = "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
346
+ @o << msg
347
+ total_sent += msg.bytesize
348
+ chunks << chunk
349
+ end
350
+ msg = "0\r\n\r\n"
351
+ @o << msg
352
+ total_sent += msg.bytesize
353
+ end
354
+ headers = @parser.parse_headers
355
+ assert_equal 'chunked', headers['transfer-encoding']
356
+
357
+ received = []
358
+ while (chunk = @parser.read_body_chunk(false))
359
+ received << chunk
360
+ end
361
+ assert_equal chunks, received
362
+ assert_equal total_sent, headers[':rx']
363
+ end
364
+
365
+ def test_read_body_with_chunked_encoding_malformed
366
+ spin do
367
+ @o << "POST /foo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
368
+ chunk = ' '.to_s * rand(40000..360000)
369
+ @o << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n3"
370
+ @o << "0\r\n\r\n"
371
+ @o.close
372
+ end
373
+ headers = @parser.parse_headers
374
+ assert_raises(Tipi::HTTP1Parser::Error) { @parser.read_body }
375
+
376
+ reset_parser
377
+ # missing last empty chunk
378
+ spin do
379
+ @o << "POST /foo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
380
+ chunk = ' '.to_s * rand(40000..360000)
381
+ @o << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
382
+ @o.close
383
+ end
384
+ headers = @parser.parse_headers
385
+ assert_raises(Tipi::HTTP1Parser::Error) { @parser.read_body }
386
+
387
+ reset_parser
388
+ # bad chunk size
389
+ spin do
390
+ @o << "POST /foo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
391
+ chunk = ' '.to_s * rand(40000..360000)
392
+ @o << "-#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
393
+ @o.close
394
+ end
395
+ headers = @parser.parse_headers
396
+ assert_raises(Tipi::HTTP1Parser::Error) { @parser.read_body }
397
+ end
398
+
399
+ def test_read_body_chunk_with_chunked_encoding_malformed
400
+ chunk = nil
401
+ spin do
402
+ @o << "POST /foo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
403
+ chunk = ' ' * rand(40000..360000)
404
+ @o << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n3"
405
+ @o << "0\r\n\r\n"
406
+ @o.close
407
+ end
408
+ headers = @parser.parse_headers
409
+ read = @parser.read_body_chunk(false)
410
+ assert_equal chunk, read
411
+ assert_raises(Tipi::HTTP1Parser::Error) { @parser.read_body_chunk(false) }
412
+
413
+ reset_parser
414
+ Fiber.current.shutdown_all_children
415
+ # missing last empty chunk
416
+ chunk = nil
417
+ spin do
418
+ @o << "POST /foo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
419
+ chunk = ' '.to_s * rand(20..1600)
420
+ @o << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
421
+ @o.close
422
+ end
423
+ headers = @parser.parse_headers
424
+ read = @parser.read_body_chunk(false)
425
+ assert_equal chunk, read
426
+ assert_raises(Tipi::HTTP1Parser::Error) { @parser.read_body_chunk(false) }
427
+
428
+ reset_parser
429
+ Fiber.current.shutdown_all_children
430
+ # bad chunk size
431
+ spin do
432
+ @o << "POST /foo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
433
+ chunk = ' '.to_s * rand(20..1600)
434
+ @o << "-#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
435
+ @o.close
436
+ end
437
+ headers = @parser.parse_headers
438
+ assert_raises(Tipi::HTTP1Parser::Error) { @parser.read_body_chunk(false) }
439
+
440
+ reset_parser
441
+ Fiber.current.shutdown_all_children
442
+ # missing body
443
+ @o << "POST /foo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
444
+ @o.close
445
+ headers = @parser.parse_headers
446
+ assert_raises(Tipi::HTTP1Parser::Error) { @parser.read_body_chunk(false) }
447
+ ensure
448
+ Fiber.current.shutdown_all_children
449
+ end
450
+
451
+ def test_complete?
452
+ @o << "GET / HTTP/1.1\r\n\r\n"
453
+ headers = @parser.parse_headers
454
+ assert_equal true, @parser.complete?
455
+
456
+ reset_parser
457
+ @o << "GET / HTTP/1.1\r\nContent-Length: 3\r\n\r\n"
458
+ headers = @parser.parse_headers
459
+ assert_equal false, @parser.complete?
460
+ @o << 'foo'
461
+ body = @parser.read_body
462
+ assert_equal 'foo', body
463
+ assert_equal true, @parser.complete?
464
+
465
+ reset_parser
466
+ @o << "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
467
+ headers = @parser.parse_headers
468
+ assert_equal false, @parser.complete?
469
+ @o << "3\r\nfoo\r\n"
470
+ chunk = @parser.read_body_chunk(false)
471
+ assert_equal 'foo', chunk
472
+ assert_equal false, @parser.complete?
473
+ @o << "0\r\n\r\n"
474
+ chunk = @parser.read_body_chunk(false)
475
+ assert_nil chunk
476
+ assert_equal true, @parser.complete?
477
+ end
478
+
479
+ def test_buffered_body_chunk
480
+ @o << "GET / HTTP/1.1\r\nContent-Length: 3\r\n\r\nfoo"
481
+ headers = @parser.parse_headers
482
+ assert_equal false, @parser.complete?
483
+
484
+ chunk = @parser.read_body_chunk(true)
485
+ assert_equal 'foo', chunk
486
+ assert_equal true, @parser.complete?
487
+ chunk = @parser.read_body_chunk(false)
488
+ assert_nil chunk
489
+ assert_equal true, @parser.complete?
490
+
491
+ reset_parser
492
+ @o << "GET / HTTP/1.1\r\nContent-Length: 6\r\n\r\nfoo"
493
+ headers = @parser.parse_headers
494
+ assert_equal false, @parser.complete?
495
+
496
+ chunk = @parser.read_body_chunk(true)
497
+ assert_equal 'foo', chunk
498
+ assert_equal false, @parser.complete?
499
+ @o << 'bar'
500
+ chunk = @parser.read_body_chunk(false)
501
+ assert_equal 'bar', chunk
502
+ assert_equal true, @parser.complete?
503
+
504
+ reset_parser
505
+ @o << "GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nfoo\r\n"
506
+ headers = @parser.parse_headers
507
+ assert_equal false, @parser.complete?
508
+
509
+ chunk = @parser.read_body_chunk(true)
510
+ assert_equal 'foo', chunk
511
+ assert_equal false, @parser.complete?
512
+ @o << "0\r\n\r\n"
513
+ chunk = @parser.read_body_chunk(true)
514
+ assert_nil chunk
515
+ assert_equal true, @parser.complete?
516
+ end
517
+
518
+ def test_parser_with_tcp_socket
519
+ port = rand(1234..5678)
520
+ server = TCPServer.new('127.0.0.1', port)
521
+ server_fiber = spin do
522
+ while (socket = server.accept)
523
+ spin do
524
+ parser = Tipi::HTTP1Parser.new(socket)
525
+ headers = parser.parse_headers
526
+ socket << headers.inspect
527
+ socket.shutdown
528
+ socket.close
529
+ end
530
+ end
531
+ end
532
+
533
+ snooze
534
+ client = TCPSocket.new('127.0.0.1', port)
535
+ msg = "get /foo HTTP/1.1\r\nCookie: abc=def\r\n\r\n"
536
+ client << msg
537
+ reply = client.read
538
+ assert_equal({
539
+ ':method' => 'get',
540
+ ':path' => '/foo',
541
+ ':protocol' => 'http/1.1',
542
+ 'cookie' => 'abc=def',
543
+ ':rx' => msg.bytesize,
544
+ }, eval(reply))
545
+ ensure
546
+ client.shutdown rescue nil
547
+ client&.close
548
+ server_fiber&.stop
549
+ server_fiber&.await
550
+ server&.close
551
+ end
552
+
553
+ def test_parser_with_callable
554
+ buf = []
555
+ request = +"GET /foo HTTP/1.1\r\nHost: bar\r\n\r\n"
556
+ callable = proc do |len|
557
+ buf << {len: len}
558
+ request
559
+ end
560
+
561
+ parser = Tipi::HTTP1Parser.new(callable)
562
+
563
+ headers = parser.parse_headers
564
+ assert_equal({
565
+ ':method' => 'get',
566
+ ':path' => '/foo',
567
+ ':protocol' => 'http/1.1',
568
+ 'host' => 'bar',
569
+ ':rx' => request.bytesize,
570
+
571
+ }, headers)
572
+ assert_equal [{len: 4096}], buf
573
+
574
+ request = +"GET /bar HTTP/1.1\r\nHost: baz\r\n\r\n"
575
+ headers = parser.parse_headers
576
+ assert_equal({
577
+ ':method' => 'get',
578
+ ':path' => '/bar',
579
+ ':protocol' => 'http/1.1',
580
+ 'host' => 'baz',
581
+ ':rx' => request.bytesize,
582
+
583
+ }, headers)
584
+ assert_equal [{len: 4096}, {len: 4096}], buf
585
+ end
586
+ end