h1p 0.2 → 0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -1
- data/Gemfile.lock +1 -1
- data/README.md +61 -15
- data/Rakefile +1 -1
- data/benchmarks/bm_http1_parser.rb +1 -1
- data/benchmarks/pipelined.rb +101 -0
- data/examples/callable.rb +1 -1
- data/examples/http_server.rb +2 -2
- data/ext/h1p/h1p.c +525 -235
- data/ext/h1p/h1p.h +5 -0
- data/ext/h1p/limits.rb +7 -6
- data/lib/h1p/version.rb +1 -1
- data/lib/h1p.rb +16 -10
- data/test/run.rb +5 -0
- data/test/test_h1p_client.rb +532 -0
- data/test/{test_h1p.rb → test_h1p_server.rb} +91 -36
- metadata +7 -4
@@ -4,14 +4,15 @@ require_relative 'helper'
|
|
4
4
|
require 'h1p'
|
5
5
|
require 'socket'
|
6
6
|
require_relative '../ext/h1p/limits'
|
7
|
+
require 'securerandom'
|
7
8
|
|
8
|
-
class
|
9
|
+
class H1PRequestTest < MiniTest::Test
|
9
10
|
Error = H1P::Error
|
10
11
|
|
11
12
|
def setup
|
12
13
|
super
|
13
14
|
@i, @o = IO.pipe
|
14
|
-
@parser = H1P::Parser.new(@i)
|
15
|
+
@parser = H1P::Parser.new(@i, :server)
|
15
16
|
end
|
16
17
|
alias_method :reset_parser, :setup
|
17
18
|
|
@@ -75,7 +76,7 @@ class H1PTest < MiniTest::Test
|
|
75
76
|
@o.close
|
76
77
|
|
77
78
|
assert_raises(Error) { @parser.parse_headers }
|
78
|
-
|
79
|
+
|
79
80
|
max_length = H1P_LIMITS[:max_method_length]
|
80
81
|
|
81
82
|
reset_parser
|
@@ -96,7 +97,7 @@ class H1PTest < MiniTest::Test
|
|
96
97
|
def test_bad_path
|
97
98
|
@o << "GET HTTP/1.1\r\n\r\n"
|
98
99
|
assert_raises(Error) { @parser.parse_headers }
|
99
|
-
|
100
|
+
|
100
101
|
max_length = H1P_LIMITS[:max_path_length]
|
101
102
|
|
102
103
|
reset_parser
|
@@ -149,26 +150,26 @@ class H1PTest < MiniTest::Test
|
|
149
150
|
assert_raises(Error) { @parser.parse_headers }
|
150
151
|
|
151
152
|
reset_parser
|
152
|
-
@o << "GET /
|
153
|
+
@o << "GET / HTTP/1.1.1\r\n\r\n"
|
153
154
|
assert_raises(Error) { @parser.parse_headers }
|
154
155
|
end
|
155
156
|
|
156
157
|
def test_headers_eof
|
157
158
|
@o << "GET / HTTP/1.1\r\na"
|
158
159
|
@o.close
|
159
|
-
|
160
|
+
|
160
161
|
assert_nil @parser.parse_headers
|
161
162
|
|
162
163
|
reset_parser
|
163
164
|
@o << "GET / HTTP/1.1\r\na:"
|
164
165
|
@o.close
|
165
|
-
|
166
|
+
|
166
167
|
assert_nil @parser.parse_headers
|
167
168
|
|
168
169
|
reset_parser
|
169
170
|
@o << "GET / HTTP/1.1\r\na: "
|
170
171
|
@o.close
|
171
|
-
|
172
|
+
|
172
173
|
assert_nil @parser.parse_headers
|
173
174
|
end
|
174
175
|
|
@@ -197,46 +198,46 @@ class H1PTest < MiniTest::Test
|
|
197
198
|
end
|
198
199
|
|
199
200
|
def test_bad_headers
|
200
|
-
@o << "GET /
|
201
|
+
@o << "GET / HTTP/1.1\r\n a: b\r\n\r\n"
|
201
202
|
assert_raises(Error) { @parser.parse_headers }
|
202
203
|
|
203
204
|
reset_parser
|
204
|
-
@o << "GET /
|
205
|
+
@o << "GET / HTTP/1.1\r\na b\r\n\r\n"
|
205
206
|
assert_raises(Error) { @parser.parse_headers }
|
206
207
|
|
207
208
|
max_key_length = H1P_LIMITS[:max_header_key_length]
|
208
209
|
|
209
210
|
reset_parser
|
210
|
-
@o << "GET /
|
211
|
+
@o << "GET / HTTP/1.1\r\n#{'a' * max_key_length}: b\r\n\r\n"
|
211
212
|
headers = @parser.parse_headers
|
212
213
|
assert_equal 'b', headers['a' * max_key_length]
|
213
214
|
|
214
215
|
reset_parser
|
215
|
-
@o << "GET /
|
216
|
+
@o << "GET / HTTP/1.1\r\n#{'a' * (max_key_length + 1)}: b\r\n\r\n"
|
216
217
|
assert_raises(Error) { @parser.parse_headers }
|
217
218
|
|
218
219
|
max_value_length = H1P_LIMITS[:max_header_value_length]
|
219
220
|
|
220
221
|
reset_parser
|
221
|
-
@o << "GET /
|
222
|
+
@o << "GET / HTTP/1.1\r\nfoo: #{'a' * max_value_length}\r\n\r\n"
|
222
223
|
headers = @parser.parse_headers
|
223
224
|
assert_equal 'a' * max_value_length, headers['foo']
|
224
225
|
|
225
226
|
reset_parser
|
226
|
-
@o << "GET /
|
227
|
+
@o << "GET / HTTP/1.1\r\nfoo: #{'a' * (max_value_length + 1)}\r\n\r\n"
|
227
228
|
assert_raises(Error) { @parser.parse_headers }
|
228
229
|
|
229
230
|
max_header_count = H1P_LIMITS[:max_header_count]
|
230
231
|
|
231
232
|
reset_parser
|
232
233
|
hdrs = (1..max_header_count).map { |i| "foo#{i}: bar\r\n" }.join
|
233
|
-
@o << "GET /
|
234
|
+
@o << "GET / HTTP/1.1\r\n#{hdrs}\r\n"
|
234
235
|
headers = @parser.parse_headers
|
235
236
|
assert_equal (max_header_count + 4), headers.size
|
236
237
|
|
237
238
|
reset_parser
|
238
239
|
hdrs = (1..(max_header_count + 1)).map { |i| "foo#{i}: bar\r\n" }.join
|
239
|
-
@o << "GET /
|
240
|
+
@o << "GET / HTTP/1.1\r\n#{hdrs}\r\n"
|
240
241
|
assert_raises(Error) { @parser.parse_headers }
|
241
242
|
end
|
242
243
|
|
@@ -256,7 +257,7 @@ class H1PTest < MiniTest::Test
|
|
256
257
|
def test_read_body_with_content_length
|
257
258
|
10.times do
|
258
259
|
data = ' ' * rand(20..60000)
|
259
|
-
msg = "
|
260
|
+
msg = "GET / HTTP/1.1\r\nContent-Length: #{data.bytesize}\r\n\r\n#{data}"
|
260
261
|
Thread.new { @o << msg }
|
261
262
|
|
262
263
|
headers = @parser.parse_headers
|
@@ -270,7 +271,7 @@ class H1PTest < MiniTest::Test
|
|
270
271
|
|
271
272
|
def test_read_body_chunk_with_content_length
|
272
273
|
data = 'abc' * 20000
|
273
|
-
msg = "
|
274
|
+
msg = "GET / HTTP/1.1\r\nContent-Length: #{data.bytesize}\r\n\r\n#{data}"
|
274
275
|
Thread.new { @o << msg }
|
275
276
|
headers = @parser.parse_headers
|
276
277
|
assert_equal data.bytesize.to_s, headers['content-length']
|
@@ -290,7 +291,7 @@ class H1PTest < MiniTest::Test
|
|
290
291
|
def test_read_body_with_content_length_incomplete
|
291
292
|
data = ' ' * rand(20..60000)
|
292
293
|
Thread.new do
|
293
|
-
@o << "
|
294
|
+
@o << "GET / HTTP/1.1\r\nContent-Length: #{data.bytesize + 1}\r\n\r\n#{data}"
|
294
295
|
@o.close # !!! otherwise the parser will keep waiting
|
295
296
|
end
|
296
297
|
headers = @parser.parse_headers
|
@@ -300,7 +301,7 @@ class H1PTest < MiniTest::Test
|
|
300
301
|
|
301
302
|
def test_read_body_chunk_with_content_length_incomplete
|
302
303
|
data = 'abc' * 50
|
303
|
-
@o << "
|
304
|
+
@o << "GET / HTTP/1.1\r\nContent-Length: #{data.bytesize + 1}\r\n\r\n#{data}"
|
304
305
|
@o.close
|
305
306
|
headers = @parser.parse_headers
|
306
307
|
|
@@ -311,7 +312,7 @@ class H1PTest < MiniTest::Test
|
|
311
312
|
chunks = []
|
312
313
|
total_sent = 0
|
313
314
|
Thread.new do
|
314
|
-
msg = "
|
315
|
+
msg = "GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
|
315
316
|
@o << msg
|
316
317
|
total_sent += msg.bytesize
|
317
318
|
rand(8..16).times do |i|
|
@@ -324,7 +325,7 @@ class H1PTest < MiniTest::Test
|
|
324
325
|
msg = "0\r\n\r\n"
|
325
326
|
@o << msg
|
326
327
|
total_sent += msg.bytesize
|
327
|
-
end
|
328
|
+
end
|
328
329
|
headers = @parser.parse_headers
|
329
330
|
assert_equal 'chunked', headers['transfer-encoding']
|
330
331
|
|
@@ -337,7 +338,7 @@ class H1PTest < MiniTest::Test
|
|
337
338
|
chunks = []
|
338
339
|
total_sent = 0
|
339
340
|
Thread.new do
|
340
|
-
msg = "
|
341
|
+
msg = "GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
|
341
342
|
@o << msg
|
342
343
|
total_sent += msg.bytesize
|
343
344
|
rand(8..16).times do |i|
|
@@ -350,7 +351,7 @@ class H1PTest < MiniTest::Test
|
|
350
351
|
msg = "0\r\n\r\n"
|
351
352
|
@o << msg
|
352
353
|
total_sent += msg.bytesize
|
353
|
-
end
|
354
|
+
end
|
354
355
|
headers = @parser.parse_headers
|
355
356
|
assert_equal 'chunked', headers['transfer-encoding']
|
356
357
|
|
@@ -364,7 +365,7 @@ class H1PTest < MiniTest::Test
|
|
364
365
|
|
365
366
|
def test_read_body_with_chunked_encoding_malformed
|
366
367
|
Thread.new do
|
367
|
-
@o << "
|
368
|
+
@o << "GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
|
368
369
|
chunk = ' '.to_s * rand(40000..360000)
|
369
370
|
@o << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n3"
|
370
371
|
@o << "0\r\n\r\n"
|
@@ -376,22 +377,22 @@ class H1PTest < MiniTest::Test
|
|
376
377
|
reset_parser
|
377
378
|
# missing last empty chunk
|
378
379
|
Thread.new do
|
379
|
-
@o << "
|
380
|
+
@o << "GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
|
380
381
|
chunk = ' '.to_s * rand(40000..360000)
|
381
382
|
@o << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
|
382
383
|
@o.close
|
383
|
-
end
|
384
|
+
end
|
384
385
|
headers = @parser.parse_headers
|
385
386
|
assert_raises(H1P::Error) { @parser.read_body }
|
386
387
|
|
387
388
|
reset_parser
|
388
389
|
# bad chunk size
|
389
390
|
Thread.new do
|
390
|
-
@o << "
|
391
|
+
@o << "GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
|
391
392
|
chunk = ' '.to_s * rand(40000..360000)
|
392
393
|
@o << "-#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
|
393
394
|
@o.close
|
394
|
-
end
|
395
|
+
end
|
395
396
|
headers = @parser.parse_headers
|
396
397
|
assert_raises(H1P::Error) { @parser.read_body }
|
397
398
|
end
|
@@ -399,7 +400,7 @@ class H1PTest < MiniTest::Test
|
|
399
400
|
def test_read_body_chunk_with_chunked_encoding_malformed
|
400
401
|
chunk = nil
|
401
402
|
Thread.new do
|
402
|
-
@o << "
|
403
|
+
@o << "GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
|
403
404
|
chunk = ' ' * rand(40000..360000)
|
404
405
|
@o << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n3"
|
405
406
|
@o << "0\r\n\r\n"
|
@@ -415,7 +416,7 @@ class H1PTest < MiniTest::Test
|
|
415
416
|
# missing last empty chunk
|
416
417
|
chunk = nil
|
417
418
|
Thread.new do
|
418
|
-
@o << "
|
419
|
+
@o << "GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
|
419
420
|
chunk = ' '.to_s * rand(20..1600)
|
420
421
|
@o << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
|
421
422
|
@o.close
|
@@ -429,23 +430,77 @@ class H1PTest < MiniTest::Test
|
|
429
430
|
|
430
431
|
# bad chunk size
|
431
432
|
Thread.new do
|
432
|
-
@o << "
|
433
|
+
@o << "GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
|
433
434
|
chunk = ' '.to_s * rand(20..1600)
|
434
435
|
@o << "-#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
|
435
436
|
@o.close
|
436
|
-
end
|
437
|
+
end
|
437
438
|
headers = @parser.parse_headers
|
438
439
|
assert_raises(H1P::Error) { @parser.read_body_chunk(false) }
|
439
440
|
|
440
441
|
reset_parser
|
441
442
|
|
442
443
|
# missing body
|
443
|
-
@o << "
|
444
|
+
@o << "GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
|
444
445
|
@o.close
|
445
446
|
headers = @parser.parse_headers
|
446
447
|
assert_raises(H1P::Error) { @parser.read_body_chunk(false) }
|
447
448
|
end
|
448
449
|
|
450
|
+
PolyphonyMockup = Object.new
|
451
|
+
def PolyphonyMockup.backend_write(io, buf)
|
452
|
+
io << buf
|
453
|
+
end
|
454
|
+
def PolyphonyMockup.backend_splice(src, dest, len)
|
455
|
+
buf = src.read(len)
|
456
|
+
len = dest.write(buf)
|
457
|
+
len
|
458
|
+
end
|
459
|
+
Object::Polyphony = PolyphonyMockup
|
460
|
+
|
461
|
+
def test_splice_body_to_chunked_encoding
|
462
|
+
req_body = SecureRandom.alphanumeric(60000)
|
463
|
+
req_headers = "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
|
464
|
+
r, w = IO.pipe
|
465
|
+
|
466
|
+
Thread.new do
|
467
|
+
@o << req_headers
|
468
|
+
@o << "#{req_body.bytesize.to_s(16)}\r\n"
|
469
|
+
@o << req_body
|
470
|
+
@o << "\r\n0\r\n\r\n"
|
471
|
+
@o.close
|
472
|
+
end
|
473
|
+
def w.__write_method__; :backend_write; end
|
474
|
+
|
475
|
+
headers = @parser.parse_headers
|
476
|
+
@parser.splice_body_to(w)
|
477
|
+
w.close
|
478
|
+
assert_equal req_body, r.read
|
479
|
+
|
480
|
+
chunk_header_size = "#{req_body.bytesize.to_s(16)}\r\n".bytesize + "\r\n0\r\n\r\n".bytesize
|
481
|
+
assert_equal req_headers.bytesize + req_body.bytesize + chunk_header_size, headers[':rx']
|
482
|
+
end
|
483
|
+
|
484
|
+
def test_splice_body_to_content_length
|
485
|
+
req_body = SecureRandom.alphanumeric(60000)
|
486
|
+
req_headers = "POST / HTTP/1.1\r\nContent-Length: #{req_body.bytesize}\r\n\r\n"
|
487
|
+
r, w = IO.pipe
|
488
|
+
|
489
|
+
Thread.new do
|
490
|
+
@o << req_headers
|
491
|
+
@o << req_body
|
492
|
+
@o.close
|
493
|
+
end
|
494
|
+
def w.__write_method__; :backend_write; end
|
495
|
+
|
496
|
+
headers = @parser.parse_headers
|
497
|
+
@parser.splice_body_to(w)
|
498
|
+
w.close
|
499
|
+
assert_equal req_body, r.read
|
500
|
+
|
501
|
+
assert_equal req_headers.bytesize + req_body.bytesize, headers[':rx']
|
502
|
+
end
|
503
|
+
|
449
504
|
def test_complete?
|
450
505
|
@o << "GET / HTTP/1.1\r\n\r\n"
|
451
506
|
headers = @parser.parse_headers
|
@@ -519,7 +574,7 @@ class H1PTest < MiniTest::Test
|
|
519
574
|
server_thread = Thread.new do
|
520
575
|
while (socket = server.accept)
|
521
576
|
Thread.new do
|
522
|
-
parser = H1P::Parser.new(socket)
|
577
|
+
parser = H1P::Parser.new(socket, :server)
|
523
578
|
headers = parser.parse_headers
|
524
579
|
socket << headers.inspect
|
525
580
|
socket.shutdown
|
@@ -556,7 +611,7 @@ class H1PTest < MiniTest::Test
|
|
556
611
|
request
|
557
612
|
end
|
558
613
|
|
559
|
-
parser = H1P::Parser.new(callable)
|
614
|
+
parser = H1P::Parser.new(callable, :server)
|
560
615
|
|
561
616
|
headers = parser.parse_headers
|
562
617
|
assert_equal({
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: h1p
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.5'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-03-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake-compiler
|
@@ -70,6 +70,7 @@ files:
|
|
70
70
|
- Rakefile
|
71
71
|
- TODO.md
|
72
72
|
- benchmarks/bm_http1_parser.rb
|
73
|
+
- benchmarks/pipelined.rb
|
73
74
|
- examples/callable.rb
|
74
75
|
- examples/http_server.rb
|
75
76
|
- ext/h1p/extconf.rb
|
@@ -80,7 +81,9 @@ files:
|
|
80
81
|
- lib/h1p.rb
|
81
82
|
- lib/h1p/version.rb
|
82
83
|
- test/helper.rb
|
83
|
-
- test/
|
84
|
+
- test/run.rb
|
85
|
+
- test/test_h1p_client.rb
|
86
|
+
- test/test_h1p_server.rb
|
84
87
|
homepage: http://github.com/digital-fabric/h1p
|
85
88
|
licenses:
|
86
89
|
- MIT
|
@@ -105,7 +108,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
105
108
|
- !ruby/object:Gem::Version
|
106
109
|
version: '0'
|
107
110
|
requirements: []
|
108
|
-
rubygems_version: 3.1.
|
111
|
+
rubygems_version: 3.1.6
|
109
112
|
signing_key:
|
110
113
|
specification_version: 4
|
111
114
|
summary: H1P is a blocking HTTP/1 parser for Ruby
|