h1p 0.2 → 0.5
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.
- 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
|