gross 1.7.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG +410 -0
- data/README.md +102 -0
- data/Rakefile +25 -0
- data/bin/thin +6 -0
- data/example/adapter.rb +32 -0
- data/example/async_app.ru +126 -0
- data/example/async_chat.ru +247 -0
- data/example/async_tailer.ru +100 -0
- data/example/config.ru +22 -0
- data/example/monit_sockets +20 -0
- data/example/monit_unixsock +20 -0
- data/example/myapp.rb +1 -0
- data/example/ramaze.ru +12 -0
- data/example/thin.god +80 -0
- data/example/thin_solaris_smf.erb +36 -0
- data/example/thin_solaris_smf.readme.txt +150 -0
- data/example/vlad.rake +72 -0
- data/ext/thin_parser/common.rl +59 -0
- data/ext/thin_parser/ext_help.h +14 -0
- data/ext/thin_parser/extconf.rb +6 -0
- data/ext/thin_parser/parser.c +1447 -0
- data/ext/thin_parser/parser.h +49 -0
- data/ext/thin_parser/parser.rl +152 -0
- data/ext/thin_parser/thin.c +435 -0
- data/lib/rack/adapter/loader.rb +75 -0
- data/lib/rack/adapter/rails.rb +178 -0
- data/lib/thin.rb +45 -0
- data/lib/thin/backends/base.rb +167 -0
- data/lib/thin/backends/swiftiply_client.rb +56 -0
- data/lib/thin/backends/tcp_server.rb +34 -0
- data/lib/thin/backends/unix_server.rb +56 -0
- data/lib/thin/command.rb +53 -0
- data/lib/thin/connection.rb +215 -0
- data/lib/thin/controllers/cluster.rb +178 -0
- data/lib/thin/controllers/controller.rb +189 -0
- data/lib/thin/controllers/service.rb +76 -0
- data/lib/thin/controllers/service.sh.erb +39 -0
- data/lib/thin/daemonizing.rb +180 -0
- data/lib/thin/headers.rb +40 -0
- data/lib/thin/logging.rb +174 -0
- data/lib/thin/request.rb +162 -0
- data/lib/thin/response.rb +117 -0
- data/lib/thin/runner.rb +238 -0
- data/lib/thin/server.rb +290 -0
- data/lib/thin/stats.html.erb +216 -0
- data/lib/thin/stats.rb +52 -0
- data/lib/thin/statuses.rb +44 -0
- data/lib/thin/version.rb +32 -0
- metadata +156 -0
data/lib/thin/headers.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
module Thin
|
2
|
+
# Store HTTP header name-value pairs direcly to a string
|
3
|
+
# and allow duplicated entries on some names.
|
4
|
+
class Headers
|
5
|
+
HEADER_FORMAT = "%s: %s\r\n".freeze
|
6
|
+
ALLOWED_DUPLICATES = %w(set-cookie set-cookie2 warning www-authenticate).freeze
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@sent = {}
|
10
|
+
@out = []
|
11
|
+
end
|
12
|
+
|
13
|
+
# Add <tt>key: value</tt> pair to the headers.
|
14
|
+
# Ignore if already sent and no duplicates are allowed
|
15
|
+
# for this +key+.
|
16
|
+
def []=(key, value)
|
17
|
+
downcase_key = key.downcase
|
18
|
+
if !@sent.has_key?(downcase_key) || ALLOWED_DUPLICATES.include?(downcase_key)
|
19
|
+
@sent[downcase_key] = true
|
20
|
+
value = case value
|
21
|
+
when Time
|
22
|
+
value.httpdate
|
23
|
+
when NilClass
|
24
|
+
return
|
25
|
+
else
|
26
|
+
value.to_s
|
27
|
+
end
|
28
|
+
@out << HEADER_FORMAT % [key, value]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def has_key?(key)
|
33
|
+
@sent[key.downcase]
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
@out.join
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/thin/logging.rb
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Thin
|
4
|
+
# To be included in classes to allow some basic logging
|
5
|
+
# that can be silenced (<tt>Logging.silent=</tt>) or made
|
6
|
+
# more verbose.
|
7
|
+
# <tt>Logging.trace=</tt>: log all raw request and response and
|
8
|
+
# messages logged with +trace+.
|
9
|
+
# <tt>Logging.silent=</tt>: silence all log all log messages
|
10
|
+
# altogether.
|
11
|
+
module Logging
|
12
|
+
# Simple formatter which only displays the message.
|
13
|
+
# Taken from ActiveSupport
|
14
|
+
class SimpleFormatter < Logger::Formatter
|
15
|
+
def call(severity, timestamp, progname, msg)
|
16
|
+
"#{String === msg ? msg : msg.inspect}\n"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
@trace_logger = nil
|
21
|
+
|
22
|
+
class << self
|
23
|
+
attr_reader :logger
|
24
|
+
attr_reader :trace_logger
|
25
|
+
|
26
|
+
def trace=(enabled)
|
27
|
+
if enabled
|
28
|
+
@trace_logger ||= Logger.new(STDOUT)
|
29
|
+
else
|
30
|
+
@trace_logger = nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def trace?
|
35
|
+
!@trace_logger.nil?
|
36
|
+
end
|
37
|
+
|
38
|
+
def silent=(shh)
|
39
|
+
if shh
|
40
|
+
@logger = nil
|
41
|
+
else
|
42
|
+
@logger ||= Logger.new(STDOUT)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def silent?
|
47
|
+
!@logger.nil?
|
48
|
+
end
|
49
|
+
|
50
|
+
def level
|
51
|
+
@logger ? @logger.level : nil # or 'silent'
|
52
|
+
end
|
53
|
+
|
54
|
+
def level=(value)
|
55
|
+
# If logging has been silenced, then re-enable logging
|
56
|
+
@logger = Logger.new(STDOUT) if @logger.nil?
|
57
|
+
@logger.level = value
|
58
|
+
end
|
59
|
+
|
60
|
+
# Allow user to specify a custom logger to use.
|
61
|
+
# This object must respond to:
|
62
|
+
# +level+, +level=+ and +debug+, +info+, +warn+, +error+, +fatal+
|
63
|
+
def logger=(custom_logger)
|
64
|
+
[ :level ,
|
65
|
+
:level= ,
|
66
|
+
:debug ,
|
67
|
+
:info ,
|
68
|
+
:warn ,
|
69
|
+
:error ,
|
70
|
+
:fatal ,
|
71
|
+
:unknown ,
|
72
|
+
].each do |method|
|
73
|
+
if not custom_logger.respond_to?(method)
|
74
|
+
raise ArgumentError, "logger must respond to #{method}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
@logger = custom_logger
|
79
|
+
end
|
80
|
+
|
81
|
+
def trace_logger=(custom_tracer)
|
82
|
+
[ :level ,
|
83
|
+
:level= ,
|
84
|
+
:debug ,
|
85
|
+
:info ,
|
86
|
+
:warn ,
|
87
|
+
:error ,
|
88
|
+
:fatal ,
|
89
|
+
:unknown ,
|
90
|
+
].each do |method|
|
91
|
+
if not custom_tracer.respond_to?(method)
|
92
|
+
raise ArgumentError, "trace logger must respond to #{method}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
@trace_logger = custom_tracer
|
97
|
+
end
|
98
|
+
|
99
|
+
def log_msg(msg, level=Logger::INFO)
|
100
|
+
return unless @logger
|
101
|
+
@logger.add(level, msg)
|
102
|
+
end
|
103
|
+
|
104
|
+
def trace_msg(msg)
|
105
|
+
return unless @trace_logger
|
106
|
+
@trace_logger.info(msg)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Provided for backwards compatibility.
|
110
|
+
# Callers should be using the +level+ (on the +Logging+ module
|
111
|
+
# or on the instance) to figure out what the log level is.
|
112
|
+
def debug?
|
113
|
+
self.level == Logger::DEBUG
|
114
|
+
end
|
115
|
+
def debug=(val)
|
116
|
+
self.level = (val ? Logger::DEBUG : Logger::INFO)
|
117
|
+
end
|
118
|
+
|
119
|
+
end # module methods
|
120
|
+
|
121
|
+
# Default logger to stdout.
|
122
|
+
self.logger = Logger.new(STDOUT)
|
123
|
+
self.logger.level = Logger::INFO
|
124
|
+
self.logger.formatter = Logging::SimpleFormatter.new
|
125
|
+
|
126
|
+
def silent
|
127
|
+
Logging.silent?
|
128
|
+
end
|
129
|
+
|
130
|
+
def silent=(value)
|
131
|
+
Logging.silent = value
|
132
|
+
end
|
133
|
+
|
134
|
+
# Log a message if tracing is activated
|
135
|
+
def trace(msg=nil)
|
136
|
+
Logging.trace_msg(msg) if msg
|
137
|
+
end
|
138
|
+
module_function :trace
|
139
|
+
public :trace
|
140
|
+
|
141
|
+
# Log a message at DEBUG level
|
142
|
+
def log_debug(msg=nil)
|
143
|
+
Logging.log_msg(msg || yield, Logger::DEBUG)
|
144
|
+
end
|
145
|
+
module_function :log_debug
|
146
|
+
public :log_debug
|
147
|
+
|
148
|
+
# Log a message at INFO level
|
149
|
+
def log_info(msg)
|
150
|
+
Logging.log_msg(msg || yield, Logger::INFO)
|
151
|
+
end
|
152
|
+
module_function :log_info
|
153
|
+
public :log_info
|
154
|
+
|
155
|
+
# Log a message at ERROR level (and maybe a backtrace)
|
156
|
+
def log_error(msg, e=nil)
|
157
|
+
log_msg = msg
|
158
|
+
if e
|
159
|
+
log_msg += ": #{e}\n\t" + e.backtrace.join("\n\t") + "\n"
|
160
|
+
end
|
161
|
+
Logging.log_msg(log_msg, Logger::ERROR)
|
162
|
+
end
|
163
|
+
module_function :log_error
|
164
|
+
public :log_error
|
165
|
+
|
166
|
+
# For backwards compatibility
|
167
|
+
def log msg
|
168
|
+
STDERR.puts('#log has been deprecated, please use the ' \
|
169
|
+
'log_level function instead (e.g. - log_info).')
|
170
|
+
log_info(msg)
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
end
|
data/lib/thin/request.rb
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
module Thin
|
4
|
+
# Raised when an incoming request is not valid
|
5
|
+
# and the server can not process it.
|
6
|
+
class InvalidRequest < IOError; end
|
7
|
+
|
8
|
+
# A request sent by the client to the server.
|
9
|
+
class Request
|
10
|
+
# Maximum request body size before it is moved out of memory
|
11
|
+
# and into a tempfile for reading.
|
12
|
+
MAX_BODY = 1024 * (80 + 32)
|
13
|
+
BODY_TMPFILE = 'thin-body'.freeze
|
14
|
+
MAX_HEADER = 1024 * (80 + 32)
|
15
|
+
|
16
|
+
INITIAL_BODY = String.new
|
17
|
+
# Force external_encoding of request's body to ASCII_8BIT
|
18
|
+
INITIAL_BODY.encode!(Encoding::ASCII_8BIT) if INITIAL_BODY.respond_to?(:encode!) && defined?(Encoding::ASCII_8BIT)
|
19
|
+
|
20
|
+
# Freeze some HTTP header names & values
|
21
|
+
SERVER_SOFTWARE = 'SERVER_SOFTWARE'.freeze
|
22
|
+
SERVER_NAME = 'SERVER_NAME'.freeze
|
23
|
+
REQUEST_METHOD = 'REQUEST_METHOD'.freeze
|
24
|
+
LOCALHOST = 'localhost'.freeze
|
25
|
+
HTTP_VERSION = 'HTTP_VERSION'.freeze
|
26
|
+
HTTP_1_0 = 'HTTP/1.0'.freeze
|
27
|
+
REMOTE_ADDR = 'REMOTE_ADDR'.freeze
|
28
|
+
CONTENT_LENGTH = 'CONTENT_LENGTH'.freeze
|
29
|
+
CONNECTION = 'HTTP_CONNECTION'.freeze
|
30
|
+
KEEP_ALIVE_REGEXP = /\bkeep-alive\b/i.freeze
|
31
|
+
CLOSE_REGEXP = /\bclose\b/i.freeze
|
32
|
+
HEAD = 'HEAD'.freeze
|
33
|
+
|
34
|
+
# Freeze some Rack header names
|
35
|
+
RACK_INPUT = 'rack.input'.freeze
|
36
|
+
RACK_VERSION = 'rack.version'.freeze
|
37
|
+
RACK_ERRORS = 'rack.errors'.freeze
|
38
|
+
RACK_MULTITHREAD = 'rack.multithread'.freeze
|
39
|
+
RACK_MULTIPROCESS = 'rack.multiprocess'.freeze
|
40
|
+
RACK_RUN_ONCE = 'rack.run_once'.freeze
|
41
|
+
ASYNC_CALLBACK = 'async.callback'.freeze
|
42
|
+
ASYNC_CLOSE = 'async.close'.freeze
|
43
|
+
|
44
|
+
# CGI-like request environment variables
|
45
|
+
attr_reader :env
|
46
|
+
|
47
|
+
# Unparsed data of the request
|
48
|
+
attr_reader :data
|
49
|
+
|
50
|
+
# Request body
|
51
|
+
attr_reader :body
|
52
|
+
|
53
|
+
def initialize
|
54
|
+
@parser = Thin::HttpParser.new
|
55
|
+
@data = String.new
|
56
|
+
@nparsed = 0
|
57
|
+
@body = StringIO.new(INITIAL_BODY.dup)
|
58
|
+
@env = {
|
59
|
+
SERVER_SOFTWARE => SERVER,
|
60
|
+
SERVER_NAME => LOCALHOST,
|
61
|
+
|
62
|
+
# Rack stuff
|
63
|
+
RACK_INPUT => @body,
|
64
|
+
|
65
|
+
RACK_VERSION => VERSION::RACK,
|
66
|
+
RACK_ERRORS => STDERR,
|
67
|
+
|
68
|
+
RACK_MULTITHREAD => false,
|
69
|
+
RACK_MULTIPROCESS => false,
|
70
|
+
RACK_RUN_ONCE => false
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
# Parse a chunk of data into the request environment
|
75
|
+
# Raises an +InvalidRequest+ if invalid.
|
76
|
+
# Returns +true+ if the parsing is complete.
|
77
|
+
def parse(data)
|
78
|
+
if @parser.finished? # Header finished, can only be some more body
|
79
|
+
@body << data
|
80
|
+
else # Parse more header using the super parser
|
81
|
+
@data << data
|
82
|
+
raise InvalidRequest, 'Header longer than allowed' if @data.size > MAX_HEADER
|
83
|
+
|
84
|
+
@nparsed = @parser.execute(@env, @data, @nparsed)
|
85
|
+
|
86
|
+
# Transfer to a tempfile if body is very big
|
87
|
+
move_body_to_tempfile if @parser.finished? && content_length > MAX_BODY
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
if finished? # Check if header and body are complete
|
92
|
+
@data = nil
|
93
|
+
@body.rewind
|
94
|
+
true # Request is fully parsed
|
95
|
+
else
|
96
|
+
false # Not finished, need more data
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# +true+ if headers and body are finished parsing
|
101
|
+
def finished?
|
102
|
+
@parser.finished? && @body.size >= content_length
|
103
|
+
end
|
104
|
+
|
105
|
+
# Expected size of the body
|
106
|
+
def content_length
|
107
|
+
@env[CONTENT_LENGTH].to_i
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns +true+ if the client expects the connection to be persistent.
|
111
|
+
def persistent?
|
112
|
+
# Clients and servers SHOULD NOT assume that a persistent connection
|
113
|
+
# is maintained for HTTP versions less than 1.1 unless it is explicitly
|
114
|
+
# signaled. (http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html)
|
115
|
+
if @env[HTTP_VERSION] == HTTP_1_0
|
116
|
+
@env[CONNECTION] =~ KEEP_ALIVE_REGEXP
|
117
|
+
|
118
|
+
# HTTP/1.1 client intends to maintain a persistent connection unless
|
119
|
+
# a Connection header including the connection-token "close" was sent
|
120
|
+
# in the request
|
121
|
+
else
|
122
|
+
@env[CONNECTION].nil? || @env[CONNECTION] !~ CLOSE_REGEXP
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def remote_address=(address)
|
127
|
+
@env[REMOTE_ADDR] = address
|
128
|
+
end
|
129
|
+
|
130
|
+
def threaded=(value)
|
131
|
+
@env[RACK_MULTITHREAD] = value
|
132
|
+
end
|
133
|
+
|
134
|
+
def async_callback=(callback)
|
135
|
+
@env[ASYNC_CALLBACK] = callback
|
136
|
+
@env[ASYNC_CLOSE] = EventMachine::DefaultDeferrable.new
|
137
|
+
end
|
138
|
+
|
139
|
+
def async_close
|
140
|
+
@async_close ||= @env[ASYNC_CLOSE]
|
141
|
+
end
|
142
|
+
|
143
|
+
def head?
|
144
|
+
@env[REQUEST_METHOD] == HEAD
|
145
|
+
end
|
146
|
+
|
147
|
+
# Close any resource used by the request
|
148
|
+
def close
|
149
|
+
@body.close! if @body.class == Tempfile
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
def move_body_to_tempfile
|
154
|
+
current_body = @body
|
155
|
+
current_body.rewind
|
156
|
+
@body = Tempfile.new(BODY_TMPFILE)
|
157
|
+
@body.binmode
|
158
|
+
@body << current_body.read
|
159
|
+
@env[RACK_INPUT] = @body
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module Thin
|
2
|
+
# A response sent to the client.
|
3
|
+
class Response
|
4
|
+
CONNECTION = 'Connection'.freeze
|
5
|
+
CLOSE = 'close'.freeze
|
6
|
+
KEEP_ALIVE = 'keep-alive'.freeze
|
7
|
+
SERVER = 'Server'.freeze
|
8
|
+
CONTENT_LENGTH = 'Content-Length'.freeze
|
9
|
+
|
10
|
+
PERSISTENT_STATUSES = [100, 101].freeze
|
11
|
+
|
12
|
+
#Error Responses
|
13
|
+
ERROR = [500, {'Content-Type' => 'text/plain'}, ['Internal server error']].freeze
|
14
|
+
PERSISTENT_ERROR = [500, {'Content-Type' => 'text/plain', 'Connection' => 'keep-alive', 'Content-Length' => "21"}, ['Internal server error']].freeze
|
15
|
+
BAD_REQUEST = [400, {'Content-Type' => 'text/plain'}, ['Bad Request']].freeze
|
16
|
+
|
17
|
+
# Status code
|
18
|
+
attr_accessor :status
|
19
|
+
|
20
|
+
# Response body, must respond to +each+.
|
21
|
+
attr_accessor :body
|
22
|
+
|
23
|
+
# Headers key-value hash
|
24
|
+
attr_reader :headers
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
@headers = Headers.new
|
28
|
+
@status = 200
|
29
|
+
@persistent = false
|
30
|
+
@skip_body = false
|
31
|
+
end
|
32
|
+
|
33
|
+
# String representation of the headers
|
34
|
+
# to be sent in the response.
|
35
|
+
def headers_output
|
36
|
+
# Set default headers
|
37
|
+
@headers[CONNECTION] = persistent? ? KEEP_ALIVE : CLOSE unless @headers.has_key?(CONNECTION)
|
38
|
+
@headers[SERVER] = Thin::NAME unless @headers.has_key?(SERVER)
|
39
|
+
|
40
|
+
@headers.to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
# Top header of the response,
|
44
|
+
# containing the status code and response headers.
|
45
|
+
def head
|
46
|
+
"HTTP/1.1 #{@status} #{HTTP_STATUS_CODES[@status.to_i]}\r\n#{headers_output}\r\n"
|
47
|
+
end
|
48
|
+
|
49
|
+
if Thin.ruby_18?
|
50
|
+
|
51
|
+
# Ruby 1.8 implementation.
|
52
|
+
# Respects Rack specs.
|
53
|
+
#
|
54
|
+
# See http://rack.rubyforge.org/doc/files/SPEC.html
|
55
|
+
def headers=(key_value_pairs)
|
56
|
+
key_value_pairs.each do |k, vs|
|
57
|
+
vs.each { |v| @headers[k] = v.chomp } if vs
|
58
|
+
end if key_value_pairs
|
59
|
+
end
|
60
|
+
|
61
|
+
else
|
62
|
+
|
63
|
+
# Ruby 1.9 doesn't have a String#each anymore.
|
64
|
+
# Rack spec doesn't take care of that yet, for now we just use
|
65
|
+
# +each+ but fallback to +each_line+ on strings.
|
66
|
+
# I wish we could remove that condition.
|
67
|
+
# To be reviewed when a new Rack spec comes out.
|
68
|
+
def headers=(key_value_pairs)
|
69
|
+
key_value_pairs.each do |k, vs|
|
70
|
+
next unless vs
|
71
|
+
if vs.is_a?(String)
|
72
|
+
vs.each_line { |v| @headers[k] = v.chomp }
|
73
|
+
else
|
74
|
+
vs.each { |v| @headers[k] = v.chomp }
|
75
|
+
end
|
76
|
+
end if key_value_pairs
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
# Close any resource used by the response
|
82
|
+
def close
|
83
|
+
@body.close if @body.respond_to?(:close)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Yields each chunk of the response.
|
87
|
+
# To control the size of each chunk
|
88
|
+
# define your own +each+ method on +body+.
|
89
|
+
def each
|
90
|
+
yield head
|
91
|
+
|
92
|
+
unless @skip_body
|
93
|
+
if @body.is_a?(String)
|
94
|
+
yield @body
|
95
|
+
else
|
96
|
+
@body.each { |chunk| yield chunk }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Tell the client the connection should stay open
|
102
|
+
def persistent!
|
103
|
+
@persistent = true
|
104
|
+
end
|
105
|
+
|
106
|
+
# Persistent connection must be requested as keep-alive
|
107
|
+
# from the server and have a Content-Length, or the response
|
108
|
+
# status must require that the connection remain open.
|
109
|
+
def persistent?
|
110
|
+
(@persistent && @headers.has_key?(CONTENT_LENGTH)) || PERSISTENT_STATUSES.include?(@status)
|
111
|
+
end
|
112
|
+
|
113
|
+
def skip_body!
|
114
|
+
@skip_body = true
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|