thin 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of thin might be problematic. Click here for more details.

Files changed (91) hide show
  1. data/COPYING +18 -0
  2. data/README +32 -45
  3. data/Rakefile +66 -23
  4. data/bin/thin +76 -45
  5. data/doc/benchmarks.txt +64 -249
  6. data/doc/rdoc/created.rid +1 -1
  7. data/doc/rdoc/files/README.html +37 -100
  8. data/doc/rdoc/rdoc-style.css +5 -0
  9. data/example/config.ru +9 -0
  10. data/ext/thin_parser/common.rl +54 -0
  11. data/ext/thin_parser/ext_help.h +14 -0
  12. data/ext/thin_parser/extconf.rb +6 -0
  13. data/ext/thin_parser/parser.c +1199 -0
  14. data/ext/thin_parser/parser.h +49 -0
  15. data/ext/thin_parser/parser.rl +143 -0
  16. data/ext/thin_parser/thin.c +424 -0
  17. data/lib/rack/adapter/rails.rb +136 -0
  18. data/lib/rack/handler/thin.rb +13 -0
  19. data/lib/thin.rb +28 -12
  20. data/lib/thin/connection.rb +47 -0
  21. data/lib/thin/daemonizing.rb +5 -1
  22. data/lib/thin/headers.rb +3 -2
  23. data/lib/thin/logging.rb +6 -13
  24. data/lib/thin/request.rb +53 -133
  25. data/lib/thin/response.rb +21 -25
  26. data/lib/thin/server.rb +30 -94
  27. data/lib/thin/version.rb +2 -2
  28. data/lib/thin_parser.bundle +0 -0
  29. data/spec/daemonizing_spec.rb +94 -0
  30. data/spec/headers_spec.rb +35 -0
  31. data/spec/request_spec.rb +258 -0
  32. data/spec/response_spec.rb +40 -0
  33. data/spec/server_spec.rb +75 -0
  34. data/spec/spec_helper.rb +126 -0
  35. metadata +79 -99
  36. data/bin/thin_cluster +0 -53
  37. data/doc/rdoc/classes/Kernel.html +0 -182
  38. data/doc/rdoc/classes/Process.html +0 -175
  39. data/doc/rdoc/classes/Thin.html +0 -184
  40. data/doc/rdoc/classes/Thin/CGIWrapper.html +0 -438
  41. data/doc/rdoc/classes/Thin/Cluster.html +0 -392
  42. data/doc/rdoc/classes/Thin/Command.html +0 -221
  43. data/doc/rdoc/classes/Thin/CommandError.html +0 -154
  44. data/doc/rdoc/classes/Thin/Commands.html +0 -145
  45. data/doc/rdoc/classes/Thin/Daemonizable.html +0 -250
  46. data/doc/rdoc/classes/Thin/Daemonizable/ClassMethods.html +0 -203
  47. data/doc/rdoc/classes/Thin/DirHandler.html +0 -250
  48. data/doc/rdoc/classes/Thin/Handler.html +0 -195
  49. data/doc/rdoc/classes/Thin/Headers.html +0 -244
  50. data/doc/rdoc/classes/Thin/InvalidRequest.html +0 -150
  51. data/doc/rdoc/classes/Thin/Logging.html +0 -214
  52. data/doc/rdoc/classes/Thin/RailsHandler.html +0 -234
  53. data/doc/rdoc/classes/Thin/RailsServer.html +0 -175
  54. data/doc/rdoc/classes/Thin/Request.html +0 -379
  55. data/doc/rdoc/classes/Thin/Response.html +0 -311
  56. data/doc/rdoc/classes/Thin/Server.html +0 -381
  57. data/doc/rdoc/files/bin/thin.html +0 -188
  58. data/doc/rdoc/files/bin/thin_cluster.html +0 -175
  59. data/doc/rdoc/files/lib/thin/cgi_rb.html +0 -263
  60. data/doc/rdoc/files/lib/thin/cluster_rb.html +0 -263
  61. data/doc/rdoc/files/lib/thin/command_rb.html +0 -263
  62. data/doc/rdoc/files/lib/thin/consts_rb.html +0 -263
  63. data/doc/rdoc/files/lib/thin/daemonizing_rb.html +0 -263
  64. data/doc/rdoc/files/lib/thin/handler_rb.html +0 -263
  65. data/doc/rdoc/files/lib/thin/headers_rb.html +0 -263
  66. data/doc/rdoc/files/lib/thin/logging_rb.html +0 -263
  67. data/doc/rdoc/files/lib/thin/mime_types_rb.html +0 -263
  68. data/doc/rdoc/files/lib/thin/rails_rb.html +0 -263
  69. data/doc/rdoc/files/lib/thin/recipes_rb.html +0 -171
  70. data/doc/rdoc/files/lib/thin/request_rb.html +0 -171
  71. data/doc/rdoc/files/lib/thin/response_rb.html +0 -171
  72. data/doc/rdoc/files/lib/thin/server_rb.html +0 -171
  73. data/doc/rdoc/files/lib/thin/statuses_rb.html +0 -171
  74. data/doc/rdoc/files/lib/thin/version_rb.html +0 -171
  75. data/lib/thin/cgi.rb +0 -159
  76. data/lib/thin/cluster.rb +0 -147
  77. data/lib/thin/command.rb +0 -49
  78. data/lib/thin/commands/cluster/base.rb +0 -24
  79. data/lib/thin/commands/cluster/config.rb +0 -36
  80. data/lib/thin/commands/cluster/restart.rb +0 -35
  81. data/lib/thin/commands/cluster/start.rb +0 -40
  82. data/lib/thin/commands/cluster/stop.rb +0 -28
  83. data/lib/thin/commands/server/base.rb +0 -7
  84. data/lib/thin/commands/server/start.rb +0 -33
  85. data/lib/thin/commands/server/stop.rb +0 -29
  86. data/lib/thin/consts.rb +0 -37
  87. data/lib/thin/handler.rb +0 -57
  88. data/lib/thin/mime_types.rb +0 -619
  89. data/lib/thin/rails.rb +0 -44
  90. data/lib/thin/recipes.rb +0 -36
  91. data/lib/transat/parser.rb +0 -247
