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.
data/ext/h1p/h1p.h CHANGED
@@ -14,5 +14,10 @@
14
14
  for (unsigned long i = 0; i < size; i++) printf("%s\n", strings[i]); \
15
15
  free(strings); \
16
16
  }
17
+ #define PRINT_BUFFER(prefix, ptr, len) { \
18
+ printf("%s buffer (%d): ", prefix, (int)len); \
19
+ for (int i = 0; i < len; i++) printf("%02X ", ptr[i]); \
20
+ printf("\n"); \
21
+ }
17
22
 
18
23
  #endif /* H1P_H */
data/ext/h1p/limits.rb CHANGED
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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,
4
+ max_method_length: 16,
5
+ max_path_length: 4096,
6
+ max_status_message_length: 256,
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,
10
11
  }
data/lib/h1p/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module H1P
4
- VERSION = '0.2'
4
+ VERSION = '0.5'
5
5
  end
data/lib/h1p.rb CHANGED
@@ -2,29 +2,35 @@
2
2
 
3
3
  require_relative './h1p_ext'
4
4
 
5
- unless Object.const_defined?('Polyphony')
6
- class IO
7
- def __parser_read_method__
5
+ class ::IO
6
+ if !method_defined?(:__read_method__)
7
+ def __read_method__
8
8
  :stock_readpartial
9
9
  end
10
10
  end
11
+ end
11
12
 
12
- require 'socket'
13
+ require 'socket'
13
14
 
14
- class Socket
15
- def __parser_read_method__
15
+ class Socket
16
+ if !method_defined?(:__read_method__)
17
+ def __read_method__
16
18
  :stock_readpartial
17
19
  end
18
20
  end
21
+ end
19
22
 
20
- class TCPSocket
21
- def __parser_read_method__
23
+ class TCPSocket
24
+ if !method_defined?(:__read_method__)
25
+ def __read_method__
22
26
  :stock_readpartial
23
27
  end
24
28
  end
29
+ end
25
30
 
26
- class UNIXSocket
27
- def __parser_read_method__
31
+ class UNIXSocket
32
+ if !method_defined?(:__read_method__)
33
+ def __read_method__
28
34
  :stock_readpartial
29
35
  end
30
36
  end
