thin 0.3.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/README +35 -0
- data/Rakefile +101 -0
- data/bin/thin +48 -0
- data/bin/thin_cluster +53 -0
- data/doc/benchmarks.txt +271 -0
- data/lib/thin.rb +19 -0
- data/lib/thin/cgi.rb +159 -0
- data/lib/thin/cluster.rb +147 -0
- data/lib/thin/command.rb +49 -0
- data/lib/thin/commands/cluster/base.rb +24 -0
- data/lib/thin/commands/cluster/config.rb +34 -0
- data/lib/thin/commands/cluster/restart.rb +35 -0
- data/lib/thin/commands/cluster/start.rb +40 -0
- data/lib/thin/commands/cluster/stop.rb +28 -0
- data/lib/thin/commands/server/base.rb +7 -0
- data/lib/thin/commands/server/start.rb +33 -0
- data/lib/thin/commands/server/stop.rb +29 -0
- data/lib/thin/consts.rb +33 -0
- data/lib/thin/daemonizing.rb +122 -0
- data/lib/thin/handler.rb +57 -0
- data/lib/thin/headers.rb +36 -0
- data/lib/thin/logging.rb +30 -0
- data/lib/thin/mime_types.rb +619 -0
- data/lib/thin/rails.rb +44 -0
- data/lib/thin/recipes.rb +36 -0
- data/lib/thin/request.rb +132 -0
- data/lib/thin/response.rb +54 -0
- data/lib/thin/server.rb +141 -0
- data/lib/thin/statuses.rb +43 -0
- data/lib/thin/version.rb +9 -0
- data/lib/transat/parser.rb +247 -0
- metadata +82 -0
data/lib/thin/rails.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module Thin
|
2
|
+
# Forwards incoming request to Rails dispatcher.
|
3
|
+
class RailsHandler < Handler
|
4
|
+
def initialize(pwd, env='development')
|
5
|
+
@env = env
|
6
|
+
@pwd = pwd
|
7
|
+
end
|
8
|
+
|
9
|
+
def start
|
10
|
+
ENV['RAILS_ENV'] = @env
|
11
|
+
|
12
|
+
require "#{@pwd}/config/environment"
|
13
|
+
require 'dispatcher'
|
14
|
+
end
|
15
|
+
|
16
|
+
def process(request, response)
|
17
|
+
# Rails doesn't serve static files
|
18
|
+
# TODO handle Rails page caching
|
19
|
+
return false if File.file?(File.join(@pwd, 'public', request.path))
|
20
|
+
|
21
|
+
cgi = CGIWrapper.new(request, response)
|
22
|
+
|
23
|
+
Dispatcher.dispatch(cgi, ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, response.body)
|
24
|
+
|
25
|
+
# This finalizes the output using the proper HttpResponse way
|
26
|
+
cgi.out("text/html", true) {""}
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
"Rails on #{@pwd} (env=#{@env})"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Serve the Rails application in the current directory.
|
35
|
+
class RailsServer < Server
|
36
|
+
def initialize(address, port, environment='development', cwd='.')
|
37
|
+
super address, port,
|
38
|
+
# Let Rails handle his thing and ignore files
|
39
|
+
Thin::RailsHandler.new(cwd, environment),
|
40
|
+
# Serve static files
|
41
|
+
Thin::DirHandler.new(File.join(cwd, 'public'))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/thin/recipes.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# == Set of Capistrano 2 recipes
|
2
|
+
# To use, add on top of your Capfile file:
|
3
|
+
# load 'config/deploy'
|
4
|
+
# # ...
|
5
|
+
# require 'thin'
|
6
|
+
# require 'thin/recipes'
|
7
|
+
#
|
8
|
+
# === Configurable parameters
|
9
|
+
# You can configure some parameters but it should work out of the box.
|
10
|
+
# Path to the thin_cluster script, don't need to change this if
|
11
|
+
# you installed thin as a gem on the server.
|
12
|
+
# set :thin_cluster, "thin_cluster"
|
13
|
+
# Location of the config file:
|
14
|
+
# set :thin_config, "#{release_path}/config/thin.yml"
|
15
|
+
|
16
|
+
Capistrano::Configuration.instance.load do
|
17
|
+
set :thin_cluster, "thin_cluster"
|
18
|
+
set :thin_config, "#{current_path}/config/thin.yml"
|
19
|
+
|
20
|
+
namespace :deploy do
|
21
|
+
desc 'Start Thin processes on the app server.'
|
22
|
+
task :start, :roles => :app do
|
23
|
+
run "#{thin_cluster} start -C #{thin_config}"
|
24
|
+
end
|
25
|
+
|
26
|
+
desc 'Stop the Thin processes on the app server.'
|
27
|
+
task :stop, :roles => :app do
|
28
|
+
run "#{thin_cluster} stop -C #{thin_config}"
|
29
|
+
end
|
30
|
+
|
31
|
+
desc 'Restart the Thin processes on the app server by starting and stopping the cluster.'
|
32
|
+
task :restart, :roles => :app do
|
33
|
+
run "#{thin_cluster} restart -C #{thin_config}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/thin/request.rb
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
module Thin
|
2
|
+
# Raised when an incoming request is not valid
|
3
|
+
# and the server can not process it.
|
4
|
+
class InvalidRequest < StandardError; end
|
5
|
+
|
6
|
+
# A request made to the server.
|
7
|
+
class Request
|
8
|
+
HTTP_LESS_HEADERS = %w(Content-Length Content-Type).freeze
|
9
|
+
BODYFUL_METHODS = %w(POST PUT).freeze
|
10
|
+
|
11
|
+
# We control max length of different part of the request
|
12
|
+
# to prevent attack and resource overflow.
|
13
|
+
MAX_FIELD_NAME_LENGTH = 256
|
14
|
+
MAX_FIELD_VALUE_LENGTH = 80 * 1024
|
15
|
+
MAX_REQUEST_URI_LENGTH = 1024 * 12
|
16
|
+
MAX_FRAGMENT_LENGTH = 1024
|
17
|
+
MAX_REQUEST_PATH_LENGTH = 1024
|
18
|
+
MAX_QUERY_STRING_LENGTH = 1024 * 10
|
19
|
+
MAX_HEADER_LENGTH = 1024 * (80 + 32)
|
20
|
+
|
21
|
+
attr_reader :body, :params, :verb, :path
|
22
|
+
attr_accessor :trace, :raw # For debugging and trace
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@params = {
|
26
|
+
'GATEWAY_INTERFACE' => 'CGI/1.2',
|
27
|
+
'HTTP_VERSION' => 'HTTP/1.1',
|
28
|
+
'SERVER_PROTOCOL' => 'HTTP/1.1'
|
29
|
+
}
|
30
|
+
@body = StringIO.new
|
31
|
+
@raw = ''
|
32
|
+
@trace = false
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse!(content)
|
36
|
+
parse_headers! content
|
37
|
+
parse_body! content if BODYFUL_METHODS.include?(verb)
|
38
|
+
rescue InvalidRequest => e
|
39
|
+
raise
|
40
|
+
rescue Object => e
|
41
|
+
raise InvalidRequest, e.message
|
42
|
+
end
|
43
|
+
|
44
|
+
# Parse the request headers from the socket into CGI like variables.
|
45
|
+
# Parse the request according to http://www.w3.org/Protocols/rfc2616/rfc2616.html
|
46
|
+
# Parse env variables according to http://www.ietf.org/rfc/rfc3875
|
47
|
+
def parse_headers!(content)
|
48
|
+
if matches = readline(content).match(/^([A-Z]+) (.*?)(?:#(.*))? HTTP/)
|
49
|
+
@verb, uri, fragment = matches[1,3]
|
50
|
+
else
|
51
|
+
raise InvalidRequest, 'No valid request line found'
|
52
|
+
end
|
53
|
+
|
54
|
+
raise InvalidRequest, 'No method specified' unless @verb
|
55
|
+
raise InvalidRequest, 'No URI specified' unless uri
|
56
|
+
|
57
|
+
# Validation various length for security
|
58
|
+
raise InvalidRequest, 'URI too long' if uri.size > MAX_REQUEST_URI_LENGTH
|
59
|
+
raise InvalidRequest, 'Fragment too long' if fragment && fragment.size > MAX_FRAGMENT_LENGTH
|
60
|
+
|
61
|
+
if matches = uri.match(/^(.*?)(?:\?(.*))?$/)
|
62
|
+
@path, query_string = matches[1,2]
|
63
|
+
else
|
64
|
+
raise InvalidRequest, "No valid path found in #{uri}"
|
65
|
+
end
|
66
|
+
|
67
|
+
raise InvalidRequest, 'Request path too long' if @path.size > MAX_REQUEST_PATH_LENGTH
|
68
|
+
raise InvalidRequest, 'Query string path too long' if query_string && query_string.size > MAX_QUERY_STRING_LENGTH
|
69
|
+
|
70
|
+
@params['REQUEST_URI'] = uri
|
71
|
+
@params['FRAGMENT'] = fragment if fragment
|
72
|
+
@params['REQUEST_PATH'] =
|
73
|
+
@params['PATH_INFO'] = @path
|
74
|
+
@params['SCRIPT_NAME'] = '/'
|
75
|
+
@params['REQUEST_METHOD'] = @verb
|
76
|
+
@params['QUERY_STRING'] = query_string if query_string
|
77
|
+
|
78
|
+
headers_size = 0
|
79
|
+
until content.eof?
|
80
|
+
line = readline(content)
|
81
|
+
headers_size += line.size
|
82
|
+
if [?\r, ?\n].include?(line[0])
|
83
|
+
break # Reached the end of the headers
|
84
|
+
elsif matches = line.match(/^([\w\-]+): (.*)$/)
|
85
|
+
name, value = matches[1,2]
|
86
|
+
raise InvalidRequest, 'Header name too long' if name.size > MAX_FIELD_NAME_LENGTH
|
87
|
+
raise InvalidRequest, 'Header value too long' if value.size > MAX_FIELD_VALUE_LENGTH
|
88
|
+
# Transform headers into a HTTP_NAME => value hash
|
89
|
+
prefix = HTTP_LESS_HEADERS.include?(name) ? '' : 'HTTP_'
|
90
|
+
params["#{prefix}#{name.upcase.gsub('-', '_')}"] = value.chomp
|
91
|
+
else
|
92
|
+
raise InvalidRequest, "Expected header : #{line}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
raise InvalidRequest, 'Headers too long' if headers_size > MAX_HEADER_LENGTH
|
97
|
+
|
98
|
+
@params['SERVER_NAME'] = @params['HTTP_HOST'].split(':')[0] if @params['HTTP_HOST']
|
99
|
+
end
|
100
|
+
|
101
|
+
def parse_body!(content)
|
102
|
+
# Parse by chunks
|
103
|
+
length = content_length
|
104
|
+
while @body.size < length
|
105
|
+
chunk = content.readpartial(CHUNK_SIZE)
|
106
|
+
break unless chunk && chunk.size > 0
|
107
|
+
@body << chunk
|
108
|
+
end
|
109
|
+
|
110
|
+
@body.rewind
|
111
|
+
end
|
112
|
+
|
113
|
+
def close
|
114
|
+
@body.close
|
115
|
+
end
|
116
|
+
|
117
|
+
def content_length
|
118
|
+
@params['CONTENT_LENGTH'].to_i
|
119
|
+
end
|
120
|
+
|
121
|
+
def to_s
|
122
|
+
"#{@params['REQUEST_METHOD']} #{@params['REQUEST_URI']}"
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
def readline(io)
|
127
|
+
out = io.gets(LF)
|
128
|
+
@raw << out if @trace # Build a gigantic string to later print trace for the request
|
129
|
+
out
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Thin
|
2
|
+
# A response sent to the client.
|
3
|
+
class Response
|
4
|
+
CONNECTION = 'Connection'.freeze
|
5
|
+
CLOSE = 'close'.freeze
|
6
|
+
|
7
|
+
attr_accessor :body, :headers, :status
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@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]
|
21
|
+
end
|
22
|
+
|
23
|
+
def headers_output
|
24
|
+
@headers[CONTENT_LENGTH] = @body.size
|
25
|
+
@headers[CONNECTION] = CLOSE
|
26
|
+
@headers.to_s
|
27
|
+
end
|
28
|
+
|
29
|
+
def head
|
30
|
+
"HTTP/1.1 #{@status} #{HTTP_STATUS_CODES[@status.to_i]}\r\n#{headers_output}\r\n"
|
31
|
+
end
|
32
|
+
|
33
|
+
def write(socket)
|
34
|
+
socket << head
|
35
|
+
@body.rewind
|
36
|
+
socket << @body.read
|
37
|
+
end
|
38
|
+
|
39
|
+
def close
|
40
|
+
@body.close
|
41
|
+
end
|
42
|
+
|
43
|
+
def start(status)
|
44
|
+
@status = status
|
45
|
+
yield @headers, @body
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_s
|
49
|
+
out = ''
|
50
|
+
write out
|
51
|
+
out
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/thin/server.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module Thin
|
4
|
+
# The Thin HTTP server used to served request.
|
5
|
+
# It listen for incoming request on a given port
|
6
|
+
# and forward all request to all the handlers in the order
|
7
|
+
# they were registered.
|
8
|
+
# Based on HTTP 1.1 protocol specs
|
9
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616.html
|
10
|
+
class Server
|
11
|
+
include Logging
|
12
|
+
include Daemonizable
|
13
|
+
|
14
|
+
# Addresse and port on which the server is listening for connections.
|
15
|
+
attr_accessor :port, :host
|
16
|
+
|
17
|
+
# List of handlers to process the request in the order they are given.
|
18
|
+
attr_accessor :handlers
|
19
|
+
|
20
|
+
# Maximum time for a request to be red and parsed.
|
21
|
+
attr_accessor :timeout
|
22
|
+
|
23
|
+
# Creates a new server binded to <tt>host:port</tt>
|
24
|
+
# that will pass request to +handlers+.
|
25
|
+
def initialize(host, port, *handlers)
|
26
|
+
@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)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Starts the handlers.
|
39
|
+
def start
|
40
|
+
log ">> Thin web server (v#{VERSION})"
|
41
|
+
trace ">> Tracing ON"
|
42
|
+
|
43
|
+
@handlers.each do |handler|
|
44
|
+
log ">> Starting #{handler} ..."
|
45
|
+
handler.start
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Start the server and listen for connections
|
50
|
+
def start!
|
51
|
+
start
|
52
|
+
listen!
|
53
|
+
end
|
54
|
+
|
55
|
+
# Start listening for connections
|
56
|
+
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
|
110
|
+
|
111
|
+
trace { 'Request finished'.center(80, '=') }
|
112
|
+
|
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
|
126
|
+
end
|
127
|
+
|
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
|
+
def stop
|
132
|
+
@stop = true
|
133
|
+
stop! unless @processing # Not processing a request, so we can stop now
|
134
|
+
end
|
135
|
+
|
136
|
+
# Force the server to stop right now!
|
137
|
+
def stop!
|
138
|
+
@socket.close rescue nil # break the accept loop by closing the socket
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Thin
|
2
|
+
# Every standard HTTP code mapped to the appropriate message.
|
3
|
+
# Stolent from Mongrel.
|
4
|
+
HTTP_STATUS_CODES = {
|
5
|
+
100 => 'Continue',
|
6
|
+
101 => 'Switching Protocols',
|
7
|
+
200 => 'OK',
|
8
|
+
201 => 'Created',
|
9
|
+
202 => 'Accepted',
|
10
|
+
203 => 'Non-Authoritative Information',
|
11
|
+
204 => 'No Content',
|
12
|
+
205 => 'Reset Content',
|
13
|
+
206 => 'Partial Content',
|
14
|
+
300 => 'Multiple Choices',
|
15
|
+
301 => 'Moved Permanently',
|
16
|
+
302 => 'Moved Temporarily',
|
17
|
+
303 => 'See Other',
|
18
|
+
304 => 'Not Modified',
|
19
|
+
305 => 'Use Proxy',
|
20
|
+
400 => 'Bad Request',
|
21
|
+
401 => 'Unauthorized',
|
22
|
+
402 => 'Payment Required',
|
23
|
+
403 => 'Forbidden',
|
24
|
+
404 => 'Not Found',
|
25
|
+
405 => 'Method Not Allowed',
|
26
|
+
406 => 'Not Acceptable',
|
27
|
+
407 => 'Proxy Authentication Required',
|
28
|
+
408 => 'Request Time-out',
|
29
|
+
409 => 'Conflict',
|
30
|
+
410 => 'Gone',
|
31
|
+
411 => 'Length Required',
|
32
|
+
412 => 'Precondition Failed',
|
33
|
+
413 => 'Request Entity Too Large',
|
34
|
+
414 => 'Request-URI Too Large',
|
35
|
+
415 => 'Unsupported Media Type',
|
36
|
+
500 => 'Internal Server Error',
|
37
|
+
501 => 'Not Implemented',
|
38
|
+
502 => 'Bad Gateway',
|
39
|
+
503 => 'Service Unavailable',
|
40
|
+
504 => 'Gateway Time-out',
|
41
|
+
505 => 'HTTP Version not supported'
|
42
|
+
}
|
43
|
+
end
|