@@ -1,23 +1,17 @@
1
1
  module Thin
2
2
  # A response sent to the client.
3
3
  class Response
4
- CONNECTION = 'Connection'.freeze
5
- CLOSE = 'close'.freeze
4
+ CONTENT_LENGTH = 'Content-Length'.freeze
5
+ CONNECTION = 'Connection'.freeze
6
+ CLOSE = 'close'.freeze
6
7
 
7
- attr_accessor :body, :headers, :status
8
+ attr_accessor :status
9
+ attr_reader :headers, :body
8
10
 
9
11
  def initialize
10
12
  @headers = Headers.new
11
- @body = StringIO.new
12
- @status = 200
13
- end
14
-
15
- def content_type=(type)
16
- @headers[CONTENT_TYPE] = type
17
- end
18
-
19
- def content_type
20
- @headers[CONTENT_TYPE]
13
+ @body = StringIO.new
14
+ @status = 200
21
15
  end
22
16
 
23
17
  def headers_output
@@ -30,25 +24,27 @@ module Thin
30
24
  "HTTP/1.1 #{@status} #{HTTP_STATUS_CODES[@status.to_i]}\r\n#{headers_output}\r\n"
31
25
  end
32
26
 
33
- def write(socket)
34
- socket << head
35
- @body.rewind
36
- socket << @body.read
27
+ def headers=(key_value_pairs)
28
+ key_value_pairs.each do |k, vs|
29
+ vs.each do |v|
30
+ @headers[k] = v
31
+ end
32
+ end
37
33
  end
38
34
 
39
- def close
40
- @body.close
35
+ def body=(stream)
36
+ stream.each do |part|
37
+ @body << part
38
+ end
41
39
  end
42
40
 
