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.
- data/COPYING +18 -0
- data/README +32 -45
- data/Rakefile +66 -23
- data/bin/thin +76 -45
- data/doc/benchmarks.txt +64 -249
- data/doc/rdoc/created.rid +1 -1
- data/doc/rdoc/files/README.html +37 -100
- data/doc/rdoc/rdoc-style.css +5 -0
- data/example/config.ru +9 -0
- data/ext/thin_parser/common.rl +54 -0
- data/ext/thin_parser/ext_help.h +14 -0
- data/ext/thin_parser/extconf.rb +6 -0
- data/ext/thin_parser/parser.c +1199 -0
- data/ext/thin_parser/parser.h +49 -0
- data/ext/thin_parser/parser.rl +143 -0
- data/ext/thin_parser/thin.c +424 -0
- data/lib/rack/adapter/rails.rb +136 -0
- data/lib/rack/handler/thin.rb +13 -0
- data/lib/thin.rb +28 -12
- data/lib/thin/connection.rb +47 -0
- data/lib/thin/daemonizing.rb +5 -1
- data/lib/thin/headers.rb +3 -2
- data/lib/thin/logging.rb +6 -13
- data/lib/thin/request.rb +53 -133
- data/lib/thin/response.rb +21 -25
- data/lib/thin/server.rb +30 -94
- data/lib/thin/version.rb +2 -2
- data/lib/thin_parser.bundle +0 -0
- data/spec/daemonizing_spec.rb +94 -0
- data/spec/headers_spec.rb +35 -0
- data/spec/request_spec.rb +258 -0
- data/spec/response_spec.rb +40 -0
- data/spec/server_spec.rb +75 -0
- data/spec/spec_helper.rb +126 -0
- metadata +79 -99
- data/bin/thin_cluster +0 -53
- data/doc/rdoc/classes/Kernel.html +0 -182
- data/doc/rdoc/classes/Process.html +0 -175
- data/doc/rdoc/classes/Thin.html +0 -184
- data/doc/rdoc/classes/Thin/CGIWrapper.html +0 -438
- data/doc/rdoc/classes/Thin/Cluster.html +0 -392
- data/doc/rdoc/classes/Thin/Command.html +0 -221
- data/doc/rdoc/classes/Thin/CommandError.html +0 -154
- data/doc/rdoc/classes/Thin/Commands.html +0 -145
- data/doc/rdoc/classes/Thin/Daemonizable.html +0 -250
- data/doc/rdoc/classes/Thin/Daemonizable/ClassMethods.html +0 -203
- data/doc/rdoc/classes/Thin/DirHandler.html +0 -250
- data/doc/rdoc/classes/Thin/Handler.html +0 -195
- data/doc/rdoc/classes/Thin/Headers.html +0 -244
- data/doc/rdoc/classes/Thin/InvalidRequest.html +0 -150
- data/doc/rdoc/classes/Thin/Logging.html +0 -214
- data/doc/rdoc/classes/Thin/RailsHandler.html +0 -234
- data/doc/rdoc/classes/Thin/RailsServer.html +0 -175
- data/doc/rdoc/classes/Thin/Request.html +0 -379
- data/doc/rdoc/classes/Thin/Response.html +0 -311
- data/doc/rdoc/classes/Thin/Server.html +0 -381
- data/doc/rdoc/files/bin/thin.html +0 -188
- data/doc/rdoc/files/bin/thin_cluster.html +0 -175
- data/doc/rdoc/files/lib/thin/cgi_rb.html +0 -263
- data/doc/rdoc/files/lib/thin/cluster_rb.html +0 -263
- data/doc/rdoc/files/lib/thin/command_rb.html +0 -263
- data/doc/rdoc/files/lib/thin/consts_rb.html +0 -263
- data/doc/rdoc/files/lib/thin/daemonizing_rb.html +0 -263
- data/doc/rdoc/files/lib/thin/handler_rb.html +0 -263
- data/doc/rdoc/files/lib/thin/headers_rb.html +0 -263
- data/doc/rdoc/files/lib/thin/logging_rb.html +0 -263
- data/doc/rdoc/files/lib/thin/mime_types_rb.html +0 -263
- data/doc/rdoc/files/lib/thin/rails_rb.html +0 -263
- data/doc/rdoc/files/lib/thin/recipes_rb.html +0 -171
- data/doc/rdoc/files/lib/thin/request_rb.html +0 -171
- data/doc/rdoc/files/lib/thin/response_rb.html +0 -171
- data/doc/rdoc/files/lib/thin/server_rb.html +0 -171
- data/doc/rdoc/files/lib/thin/statuses_rb.html +0 -171
- data/doc/rdoc/files/lib/thin/version_rb.html +0 -171
- data/lib/thin/cgi.rb +0 -159
- data/lib/thin/cluster.rb +0 -147
- data/lib/thin/command.rb +0 -49
- data/lib/thin/commands/cluster/base.rb +0 -24
- data/lib/thin/commands/cluster/config.rb +0 -36
- data/lib/thin/commands/cluster/restart.rb +0 -35
- data/lib/thin/commands/cluster/start.rb +0 -40
- data/lib/thin/commands/cluster/stop.rb +0 -28
- data/lib/thin/commands/server/base.rb +0 -7
- data/lib/thin/commands/server/start.rb +0 -33
- data/lib/thin/commands/server/stop.rb +0 -29
- data/lib/thin/consts.rb +0 -37
- data/lib/thin/handler.rb +0 -57
- data/lib/thin/mime_types.rb +0 -619
- data/lib/thin/rails.rb +0 -44
- data/lib/thin/recipes.rb +0 -36
- data/lib/transat/parser.rb +0 -247
data/lib/thin/response.rb
CHANGED
@@ -1,23 +1,17 @@
|
|
1
1
|
module Thin
|
2
2
|
# A response sent to the client.
|
3
3
|
class Response
|
4
|
-
|
5
|
-
|
4
|
+
CONTENT_LENGTH = 'Content-Length'.freeze
|
5
|
+
CONNECTION = 'Connection'.freeze
|
6
|
+
CLOSE = 'close'.freeze
|
6
7
|
|
7
|
-
attr_accessor :
|
8
|
+
attr_accessor :status
|
9
|
+
attr_reader :headers, :body
|
8
10
|
|
9
11
|
def initialize
|
10
12
|
@headers = Headers.new
|
11
|
-
@body
|
12
|
-
@status
|
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
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
40
|
-
|
35
|
+
def body=(stream)
|
36
|
+
stream.each do |part|
|
37
|
+
@body << part
|
38
|
+
end
|
41
39
|
end
|
42
40
|
|
43
|
-
def
|
44
|
-
@
|
45
|
-
yield @headers, @body
|
41
|
+
def close
|
42
|
+
@body.close
|
46
43
|
end
|
47
44
|
|
48
45
|
def to_s
|
49
|
-
|
50
|
-
|
51
|
-
out
|
46
|
+
@body.rewind
|
47
|
+
head + @body.read
|
52
48
|
end
|
53
49
|
end
|
54
50
|
end
|
data/lib/thin/server.rb
CHANGED
@@ -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
|
-
#
|
18
|
-
attr_accessor :
|
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 +
|
25
|
-
def initialize(host, port,
|
25
|
+
# that will pass request to +app+.
|
26
|
+
def initialize(host, port, app)
|
26
27
|
@host = host
|
27
|
-
@port = port
|
28
|
-
@
|
29
|
-
@timeout = 60
|
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
|
-
|
58
|
-
|
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
|
-
|
50
|
+
# See http://rubyeventmachine.com/pub/rdoc/files/EPOLL.html
|
51
|
+
EventMachine.epoll
|
112
52
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
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
|
-
|
133
|
-
|
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
|
-
|
74
|
+
raise StopServer
|
139
75
|
end
|
140
76
|
end
|
141
77
|
end
|
data/lib/thin/version.rb
CHANGED
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
|