h1p 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.
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