h1p 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/ext/h1p/h1p.h ADDED
@@ -0,0 +1,18 @@
1
+ #ifndef H1P_H
2
+ #define H1P_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 /* H1P_H */
data/ext/h1p/limits.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ H1P_LIMITS = {
4
+ max_method_length: 16,
5
+ max_path_length: 4096,
6
+ max_header_key_length: 128,
7
+ max_header_value_length: 2048,
8
+ max_header_count: 256,
9
+ max_chunked_encoding_chunk_size_length: 16,
10
+ }
data/h1p.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ require_relative './lib/h1p/version'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'h1p'
5
+ s.version = H1P::VERSION
6
+ s.licenses = ['MIT']
7
+ s.summary = 'H1P is a blocking HTTP/1 parser for Ruby'
8
+ s.author = 'Sharon Rosner'
9
+ s.email = 'sharon@noteflakes.com'
10
+ s.files = `git ls-files`.split
11
+ s.homepage = 'http://github.com/digital-fabric/h1p'
12
+ s.metadata = {
13
+ "source_code_uri" => "https://github.com/digital-fabric/h1p"
14
+ }
15
+ s.rdoc_options = ["--title", "h1p", "--main", "README.md"]
16
+ s.extra_rdoc_files = ["README.md"]
17
+ s.extensions = ["ext/h1p/extconf.rb"]
18
+ s.require_paths = ["lib"]
19
+ s.required_ruby_version = '>= 2.6'
20
+
21
+ s.add_development_dependency 'rake-compiler', '1.1.1'
22
+ s.add_development_dependency 'rake', '~>12.3.3'
23
+ s.add_development_dependency 'minitest', '~>5.11.3'
24
+ s.add_development_dependency 'minitest-reporters', '~>1.4.2'
25
+ end
data/lib/h1p.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'h1p_ext'
4
+
5
+ unless Object.const_defined?('Polyphony')
6
+ class IO
7
+ def __parser_read_method__
8
+ :stock_readpartial
9
+ end
10
+ end
11
+
12
+ require 'socket'
13
+
14
+ class Socket
15
+ def __parser_read_method__
16
+ :stock_readpartial
17
+ end
18
+ end
19
+
20
+ class TCPSocket
21
+ def __parser_read_method__
22
+ :stock_readpartial
23
+ end
24
+ end
25
+
26
+ class UNIXSocket
27
+ def __parser_read_method__
28
+ :stock_readpartial
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module H1P
4
+ VERSION = '0.1'
5
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ require 'fileutils'
6
+
7
+ require 'minitest/autorun'
8
+ require 'minitest/reporters'
9
+
10
+ module Minitest::Assertions
11
+ def assert_in_range exp_range, act
12
+ msg = message(msg) { "Expected #{mu_pp(act)} to be in range #{mu_pp(exp_range)}" }
13
+ assert exp_range.include?(act), msg
14
+ end
15
+ end
data/test/test_h1p.rb ADDED
@@ -0,0 +1,584 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+ require 'h1p'
5
+ require 'socket'
6
+ require_relative '../ext/h1p/limits'
7
+
8
+ class H1PTest < MiniTest::Test
9
+ Error = H1P::Error
10
+
11
+ def setup
12
+ super
13
+ @i, @o = IO.pipe
14
+ @parser = H1P::Parser.new(@i)
15
+ end
16
+ alias_method :reset_parser, :setup
17
+
18
+ def test_request_line
19
+ msg = "GET / HTTP/1.1\r\n\r\n"
20
+ @o << msg
21
+ headers = @parser.parse_headers
22
+
23
+ assert_equal(
24
+ {
25
+ ':method' => 'get',
26
+ ':path' => '/',
27
+ ':protocol' => 'http/1.1',
28
+ ':rx' => msg.bytesize
29
+ },
30
+ headers
31
+ )
32
+ end
33
+
34
+ def test_request_line_whitespace
35
+ msg = "GET / HTTP/1.1\r\n\r\n"
36
+ @o << msg
37
+ headers = @parser.parse_headers
38
+
39
+ assert_equal(
40
+ {
41
+ ':method' => 'get',
42
+ ':path' => '/',
43
+ ':protocol' => 'http/1.1',
44
+ ':rx' => msg.bytesize
45
+ },
46
+ headers
47
+ )
48
+ end
49
+
50
+ def test_eof
51
+ @o << "GET / HTTP/1.1"
52
+ @o.close
53
+
54
+ assert_nil @parser.parse_headers
55
+ end
56
+
57
+ def test_method_case
58
+ @o << "GET / HTTP/1.1\r\n\r\n"
59
+ headers = @parser.parse_headers
60
+ assert_equal 'get', headers[':method']
61
+
62
+ reset_parser
63
+ @o << "post / HTTP/1.1\r\n\r\n"
64
+ headers = @parser.parse_headers
65
+ assert_equal 'post', headers[':method']
66
+
67
+ reset_parser
68
+ @o << "PoST / HTTP/1.1\r\n\r\n"
69
+ headers = @parser.parse_headers
70
+ assert_equal 'post', headers[':method']
71
+ end
72
+
73
+ def test_bad_method
74
+ @o << " / HTTP/1.1\r\n\r\n"
75
+ @o.close
76
+
77
+ assert_raises(Error) { @parser.parse_headers }
78
+
79
+ max_length = H1P_LIMITS[:max_method_length]
80
+
81
+ reset_parser
82
+ @o << "#{'a' * max_length} / HTTP/1.1\r\n\r\n"
83
+ assert_equal 'a' * max_length, @parser.parse_headers[':method']
84
+
85
+ reset_parser
86
+ @o << "#{'a' * (max_length + 1)} / HTTP/1.1\r\n\r\n"
87
+ assert_raises(Error) { @parser.parse_headers }
88
+ end
89
+
90
+ def test_path_characters
91
+ @o << "GET /äBçDé¤23~{@€ HTTP/1.1\r\n\r\n"
92
+ headers = @parser.parse_headers
93
+ assert_equal '/äBçDé¤23~{@€', headers[':path']
94
+ end
95
+
96
+ def test_bad_path
97
+ @o << "GET HTTP/1.1\r\n\r\n"
98
+ assert_raises(Error) { @parser.parse_headers }
99
+
100
+ max_length = H1P_LIMITS[:max_path_length]
101
+
102
+ reset_parser
103
+ @o << "get #{'a' * max_length} HTTP/1.1\r\n\r\n"
104
+ assert_equal 'a' * max_length, @parser.parse_headers[':path']
105
+
106
+ reset_parser
107
+ @o << "get #{'a' * (max_length + 1)} HTTP/1.1\r\n\r\n"
108
+ assert_raises(Error) { @parser.parse_headers }
109
+ end
110
+
111
+ def test_protocol
112
+ @o << "GET / http/1\r\n\r\n"
113
+ headers = @parser.parse_headers
114
+ assert_equal 'http/1', headers[':protocol']
115
+
116
+ reset_parser
117
+ @o << "GET / HTTP/1\r\n\r\n"
118
+ headers = @parser.parse_headers
119
+ assert_equal 'http/1', headers[':protocol']
120
+
121
+ reset_parser
122
+ @o << "GET / HTTP/1.0\r\n\r\n"
123
+ headers = @parser.parse_headers
124
+ assert_equal 'http/1.0', headers[':protocol']
125
+
126
+ @o << "GET / HttP/1.1\r\n\r\n"
127
+ headers = @parser.parse_headers
128
+ assert_equal 'http/1.1', headers[':protocol']
129
+ end
130
+
131
+ def test_bad_protocol
132
+ @o << "GET / blah\r\n\r\n"
133
+ assert_raises(Error) { @parser.parse_headers }
134
+
135
+ reset_parser
136
+ @o << "GET / http\r\n\r\n"
137
+ assert_raises(Error) { @parser.parse_headers }
138
+
139
+ reset_parser
140
+ @o << "GET / http/2\r\n\r\n"
141
+ assert_raises(Error) { @parser.parse_headers }
142
+
143
+ reset_parser
144
+ @o << "GET / http/1.\r\n\r\n"
145
+ assert_raises(Error) { @parser.parse_headers }
146
+
147
+ reset_parser
148
+ @o << "GET / http/a.1\r\n\r\n"
149
+ assert_raises(Error) { @parser.parse_headers }
150
+
151
+ reset_parser
152
+ @o << "GET / http/1.1.1\r\n\r\n"
153
+ assert_raises(Error) { @parser.parse_headers }
154
+ end
155
+
156
+ def test_headers_eof
157
+ @o << "GET / HTTP/1.1\r\na"
158
+ @o.close
159
+
160
+ assert_nil @parser.parse_headers
161
+
162
+ reset_parser
163
+ @o << "GET / HTTP/1.1\r\na:"
164
+ @o.close
165
+
166
+ assert_nil @parser.parse_headers
167
+
168
+ reset_parser
169
+ @o << "GET / HTTP/1.1\r\na: "
170
+ @o.close
171
+
172
+ assert_nil @parser.parse_headers
173
+ end
174
+
175
+ def test_headers
176
+ @o << "GET / HTTP/1.1\r\nFoo: Bar\r\n\r\n"
177
+ headers = @parser.parse_headers
178
+ assert_equal [':method', ':path', ':protocol', 'foo', ':rx'], headers.keys
179
+ assert_equal 'Bar', headers['foo']
180
+
181
+ reset_parser
182
+ @o << "GET / HTTP/1.1\r\nFOO: baR\r\n\r\n"
183
+ headers = @parser.parse_headers
184
+ assert_equal 'baR', headers['foo']
185
+
186
+ reset_parser
187
+ @o << "GET / HTTP/1.1\r\na: bbb\r\nc: ddd\r\n\r\n"
188
+ headers = @parser.parse_headers
189
+ assert_equal 'bbb', headers['a']
190
+ assert_equal 'ddd', headers['c']
191
+ end
192
+
193
+ def test_headers_multiple_values
194
+ @o << "GET / HTTP/1.1\r\nFoo: Bar\r\nfoo: baz\r\n\r\n"
195
+ headers = @parser.parse_headers
196
+ assert_equal ['Bar', 'baz'], headers['foo']
197
+ end
198
+
199
+ def test_bad_headers
200
+ @o << "GET / http/1.1\r\n a: b\r\n\r\n"
201
+ assert_raises(Error) { @parser.parse_headers }
202
+
203
+ reset_parser
204
+ @o << "GET / http/1.1\r\na b\r\n\r\n"
205
+ assert_raises(Error) { @parser.parse_headers }
206
+
207
+ max_key_length = H1P_LIMITS[:max_header_key_length]
208
+
209
+ reset_parser
210
+ @o << "GET / http/1.1\r\n#{'a' * max_key_length}: b\r\n\r\n"
211
+ headers = @parser.parse_headers
212
+ assert_equal 'b', headers['a' * max_key_length]
213
+
214
+ reset_parser
215
+ @o << "GET / http/1.1\r\n#{'a' * (max_key_length + 1)}: b\r\n\r\n"
216
+ assert_raises(Error) { @parser.parse_headers }
217
+
218
+ max_value_length = H1P_LIMITS[:max_header_value_length]
219
+
220
+ reset_parser
221
+ @o << "GET / http/1.1\r\nfoo: #{'a' * max_value_length}\r\n\r\n"
222
+ headers = @parser.parse_headers
223
+ assert_equal 'a' * max_value_length, headers['foo']
224
+
225
+ reset_parser
226
+ @o << "GET / http/1.1\r\nfoo: #{'a' * (max_value_length + 1)}\r\n\r\n"
227
+ assert_raises(Error) { @parser.parse_headers }
228
+
229
+ max_header_count = H1P_LIMITS[:max_header_count]
230
+
231
+ reset_parser
232
+ hdrs = (1..max_header_count).map { |i| "foo#{i}: bar\r\n" }.join
233
+ @o << "GET / http/1.1\r\n#{hdrs}\r\n"
234
+ headers = @parser.parse_headers
235
+ assert_equal (max_header_count + 4), headers.size
236
+
237
+ reset_parser
238
+ hdrs = (1..(max_header_count + 1)).map { |i| "foo#{i}: bar\r\n" }.join
239
+ @o << "GET / http/1.1\r\n#{hdrs}\r\n"
240
+ assert_raises(Error) { @parser.parse_headers }
241
+ end
242
+
243
+ def test_request_without_cr
244
+ msg = "GET /foo HTTP/1.1\nBar: baz\n\n"
245
+ @o << msg
246
+ headers = @parser.parse_headers
247
+ assert_equal({
248
+ ':method' => 'get',
249
+ ':path' => '/foo',
250
+ ':protocol' => 'http/1.1',
251
+ 'bar' => 'baz',
252
+ ':rx' => msg.bytesize
253
+ }, headers)
254
+ end
255
+
256
+ def test_read_body_with_content_length
257
+ 10.times do
258
+ data = ' ' * rand(20..60000)
259
+ msg = "POST /foo HTTP/1.1\r\nContent-Length: #{data.bytesize}\r\n\r\n#{data}"
260
+ Thread.new { @o << msg }
261
+
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
+ Thread.new { @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..20, 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
+ Thread.new 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(H1P::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(H1P::Error) { @parser.read_body_chunk(false) }
308
+ end
309
+
310
+ def test_read_body_with_chunked_encoding
311
+ chunks = []
312
+ total_sent = 0
313
+ Thread.new 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
+ Thread.new 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
+ Thread.new 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(H1P::Error) { @parser.read_body }
375
+
376
+ reset_parser
377
+ # missing last empty chunk
378
+ Thread.new 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(H1P::Error) { @parser.read_body }
386
+
387
+ reset_parser
388
+ # bad chunk size
389
+ Thread.new 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(H1P::Error) { @parser.read_body }
397
+ end
398
+
399
+ def test_read_body_chunk_with_chunked_encoding_malformed
400
+ chunk = nil
401
+ Thread.new 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(H1P::Error) { @parser.read_body_chunk(false) }
412
+
413
+ reset_parser
414
+
415
+ # missing last empty chunk
416
+ chunk = nil
417
+ Thread.new 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(H1P::Error) { @parser.read_body_chunk(false) }
427
+
428
+ reset_parser
429
+
430
+ # bad chunk size
431
+ Thread.new 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(H1P::Error) { @parser.read_body_chunk(false) }
439
+
440
+ reset_parser
441
+
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(H1P::Error) { @parser.read_body_chunk(false) }
447
+ end
448
+
449
+ def test_complete?
450
+ @o << "GET / HTTP/1.1\r\n\r\n"
451
+ headers = @parser.parse_headers
452
+ assert_equal true, @parser.complete?
453
+
454
+ reset_parser
455
+ @o << "GET / HTTP/1.1\r\nContent-Length: 3\r\n\r\n"
456
+ headers = @parser.parse_headers
457
+ assert_equal false, @parser.complete?
458
+ @o << 'foo'
459
+ body = @parser.read_body
460
+ assert_equal 'foo', body
461
+ assert_equal true, @parser.complete?
462
+
463
+ reset_parser
464
+ @o << "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
465
+ headers = @parser.parse_headers
466
+ assert_equal false, @parser.complete?
467
+ @o << "3\r\nfoo\r\n"
468
+ chunk = @parser.read_body_chunk(false)
469
+ assert_equal 'foo', chunk
470
+ assert_equal false, @parser.complete?
471
+ @o << "0\r\n\r\n"
472
+ chunk = @parser.read_body_chunk(false)
473
+ assert_nil chunk
474
+ assert_equal true, @parser.complete?
475
+ end
476
+
477
+ def test_buffered_body_chunk
478
+ @o << "GET / HTTP/1.1\r\nContent-Length: 3\r\n\r\nfoo"
479
+ headers = @parser.parse_headers
480
+ assert_equal false, @parser.complete?
481
+
482
+ chunk = @parser.read_body_chunk(true)
483
+ assert_equal 'foo', chunk
484
+ assert_equal true, @parser.complete?
485
+ chunk = @parser.read_body_chunk(false)
486
+ assert_nil chunk
487
+ assert_equal true, @parser.complete?
488
+
489
+ reset_parser
490
+ @o << "GET / HTTP/1.1\r\nContent-Length: 6\r\n\r\nfoo"
491
+ headers = @parser.parse_headers
492
+ assert_equal false, @parser.complete?
493
+
494
+ chunk = @parser.read_body_chunk(true)
495
+ assert_equal 'foo', chunk
496
+ assert_equal false, @parser.complete?
497
+ @o << 'bar'
498
+ chunk = @parser.read_body_chunk(false)
499
+ assert_equal 'bar', chunk
500
+ assert_equal true, @parser.complete?
501
+
502
+ reset_parser
503
+ @o << "GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nfoo\r\n"
504
+ headers = @parser.parse_headers
505
+ assert_equal false, @parser.complete?
506
+
507
+ chunk = @parser.read_body_chunk(true)
508
+ assert_equal 'foo', chunk
509
+ assert_equal false, @parser.complete?
510
+ @o << "0\r\n\r\n"
511
+ chunk = @parser.read_body_chunk(true)
512
+ assert_nil chunk
513
+ assert_equal true, @parser.complete?
514
+ end
515
+
516
+ def test_parser_with_tcp_socket
517
+ port = rand(1234..5678)
518
+ server = TCPServer.new('127.0.0.1', port)
519
+ server_thread = Thread.new do
520
+ while (socket = server.accept)
521
+ Thread.new do
522
+ parser = H1P::Parser.new(socket)
523
+ headers = parser.parse_headers
524
+ socket << headers.inspect
525
+ socket.shutdown
526
+ socket.close
527
+ end
528
+ end
529
+ end
530
+
531
+ sleep 0.001
532
+ client = TCPSocket.new('127.0.0.1', port)
533
+ msg = "get /foo HTTP/1.1\r\nCookie: abc=def\r\n\r\n"
534
+ client << msg
535
+ reply = client.read
536
+ assert_equal({
537
+ ':method' => 'get',
538
+ ':path' => '/foo',
539
+ ':protocol' => 'http/1.1',
540
+ 'cookie' => 'abc=def',
541
+ ':rx' => msg.bytesize,
542
+ }, eval(reply))
543
+ ensure
544
+ client.shutdown rescue nil
545
+ client&.close
546
+ server_thread&.kill
547
+ server_thread&.join
548
+ server&.close
549
+ end
550
+
551
+ def test_parser_with_callable
552
+ buf = []
553
+ request = +"GET /foo HTTP/1.1\r\nHost: bar\r\n\r\n"
554
+ callable = proc do |len|
555
+ buf << {len: len}
556
+ request
557
+ end
558
+
559
+ parser = H1P::Parser.new(callable)
560
+
561
+ headers = parser.parse_headers
562
+ assert_equal({
563
+ ':method' => 'get',
564
+ ':path' => '/foo',
565
+ ':protocol' => 'http/1.1',
566
+ 'host' => 'bar',
567
+ ':rx' => request.bytesize,
568
+
569
+ }, headers)
570
+ assert_equal [{len: 4096}], buf
571
+
572
+ request = +"GET /bar HTTP/1.1\r\nHost: baz\r\n\r\n"
573
+ headers = parser.parse_headers
574
+ assert_equal({
575
+ ':method' => 'get',
576
+ ':path' => '/bar',
577
+ ':protocol' => 'http/1.1',
578
+ 'host' => 'baz',
579
+ ':rx' => request.bytesize,
580
+
581
+ }, headers)
582
+ assert_equal [{len: 4096}, {len: 4096}], buf
583
+ end
584
+ end