43
- def start(status)
44
- @status = status
45
- yield @headers, @body
41
+ def close
42
+ @body.close
46
43
  end
47
44
 
48
45
  def to_s
49
- out = ''
50
- write out
51
- out
46
+ @body.rewind
47
+ head + @body.read
52
48
  end
53
49
  end
54
50
  end
@@ -1,6 +1,7 @@
1
- require 'socket'
2
-
3
1
  module Thin
2
+ # Raise when we require the server to stop
3
+ class StopServer < Exception; end
4
+
4
5
  # The Thin HTTP server used to served request.
5
6
  # It listen for incoming request on a given port
6
7
  # and forward all request to all the handlers in the order
@@ -14,36 +15,25 @@ module Thin
14
15
  # Addresse and port on which the server is listening for connections.
15
16
  attr_accessor :port, :host
16
17
 
17
- # List of handlers to process the request in the order they are given.
18
- attr_accessor :handlers
18
+ # App called with the request that produce the response.
19
+ attr_accessor :app
19
20
 
20
21
  # Maximum time for a request to be red and parsed.
21
22
  attr_accessor :timeout
22
23
 
23
24
  # Creates a new server binded to <tt>host:port</tt>
24
- # that will pass request to +handlers+.
25
- def initialize(host, port, *handlers)
25
+ # that will pass request to +app+.
26
+ def initialize(host, port, app)
26
27
  @host = host
27
- @port = port
28
- @handlers = handlers
29
- @timeout = 60 # sec, max time to read and parse a request
30
- @trace = false
31
-
32
- @stop = true # true is server is stopped
33
- @processing = false # true is processing a request
34
-
35
- @socket = TCPServer.new(host, port)
28
+ @port = port.to_i
29
+ @app = app
30
+ @timeout = 60 # sec
36
31
  end
37
32
 
38
33
  # Starts the handlers.
39
34
  def start
40
35
  log ">> Thin web server (v#{VERSION::STRING})"
41
36
  trace ">> Tracing ON"
42
-
43
- @handlers.each do |handler|
44
- log ">> Starting #{handler} ..."
45
- handler.start
46
- end
47
37
  end
48
38
 
49
39
  # Start the server and listen for connections
@@ -54,88 +44,34 @@ module Thin
54
44
 
55
45
  # Start listening for connections
56
46
  def listen!
57
- @stop = false
58
- trap('INT') do
59
- log '>> Caught INT signal, stopping ...'
60
- stop
61
- end
62
-
63
- log ">> Listening on #{host}:#{port}, CTRL+C to stop"
64
- until @stop
65
- @processing = false
66
- client = @socket.accept rescue nil
67
- break if @socket.closed? || client.nil?
68
- @processing = true
69
- process(client)
70
- end
71
- ensure
72
- @socket.close unless @socket.closed? rescue nil
73
- end
74
-
75
- # Process one request from a client
76
- def process(client)
77
- return if client.eof?
78
-
79
- trace { 'Request started'.center(80, '=') }
80
-
81
- request = Request.new
82
- response = Response.new
83
-
84
- request.trace = @trace
85
- trace { ">> Tracing request parsing ... " }
86
-
87
- # Parse the request checking for timeout to prevent DOS attacks
88
- Timeout.timeout(@timeout) { request.parse!(client) }
89
- trace { request.raw }
90
-
91
- # Add client info to the request env
92
- request.params['REMOTE_ADDR'] = client.peeraddr.last
93
-
94
- # Add server info to the request env
95
- request.params['SERVER_SOFTWARE'] = SERVER
96
- request.params['SERVER_PORT'] = @port.to_s
97
-
98
- served = false
99
- @handlers.each do |handler|
100
- served = handler.process(request, response)
101
- break if served
102
- end
103
-
104
- if served
105
- trace { ">> Sending response:\n" + response.to_s }
106
- response.write client
107
- else
108
- client << ERROR_404_RESPONSE
109
- end
47
+ trap('INT') { stop }
48
+ trap('TERM') { stop! }
110
49
 