data/test/run.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir.glob("#{__dir__}/test_*.rb").each do |path|
4
+ require(path)
5
+ end
@@ -0,0 +1,532 @@
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 H1PClientTest < MiniTest::Test
9
+ Error = H1P::Error
10
+
11
+ def setup
12
+ super
13
+ @i, @o = IO.pipe
14
+ @parser = H1P::Parser.new(@i, :client)
15
+ end
16
+ alias_method :reset_parser, :setup
17
+
18
+ def test_status_line
19
+ msg = "HTTP/1.1 200 OK\r\n\r\n"
20
+ @o << msg
21
+ headers = @parser.parse_headers
22
+
23
+ assert_equal(
24
+ {
25
+ ':status' => 200,
26
+ ':status_message' => 'OK',
27
+ ':protocol' => 'http/1.1',
28
+ ':rx' => msg.bytesize
29
+ },
30
+ headers
31
+ )
32
+ end
33
+
34
+ def test_status_line_whitespace
35
+ msg = "HTTP/1.1 404 Not found\r\n\r\n"
36
+ @o << msg
37
+ headers = @parser.parse_headers
38
+
39
+ assert_equal(
40
+ {
41
+ ':protocol' => 'http/1.1',
42
+ ':status' => 404,
43
+ ':status_message' => 'Not found',
44
+ ':rx' => msg.bytesize
45
+ },
46
+ headers
47
+ )
48
+ end
49
+
50
+ def test_eof
51
+ @o << "HTTP/1.1 200 OK"
52
+ @o.close
53
+
54
+ assert_nil @parser.parse_headers
55
+ end
56
+
57
+ def test_protocol_case
58
+ @o << "HTTP/1.1 200\r\n\r\n"
59
+ headers = @parser.parse_headers
60
+ assert_equal 'http/1.1', headers[':protocol']
61
+
62
+ reset_parser
63
+ @o << "http/1.1 200\r\n\r\n"
64
+ headers = @parser.parse_headers
65
+ assert_equal 'http/1.1', headers[':protocol']
66
+ end
67
+
68
+ def test_bad_status_line
69
+ @o << " HTTP/1.1 200\r\n\r\n"
70
+ @o.close
71
+
72
+ assert_raises(Error) { @parser.parse_headers }
73
+
74
+ max_length = H1P_LIMITS[:max_status_message_length]
75
+
76
+ reset_parser
77
+ @o << "HTTP/1.1 200 #{'a' * max_length}\r\n\r\n"
78
+ assert_equal 'a' * max_length, @parser.parse_headers[':status_message']
79
+
80
+ reset_parser
81
+ @o << "HTTP/1.1 200 #{'a' * (max_length + 1)}\r\n\r\n"
82
+ assert_raises(Error) { @parser.parse_headers }
83
+ end
84
+
85
+ def test_path_characters
86
+ @o << "HTTP/1.1 200 äBçDé¤23~{@€\r\n\r\n"
87
+ headers = @parser.parse_headers
88
+ assert_equal 'äBçDé¤23~{@€', headers[':status_message']
89
+ end
90
+
91
+ def test_bad_status
92
+ @o << "HTTP/1.1 abc\r\n\r\n"
93
+ assert_raises(Error) { @parser.parse_headers }
94
+
95
+ reset_parser
96
+ @o << "HTTP/1.1 1111111111111111111\r\n\r\n"
97
+ assert_raises(Error) { @parser.parse_headers }
98
+
99
+ reset_parser
100
+ @o << "HTTP/1.1 200a\r\n\r\n"
101
+ assert_raises(Error) { @parser.parse_headers }
102
+ end
103
+
104
+ def test_headers_eof
105
+ @o << "HTTP/1.1 200 OK\r\na"
106
+ @o.close
107
+
108
+ assert_nil @parser.parse_headers
109
+
110
+ reset_parser
111
+ @o << "HTTP/1.1 200 OK\r\na:"
112
+ @o.close
113
+
114
+ assert_nil @parser.parse_headers
115
+
116
+ reset_parser
117
+ @o << "HTTP/1.1 200 OK\r\na: "
118
+ @o.close
119
+
120
+ assert_nil @parser.parse_headers
121
+ end
122
+
123
+ def test_headers
124
+ @o << "HTTP/1.1 200 OK\r\nFoo: Bar\r\n\r\n"
125
+ headers = @parser.parse_headers
126
+ assert_equal [':protocol', ':status', ':status_message', 'foo', ':rx'], headers.keys
127
+ assert_equal 'Bar', headers['foo']
128
+
129
+ reset_parser
130
+ @o << "HTTP/1.1 200 OK\r\nFOO: baR\r\n\r\n"
131
+ headers = @parser.parse_headers
132
+ assert_equal 'baR', headers['foo']
133
+
134
+ reset_parser
135
+ @o << "HTTP/1.1 200 OK\r\na: bbb\r\nc: ddd\r\n\r\n"
136
+ headers = @parser.parse_headers
137
+ assert_equal 'bbb', headers['a']
138
+ assert_equal 'ddd', headers['c']
139
+ end
140
+
141
+ def test_headers_multiple_values
142
+ @o << "HTTP/1.1 200 OK\r\nFoo: Bar\r\nfoo: baz\r\n\r\n"
143
+ headers = @parser.parse_headers
144
+ assert_equal ['Bar', 'baz'], headers['foo']
145
+ end
146
+
147
+ def test_bad_headers
148
+ @o << "HTTP/1.1 200 OK\r\n a: b\r\n\r\n"
149
+ assert_raises(Error) { @parser.parse_headers }
150
+
151
+ reset_parser
152
+ @o << "HTTP/1.1 200 OK\r\na b\r\n\r\n"
153
+ assert_raises(Error) { @parser.parse_headers }
154
+
155
+ max_key_length = H1P_LIMITS[:max_header_key_length]
156
+
157
+ reset_parser
158
+ @o << "HTTP/1.1 200 OK\r\n#{'a' * max_key_length}: b\r\n\r\n"
159
+ headers = @parser.parse_headers
160
+ assert_equal 'b', headers['a' * max_key_length]
161
+
162
+ reset_parser
163
+ @o << "HTTP/1.1 200 OK\r\n#{'a' * (max_key_length + 1)}: b\r\n\r\n"
164
+ assert_raises(Error) { @parser.parse_headers }
165
+
166
+ max_value_length = H1P_LIMITS[:max_header_value_length]
167
+
168
+ reset_parser
169
+ @o << "HTTP/1.1 200 OK\r\nfoo: #{'a' * max_value_length}\r\n\r\n"
170
+ headers = @parser.parse_headers
171
+ assert_equal 'a' * max_value_length, headers['foo']
172
+
173
+ reset_parser
174
+ @o << "HTTP/1.1 200 OK\r\nfoo: #{'a' * (max_value_length + 1)}\r\n\r\n"
175
+ assert_raises(Error) { @parser.parse_headers }
176
+
177
+ max_header_count = H1P_LIMITS[:max_header_count]
178
+
179
+ reset_parser
180
+ hdrs = (1..max_header_count).map { |i| "foo#{i}: bar\r\n" }.join
181
+ @o << "HTTP/1.1 200 OK\r\n#{hdrs}\r\n"
182
+ headers = @parser.parse_headers
183
+ assert_equal (max_header_count + 4), headers.size
184
+
185
+ reset_parser
186
+ hdrs = (1..(max_header_count + 1)).map { |i| "foo#{i}: bar\r\n" }.join
187
+ @o << "HTTP/1.1 200 OK\r\n#{hdrs}\r\n"
188
+ assert_raises(Error) { @parser.parse_headers }
189
+ end
190
+
191
+ def test_request_without_cr
192
+ msg = "HTTP/1.1 200 OK\nBar: baz\n\n"
193
+ @o << msg
194
+ headers = @parser.parse_headers
195
+ assert_equal({
196
+ ':protocol' => 'http/1.1',
197
+ ':status' => 200,
198
+ ':status_message' => 'OK',
199
+ 'bar' => 'baz',
200
+ ':rx' => msg.bytesize
201
+ }, headers)
202
+ end
203
+
204
+ def test_read_body_with_content_length
205
+ 10.times do
206
+ data = ' ' * rand(20..60000)
207
+ msg = "HTTP/1.1 200 OK\r\nContent-Length: #{data.bytesize}\r\n\r\n#{data}"
208
+ Thread.new { @o << msg }
209
+
210
+ headers = @parser.parse_headers
211
+ assert_equal data.bytesize.to_s, headers['content-length']
212
+
213
+ body = @parser.read_body
214
+ assert_equal data, body
215
+ assert_equal msg.bytesize, headers[':rx']
216
+ end
217
+ end
218
+
219
+ def test_read_body_chunk_with_content_length
220
+ data = 'abc' * 20000
221
+ msg = "HTTP/1.1 200 OK\r\nContent-Length: #{data.bytesize}\r\n\r\n#{data}"
222
+ Thread.new { @o << msg }
223
+ headers = @parser.parse_headers
224
+ assert_equal data.bytesize.to_s, headers['content-length']
225
+
226
+ buf = +''
227
+ count = 0
228
+ while (chunk = @parser.read_body_chunk(false))
229
+ count += 1
230
+ buf += chunk
231
+ end
232
+ assert_equal data.bytesize, data.bytesize
233
+ assert_equal data, buf
234
+ assert_in_range 1..20, count
235
+ assert_equal msg.bytesize, headers[':rx']
236
+ end
237
+
238
+ def test_read_body_with_content_length_incomplete
239
+ data = ' ' * rand(20..60000)
240
+ Thread.new do
241
+ @o << "HTTP/1.1 200 OK\r\nContent-Length: #{data.bytesize + 1}\r\n\r\n#{data}"
242
+ @o.close # !!! otherwise the parser will keep waiting
243
+ end
244
+ headers = @parser.parse_headers
245
+
246
+ assert_raises(H1P::Error) { @parser.read_body }
247
+ end
248
+
249
+ def test_read_body_chunk_with_content_length_incomplete
250
+ data = 'abc' * 50
251
+ @o << "HTTP/1.1 200 OK\r\nContent-Length: #{data.bytesize + 1}\r\n\r\n#{data}"
252
+ @o.close
253
+ headers = @parser.parse_headers
254
+
255
+ assert_raises(H1P::Error) { @parser.read_body_chunk(false) }
256
+ end
257
+
258
+ def test_read_body_with_chunked_encoding
259
+ chunks = []
260
+ total_sent = 0
261
+ Thread.new do
262
+ msg = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
263
+ @o << msg
264
+ total_sent += msg.bytesize
265
+ rand(8..16).times do |i|
266
+ chunk = i.to_s * rand(200..360000)
267
+ msg = "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
268
+ @o << msg
269
+ chunks << chunk
270
+ total_sent += msg.bytesize
271
+ end
272
+ msg = "0\r\n\r\n"
273
+ @o << msg
274
+ total_sent += msg.bytesize
275
+ end
276
+ headers = @parser.parse_headers
277
+ assert_equal 'chunked', headers['transfer-encoding']
278
+
279
+ body = @parser.read_body
280
+ assert_equal chunks.join, body
281
+ assert_equal total_sent, headers[':rx']
282
+ end
283
+
284
+ def test_read_body_chunk_with_chunked_encoding
285
+ chunks = []
286
+ total_sent = 0
287
+ Thread.new do
288
+ msg = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
289
+ @o << msg
290
+ total_sent += msg.bytesize
291
+ rand(8..16).times do |i|
292
+ chunk = i.to_s * rand(40000..360000)
293
+ msg = "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
294
+ @o << msg
295
+ total_sent += msg.bytesize
296
+ chunks << chunk
297
+ end
298
+ msg = "0\r\n\r\n"
299
+ @o << msg
300
+ total_sent += msg.bytesize
301
+ end
302
+ headers = @parser.parse_headers
303
+ assert_equal 'chunked', headers['transfer-encoding']
304
+
305
+ received = []
306
+ while (chunk = @parser.read_body_chunk(false))
307
+ received << chunk
308
+ end
309
+ assert_equal chunks, received
310
+ assert_equal total_sent, headers[':rx']
311
+ end
312
+
313
+ def test_read_body_with_chunked_encoding_malformed
314
+ Thread.new do
315
+ @o << "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
316
+ chunk = ' '.to_s * rand(40000..360000)
317
+ @o << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n3"
318
+ @o << "0\r\n\r\n"
319
+ @o.close
320
+ end
321
+ headers = @parser.parse_headers
322
+ assert_raises(H1P::Error) { @parser.read_body }
323
+
324
+ reset_parser
325
+ # missing last empty chunk
326
+ Thread.new do
327
+ @o << "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
328
+ chunk = ' '.to_s * rand(40000..360000)
329
+ @o << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
330
+ @o.close
331
+ end
332
+ headers = @parser.parse_headers
333
+ assert_raises(H1P::Error) { @parser.read_body }
334
+
335
+ reset_parser
336
+ # bad chunk size
337
+ Thread.new do
338
+ @o << "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
339
+ chunk = ' '.to_s * rand(40000..360000)
340
+ @o << "-#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
341
+ @o.close
342
+ end
343
+ headers = @parser.parse_headers
344
+ assert_raises(H1P::Error) { @parser.read_body }
345
+ end
346
+
347
+ def test_read_body_chunk_with_chunked_encoding_malformed
348
+ chunk = nil
349
+ Thread.new do
350
+ @o << "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
351
+ chunk = ' ' * rand(40000..360000)
352
+ @o << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n3"
353
+ @o << "0\r\n\r\n"
354
+ @o.close
355
+ end
356
+ headers = @parser.parse_headers
357
+ read = @parser.read_body_chunk(false)
358
+ assert_equal chunk, read
359
+ assert_raises(H1P::Error) { @parser.read_body_chunk(false) }
360
+
361
+ reset_parser
362
+
363
+ # missing last empty chunk
364
+ chunk = nil
365
+ Thread.new do
366
+ @o << "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
367
+ chunk = ' '.to_s * rand(20..1600)
368
+ @o << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
369
+ @o.close
370
+ end
371
+ headers = @parser.parse_headers
372
+ read = @parser.read_body_chunk(false)
373
+ assert_equal chunk, read
374
+ assert_raises(H1P::Error) { @parser.read_body_chunk(false) }
375
+
376
+ reset_parser
377
+
378
+ # bad chunk size
379
+ Thread.new do
380
+ @o << "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
381
+ chunk = ' '.to_s * rand(20..1600)
382
+ @o << "-#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
383
+ @o.close
384
+ end
385
+ headers = @parser.parse_headers
386
+ assert_raises(H1P::Error) { @parser.read_body_chunk(false) }
387
+
388
+ reset_parser
389
+
390
+ # missing body
391
+ @o << "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
392
+ @o.close
393
+ headers = @parser.parse_headers
394
+ assert_raises(H1P::Error) { @parser.read_body_chunk(false) }
395
+ end
396
+
397
+ def test_complete?
398
+ @o << "HTTP/1.1 200 OK\r\n\r\n"
399
+ headers = @parser.parse_headers
400
+ assert_equal true, @parser.complete?
401
+
402
+ reset_parser
403
+ @o << "HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\n"
404
+ headers = @parser.parse_headers
405
+ assert_equal false, @parser.complete?
406
+ @o << 'foo'
407
+ body = @parser.read_body
408
+ assert_equal 'foo', body
409
+ assert_equal true, @parser.complete?
410
+
411
+ reset_parser
412
+ @o << "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
413
+ headers = @parser.parse_headers
414
+ assert_equal false, @parser.complete?
415
+ @o << "3\r\nfoo\r\n"
416
+ chunk = @parser.read_body_chunk(false)
417
+ assert_equal 'foo', chunk
418
+ assert_equal false, @parser.complete?
419
+ @o << "0\r\n\r\n"
420
+ chunk = @parser.read_body_chunk(false)
421
+ assert_nil chunk
422
+ assert_equal true, @parser.complete?
423
+ end
424
+
425
+ def test_buffered_body_chunk
426
+ @o << "HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nfoo"
427
+ headers = @parser.parse_headers
428
+ assert_equal false, @parser.complete?
429
+
430
+ chunk = @parser.read_body_chunk(true)
431
+ assert_equal 'foo', chunk
432
+ assert_equal true, @parser.complete?
433
+ chunk = @parser.read_body_chunk(false)
434
+ assert_nil chunk
435
+ assert_equal true, @parser.complete?
436
+
437
+ reset_parser
438
+ @o << "HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\nfoo"
439
+ headers = @parser.parse_headers
440
+ assert_equal false, @parser.complete?
441
+
442
+ chunk = @parser.read_body_chunk(true)
443
+ assert_equal 'foo', chunk
444
+ assert_equal false, @parser.complete?
445
+ @o << 'bar'
446
+ chunk = @parser.read_body_chunk(false)
447
+ assert_equal 'bar', chunk
448
+ assert_equal true, @parser.complete?
449
+
450
+ reset_parser
451
+ @o << "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nfoo\r\n"
452
+ headers = @parser.parse_headers
453
+ assert_equal false, @parser.complete?
454
+
455
+ chunk = @parser.read_body_chunk(true)
456
+ assert_equal 'foo', chunk
457
+ assert_equal false, @parser.complete?
458
+ @o << "0\r\n\r\n"
459
+ chunk = @parser.read_body_chunk(true)
460
+ assert_nil chunk
461
+ assert_equal true, @parser.complete?
462
+ end
463
+
464
+ def test_parser_with_tcp_socket
465
+ port = rand(1234..5678)
466
+ server = TCPServer.new('127.0.0.1', port)
467
+ server_thread = Thread.new do
468
+ while (socket = server.accept)
469
+ Thread.new do
470
+ parser = H1P::Parser.new(socket, :client)
471
+ headers = parser.parse_headers
472
+ socket << headers.inspect
473
+ socket.shutdown
474
+ socket.close
475
+ end
476
+ end
477
+ end
478
+
479
+ sleep 0.001
480
+ client = TCPSocket.new('127.0.0.1', port)
481
+ msg = "HTTP/1.1 418 I'm a teapot\r\nCookie: abc=def\r\n\r\n"
482
+ client << msg
483
+ reply = client.read
484
+ assert_equal({
485
+ ':protocol' => 'http/1.1',
486
+ ':status' => 418,
487
+ ':status_message' => "I'm a teapot",
488
+ 'cookie' => 'abc=def',
489
+ ':rx' => msg.bytesize,
490
+ }, eval(reply))
491
+ ensure
492
+ client.shutdown rescue nil
493
+ client&.close
494
+ server_thread&.kill
495
+ server_thread&.join
496
+ server&.close
497
+ end
498
+
499
+ def test_parser_with_callable
500
+ buf = []
501
+ request = +"HTTP/1.1 200 OK\r\nHost: bar\r\n\r\n"
502
+ callable = proc do |len|
503
+ buf << {len: len}
504
+ request
505
+ end
506
+
507
+ parser = H1P::Parser.new(callable, :client)
508
+
509
+ headers = parser.parse_headers
510
+ assert_equal({
511
+ ':protocol' => 'http/1.1',
512
+ ':status' => 200,
513
+ ':status_message' => 'OK',
514
+ 'host' => 'bar',
515
+ ':rx' => request.bytesize,
516
+
517
+ }, headers)
518
+ assert_equal [{len: 4096}], buf
519
+
520
+ request = +"HTTP/1.1 404 Not found\r\nHost: baz\r\n\r\n"
521
+ headers = parser.parse_headers
522
+ assert_equal({
523
+ ':protocol' => 'http/1.1',
524
+ ':status' => 404,
525
+ ':status_message' => 'Not found',
526
+ 'host' => 'baz',
527
+ ':rx' => request.bytesize,
528
+
529
+ }, headers)
530
+ assert_equal [{len: 4096}, {len: 4096}], buf
531
+ end
532
+ end