http_tools 0.4.3 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -13,8 +13,8 @@ should run across all Ruby implementations compatible with 1.8 or later, and
13
13
  install in environments without a compiler available.
14
14
 
15
15
  Tests are currently run on travis-ci[http://travis-ci.org/matsadler/http_tools]
16
- against Ruby 1.8.6, 1.8.7, 1.9.2, 1.9.3, JRuby, Rubinius, Rubinius 2, and Ruby
17
- Enterprise Edition. Additionally tests are run against MacRuby 0.10
16
+ against Ruby 1.8.7, 1.9.2, 1.9.3, JRuby, Rubinius, and Ruby
17
+ Enterprise Edition. Additionally tests are run against 1.8.6 and MacRuby
18
18
 
19
19
  Performance tuning is mainly aimed at Ruby 1.9, with Ruby 1.8 and JRuby taken in
20
20
  to consideration. JRuby is generally fastest.
@@ -1,7 +1,8 @@
1
- base = File.expand_path(File.dirname(__FILE__) + '/../../lib')
2
- require base + '/http_tools'
1
+ require File.expand_path('../../../lib/http_tools', __FILE__)
3
2
  require 'benchmark'
4
3
 
4
+ repeats = 200
5
+
5
6
  Benchmark.bm(26) do |x|
6
7
  body = "x" * 1024 * 1024
7
8
  chunks = []
@@ -9,7 +10,7 @@ Benchmark.bm(26) do |x|
9
10
 
10
11
  header = "HTTP/1.1 200 OK\r\nDate: Mon, 06 Jun 2011 14:55:51 GMT\r\nServer: Apache/2.2.17 (Unix) mod_ssl/2.2.17 OpenSSL/0.9.8l DAV/2 mod_fastcgi/2.4.2\r\nLast-Modified: Mon, 06 Jun 2011 14:55:49 GMT\r\nETag: \"3f18045-400-4a50c4c87c740\"\r\nAccept-Ranges: bytes\r\nContent-Length: #{body.length}\r\nContent-Type: text/plain\r\n\r\n"
11
12
  x.report("content_length") do
12
- 200.times do
13
+ repeats.times do
13
14
  parser = HTTPTools::Parser.new
14
15
  parser << header
15
16
  chunks.each {|chunk| parser << chunk}
@@ -18,7 +19,7 @@ Benchmark.bm(26) do |x|
18
19
 
19
20
  header = "HTTP/1.1 200 OK\r\nDate: Mon, 06 Jun 2011 14:55:51 GMT\r\nServer: Apache/2.2.17 (Unix) mod_ssl/2.2.17 OpenSSL/0.9.8l DAV/2 mod_fastcgi/2.4.2\r\nLast-Modified: Mon, 06 Jun 2011 14:55:49 GMT\r\nETag: \"3f18045-400-4a50c4c87c740\"\r\nAccept-Ranges: bytes\r\nConnection: close\r\nContent-Type: text/plain\r\n\r\n"
20
21
  x.report("close") do
21
- 200.times do
22
+ repeats.times do
22
23
  parser = HTTPTools::Parser.new
23
24
  parser << header
24
25
  chunks.each {|chunk| parser << chunk}
@@ -30,7 +31,7 @@ Benchmark.bm(26) do |x|
30
31
  chunks << nil
31
32
  chunks.map!(&HTTPTools::Encoding.method(:transfer_encoding_chunked_encode))
32
33
  x.report("chunked") do
33
- 200.times do
34
+ repeats.times do
34
35
  parser = HTTPTools::Parser.new
35
36
  parser << header
36
37
  chunks.each {|chunk| parser << chunk}
@@ -1,19 +1,19 @@
1
- base = File.expand_path(File.dirname(__FILE__) + '/../../lib')
2
- require base + '/http_tools'
1
+ require File.expand_path('../../../lib/http_tools', __FILE__)
3
2
  require 'benchmark'
4
3
 
5
4
  request = "GET / HTTP/1.1\r\nHost: example.com\r\nUser-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-gb) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16\r\nAccept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5\r\nAccept-Language: en-gb\r\nAccept-Encoding: gzip, deflate\r\nConnection: keep-alive\r\n\r\n"
5
+ repeats = 10_000
6
6
 
7
7
  Benchmark.bm(41) do |x|
8
8
  x.report("HTTPTools::Parser") do
9
- 10_000.times do
9
+ repeats.times do
10
10
  HTTPTools::Parser.new << request
11
11
  end
12
12
  end
13
13
 
14
14
  x.report("HTTPTools::Parser (reset)") do
15
15
  parser = HTTPTools::Parser.new
16
- 10_000.times do
16
+ repeats.times do
17
17
  parser << request
18
18
  parser.reset
19
19
  end
@@ -23,7 +23,7 @@ Benchmark.bm(41) do |x|
23
23
  parser = HTTPTools::Parser.new
24
24
  parser.on(:header) {}
25
25
  parser.on(:finish) {}
26
- 10_000.times do
26
+ repeats.times do
27
27
  parser << request
28
28
  parser.reset
29
29
  end
@@ -31,35 +31,10 @@ Benchmark.bm(41) do |x|
31
31
 
32
32
  x.report("HTTPTools::Parser (reset, with env)") do
33
33
  parser = HTTPTools::Parser.new
34
- 10_000.times do
34
+ repeats.times do
35
35
  parser << request
36
36
  parser.env
37
37
  parser.reset
38
38
  end
39
39
  end
40
-
41
- begin
42
- require 'rubygems'
43
- require 'http/parser'
44
- x.report("Http::Parser") do
45
- 10_000.times do
46
- parser = Http::Parser.new
47
- parser.on_headers_complete = Proc.new {}
48
- parser.on_message_complete = Proc.new {}
49
- parser << request
50
- end
51
- end
52
- rescue LoadError
53
- end
54
-
55
- begin
56
- require 'rubygems'
57
- require 'http11'
58
- x.report("Mongrel::HttpParser") do
59
- 10_000.times do
60
- Mongrel::HttpParser.new.execute({}, request.dup, 0)
61
- end
62
- end
63
- rescue LoadError
64
- end
65
- end
40
+ end
@@ -1,19 +1,19 @@
1
- base = File.expand_path(File.dirname(__FILE__) + '/../../lib')
2
- require base + '/http_tools'
1
+ require File.expand_path('../../../lib/http_tools', __FILE__)
3
2
  require 'benchmark'
4
3
 
5
4
  response = "HTTP/1.1 200 OK\r\nServer: Apache/2.2.3 (CentOS)\r\nLast-Modified: Thu, 03 Jun 2010 17:40:12 GMT\r\nETag: \"4d2c-23e-48823b2cf3700\"\r\nAccept-Ranges: bytes\r\nContent-Type: text/html; charset=UTF-8\r\nConnection: Keep-Alive\r\nDate: Wed, 21 Jul 2010 16:26:04 GMT\r\nAge: 7985 \r\nContent-Length: 574\r\n\r\n<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\r\n<HTML>\r\n<HEAD>\r\n <META http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n <TITLE>Example Web Page</TITLE>\r\n</HEAD> \r\n<body> \r\n<p>You have reached this web page by typing &quot;example.com&quot;,\r\n&quot;example.net&quot;,\r\n or &quot;example.org&quot; into your web browser.</p>\r\n<p>These domain names are reserved for use in documentation and are not available \r\n for registration. See <a href=\"http://www.rfc-editor.org/rfc/rfc2606.txt\">RFC \r\n 2606</a>, Section 3.</p>\r\n</BODY>\r\n</HTML>\r\n\r\n"
5
+ repeats = 10_000
6
6
 
7
7
  Benchmark.bm(25) do |x|
8
8
  x.report("HTTPTools::Parser") do
9
- 10_000.times do
9
+ repeats.times do
10
10
  HTTPTools::Parser.new << response
11
11
  end
12
12
  end
13
13
 
14
14
  x.report("HTTPTools::Parser (reset)") do
15
15
  parser = HTTPTools::Parser.new
16
- 10_000.times do
16
+ repeats.times do
17
17
  parser << response
18
18
  parser.reset
19
19
  end
@@ -23,7 +23,7 @@ Benchmark.bm(25) do |x|
23
23
  require 'rubygems'
24
24
  require 'http/parser'
25
25
  x.report("Http::Parser") do
26
- 10_000.times do
26
+ repeats.times do
27
27
  parser = Http::Parser.new
28
28
  parser << response
29
29
  end
@@ -1,25 +1,26 @@
1
- base = File.expand_path(File.dirname(__FILE__) + '/../lib')
2
- require base + '/http_tools'
1
+ require File.expand_path('../../lib/http_tools', __FILE__)
3
2
  require 'benchmark'
4
3
 
4
+ repeats = 1_000
5
+
5
6
  Benchmark.bm(36) do |x|
6
7
  encoded = "1\r\na\r\n" * 100 + "0\r\n"
7
8
  x.report("lots of very short chunks") do
8
- 1_000.times do
9
+ repeats.times do
9
10
  HTTPTools::Encoding.transfer_encoding_chunked_decode(encoded)
10
11
  end
11
12
  end
12
13
 
13
14
  encoded = "16\r\n<h1>Hello world</h1>\r\n\r\n12\r\n<p>Lorem ipsum</p>\r\n" * 50 + "0\r\n"
14
15
  x.report("slightly less slightly longer chunks") do
15
- 1_000.times do
16
+ repeats.times do
16
17
  HTTPTools::Encoding.transfer_encoding_chunked_decode(encoded)
17
18
  end
18
19
  end
19
20
 
20
21
  encoded = "2710\r\n#{"a" * 10000}\r\n" * 2 + "0\r\n"
21
22
  x.report("a couple of big chunks") do
22
- 1_000.times do
23
+ repeats.times do
23
24
  HTTPTools::Encoding.transfer_encoding_chunked_decode(encoded)
24
25
  end
25
26
  end
@@ -3,19 +3,20 @@ require 'rubygems'
3
3
  require 'http_tools'
4
4
 
5
5
  module HTTP
6
+
7
+ # Basic Rack HTTP server.
8
+ #
9
+ # Usage:
10
+ #
11
+ # app = lambda {|env| [200, {"Content-Length" => "3"}, ["Hi\n"]]}
12
+ # HTTP::Server.run(app)
13
+ #
6
14
  class Server
7
- CONNECTION = "Connection".freeze
8
- KEEP_ALIVE = "Keep-Alive".freeze
9
- CLOSE = "close".freeze
10
- ONE_ONE = "1.1".freeze
11
15
 
12
16
  def initialize(app, options={})
13
- host = options[:host] || options[:Host] || "0.0.0.0"
14
- port = (options[:port] || options[:Port] || 9292).to_s
17
+ @host = options[:host] || options[:Host] || "0.0.0.0"
18
+ @port = (options[:port] || options[:Port] || 9292).to_s
15
19
  @app = app
16
- @instance_env = {"rack.multithread" => true}
17
- @server = TCPServer.new(host, port)
18
- @server.listen(1024)
19
20
  end
20
21
 
21
22
  def self.run(app, options={})
@@ -23,25 +24,23 @@ module HTTP
23
24
  end
24
25
 
25
26
  def listen
26
- while socket = @server.accept
27
- Thread.new do
28
- begin
29
- on_connection(socket)
30
- rescue StandardError, LoadError, SyntaxError => e
31
- STDERR.puts("#{e.class}: #{e.message} #{e.backtrace.join("\n")}")
32
- end
33
- end
27
+ server = TCPServer.new(@host, @port)
28
+ while socket = server.accept
29
+ Thread.new {on_connection(socket)}
34
30
  end
35
31
  end
36
32
 
37
33
  private
34
+
38
35
  def on_connection(socket)
39
36
  parser = HTTPTools::Parser.new
40
37
 
41
38
  parser.on(:finish) do
42
- status, header, body = @app.call(parser.env.merge!(@instance_env))
43
- keep_alive = keep_alive?(parser.version, parser.header[CONNECTION])
44
- header[CONNECTION] = keep_alive ? KEEP_ALIVE : CLOSE
39
+ env = parser.env.merge!("rack.multithread" => true)
40
+ status, header, body = @app.call(env)
41
+
42
+ keep_alive = parser.header["Connection"] != "close"
43
+ header["Connection"] = keep_alive ? "Keep-Alive" : "close"
45
44
  socket << HTTPTools::Builder.response(status, header)
46
45
  body.each {|chunk| socket << chunk}
47
46
  body.close if body.respond_to?(:close)
@@ -56,12 +55,12 @@ module HTTP
56
55
  rescue EOFError
57
56
  break
58
57
  end until parser.finished?
58
+
59
+ rescue StandardError, LoadError, SyntaxError => e
60
+ STDERR.puts("#{e.class}: #{e.message} #{e.backtrace.join("\n")}")
61
+ ensure
59
62
  socket.close
60
63
  end
61
64
 
62
- def keep_alive?(http_version, connection)
63
- http_version == ONE_ONE && connection != CLOSE || connection == KEEP_ALIVE
64
- end
65
-
66
65
  end
67
66
  end
@@ -108,11 +108,13 @@ module HTTPTools
108
108
  STATUS_DESCRIPTIONS.values.each {|val| val.freeze}
109
109
 
110
110
  # :stopdoc: hide from rdoc as it makes a mess
111
- STATUS_LINES = Hash.new do |hash, key|
112
- code = if key.kind_of?(Integer) then key else STATUS_CODES[key] end
113
- description = STATUS_DESCRIPTIONS[code]
114
- hash[key] = "#{code} #{description}"
111
+ STATUS_LINES = {}
112
+ STATUS_CODES.each do |name, code|
113
+ line = "#{code} #{STATUS_DESCRIPTIONS[code]}"
114
+ STATUS_LINES[name] = line
115
+ STATUS_LINES[code] = line
115
116
  end
117
+ STATUS_LINES.freeze
116
118
  # :startdoc:
117
119
 
118
120
  METHODS = %W{GET POST HEAD PUT DELETE OPTIONS TRACE CONNECT}.freeze
@@ -297,7 +297,7 @@ module HTTPTools
297
297
  alias on add_listener
298
298
 
299
299
  def inspect # :nodoc:
300
- super.sub(/ .*>$/, " #{posstr} #{state}>")
300
+ super.sub(/ .*>$/, " #{posstr(false)} #{state}>")
301
301
  end
302
302
 
303
303
  private
@@ -313,6 +313,7 @@ module HTTPTools
313
313
  elsif @allow_html_without_header && @buffer.check(/\s*</i)
314
314
  skip_header
315
315
  else
316
+ @buffer.skip(/H(T(TP?)?)?/i) || @buffer.skip(/[a-z]+/i)
316
317
  raise ParseError.new("Protocol or method not recognised at " + posstr)
317
318
  end
318
319
  end
@@ -325,9 +326,10 @@ module HTTPTools
325
326
  @query_string = @path_info.slice!(/\?[a-z0-9;\/?:@&=+$,%_.!~*')(-]*/i)
326
327
  @query_string ? @query_string[0] = EMPTY : @query_string = ""
327
328
  request_http_version
328
- elsif @buffer.check(/[a-z0-9;\/?:@&=+$,%_.!~*')(#-]+\Z/i)
329
+ elsif @buffer.check(/[a-z0-9;\/?:@&=+$,%_.!~*')(-]+\Z/i)
329
330
  :uri
330
331
  else
332
+ @buffer.skip(/[a-z0-9;\/?:@&=+$,%_.!~*')(-]+/i)
331
333
  raise ParseError.new("URI or path not recognised at " + posstr)
332
334
  end
333
335
  end
@@ -343,6 +345,7 @@ module HTTPTools
343
345
  @buffer.check(/ (H(T(T(P(\/(\d+(\.(\d+\r?)?)?)?)?)?)?)?)?\Z/i)
344
346
  :request_http_version
345
347
  else
348
+ @buffer.skip(/ (H(T(T(P(\/(\d+(\.(\d+\r?)?)?)?)?)?)?)?)?/i)
346
349
  raise ParseError.new("Invalid version specifier at " + posstr)
347
350
  end
348
351
  end
@@ -356,6 +359,7 @@ module HTTPTools
356
359
  @buffer.check(/H(T(T(P(\/(\d+(\.(\d+\r?)?)?)?)?)?)?)?\Z/i)
357
360
  :response_http_version
358
361
  else
362
+ @buffer.skip(/H(T(T(P(\/(\d+(\.(\d+\r?)?)?)?)?)?)?)?/i)
359
363
  raise ParseError.new("Invalid version specifier at " + posstr)
360
364
  end
361
365
  end
@@ -379,6 +383,7 @@ module HTTPTools
379
383
  @buffer.check(/\d(\d(\d( ([^\x00-\x1f\x7f]+\r?)?)?)?)?\Z/i)
380
384
  :status
381
385
  else
386
+ @buffer.skip(/\d(\d(\d( ([^\x00-\x1f\x7f]+\r?)?)?)?)?/i)
382
387
  raise ParseError.new("Invalid status line at " + posstr)
383
388
  end
384
389
  end
@@ -397,6 +402,9 @@ module HTTPTools
397
402
  elsif @last_key = @buffer.scan(/[ -9;-~]+:(?=[^ ])/i)
398
403
  @last_key.chomp!(COLON)
399
404
  value
405
+ elsif @request_method
406
+ @buffer.skip(/[ -9;-~]+/i)
407
+ raise ParseError.new("Illegal character in field name at " + posstr)
400
408
  else
401
409
  skip_bad_header
402
410
  end
@@ -408,6 +416,7 @@ module HTTPTools
408
416
  elsif @buffer.check(/[^\x00\n\x7f]+\Z/)
409
417
  :skip_bad_header
410
418
  else
419
+ @buffer.skip(/[ -9;-~]+/i)
411
420
  raise ParseError.new("Illegal character in field name at " + posstr)
412
421
  end
413
422
  end
@@ -430,20 +439,24 @@ module HTTPTools
430
439
  elsif @buffer.eos? || @buffer.check(/[^\x00\n\x7f]+\Z/i)
431
440
  :value
432
441
  else
442
+ @buffer.skip(/[^\x00\n\x7f]+/i)
433
443
  raise ParseError.new("Illegal character in field body at " + posstr)
434
444
  end
435
445
  end
436
446
 
437
447
  def value_extention
438
- if @buffer.check(/[^ \t]/)
448
+ if @buffer.check(/[^ \t]/i)
439
449
  key_or_newline
440
- elsif value_extra = @buffer.scan(/[ \t]+[^\x00\n\x7f]*\n/)
441
- value_extra.sub!(/^[ \t]+/, SPACE)
450
+ elsif value_extra = @buffer.scan(/[ \t]+[^\x00\n\x7f]*\n/i)
451
+ value_extra.sub!(/^[ \t]+/i, SPACE)
442
452
  value_extra.chop!
443
453
  (@header[@last_key] << value_extra).strip!
444
454
  value_extention
445
- else
455
+ elsif @buffer.eos? || @buffer.check(/[ \t]+[^\x00\n\x7f]*\Z/i)
446
456
  :value_extention
457
+ else
458
+ @buffer.skip(/[ \t]+[^\x00\n\x7f]*/i)
459
+ raise ParseError.new("Illegal character in field body at " + posstr)
447
460
  end
448
461
  end
449
462
 
@@ -544,6 +557,7 @@ module HTTPTools
544
557
  @last_key.chomp!(COLON)
545
558
  trailer_value
546
559
  else
560
+ @buffer.skip(/[ -9;-~]+/i)
547
561
  raise ParseError.new("Illegal character in field name at " + posstr)
548
562
  end
549
563
  end
@@ -561,20 +575,24 @@ module HTTPTools
561
575
  elsif @buffer.eos? || @buffer.check(/[^\x00\n\x7f]+\Z/i)
562
576
  :trailer_value
563
577
  else
578
+ @buffer.skip(/[^\x00\n\x7f]+/i)
564
579
  raise ParseError.new("Illegal character in field body at " + posstr)
565
580
  end
566
581
  end
567
582
 
568
583
  def trailer_value_extention
569
- if @buffer.check(/[^ \t]/)
584
+ if @buffer.check(/[^ \t]/i)
570
585
  trailer_key_or_newline
571
- elsif value_extra = @buffer.scan(/[ \t]+[^\x00\n\x7f]*\n/)
572
- value_extra.sub!(/^[ \t]+/, SPACE)
586
+ elsif value_extra = @buffer.scan(/[ \t]+[^\x00\n\x7f]*\n/i)
587
+ value_extra.sub!(/^[ \t]+/i, SPACE)
573
588
  value_extra.chop!
574
589
  (@trailer[@last_key] << value_extra).strip!
575
590
  trailer_value_extention
576
- else
591
+ elsif @buffer.eos? || @buffer.check(/[ \t]+[^\x00\n\x7f]*\Z/i)
577
592
  :trailer_value_extention
593
+ else
594
+ @buffer.skip(/[ \t]+[^\x00\n\x7f]*/i)
595
+ raise ParseError.new("Illegal character in field body at " + posstr)
578
596
  end
579
597
  end
580
598
 
@@ -603,19 +621,28 @@ module HTTPTools
603
621
  end
604
622
 
605
623
  def line_char(string, position)
606
- line_count = 1
624
+ line_count = 0
607
625
  char_count = 0
608
626
  string.each_line do |line|
609
- break if line.length + char_count > position
627
+ break if line.length + char_count >= position
610
628
  line_count += 1
611
629
  char_count += line.length
612
630
  end
613
- [line_count, position + 1 - char_count]
614
- end
615
-
616
- def posstr
617
- line, char = line_char(@buffer.string, @buffer.pos)
618
- "line #{line}, char #{char}"
631
+ [line_count, position - char_count]
632
+ end
633
+
634
+ def posstr(visual=true)
635
+ line_num, char_num = line_char(@buffer.string, @buffer.pos)
636
+ out = "line #{line_num + 1}, char #{char_num + 1}"
637
+ return out unless visual && @buffer.string.respond_to?(:lines)
638
+ line = ""
639
+ pointer = nil
640
+ @buffer.string.lines.to_a[line_num].chars.each_with_index.map do |char, i|
641
+ line << char.dump.gsub(/(^"|"$)/, "")
642
+ pointer = "#{" " * (line.length - 1)}^" if i == char_num
643
+ end
644
+ pointer ||= " " * line.length + "^"
645
+ [out, "", line, pointer].join("\n")
619
646
  end
620
647
 
621
648
  end
@@ -1,20 +1,12 @@
1
1
  base = File.expand_path(File.dirname(__FILE__) + '/../../lib')
2
2
  require base + '/http_tools'
3
3
  require 'rubygems'
4
+ require 'ruby-prof'
4
5
 
5
6
  request = "GET / HTTP/1.1\r\nHost: example.com\r\nUser-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-gb) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16\r\nAccept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5\r\nAccept-Language: en-gb\r\nAccept-Encoding: gzip, deflate\r\nConnection: keep-alive\r\n\r\n"
6
7
  parser = HTTPTools::Parser.new
7
8
 
8
- # require 'ruby-prof'
9
- # result = RubyProf.profile do
10
- # parser << request
11
- # end
12
- # RubyProf::FlatPrinter.new(result).print(STDOUT, 0)
13
-
14
- require 'perftools'
15
- PerfTools::CpuProfiler.start("/tmp/http_tools_parser_request_profile") do
16
- 100_000.times do
17
- parser << request
18
- parser.reset
19
- end
20
- end
9
+ result = RubyProf.profile do
10
+ parser << request
11
+ end
12
+ RubyProf::FlatPrinter.new(result).print(STDOUT, 0)
@@ -1,28 +1,21 @@
1
1
  require 'coverage' # >= ruby 1.9 only
2
2
 
3
+ testing = Dir[File.expand_path("../../lib/**/*.rb", __FILE__)]
4
+
3
5
  at_exit do
4
- testing = Dir[File.expand_path("../../lib/**/*.rb", __FILE__)]
5
-
6
6
  results = Coverage.result.select {|key, value| testing.include?(key)}
7
7
 
8
- puts
9
- total = results.map(&:last).flatten.compact
10
- puts "#{total.select {|i| i > 0}.length}/#{total.length} executable lines covered"
11
- puts
8
+ all = results.map(&:last).flatten.compact
9
+ print "\n#{all.reject(&:zero?).size}/#{all.size} executable lines covered\n\n"
12
10
 
13
11
  results.each do |key, value|
14
12
  next unless value.include?(0)
15
- puts key
16
- puts " line calls code"
17
- puts
18
- File.readlines(key).zip(value).each_with_index do |(line, val), i|
19
- print val == 0 ? "> " : " "
20
- print "%3i %5s %s" % [(i + 1), val, line]
13
+ lines = File.readlines(key).zip(value).each_with_index.map do |(line,val),i|
14
+ "%-2s%3i %5s %s" % [(">" if val == 0), (i + 1), val, line]
21
15
  end
22
- puts
23
- puts
16
+ print "#{key}\n line calls code\n\n#{lines.join}\n\n"
24
17
  end
25
18
  end
26
19
 
27
20
  Coverage.start
28
- Dir[File.expand_path("../**/*_test.rb", __FILE__)].each {|test| require test}
21
+ require_relative 'runner'
@@ -75,9 +75,17 @@ class ParserRequestTest < Test::Unit::TestCase
75
75
  def test_invalid_path
76
76
  parser = HTTPTools::Parser.new
77
77
 
78
- assert_raise(HTTPTools::ParseError) do
78
+ error = assert_raise(HTTPTools::ParseError) do
79
79
  parser << "GET \\ HTTP/1.1\r\n\r\n"
80
80
  end
81
+
82
+ return unless "".respond_to?(:lines)
83
+ assert_equal(<<-MESSAGE.chomp, error.message)
84
+ URI or path not recognised at line 1, char 5
85
+
86
+ GET \\\\ HTTP/1.1\\r\\n
87
+ ^
88
+ MESSAGE
81
89
  end
82
90
 
83
91
  def test_uri
@@ -119,17 +127,49 @@ class ParserRequestTest < Test::Unit::TestCase
119
127
  def test_fragment_with_path
120
128
  parser = HTTPTools::Parser.new
121
129
 
122
- assert_raise(HTTPTools::ParseError) do
130
+ error = assert_raise(HTTPTools::ParseError) do
123
131
  parser << "GET /foo#bar HTTP/1.1\r\n\r\n"
124
132
  end
133
+
134
+ return unless "".respond_to?(:lines)
135
+ assert_equal(<<-MESSAGE.chomp, error.message)
136
+ URI or path not recognised at line 1, char 9
137
+
138
+ GET /foo#bar HTTP/1.1\\r\\n
139
+ ^
140
+ MESSAGE
125
141
  end
126
142
 
127
143
  def test_fragment_with_uri
128
144
  parser = HTTPTools::Parser.new
129
145
 
130
- assert_raise(HTTPTools::ParseError) do
146
+ error = assert_raise(HTTPTools::ParseError) do
131
147
  parser << "GET http://example.com/foo#bar HTTP/1.1\r\n\r\n"
132
148
  end
149
+
150
+ return unless "".respond_to?(:lines)
151
+ assert_equal(<<-MESSAGE.chomp, error.message)
152
+ URI or path not recognised at line 1, char 27
153
+
154
+ GET http://example.com/foo#bar HTTP/1.1\\r\\n
155
+ ^
156
+ MESSAGE
157
+ end
158
+
159
+ def test_fragment_with_unfinished_path
160
+ parser = HTTPTools::Parser.new
161
+
162
+ error = assert_raise(HTTPTools::ParseError) do
163
+ parser << "GET /foo#ba"
164
+ end
165
+
166
+ return unless "".respond_to?(:lines)
167
+ assert_equal(<<-MESSAGE.chomp, error.message)
168
+ URI or path not recognised at line 1, char 9
169
+
170
+ GET /foo#ba
171
+ ^
172
+ MESSAGE
133
173
  end
134
174
 
135
175
  def test_with_header
@@ -286,7 +326,15 @@ class ParserRequestTest < Test::Unit::TestCase
286
326
  def test_unknown_protocol
287
327
  parser = HTTPTools::Parser.new
288
328
 
289
- assert_raise(HTTPTools::ParseError) {parser << "GET / SPDY/1.1\r\n"}
329
+ error = assert_raise(HTTPTools::ParseError) {parser << "GET / SPDY/1.1\r\n"}
330
+
331
+ return unless "".respond_to?(:lines)
332
+ assert_equal(<<-MESSAGE.chomp, error.message)
333
+ Invalid version specifier at line 1, char 7
334
+
335
+ GET / SPDY/1.1\\r\\n
336
+ ^
337
+ MESSAGE
290
338
  end
291
339
 
292
340
  def test_protocol_version
@@ -303,7 +351,15 @@ class ParserRequestTest < Test::Unit::TestCase
303
351
  def test_protocol_without_version
304
352
  parser = HTTPTools::Parser.new
305
353
 
306
- assert_raise(HTTPTools::ParseError) {parser << "GET / HTTP\r\n\r\n"}
354
+ error = assert_raise(HTTPTools::ParseError) {parser << "GET / HTTP\r\n\r\n"}
355
+
356
+ return unless "".respond_to?(:lines)
357
+ assert_equal(<<-MESSAGE.chomp, error.message)
358
+ Invalid version specifier at line 1, char 11
359
+
360
+ GET / HTTP\\r\\n
361
+ ^
362
+ MESSAGE
307
363
  end
308
364
 
309
365
  def test_one_dot_x_protocol_version
@@ -432,7 +488,7 @@ class ParserRequestTest < Test::Unit::TestCase
432
488
  def test_not_a_http_request
433
489
  parser = HTTPTools::Parser.new
434
490
 
435
- assert_raise(HTTPTools::ParseError) {parser << "not a http request"}
491
+ assert_raise(HTTPTools::ParseError) {parser << "not a http/1.1 request"}
436
492
  end
437
493
 
438
494
  def test_data_past_end
@@ -458,6 +514,23 @@ class ParserRequestTest < Test::Unit::TestCase
458
514
  assert_equal("get", result)
459
515
  end
460
516
 
517
+ def test_invalid_method
518
+ parser = HTTPTools::Parser.new
519
+
520
+ error = assert_raise(HTTPTools::ParseError) do
521
+ parser << "G3T / HTTP/1.1\r\n\r\n"
522
+ end
523
+
524
+ return unless "".respond_to?(:lines)
525
+ assert_equal(<<-MESSAGE.chomp, error.message)
526
+ Protocol or method not recognised at line 1, char 2
527
+
528
+ G3T / HTTP/1.1\\r\\n
529
+ ^
530
+ MESSAGE
531
+ end
532
+
533
+
461
534
  def test_lowercase_http
462
535
  parser = HTTPTools::Parser.new
463
536
  version = nil
@@ -472,31 +545,68 @@ class ParserRequestTest < Test::Unit::TestCase
472
545
  def test_invalid_version
473
546
  parser = HTTPTools::Parser.new
474
547
 
475
- assert_raise(HTTPTools::ParseError) {parser << "GET / HTTP/one dot one\r\n"}
548
+ error = assert_raise(HTTPTools::ParseError) do
549
+ parser << "GET / HTTP/one dot one\r\n"
550
+ end
551
+
552
+ return unless "".respond_to?(:lines)
553
+ assert_equal(<<-MESSAGE.chomp, error.message)
554
+ Invalid version specifier at line 1, char 12
555
+
556
+ GET / HTTP/one dot one\\r\\n
557
+ ^
558
+ MESSAGE
476
559
  end
477
560
 
478
561
  def test_invalid_header_key_with_control_character
479
562
  parser = HTTPTools::Parser.new
480
563
 
481
- assert_raise(HTTPTools::ParseError) do
564
+ error = assert_raise(HTTPTools::ParseError) do
482
565
  parser << "GET / HTTP/1.1\r\nx-invalid\0key: text/plain\r\n"
483
566
  end
567
+
568
+ return unless "".respond_to?(:lines)
569
+ null = "\000".dump.gsub(/"/, "")
570
+ assert_equal(<<-MESSAGE.chomp, error.message)
571
+ Illegal character in field name at line 2, char 10
572
+
573
+ x-invalid#{null}key: text/plain\\r\\n
574
+ ^
575
+ MESSAGE
484
576
  end
485
577
 
486
578
  def test_invalid_header_key_with_non_ascii_character
487
579
  parser = HTTPTools::Parser.new
488
580
 
489
- assert_raise(HTTPTools::ParseError) do
490
- parser << "GET / HTTP/1.1\r\nx-invalid\000key: text/plain\r\n"
581
+ error = assert_raise(HTTPTools::ParseError) do
582
+ parser << "GET / HTTP/1.1\r\nx-invalid\342\200\224key: text/plain\r\n"
491
583
  end
584
+
585
+ em_dash = "\342\200\224".dump.gsub(/"/, "")
586
+ return unless "".respond_to?(:lines)
587
+ assert_equal(<<-MESSAGE.chomp, error.message)
588
+ Illegal character in field name at line 2, char 10
589
+
590
+ x-invalid#{em_dash}key: text/plain\\r\\n
591
+ #{" " * (em_dash.length / 4)}^
592
+ MESSAGE
492
593
  end
493
594
 
494
- def test_invalid_header_value_with_non_ascii_character
595
+ def test_invalid_header_value_with_non_control_character
495
596
  parser = HTTPTools::Parser.new
496
597
 
497
- assert_raise(HTTPTools::ParseError) do
498
- parser << "GET / HTTP/1.1\r\nAccept: \000text/plain\r\n"
598
+ error = assert_raise(HTTPTools::ParseError) do
599
+ parser << "GET / HTTP/1.1\r\nAccept: text\000plain\r\n"
499
600
  end
601
+
602
+ return unless "".respond_to?(:lines)
603
+ null = "\000".dump.gsub(/"/, "")
604
+ assert_equal(<<-MESSAGE.chomp, error.message)
605
+ Illegal character in field body at line 2, char 13
606
+
607
+ Accept: text#{null}plain\\r\\n
608
+ ^
609
+ MESSAGE
500
610
  end
501
611
 
502
612
  def test_error_callback
@@ -721,4 +831,12 @@ class ParserRequestTest < Test::Unit::TestCase
721
831
  assert_match(/#<HTTPTools::Parser:0x[a-f0-9]+ line 1, char 1 start>/, parser.inspect)
722
832
  end
723
833
 
724
- end
834
+ def test_inspect_position
835
+ parser = HTTPTools::Parser.new
836
+
837
+ parser << "GET / HTTP/1.1\r\nHost: foo."
838
+
839
+ assert_match(/#<HTTPTools::Parser:0x[a-f0-9]+ line 2, char 7 value>/, parser.inspect)
840
+ end
841
+
842
+ end
@@ -290,6 +290,26 @@ class ParserResponseTest < Test::Unit::TestCase
290
290
  assert_equal({"Content-Type" => "text/html; charset=utf-8"}, headers)
291
291
  end
292
292
 
293
+ def test_multi_line_header_invalid_value
294
+ parser = HTTPTools::Parser.new
295
+
296
+ parser << "HTTP/1.1 200 OK\r\n"
297
+ parser << "Content-Type: text/html;\r\n"
298
+
299
+ error = assert_raise(HTTPTools::ParseError) do
300
+ parser << " charset=\0\r\n\r\n"
301
+ end
302
+
303
+ return unless "".respond_to?(:lines)
304
+ null = "\000".dump.gsub(/"/, "")
305
+ assert_equal(<<-MESSAGE.chomp, error.message)
306
+ Illegal character in field body at line 3, char 10
307
+
308
+ charset=#{null}\\r\\n
309
+ ^
310
+ MESSAGE
311
+ end
312
+
293
313
  def test_header_value_leading_and_trailing_whitespace_is_stripped
294
314
  parser = HTTPTools::Parser.new
295
315
  headers = nil
@@ -380,6 +400,23 @@ class ParserResponseTest < Test::Unit::TestCase
380
400
  assert(parser.finished?, "parser should be finished")
381
401
  end
382
402
 
403
+ def test_invalid_header_key_with_control_character
404
+ parser = HTTPTools::Parser.new
405
+
406
+ error = assert_raise(HTTPTools::ParseError) do
407
+ parser << "HTTP/1.0 200 OK\r\nx-invalid\0key: valid key\r\n"
408
+ end
409
+
410
+ return unless "".respond_to?(:lines)
411
+ null = "\000".dump.gsub(/"/, "")
412
+ assert_equal(<<-MESSAGE.chomp, error.message)
413
+ Illegal character in field name at line 2, char 10
414
+
415
+ x-invalid#{null}key: valid key\\r\\n
416
+ ^
417
+ MESSAGE
418
+ end
419
+
383
420
  def test_apple_dot_com
384
421
  parser = HTTPTools::Parser.new
385
422
  code, message, headers = nil
@@ -646,6 +683,29 @@ class ParserResponseTest < Test::Unit::TestCase
646
683
  assert(parser.finished?, "parser should be finished")
647
684
  end
648
685
 
686
+ def test_body_with_no_headers
687
+ parser = HTTPTools::Parser.new
688
+ code, message, headers = nil
689
+ body = ""
690
+
691
+ parser.add_listener(:header) do
692
+ code = parser.status_code
693
+ message = parser.message
694
+ headers = parser.header
695
+ end
696
+ parser.add_listener(:stream) {|chunk| body << chunk}
697
+
698
+ parser << "HTTP/1.1 200 OK\r\n\r\n"
699
+ parser << "<h1>hello world</h1>"
700
+ parser.finish # notify parser the connection has closed
701
+
702
+ assert_equal(200, code)
703
+ assert_equal("OK", message)
704
+ assert_equal({}, headers)
705
+ assert_equal("<h1>hello world</h1>", body)
706
+ assert(parser.finished?, "parser should be finished")
707
+ end
708
+
649
709
  def test_chunked
650
710
  parser = HTTPTools::Parser.new
651
711
  code, message, headers = nil
@@ -865,9 +925,17 @@ class ParserResponseTest < Test::Unit::TestCase
865
925
  def test_html_body_only_not_allowed
866
926
  parser = HTTPTools::Parser.new
867
927
 
868
- assert_raise(HTTPTools::ParseError) do
928
+ error = assert_raise(HTTPTools::ParseError) do
869
929
  parser << "<html><p>HTTP is hard</p></html>"
870
930
  end
931
+
932
+ return unless "".respond_to?(:lines)
933
+ assert_equal(<<-MESSAGE.chomp, error.message)
934
+ Protocol or method not recognised at line 1, char 1
935
+
936
+ <html><p>HTTP is hard</p></html>
937
+ ^
938
+ MESSAGE
871
939
  end
872
940
 
873
941
  def test_html_body_only_allowed
@@ -1155,6 +1223,29 @@ class ParserResponseTest < Test::Unit::TestCase
1155
1223
  assert_equal({"X-Test" => "one two"}, trailer)
1156
1224
  end
1157
1225
 
1226
+ def test_multi_line_trailer_invalid_value
1227
+ parser = HTTPTools::Parser.new
1228
+
1229
+ parser << "HTTP/1.1 200 OK\r\n"
1230
+ parser << "Transfer-Encoding: chunked\r\n"
1231
+ parser << "Trailer: X-Test\r\n\r\n"
1232
+ parser << "14\r\n<h1>Hello world</h1>\r\n0\r\n"
1233
+ parser << "X-Test: one\r\n"
1234
+
1235
+ error = assert_raise(HTTPTools::ParseError) do
1236
+ parser << " \0two\r\n"
1237
+ end
1238
+
1239
+ return unless "".respond_to?(:lines)
1240
+ null = "\000".dump.gsub(/"/, "")
1241
+ assert_equal(<<-MESSAGE.chomp, error.message)
1242
+ Illegal character in field body at line 9, char 2
1243
+
1244
+ #{null}two\\r\\n
1245
+ ^
1246
+ MESSAGE
1247
+ end
1248
+
1158
1249
  def test_trailer_value_leading_and_trailing_whitespace_is_stripped
1159
1250
  parser = HTTPTools::Parser.new
1160
1251
  trailer = nil
@@ -1237,9 +1328,18 @@ class ParserResponseTest < Test::Unit::TestCase
1237
1328
  parser << "Transfer-Encoding: chunked\r\nTrailer: X-Checksum\r\n\r\n"
1238
1329
  parser << "14\r\n<h1>Hello world</h1>\r\n0\r\n"
1239
1330
 
1240
- assert_raise(HTTPTools::ParseError) do
1331
+ error = assert_raise(HTTPTools::ParseError) do
1241
1332
  parser << "x-invalid\0key: value\r\n\r\n"
1242
1333
  end
1334
+
1335
+ return unless "".respond_to?(:lines)
1336
+ null = "\000".dump.gsub(/"/, "")
1337
+ assert_equal(<<-MESSAGE.chomp, error.message)
1338
+ Illegal character in field name at line 8, char 10
1339
+
1340
+ x-invalid#{null}key: value\\r\\n
1341
+ ^
1342
+ MESSAGE
1243
1343
  end
1244
1344
 
1245
1345
  def test_invalid_trailer_value
@@ -1252,21 +1352,65 @@ class ParserResponseTest < Test::Unit::TestCase
1252
1352
  parser << "Transfer-Encoding: chunked\r\nTrailer: X-Checksum\r\n\r\n"
1253
1353
  parser << "14\r\n<h1>Hello world</h1>\r\n0\r\n"
1254
1354
 
1255
- assert_raise(HTTPTools::ParseError) do
1355
+ error = assert_raise(HTTPTools::ParseError) do
1256
1356
  parser << "x-test: inva\0lid\r\n\r\n"
1257
1357
  end
1358
+
1359
+ return unless "".respond_to?(:lines)
1360
+ null = "\000".dump.gsub(/"/, "")
1361
+ assert_equal(<<-MESSAGE.chomp, error.message)
1362
+ Illegal character in field body at line 8, char 13
1363
+
1364
+ x-test: inva#{null}lid\\r\\n
1365
+ ^
1366
+ MESSAGE
1258
1367
  end
1259
1368
 
1369
+ def test_invalid_protocol
1370
+ parser = HTTPTools::Parser.new
1371
+
1372
+ error = assert_raise(HTTPTools::ParseError) do
1373
+ parser << "HTTZ/1.1 200 OX\r\n"
1374
+ end
1375
+
1376
+ return unless "".respond_to?(:lines)
1377
+ assert_equal(<<-MESSAGE.chomp, error.message)
1378
+ Protocol or method not recognised at line 1, char 4
1379
+
1380
+ HTTZ/1.1 200 OX\\r\\n
1381
+ ^
1382
+ MESSAGE
1383
+ end
1384
+
1385
+
1260
1386
  def test_invalid_version
1261
1387
  parser = HTTPTools::Parser.new
1262
1388
 
1263
- assert_raise(HTTPTools::ParseError) {parser << "HTTP/one dot one 200 OK"}
1389
+ error = assert_raise(HTTPTools::ParseError) do
1390
+ parser << "HTTP/one dot one 200 OK"
1391
+ end
1392
+
1393
+ return unless "".respond_to?(:lines)
1394
+ assert_equal(<<-MESSAGE.chomp, error.message)
1395
+ Invalid version specifier at line 1, char 6
1396
+
1397
+ HTTP/one dot one 200 OK
1398
+ ^
1399
+ MESSAGE
1264
1400
  end
1265
1401
 
1266
1402
  def test_invalid_status
1267
1403
  parser = HTTPTools::Parser.new
1268
1404
 
1269
- assert_raise(HTTPTools::ParseError) {parser << "HTTP/1.1 0 Fail"}
1405
+ error = assert_raise(HTTPTools::ParseError) {parser << "HTTP/1.1 0 Fail"}
1406
+
1407
+ return unless "".respond_to?(:lines)
1408
+ assert_equal(<<-MESSAGE.chomp, error.message)
1409
+ Invalid status line at line 1, char 11
1410
+
1411
+ HTTP/1.1 0 Fail
1412
+ ^
1413
+ MESSAGE
1270
1414
  end
1271
1415
 
1272
1416
  def test_finish_early
metadata CHANGED
@@ -1,24 +1,33 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: http_tools
3
- version: !ruby/object:Gem::Version
4
- version: 0.4.3
3
+ version: !ruby/object:Gem::Version
4
+ hash: 7
5
5
  prerelease:
6
+ segments:
7
+ - 0
8
+ - 4
9
+ - 4
10
+ version: 0.4.4
6
11
  platform: ruby
7
- authors:
12
+ authors:
8
13
  - Matthew Sadler
9
14
  autorequire:
10
15
  bindir: bin
11
16
  cert_chain: []
12
- date: 2011-10-09 00:00:00.000000000Z
17
+
18
+ date: 2011-12-20 00:00:00 +00:00
19
+ default_executable:
13
20
  dependencies: []
14
- description: A fast-as-possible pure Ruby HTTP parser plus associated lower level
15
- utilities to aid working with HTTP and the web.
21
+
22
+ description: A fast-as-possible pure Ruby HTTP parser plus associated lower level utilities to aid working with HTTP and the web.
16
23
  email: mat@sourcetagsandcodes.com
17
24
  executables: []
25
+
18
26
  extensions: []
19
- extra_rdoc_files:
27
+
28
+ extra_rdoc_files:
20
29
  - README.rdoc
21
- files:
30
+ files:
22
31
  - lib/http_tools/builder.rb
23
32
  - lib/http_tools/encoding.rb
24
33
  - lib/http_tools/parser.rb
@@ -44,32 +53,42 @@ files:
44
53
  - example/websocket_client.rb
45
54
  - example/websocket_server.rb
46
55
  - README.rdoc
56
+ has_rdoc: true
47
57
  homepage: http://github.com/matsadler/http_tools
48
58
  licenses: []
59
+
49
60
  post_install_message:
50
- rdoc_options:
61
+ rdoc_options:
51
62
  - --main
52
63
  - README.rdoc
53
64
  - --charset
54
65
  - utf-8
55
- require_paths:
66
+ require_paths:
56
67
  - lib
57
- required_ruby_version: !ruby/object:Gem::Requirement
68
+ required_ruby_version: !ruby/object:Gem::Requirement
58
69
  none: false
59
- requirements:
60
- - - ! '>='
61
- - !ruby/object:Gem::Version
62
- version: '0'
63
- required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ hash: 3
74
+ segments:
75
+ - 0
76
+ version: "0"
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
78
  none: false
65
- requirements:
66
- - - ! '>='
67
- - !ruby/object:Gem::Version
68
- version: '0'
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ hash: 3
83
+ segments:
84
+ - 0
85
+ version: "0"
69
86
  requirements: []
87
+
70
88
  rubyforge_project:
71
- rubygems_version: 1.8.10
89
+ rubygems_version: 1.6.2
72
90
  signing_key:
73
91
  specification_version: 3
74
92
  summary: Pure Ruby HTTP parser and friends
75
93
  test_files: []
94
+