111
- trace { 'Request finished'.center(80, '=') }
50
+ # See http://rubyeventmachine.com/pub/rdoc/files/EPOLL.html
51
+ EventMachine.epoll
112
52
 
113
- rescue EOFError, Errno::ECONNRESET, Errno::EPIPE, Errno::EINVAL, Errno::EBADF
114
- # Can't do anything sorry, closing the socket in the ensure block
115
- rescue InvalidRequest => e
116
- log "Invalid request: #{e.message}"
117
- trace { e.backtrace.join("\n") }
118
- client << ERROR_400_RESPONSE rescue nil
119
- rescue Object => e
120
- log "Unexpected error while processing request: #{e.message}"
121
- log e.backtrace.join("\n")
122
- ensure
123
- request.close if request rescue nil
124
- response.close if response rescue nil
125
- client.close unless client.closed? rescue nil
53
+ EventMachine.run do
54
+ begin
55
+ log ">> Listening on #{@host}:#{@port}, CTRL+C to stop"
56
+ EventMachine.start_server(@host, @port, Connection) do |connection|
57
+ connection.comm_inactivity_timeout = @timeout
58
+ connection.app = @app
59
+ connection.silent = @silent
60
+ end
61
+ rescue StopServer
62
+ EventMachine.stop_event_loop
63
+ end
64
+ end
126
65
  end
127
66
 
128
- # Stop the server from accepting new request.
129
- # If a request is processing, wait for this to finish
130
- # and shutdown the server.
131
67
  def stop
132
- @stop = true
133
- stop! unless @processing # Not processing a request, so we can stop now
68
+ EventMachine.stop_event_loop
69
+ rescue
70
+ warn "Error stopping : #{$!}"
134
71
  end
135
72
 
136
- # Force the server to stop right now!
137
73
  def stop!
138
- @socket.close rescue nil # break the accept loop by closing the socket
74
+ raise StopServer
139
75
  end
140
76
  end
141
77
  end
