h1p 0.2 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
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