tipi 0.42 → 0.43

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,18 +0,0 @@
1
- #ifndef HTTP1_PARSER_H
2
- #define HTTP1_PARSER_H
3
-
4
- #include "ruby.h"
5
-
6
- // debugging
7
- #define OBJ_ID(obj) (NUM2LONG(rb_funcall(obj, rb_intern("object_id"), 0)))
8
- #define INSPECT(str, obj) { printf(str); VALUE s = rb_funcall(obj, rb_intern("inspect"), 0); printf(": %s\n", StringValueCStr(s)); }
9
- #define TRACE_CALLER() { VALUE c = rb_funcall(rb_mKernel, rb_intern("caller"), 0); INSPECT("caller: ", c); }
10
- #define TRACE_C_STACK() { \
11
- void *entries[10]; \
12
- size_t size = backtrace(entries, 10); \
13
- char **strings = backtrace_symbols(entries, size); \
14
- for (unsigned long i = 0; i < size; i++) printf("%s\n", strings[i]); \
15
- free(strings); \
16
- }
17
-
18
- #endif /* HTTP1_PARSER_H */
data/ext/tipi/tipi_ext.c DELETED
@@ -1,5 +0,0 @@
1
- void Init_HTTP1_Parser();
2
-
3
- void Init_tipi_ext() {
4
- Init_HTTP1_Parser();
5
- }
data/security/http1.rb DELETED
@@ -1,12 +0,0 @@
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
@@ -1,586 +0,0 @@
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