@@ -1,8 +1,8 @@
1
1
  module Thin
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 4
5
- TINY = 1
4
+ MINOR = 5
5
+ TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
Binary file
@@ -0,0 +1,94 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe 'Daemonizing' do
4
+ before do
5
+ @server = Server.new('0.0.0.0', 3000, nil)
6
+ @server.log_file = File.dirname(__FILE__) + '/../log/daemonizing_test.log'
7
+ @server.pid_file = 'test.pid'
8
+ @pid = nil
9
+ end
10
+
11
+ it 'should have a pid file' do
12
+ @server.should respond_to(:pid_file)
13
+ @server.should respond_to(:pid_file=)
14
+ end
15
+
16
+ it 'should create a pid file' do
17
+ @pid = fork do
18
+ @server.daemonize
19
+ sleep 1
20
+ end
21
+
22
+ Process.wait(@pid)
23
+ File.exist?(@server.pid_file).should be_true
24
+ @pid = @server.pid
25
+
26
+ proc { sleep 0.1 while File.exist?(@server.pid_file) }.should take_less_then(2)
27
+ end
28
+
29
+ it 'should redirect stdio to a log file' do
30
+ @pid = fork do
31
+ @server.log_file = 'daemon_test.log'
32
+ @server.daemonize
33
+
34
+ puts "simple puts"
35
+ STDERR.puts "STDERR.puts"
36
+ STDOUT.puts "STDOUT.puts"
37
+ end
38
+ Process.wait(@pid)
39
+
40
+ # Wait for the file to close and magical stuff to happen
41
+ proc { sleep 0.1 until File.exist?('daemon_test.log') }.should take_less_then(3)
42
+ sleep 0.5
43
+
44
+ log = File.read('daemon_test.log')
45
+ log.should include('simple puts', 'STDERR.puts', 'STDOUT.puts')
46
+
47
+ File.delete 'daemon_test.log'
48
+ end
49
+
50
+ it 'should change privilege' do
51
+ @pid = fork do
52
+ @server.daemonize
53
+ @server.change_privilege('root', 'admin')
54
+ end
55
+ Process.wait(@pid)
56
+ $?.should be_a_success
57
+ end
58
+
59
+ it 'should kill process in pid file' do
60
+ @pid = fork do
61
+ @server.daemonize
62
+ loop { sleep 1 }
63
+ end
64
+
65
+ proc { sleep 0.1 until File.exist?(@server.pid_file) }.should take_less_then(3)
66
+
67
+ silence_stream STDOUT do
68
+ Server.kill(@server.pid_file, 1)
69
+ end
70
+
71
+ File.exist?(@server.pid_file).should_not be_true
72
+ end
73
+
74
+ it 'should send kill signal if timeout' do
75
+ @pid = fork do
76
+ @server.should_receive(:stop) # pretend we cannot handle the INT signal
77
+ @server.daemonize
78
+ sleep 5
79
+ end
80
+
81
+ proc { sleep 0.1 until File.exist?(@server.pid_file) }.should take_less_then(10)
82
+
83
+ silence_stream STDOUT do
84
+ Server.kill(@server.pid_file, 1)
85
+ end
86
+
87
+ File.exist?(@server.pid_file).should be_false
88
+ Process.running?(@pid).should be_false
89
+ end
90
+
91
+ after do
92
+ Process.kill(9, @pid.to_i) if @pid && Process.running?(@pid.to_i)
93
+ end
94
+ end
@@ -0,0 +1,35 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe Headers do
4
+ before do
5
+ @headers = Headers.new
6
+ end
7
+
8
+ it 'should allow duplicate on some fields' do
9
+ @headers['Set-Cookie'] = 'twice'
10
+ @headers['Set-Cookie'] = 'is cooler the once'
11
+
12
+ @headers.size.should == 2
13
+ end
14
+
15
+ it 'should overwrite value on non duplicate fields' do
16
+ @headers['Host'] = 'this is unique'
17
+ @headers['Host'] = 'so is this'
18
+ @headers['Host'] = 'so this will overwrite ^'
19
+
20
+ @headers['Host'].should == 'so this will overwrite ^'
21
+ end
22
+
23
+ it 'should return first header value' do
24
+ @headers['Set-Cookie'] = 'hi'
25
+ @headers['Set-Cookie'].should == 'hi'
26
+ end
27
+
28
+ it 'should output to string' do
29
+ @headers['Host'] = 'localhost:3000'
30
+ @headers['Set-Cookie'] = 'twice'
31
+ @headers['Set-Cookie'] = 'is cooler the once'
32
+
33
+ @headers.to_s.should == "Host: localhost:3000\r\nSet-Cookie: twice\r\nSet-Cookie: is cooler the once\r\n"
34
+ end
35
+ end
@@ -0,0 +1,258 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require 'digest/sha1'
3
+
4
+ describe Request do
5
+ it 'should include basic headers' do
6
+ request = R("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n")
7
+ request.env['SERVER_PROTOCOL'].should == 'HTTP/1.1'
8
+ request.env['REQUEST_PATH'].should == '/'
9
+ request.env['HTTP_VERSION'].should == 'HTTP/1.1'
10
+ request.env['REQUEST_URI'].should == '/'
11
+ request.env['GATEWAY_INTERFACE'].should == 'CGI/1.2'
12
+ request.env['REQUEST_METHOD'].should == 'GET'
13
+ request.env["rack.url_scheme"].should == 'http'
14
+ request.env['FRAGMENT'].to_s.should be_empty
15
+ request.env['QUERY_STRING'].to_s.should be_empty
16
+
17
+ request.should validate_with_lint
18
+ end
19
+
20
+ it 'should not prepend HTTP_ to Content-Type and Content-Length' do
21
+ request = R("POST / HTTP/1.1\r\nHost: localhost\r\nContent-Type: text/html\r\nContent-Length: 2\r\n\r\naa")
22
+ request.env.keys.should_not include('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH')
23
+ request.env.keys.should include('CONTENT_TYPE', 'CONTENT_LENGTH')
24
+
25
+ request.should validate_with_lint
26
+ end
27
+
28
+ it 'should raise error on invalid request line' do
29
+ proc { R("GET / SsUTF/1.1") }.should raise_error(InvalidRequest)
30
+ proc { R("GET / HTTP/1.1yousmelllikecheeze") }.should raise_error(InvalidRequest)
31
+ end
32
+
33
+ it 'should support fragment in uri' do
34
+ request = R("GET /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\nHost: localhost\r\n\r\n")
35
+
36
+ request.env['REQUEST_URI'].should == '/forums/1/topics/2375?page=1'
37
+ request.env['PATH_INFO'].should == '/forums/1/topics/2375'
38
+ request.env['QUERY_STRING'].should == 'page=1'
39
+ request.env['FRAGMENT'].should == 'posts-17408'
40
+
41
+ request.should validate_with_lint
42
+ end
43
+
44
+ it 'should parse path with query string' do
45
+ request = R("GET /index.html?234235 HTTP/1.1\r\nHost: localhost\r\n\r\n")
46
+ request.env['REQUEST_PATH'].should == '/index.html'
47
+ request.env['QUERY_STRING'].should == '234235'
48
+ request.env['FRAGMENT'].should be_nil
49
+
50
+ request.should validate_with_lint
51
+ end
52
+
53
+ it 'should raise error on large header names' do
54
+ proc { R("GET /#{rand_data(10,120)} HTTP/1.1\r\nX-#{rand_data(1024, 1024+(1024))}: Test\r\n\r\n") }.
55
+ should raise_error(InvalidRequest)
56
+ end
57
+
58
+ it 'should raise error on large mangled field values' do
59
+ proc { R("GET /#{rand_data(10,120)} HTTP/1.1\r\nX-Test: #{rand_data(1024, 100*1024+(1024), false)}\r\n\r\n") }.
60
+ should raise_error(InvalidRequest)
61
+ end
62
+
63
+ it 'should raise error on big fat ugly headers' do
64
+ get = "GET /#{rand_data(10,120)} HTTP/1.1\r\n"
65
+ get << "X-Test: test\r\n" * (80 * 1024)
66
+ proc { R(get) }.should raise_error(InvalidRequest)
67
+ end
68
+
69
+ it 'should raise error on random garbage' do
70
+ proc { R("GET #{rand_data(1024, 1024+(1024), false)} #{rand_data(1024, 1024+(1024), false)}\r\n\r\n") }.
71
+ should raise_error(InvalidRequest)
72
+ end
73
+
74
+ it 'should parse headers from GET request' do
75
+ request = R(<<-EOS, true)
76
+ GET / HTTP/1.1
77
+ Host: localhost:3000
78
+ User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9
79
+ Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
80
+ Accept-Language: en-us,en;q=0.5
81
+ Accept-Encoding: gzip,deflate
82
+ Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
83
+ Cookie: mium=7
84
+ Keep-Alive: 300
85
+ Connection: keep-alive
86
+
87
+ EOS
88
+ request.env['HTTP_HOST'].should == 'localhost:3000'
89
+ request.env['SERVER_NAME'].should == 'localhost'
90
+ request.env['SERVER_PORT'].should == '3000'
91
+ request.env['HTTP_COOKIE'].should == 'mium=7'
92
+
93
+ request.should validate_with_lint
94
+ end
95
+
96
+ it 'should parse POST request with data' do
97
+ request = R(<<-EOS.chomp, true)
98
+ POST /postit HTTP/1.1
99
+ Host: localhost:3000
100
+ User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9
101
+ Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
102
+ Accept-Language: en-us,en;q=0.5
103
+ Accept-Encoding: gzip,deflate
104
+ Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
105
+ Keep-Alive: 300
106
+ Connection: keep-alive
107
+ Content-Type: text/html
108
+ Content-Length: 37
109
+
110
+ name=marc&email=macournoyer@gmail.com
111
+ EOS
112
+
113
+ request.env['REQUEST_METHOD'].should == 'POST'
114
+ request.env['REQUEST_URI'].should == '/postit'
115
+ request.env['CONTENT_TYPE'].should == 'text/html'
116
+ request.env['CONTENT_LENGTH'].should == '37'
117
+ request.env['HTTP_ACCEPT'].should == 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5'
118
+ request.env['HTTP_ACCEPT_LANGUAGE'].should == 'en-us,en;q=0.5'
119
+
120
+ request.body.rewind
121
+ request.body.read.should == 'name=marc&email=macournoyer@gmail.com'
122
+
123
+ request.should validate_with_lint
124
+ end
125
+
126
+ it 'should not fuck up on stupid fucked IE6 headers' do
127
+ body = <<-EOS
128
+ POST /codes/58-tracking-file-downloads-automatically-in-google-analytics-with-prototype/refactors HTTP/1.0
129
+ X-Real-IP: 62.24.71.95
130
+ X-Forwarded-For: 62.24.71.95
131
+ Host: refactormycode.com
132
+ Connection: close
133
+ TE: deflate,gzip;q=0.3
134
+ Accept: */*
135
+ Range: bytes=0-499999
136
+ Referer: http://refactormycode.com/codes/58-tracking-file-downloads-automatically-in-google-analytics-with-prototype
137
+ User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)
138
+ Content-Length: 1
139
+ Content-Type: application/x-www-form-urlencoded
140
+ Cookie: _refactormycode_session_id=a1b2n3jk4k5; flash=%7B%7D
141
+ Cookie2: $Version="1"
142
+
143
+ a
144
+ EOS
145
+ request = R(body, true)
146
+ request.env['HTTP_COOKIE2'].should == '$Version="1"'
147
+
148
+ request.should validate_with_lint
149
+ end
150
+
151
+ it 'shoud accept long query string' do
152
+ body = <<-EOS
153
+ GET /session?open_id_complete=1&nonce=ytPOcwni&nonce=ytPOcwni&openid.assoc_handle=%7BHMAC-SHA1%7D%7B473e38fe%7D%7BJTjJxA%3D%3D%7D&openid.identity=http%3A%2F%2Fmacournoyer.myopenid.com%2F&openid.mode=id_res&openid.op_endpoint=http%3A%2F%2Fwww.myopenid.com%2Fserver&openid.response_nonce=2007-11-29T01%3A19%3A35ZGA5FUU&openid.return_to=http%3A%2F%2Flocalhost%3A3000%2Fsession%3Fopen_id_complete%3D1%26nonce%3DytPOcwni%26nonce%3DytPOcwni&openid.sig=lPIRgwpfR6JAdGGnb0ZjcY%2FWjr8%3D&openid.signed=assoc_handle%2Cidentity%2Cmode%2Cop_endpoint%2Cresponse_nonce%2Creturn_to%2Csigned%2Csreg.email%2Csreg.nickname&openid.sreg.email=macournoyer%40yahoo.ca&openid.sreg.nickname=macournoyer HTTP/1.1
154
+ Host: localhost:3000
155
+
156
+ EOS
157
+ request = R(body, true)
158
+
159
+ request.env['QUERY_STRING'].should == 'open_id_complete=1&nonce=ytPOcwni&nonce=ytPOcwni&openid.assoc_handle=%7BHMAC-SHA1%7D%7B473e38fe%7D%7BJTjJxA%3D%3D%7D&openid.identity=http%3A%2F%2Fmacournoyer.myopenid.com%2F&openid.mode=id_res&openid.op_endpoint=http%3A%2F%2Fwww.myopenid.com%2Fserver&openid.response_nonce=2007-11-29T01%3A19%3A35ZGA5FUU&openid.return_to=http%3A%2F%2Flocalhost%3A3000%2Fsession%3Fopen_id_complete%3D1%26nonce%3DytPOcwni%26nonce%3DytPOcwni&openid.sig=lPIRgwpfR6JAdGGnb0ZjcY%2FWjr8%3D&openid.signed=assoc_handle%2Cidentity%2Cmode%2Cop_endpoint%2Cresponse_nonce%2Creturn_to%2Csigned%2Csreg.email%2Csreg.nickname&openid.sreg.email=macournoyer%40yahoo.ca&openid.sreg.nickname=macournoyer'
160
+
161
+ request.should validate_with_lint
162
+ end
163
+
164
+ it 'should parse even with stupid Content-Length' do
165
+ body = <<-EOS.chomp
166
+ POST / HTTP/1.1
167
+ Host: localhost:3000
168
+ Content-Length: 300
169
+
170
+ aye
171
+ EOS
172
+ request = R(body, true)
173
+
174
+ request.body.rewind
175
+ request.body.read.should == 'aye'
176
+ end
177
+
178
+ it 'should parse in chunks' do
179
+ request = Request.new
180
+ request.parse("POST / HTTP/1.1\r\n").should be_false
181
+ request.parse("Host: localhost\r\n").should be_false
182
+ request.parse("Content-Length: 9\r\n").should be_false
183
+ request.parse("\r\nvery ").should be_false
184
+ request.parse("cool").should be_true
185
+
186
+ request.env['CONTENT_LENGTH'].should == '9'
187
+ request.body.read.should == 'very cool'
188
+ request.should validate_with_lint
189
+ end
190
+
191
+ it "should be faster then #{max_parsing_time = 0.2} ms" do
192
+ body = <<-EOS.chomp
193
+ POST /postit HTTP/1.1
194
+ Host: localhost:3000
195
+ User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9
196
+ Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
197
+ Accept-Language: en-us,en;q=0.5
198
+ Accept-Encoding: gzip,deflate
199
+ Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
200
+ Keep-Alive: 300
201
+ Connection: keep-alive
202
+ Content-Type: text/html
203
+ Content-Length: 37
204
+
205
+ hi=there&name=marc&email=macournoyer@gmail.com
206
+ EOS
207
+
208
+ proc { R(body, true) }.should be_faster_then(max_parsing_time)
209
+ end
210
+
211
+ it 'should be comparable to Mongrel parser' do
212
+ require 'http11'
213
+
214
+ body = <<-EOS.chomp.gsub("\n", "\r\n")
215
+ POST /postit HTTP/1.1
216
+ Host: localhost:3000
217
+ User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9
218
+ Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
219
+ Accept-Language: en-us,en;q=0.5
220
+ Accept-Encoding: gzip,deflate
221
+ Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
222
+ Keep-Alive: 300
223
+ Connection: keep-alive
224
+ Content-Type: text/html
225
+ Content-Length: 37
226
+
227
+ hi=there&name=marc&email=macournoyer@gmail.com
228
+ EOS
229
+
230
+ tests = 10_000
231
+ puts
232
+ Benchmark.bmbm(10) do |results|
233
+ results.report("mongrel:") { tests.times { Mongrel::HttpParser.new.execute({}, body.dup, 0) } }
234
+ results.report("thin:") { tests.times { Thin::HttpParser.new.execute({'rack.input' => StringIO.new}, body.dup, 0) } }
235
+ end
236
+ end if ENV['BM']
237
+
238
+ private
239
+ def rand_data(min, max, readable=true)
240
+ count = min + ((rand(max)+1) *10).to_i
241
+ res = count.to_s + "/"
242
+
243
+ if readable
244
+ res << Digest::SHA1.hexdigest(rand(count * 100).to_s) * (count / 40)
245
+ else
246
+ res << Digest::SHA1.digest(rand(count * 100).to_s) * (count / 20)
247
+ end
248
+
249
+ return res
250
+ end
251
+
252
+ def R(raw, convert_line_feed=false)
253
+ raw.gsub!("\n", "\r\n") if convert_line_feed
254
+ request = Thin::Request.new
255
+ request.parse(raw)
256
+ request
257
+ end
258
+ end