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.

@@ -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
@@ -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
@@ -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
@@ -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