mongrel 1.2.0.pre2-x86-mswin32
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +55 -0
- data/History.txt +68 -0
- data/LICENSE +55 -0
- data/Manifest.txt +69 -0
- data/README.txt +80 -0
- data/Rakefile +8 -0
- data/TODO +5 -0
- data/bin/mongrel_rails +284 -0
- data/examples/builder.rb +29 -0
- data/examples/camping/README +3 -0
- data/examples/camping/blog.rb +294 -0
- data/examples/camping/tepee.rb +149 -0
- data/examples/httpd.conf +474 -0
- data/examples/mime.yaml +3 -0
- data/examples/mongrel.conf +9 -0
- data/examples/monitrc +57 -0
- data/examples/random_thrash.rb +19 -0
- data/examples/simpletest.rb +52 -0
- data/examples/webrick_compare.rb +20 -0
- data/ext/http11/Http11Service.java +13 -0
- data/ext/http11/ext_help.h +15 -0
- data/ext/http11/extconf.rb +6 -0
- data/ext/http11/http11.c +534 -0
- data/ext/http11/http11_parser.c +1243 -0
- data/ext/http11/http11_parser.h +49 -0
- data/ext/http11/http11_parser.java.rl +159 -0
- data/ext/http11/http11_parser.rl +153 -0
- data/ext/http11/http11_parser_common.rl +54 -0
- data/ext/http11/org/jruby/mongrel/Http11.java +241 -0
- data/ext/http11/org/jruby/mongrel/Http11Parser.java +486 -0
- data/lib/1.8/http11.so +0 -0
- data/lib/1.9/http11.so +0 -0
- data/lib/mongrel.rb +366 -0
- data/lib/mongrel/camping.rb +107 -0
- data/lib/mongrel/cgi.rb +181 -0
- data/lib/mongrel/command.rb +220 -0
- data/lib/mongrel/configurator.rb +388 -0
- data/lib/mongrel/const.rb +110 -0
- data/lib/mongrel/debug.rb +203 -0
- data/lib/mongrel/gems.rb +22 -0
- data/lib/mongrel/handlers.rb +468 -0
- data/lib/mongrel/header_out.rb +28 -0
- data/lib/mongrel/http_request.rb +155 -0
- data/lib/mongrel/http_response.rb +166 -0
- data/lib/mongrel/init.rb +10 -0
- data/lib/mongrel/mime_types.yml +616 -0
- data/lib/mongrel/rails.rb +185 -0
- data/lib/mongrel/stats.rb +89 -0
- data/lib/mongrel/tcphack.rb +18 -0
- data/lib/mongrel/uri_classifier.rb +76 -0
- data/setup.rb +1585 -0
- data/tasks/gem.rake +28 -0
- data/tasks/native.rake +24 -0
- data/tasks/ragel.rake +20 -0
- data/test/mime.yaml +3 -0
- data/test/mongrel.conf +1 -0
- data/test/test_cgi_wrapper.rb +26 -0
- data/test/test_command.rb +86 -0
- data/test/test_conditional.rb +107 -0
- data/test/test_configurator.rb +87 -0
- data/test/test_debug.rb +25 -0
- data/test/test_handlers.rb +135 -0
- data/test/test_http11.rb +156 -0
- data/test/test_redirect_handler.rb +44 -0
- data/test/test_request_progress.rb +99 -0
- data/test/test_response.rb +127 -0
- data/test/test_stats.rb +35 -0
- data/test/test_uriclassifier.rb +261 -0
- data/test/test_ws.rb +117 -0
- data/test/testhelp.rb +71 -0
- data/tools/trickletest.rb +45 -0
- metadata +197 -0
@@ -0,0 +1,110 @@
|
|
1
|
+
|
2
|
+
module Mongrel
|
3
|
+
|
4
|
+
# Every standard HTTP code mapped to the appropriate message. These are
|
5
|
+
# used so frequently that they are placed directly in Mongrel for easy
|
6
|
+
# access rather than Mongrel::Const itself.
|
7
|
+
HTTP_STATUS_CODES = {
|
8
|
+
100 => 'Continue',
|
9
|
+
101 => 'Switching Protocols',
|
10
|
+
200 => 'OK',
|
11
|
+
201 => 'Created',
|
12
|
+
202 => 'Accepted',
|
13
|
+
203 => 'Non-Authoritative Information',
|
14
|
+
204 => 'No Content',
|
15
|
+
205 => 'Reset Content',
|
16
|
+
206 => 'Partial Content',
|
17
|
+
300 => 'Multiple Choices',
|
18
|
+
301 => 'Moved Permanently',
|
19
|
+
302 => 'Moved Temporarily',
|
20
|
+
303 => 'See Other',
|
21
|
+
304 => 'Not Modified',
|
22
|
+
305 => 'Use Proxy',
|
23
|
+
400 => 'Bad Request',
|
24
|
+
401 => 'Unauthorized',
|
25
|
+
402 => 'Payment Required',
|
26
|
+
403 => 'Forbidden',
|
27
|
+
404 => 'Not Found',
|
28
|
+
405 => 'Method Not Allowed',
|
29
|
+
406 => 'Not Acceptable',
|
30
|
+
407 => 'Proxy Authentication Required',
|
31
|
+
408 => 'Request Time-out',
|
32
|
+
409 => 'Conflict',
|
33
|
+
410 => 'Gone',
|
34
|
+
411 => 'Length Required',
|
35
|
+
412 => 'Precondition Failed',
|
36
|
+
413 => 'Request Entity Too Large',
|
37
|
+
414 => 'Request-URI Too Large',
|
38
|
+
415 => 'Unsupported Media Type',
|
39
|
+
500 => 'Internal Server Error',
|
40
|
+
501 => 'Not Implemented',
|
41
|
+
502 => 'Bad Gateway',
|
42
|
+
503 => 'Service Unavailable',
|
43
|
+
504 => 'Gateway Time-out',
|
44
|
+
505 => 'HTTP Version not supported'
|
45
|
+
}
|
46
|
+
|
47
|
+
# Frequently used constants when constructing requests or responses. Many times
|
48
|
+
# the constant just refers to a string with the same contents. Using these constants
|
49
|
+
# gave about a 3% to 10% performance improvement over using the strings directly.
|
50
|
+
# Symbols did not really improve things much compared to constants.
|
51
|
+
#
|
52
|
+
# While Mongrel does try to emulate the CGI/1.2 protocol, it does not use the REMOTE_IDENT,
|
53
|
+
# REMOTE_USER, or REMOTE_HOST parameters since those are either a security problem or
|
54
|
+
# too taxing on performance.
|
55
|
+
module Const
|
56
|
+
DATE = "Date".freeze
|
57
|
+
|
58
|
+
# This is the part of the path after the SCRIPT_NAME. URIClassifier will determine this.
|
59
|
+
PATH_INFO="PATH_INFO".freeze
|
60
|
+
|
61
|
+
# This is the initial part that your handler is identified as by URIClassifier.
|
62
|
+
SCRIPT_NAME="SCRIPT_NAME".freeze
|
63
|
+
|
64
|
+
# The original URI requested by the client. Passed to URIClassifier to build PATH_INFO and SCRIPT_NAME.
|
65
|
+
REQUEST_URI='REQUEST_URI'.freeze
|
66
|
+
REQUEST_PATH='REQUEST_PATH'.freeze
|
67
|
+
|
68
|
+
MONGREL_VERSION = VERSION = "1.2.0.pre2".freeze
|
69
|
+
|
70
|
+
MONGREL_TMP_BASE="mongrel".freeze
|
71
|
+
|
72
|
+
# The standard empty 404 response for bad requests. Use Error4040Handler for custom stuff.
|
73
|
+
ERROR_404_RESPONSE="HTTP/1.1 404 Not Found\r\nConnection: close\r\nServer: Mongrel #{MONGREL_VERSION}\r\n\r\nNOT FOUND".freeze
|
74
|
+
|
75
|
+
CONTENT_LENGTH="CONTENT_LENGTH".freeze
|
76
|
+
|
77
|
+
# A common header for indicating the server is too busy. Not used yet.
|
78
|
+
ERROR_503_RESPONSE="HTTP/1.1 503 Service Unavailable\r\n\r\nBUSY".freeze
|
79
|
+
|
80
|
+
# The basic max request size we'll try to read.
|
81
|
+
CHUNK_SIZE=(16 * 1024)
|
82
|
+
|
83
|
+
# This is the maximum header that is allowed before a client is booted. The parser detects
|
84
|
+
# this, but we'd also like to do this as well.
|
85
|
+
MAX_HEADER=1024 * (80 + 32)
|
86
|
+
|
87
|
+
# Maximum request body size before it is moved out of memory and into a tempfile for reading.
|
88
|
+
MAX_BODY=MAX_HEADER
|
89
|
+
|
90
|
+
# A frozen format for this is about 15% faster
|
91
|
+
STATUS_FORMAT = "HTTP/1.1 %d %s\r\nConnection: close\r\n".freeze
|
92
|
+
CONTENT_TYPE = "Content-Type".freeze
|
93
|
+
LAST_MODIFIED = "Last-Modified".freeze
|
94
|
+
ETAG = "ETag".freeze
|
95
|
+
SLASH = "/".freeze
|
96
|
+
REQUEST_METHOD="REQUEST_METHOD".freeze
|
97
|
+
GET="GET".freeze
|
98
|
+
HEAD="HEAD".freeze
|
99
|
+
# ETag is based on the apache standard of hex mtime-size-inode (inode is 0 on win32)
|
100
|
+
ETAG_FORMAT="\"%x-%x-%x\"".freeze
|
101
|
+
HEADER_FORMAT="%s: %s\r\n".freeze
|
102
|
+
LINE_END="\r\n".freeze
|
103
|
+
REMOTE_ADDR="REMOTE_ADDR".freeze
|
104
|
+
HTTP_X_FORWARDED_FOR="HTTP_X_FORWARDED_FOR".freeze
|
105
|
+
HTTP_IF_MODIFIED_SINCE="HTTP_IF_MODIFIED_SINCE".freeze
|
106
|
+
HTTP_IF_NONE_MATCH="HTTP_IF_NONE_MATCH".freeze
|
107
|
+
REDIRECT = "HTTP/1.1 302 Found\r\nLocation: %s\r\nConnection: close\r\n\r\n".freeze
|
108
|
+
HOST = "HOST".freeze
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
# Copyright (c) 2005 Zed A. Shaw
|
2
|
+
# You can redistribute it and/or modify it under the same terms as Ruby.
|
3
|
+
#
|
4
|
+
# Additional work donated by contributors. See http://mongrel.rubyforge.org/attributions.html
|
5
|
+
# for more information.
|
6
|
+
|
7
|
+
require 'logger'
|
8
|
+
require 'set'
|
9
|
+
require 'socket'
|
10
|
+
require 'fileutils'
|
11
|
+
|
12
|
+
module MongrelDbg
|
13
|
+
SETTINGS = { :tracing => {}}
|
14
|
+
LOGGING = { }
|
15
|
+
|
16
|
+
def MongrelDbg::configure(log_dir = File.join("log","mongrel_debug"))
|
17
|
+
FileUtils.mkdir_p(log_dir)
|
18
|
+
@log_dir = log_dir
|
19
|
+
$objects_out=open(File.join("log","mongrel_debug","objects.log"),"w")
|
20
|
+
$objects_out.puts "run,classname,last,count,delta,lenmean,lensd,lenmax"
|
21
|
+
$objects_out.sync = true
|
22
|
+
$last_stat = nil
|
23
|
+
$run_count = 0
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def MongrelDbg::trace(target, message)
|
28
|
+
if SETTINGS[:tracing][target] and LOGGING[target]
|
29
|
+
LOGGING[target].log(Logger::DEBUG, message)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def MongrelDbg::begin_trace(target)
|
34
|
+
SETTINGS[:tracing][target] = true
|
35
|
+
if not LOGGING[target]
|
36
|
+
LOGGING[target] = Logger.new(File.join(@log_dir, "#{target.to_s}.log"))
|
37
|
+
end
|
38
|
+
MongrelDbg::trace(target, "TRACING ON #{Time.now}")
|
39
|
+
end
|
40
|
+
|
41
|
+
def MongrelDbg::end_trace(target)
|
42
|
+
SETTINGS[:tracing][target] = false
|
43
|
+
MongrelDbg::trace(target, "TRACING OFF #{Time.now}")
|
44
|
+
LOGGING[target].close
|
45
|
+
LOGGING[target] = nil
|
46
|
+
end
|
47
|
+
|
48
|
+
def MongrelDbg::tracing?(target)
|
49
|
+
SETTINGS[:tracing][target]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
|
55
|
+
$open_files = {}
|
56
|
+
|
57
|
+
class IO
|
58
|
+
alias_method :orig_open, :open
|
59
|
+
alias_method :orig_close, :close
|
60
|
+
|
61
|
+
def open(*arg, &blk)
|
62
|
+
$open_files[self] = args.inspect
|
63
|
+
orig_open(*arg,&blk)
|
64
|
+
end
|
65
|
+
|
66
|
+
def close(*arg,&blk)
|
67
|
+
$open_files.delete self
|
68
|
+
orig_close(*arg,&blk)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
module Kernel
|
74
|
+
alias_method :orig_open, :open
|
75
|
+
|
76
|
+
def open(*arg, &blk)
|
77
|
+
$open_files[self] = arg[0]
|
78
|
+
orig_open(*arg,&blk)
|
79
|
+
end
|
80
|
+
|
81
|
+
def log_open_files
|
82
|
+
open_counts = {}
|
83
|
+
$open_files.each do |f,args|
|
84
|
+
open_counts[args] ||= 0
|
85
|
+
open_counts[args] += 1
|
86
|
+
end
|
87
|
+
MongrelDbg::trace(:files, open_counts.to_yaml)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
|
93
|
+
module RequestLog
|
94
|
+
|
95
|
+
# Just logs whatever requests it gets to STDERR (which ends up in the mongrel
|
96
|
+
# log when daemonized).
|
97
|
+
class Access < GemPlugin::Plugin "/handlers"
|
98
|
+
include Mongrel::HttpHandlerPlugin
|
99
|
+
|
100
|
+
def process(request,response)
|
101
|
+
p = request.params
|
102
|
+
STDERR.puts "#{p['REMOTE_ADDR']} - [#{Time.now.httpdate}] \"#{p['REQUEST_METHOD']} #{p["REQUEST_URI"]} HTTP/1.1\""
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
class Files < GemPlugin::Plugin "/handlers"
|
108
|
+
include Mongrel::HttpHandlerPlugin
|
109
|
+
|
110
|
+
def process(request, response)
|
111
|
+
MongrelDbg::trace(:files, "#{Time.now} FILES OPEN BEFORE REQUEST #{request.params['PATH_INFO']}")
|
112
|
+
log_open_files
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
# stolen from Robert Klemme
|
118
|
+
class Objects < GemPlugin::Plugin "/handlers"
|
119
|
+
include Mongrel::HttpHandlerPlugin
|
120
|
+
|
121
|
+
def process(request,response)
|
122
|
+
begin
|
123
|
+
stats = Hash.new(0)
|
124
|
+
lengths = {}
|
125
|
+
begin
|
126
|
+
ObjectSpace.each_object do |o|
|
127
|
+
begin
|
128
|
+
if o.respond_to? :length
|
129
|
+
len = o.length
|
130
|
+
lengths[o.class] ||= Mongrel::Stats.new(o.class)
|
131
|
+
lengths[o.class].sample(len)
|
132
|
+
end
|
133
|
+
rescue Object
|
134
|
+
end
|
135
|
+
|
136
|
+
stats[o.class] += 1
|
137
|
+
end
|
138
|
+
rescue Object # Ignore since ObjectSpace might not be loaded on JRuby
|
139
|
+
end
|
140
|
+
|
141
|
+
stats.sort {|(k1,v1),(k2,v2)| v2 <=> v1}.each do |k,v|
|
142
|
+
if $last_stat
|
143
|
+
delta = v - $last_stat[k]
|
144
|
+
if v > 10 and delta != 0
|
145
|
+
if lengths[k]
|
146
|
+
$objects_out.printf "%d,%s,%d,%d,%d,%f,%f,%f\n", $run_count, k, $last_stat[k], v, delta,lengths[k].mean,lengths[k].sd,lengths[k].max
|
147
|
+
else
|
148
|
+
$objects_out.printf "%d,%s,%d,%d,%d,,,\n", $run_count, k, $last_stat[k], v, delta
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
$run_count += 1
|
155
|
+
$last_stat = stats
|
156
|
+
rescue Object
|
157
|
+
STDERR.puts "object.log ERROR: #$!"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
class Params < GemPlugin::Plugin "/handlers"
|
163
|
+
include Mongrel::HttpHandlerPlugin
|
164
|
+
|
165
|
+
def process(request, response)
|
166
|
+
MongrelDbg::trace(:rails, "#{Time.now} REQUEST #{request.params['PATH_INFO']}")
|
167
|
+
MongrelDbg::trace(:rails, request.params.to_yaml)
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
class Threads < GemPlugin::Plugin "/handlers"
|
173
|
+
include Mongrel::HttpHandlerPlugin
|
174
|
+
|
175
|
+
def process(request, response)
|
176
|
+
MongrelDbg::trace(:threads, "#{Time.now} REQUEST #{request.params['PATH_INFO']}")
|
177
|
+
begin
|
178
|
+
ObjectSpace.each_object do |obj|
|
179
|
+
begin
|
180
|
+
if obj.class == Mongrel::HttpServer
|
181
|
+
worker_list = obj.workers.list
|
182
|
+
|
183
|
+
if worker_list.length > 0
|
184
|
+
keys = "-----\n\tKEYS:"
|
185
|
+
worker_list.each {|t| keys << "\n\t\t-- #{t}: #{t.keys.inspect}" }
|
186
|
+
end
|
187
|
+
|
188
|
+
MongrelDbg::trace(:threads, "#{obj.host}:#{obj.port} -- THREADS: #{worker_list.length} #{keys}")
|
189
|
+
end
|
190
|
+
rescue Object # Ignore since obj.class can sometimes take parameters
|
191
|
+
end
|
192
|
+
end
|
193
|
+
rescue Object # Ignore since ObjectSpace might not be loaded on JRuby
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
|
200
|
+
END {
|
201
|
+
MongrelDbg::trace(:files, "FILES OPEN AT EXIT")
|
202
|
+
log_open_files
|
203
|
+
}
|
data/lib/mongrel/gems.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
module Mongrel
|
2
|
+
module Gems
|
3
|
+
class << self
|
4
|
+
|
5
|
+
def require(library, version = nil)
|
6
|
+
begin
|
7
|
+
Kernel.require library
|
8
|
+
rescue LoadError, RuntimeError => e
|
9
|
+
begin
|
10
|
+
# ActiveSupport breaks 'require' by making it always return a true value
|
11
|
+
Kernel.require 'rubygems'
|
12
|
+
version ? gem(library, version) : gem(library)
|
13
|
+
retry
|
14
|
+
rescue Gem::LoadError, LoadError, RuntimeError
|
15
|
+
# puts "** #{library.inspect} could not be loaded" unless library == "mongrel_experimental"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,468 @@
|
|
1
|
+
# Copyright (c) 2005 Zed A. Shaw
|
2
|
+
# You can redistribute it and/or modify it under the same terms as Ruby.
|
3
|
+
#
|
4
|
+
# Additional work donated by contributors. See http://mongrel.rubyforge.org/attributions.html
|
5
|
+
# for more information.
|
6
|
+
|
7
|
+
require 'mongrel/stats'
|
8
|
+
require 'zlib'
|
9
|
+
require 'yaml'
|
10
|
+
|
11
|
+
module Mongrel
|
12
|
+
|
13
|
+
# You implement your application handler with this. It's very light giving
|
14
|
+
# just the minimum necessary for you to handle a request and shoot back
|
15
|
+
# a response. Look at the HttpRequest and HttpResponse objects for how
|
16
|
+
# to use them.
|
17
|
+
#
|
18
|
+
# This is used for very simple handlers that don't require much to operate.
|
19
|
+
# More extensive plugins or those you intend to distribute as GemPlugins
|
20
|
+
# should be implemented using the HttpHandlerPlugin mixin.
|
21
|
+
#
|
22
|
+
class HttpHandler
|
23
|
+
attr_reader :request_notify
|
24
|
+
attr_accessor :listener
|
25
|
+
|
26
|
+
# This will be called by Mongrel if HttpHandler.request_notify set to *true*.
|
27
|
+
# You only get the parameters for the request, with the idea that you'd "bound"
|
28
|
+
# the beginning of the request processing and the first call to process.
|
29
|
+
def request_begins(params)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Called by Mongrel for each IO chunk that is received on the request socket
|
33
|
+
# from the client, allowing you to track the progress of the IO and monitor
|
34
|
+
# the input. This will be called by Mongrel only if HttpHandler.request_notify
|
35
|
+
# set to *true*.
|
36
|
+
def request_progress(params, clen, total)
|
37
|
+
end
|
38
|
+
|
39
|
+
def process(request, response)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
# This is used when your handler is implemented as a GemPlugin.
|
46
|
+
# The plugin always takes an options hash which you can modify
|
47
|
+
# and then access later. They are stored by default for
|
48
|
+
# the process method later.
|
49
|
+
module HttpHandlerPlugin
|
50
|
+
attr_reader :options
|
51
|
+
attr_reader :request_notify
|
52
|
+
attr_accessor :listener
|
53
|
+
|
54
|
+
def request_begins(params)
|
55
|
+
end
|
56
|
+
|
57
|
+
def request_progress(params, clen, total)
|
58
|
+
end
|
59
|
+
|
60
|
+
def initialize(options={})
|
61
|
+
@options = options
|
62
|
+
@header_only = false
|
63
|
+
end
|
64
|
+
|
65
|
+
def process(request, response)
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
#
|
72
|
+
# The server normally returns a 404 response if an unknown URI is requested, but it
|
73
|
+
# also returns a lame empty message. This lets you do a 404 response
|
74
|
+
# with a custom message for special URIs.
|
75
|
+
#
|
76
|
+
class Error404Handler < HttpHandler
|
77
|
+
|
78
|
+
# Sets the message to return. This is constructed once for the handler
|
79
|
+
# so it's pretty efficient.
|
80
|
+
def initialize(msg)
|
81
|
+
@response = Const::ERROR_404_RESPONSE + msg
|
82
|
+
end
|
83
|
+
|
84
|
+
# Just kicks back the standard 404 response with your special message.
|
85
|
+
def process(request, response)
|
86
|
+
response.socket.write(@response)
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
#
|
92
|
+
# Serves the contents of a directory. You give it the path to the root
|
93
|
+
# where the files are located, and it tries to find the files based on
|
94
|
+
# the PATH_INFO inside the directory. If the requested path is a
|
95
|
+
# directory then it returns a simple directory listing.
|
96
|
+
#
|
97
|
+
# It does a simple protection against going outside it's root path by
|
98
|
+
# converting all paths to an absolute expanded path, and then making
|
99
|
+
# sure that the final expanded path includes the root path. If it doesn't
|
100
|
+
# than it simply gives a 404.
|
101
|
+
#
|
102
|
+
# If you pass nil as the root path, it will not check any locations or
|
103
|
+
# expand any paths. This lets you serve files from multiple drives
|
104
|
+
# on win32. It should probably not be used in a public-facing way
|
105
|
+
# without additional checks.
|
106
|
+
#
|
107
|
+
# The default content type is "text/plain; charset=ISO-8859-1" but you
|
108
|
+
# can change it anything you want using the DirHandler.default_content_type
|
109
|
+
# attribute.
|
110
|
+
#
|
111
|
+
class DirHandler < HttpHandler
|
112
|
+
attr_accessor :default_content_type
|
113
|
+
attr_reader :path
|
114
|
+
|
115
|
+
MIME_TYPES_FILE = "mime_types.yml"
|
116
|
+
MIME_TYPES = YAML.load_file(File.join(File.dirname(__FILE__), MIME_TYPES_FILE))
|
117
|
+
|
118
|
+
ONLY_HEAD_GET="Only HEAD and GET allowed.".freeze
|
119
|
+
|
120
|
+
# You give it the path to the directory root and and optional listing_allowed and index_html
|
121
|
+
def initialize(path, listing_allowed=true, index_html="index.html")
|
122
|
+
@path = File.expand_path(path) if path
|
123
|
+
@listing_allowed = listing_allowed
|
124
|
+
@index_html = index_html
|
125
|
+
@default_content_type = "application/octet-stream".freeze
|
126
|
+
end
|
127
|
+
|
128
|
+
# Checks if the given path can be served and returns the full path (or nil if not).
|
129
|
+
def can_serve(path_info)
|
130
|
+
|
131
|
+
req_path = HttpRequest.unescape(path_info)
|
132
|
+
# Add the drive letter or root path
|
133
|
+
req_path = File.join(@path, req_path) if @path
|
134
|
+
req_path = File.expand_path req_path
|
135
|
+
|
136
|
+
if File.exist? req_path and (!@path or req_path.index(@path) == 0)
|
137
|
+
# It exists and it's in the right location
|
138
|
+
if File.directory? req_path
|
139
|
+
# The request is for a directory
|
140
|
+
index = File.join(req_path, @index_html)
|
141
|
+
if File.exist? index
|
142
|
+
# Serve the index
|
143
|
+
return index
|
144
|
+
elsif @listing_allowed
|
145
|
+
# Serve the directory
|
146
|
+
return req_path
|
147
|
+
else
|
148
|
+
# Do not serve anything
|
149
|
+
return nil
|
150
|
+
end
|
151
|
+
else
|
152
|
+
# It's a file and it's there
|
153
|
+
return req_path
|
154
|
+
end
|
155
|
+
else
|
156
|
+
# does not exist or isn't in the right spot
|
157
|
+
return nil
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
|
162
|
+
# Returns a simplistic directory listing if they're enabled, otherwise a 403.
|
163
|
+
# Base is the base URI from the REQUEST_URI, dir is the directory to serve
|
164
|
+
# on the file system (comes from can_serve()), and response is the HttpResponse
|
165
|
+
# object to send the results on.
|
166
|
+
def send_dir_listing(base, dir, response)
|
167
|
+
# take off any trailing / so the links come out right
|
168
|
+
base = HttpRequest.unescape(base)
|
169
|
+
base.chop! if base[-1] == "/"[-1]
|
170
|
+
|
171
|
+
if @listing_allowed
|
172
|
+
response.start(200) do |head,out|
|
173
|
+
head[Const::CONTENT_TYPE] = "text/html"
|
174
|
+
out << "<html><head><title>Directory Listing</title></head><body>"
|
175
|
+
Dir.entries(dir).each do |child|
|
176
|
+
next if child == "."
|
177
|
+
out << "<a href=\"#{base}/#{ HttpRequest.escape(child)}\">"
|
178
|
+
out << (child == ".." ? "Up to parent.." : child)
|
179
|
+
out << "</a><br/>"
|
180
|
+
end
|
181
|
+
out << "</body></html>"
|
182
|
+
end
|
183
|
+
else
|
184
|
+
response.start(403) do |head,out|
|
185
|
+
out.write("Directory listings not allowed")
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
|
191
|
+
# Sends the contents of a file back to the user. Not terribly efficient since it's
|
192
|
+
# opening and closing the file for each read.
|
193
|
+
def send_file(req_path, request, response, header_only=false)
|
194
|
+
|
195
|
+
stat = File.stat(req_path)
|
196
|
+
|
197
|
+
# Set the last modified times as well and etag for all files
|
198
|
+
mtime = stat.mtime
|
199
|
+
# Calculated the same as apache, not sure how well the works on win32
|
200
|
+
etag = Const::ETAG_FORMAT % [mtime.to_i, stat.size, stat.ino]
|
201
|
+
|
202
|
+
modified_since = request.params[Const::HTTP_IF_MODIFIED_SINCE]
|
203
|
+
none_match = request.params[Const::HTTP_IF_NONE_MATCH]
|
204
|
+
|
205
|
+
# test to see if this is a conditional request, and test if
|
206
|
+
# the response would be identical to the last response
|
207
|
+
same_response = case
|
208
|
+
when modified_since && !last_response_time = Time.httpdate(modified_since) rescue nil then false
|
209
|
+
when modified_since && last_response_time > Time.now then false
|
210
|
+
when modified_since && mtime > last_response_time then false
|
211
|
+
when none_match && none_match == '*' then false
|
212
|
+
when none_match && !none_match.strip.split(/\s*,\s*/).include?(etag) then false
|
213
|
+
else modified_since || none_match # validation successful if we get this far and at least one of the header exists
|
214
|
+
end
|
215
|
+
|
216
|
+
header = response.header
|
217
|
+
header[Const::ETAG] = etag
|
218
|
+
|
219
|
+
if same_response
|
220
|
+
response.start(304) {}
|
221
|
+
else
|
222
|
+
|
223
|
+
# First we setup the headers and status then we do a very fast send on the socket directly
|
224
|
+
|
225
|
+
# Support custom responses except 404, which is the default. A little awkward.
|
226
|
+
response.status = 200 if response.status == 404
|
227
|
+
header[Const::LAST_MODIFIED] = mtime.httpdate
|
228
|
+
|
229
|
+
# Set the mime type from our map based on the ending
|
230
|
+
dot_at = req_path.rindex('.')
|
231
|
+
if dot_at
|
232
|
+
header[Const::CONTENT_TYPE] = MIME_TYPES[req_path[dot_at .. -1]] || @default_content_type
|
233
|
+
else
|
234
|
+
header[Const::CONTENT_TYPE] = @default_content_type
|
235
|
+
end
|
236
|
+
|
237
|
+
# send a status with out content length
|
238
|
+
response.send_status(stat.size)
|
239
|
+
response.send_header
|
240
|
+
|
241
|
+
if not header_only
|
242
|
+
response.send_file(req_path, stat.size < Const::CHUNK_SIZE * 2)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# Process the request to either serve a file or a directory listing
|
248
|
+
# if allowed (based on the listing_allowed parameter to the constructor).
|
249
|
+
def process(request, response)
|
250
|
+
req_method = request.params[Const::REQUEST_METHOD] || Const::GET
|
251
|
+
req_path = can_serve request.params[Const::PATH_INFO]
|
252
|
+
if not req_path
|
253
|
+
# not found, return a 404
|
254
|
+
response.start(404) do |head,out|
|
255
|
+
out << "File not found"
|
256
|
+
end
|
257
|
+
else
|
258
|
+
begin
|
259
|
+
if File.directory? req_path
|
260
|
+
send_dir_listing(request.params[Const::REQUEST_URI], req_path, response)
|
261
|
+
elsif req_method == Const::HEAD
|
262
|
+
send_file(req_path, request, response, true)
|
263
|
+
elsif req_method == Const::GET
|
264
|
+
send_file(req_path, request, response, false)
|
265
|
+
else
|
266
|
+
response.start(403) {|head,out| out.write(ONLY_HEAD_GET) }
|
267
|
+
end
|
268
|
+
rescue => details
|
269
|
+
STDERR.puts "Error sending file #{req_path}: #{details}"
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
# There is a small number of default mime types for extensions, but
|
275
|
+
# this lets you add any others you'll need when serving content.
|
276
|
+
def DirHandler::add_mime_type(extension, type)
|
277
|
+
MIME_TYPES[extension] = type
|
278
|
+
end
|
279
|
+
|
280
|
+
end
|
281
|
+
|
282
|
+
|
283
|
+
# When added to a config script (-S in mongrel_rails) it will
|
284
|
+
# look at the client's allowed response types and then gzip
|
285
|
+
# compress anything that is going out.
|
286
|
+
#
|
287
|
+
# Valid option is :always_deflate => false which tells the handler to
|
288
|
+
# deflate everything even if the client can't handle it.
|
289
|
+
class DeflateFilter < HttpHandler
|
290
|
+
include Zlib
|
291
|
+
HTTP_ACCEPT_ENCODING = "HTTP_ACCEPT_ENCODING"
|
292
|
+
|
293
|
+
def initialize(ops={})
|
294
|
+
@options = ops
|
295
|
+
@always_deflate = ops[:always_deflate] || false
|
296
|
+
end
|
297
|
+
|
298
|
+
def process(request, response)
|
299
|
+
accepts = request.params[HTTP_ACCEPT_ENCODING]
|
300
|
+
# only process if they support compression
|
301
|
+
if @always_deflate or (accepts and (accepts.include? "deflate" and not response.body_sent))
|
302
|
+
response.header["Content-Encoding"] = "deflate"
|
303
|
+
response.body = deflate(response.body)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
private
|
308
|
+
def deflate(stream)
|
309
|
+
deflater = Deflate.new(
|
310
|
+
DEFAULT_COMPRESSION,
|
311
|
+
# drop the zlib header which causes both Safari and IE to choke
|
312
|
+
-MAX_WBITS,
|
313
|
+
DEF_MEM_LEVEL,
|
314
|
+
DEFAULT_STRATEGY)
|
315
|
+
|
316
|
+
stream.rewind
|
317
|
+
gzout = StringIO.new(deflater.deflate(stream.read, FINISH))
|
318
|
+
stream.close
|
319
|
+
gzout.rewind
|
320
|
+
gzout
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
|
325
|
+
# Implements a few basic statistics for a particular URI. Register it anywhere
|
326
|
+
# you want in the request chain and it'll quickly gather some numbers for you
|
327
|
+
# to analyze. It is pretty fast, but don't put it out in production.
|
328
|
+
#
|
329
|
+
# You should pass the filter to StatusHandler as StatusHandler.new(:stats_filter => stats).
|
330
|
+
# This lets you then hit the status URI you want and get these stats from a browser.
|
331
|
+
#
|
332
|
+
# StatisticsFilter takes an option of :sample_rate. This is a number that's passed to
|
333
|
+
# rand and if that number gets hit then a sample is taken. This helps reduce the load
|
334
|
+
# and keeps the statistics valid (since sampling is a part of how they work).
|
335
|
+
#
|
336
|
+
# The exception to :sample_rate is that inter-request time is sampled on every request.
|
337
|
+
# If this wasn't done then it wouldn't be accurate as a measure of time between requests.
|
338
|
+
class StatisticsFilter < HttpHandler
|
339
|
+
attr_reader :stats
|
340
|
+
|
341
|
+
def initialize(ops={})
|
342
|
+
@sample_rate = ops[:sample_rate] || 300
|
343
|
+
|
344
|
+
@processors = Mongrel::Stats.new("processors")
|
345
|
+
@reqsize = Mongrel::Stats.new("request Kb")
|
346
|
+
@headcount = Mongrel::Stats.new("req param count")
|
347
|
+
@respsize = Mongrel::Stats.new("response Kb")
|
348
|
+
@interreq = Mongrel::Stats.new("inter-request time")
|
349
|
+
end
|
350
|
+
|
351
|
+
|
352
|
+
def process(request, response)
|
353
|
+
if rand(@sample_rate)+1 == @sample_rate
|
354
|
+
@processors.sample(listener.workers.list.length)
|
355
|
+
@headcount.sample(request.params.length)
|
356
|
+
@reqsize.sample(request.body.length / 1024.0)
|
357
|
+
@respsize.sample((response.body.length + response.header.out.length) / 1024.0)
|
358
|
+
end
|
359
|
+
@interreq.tick
|
360
|
+
end
|
361
|
+
|
362
|
+
def dump
|
363
|
+
"#{@processors.to_s}\n#{@reqsize.to_s}\n#{@headcount.to_s}\n#{@respsize.to_s}\n#{@interreq.to_s}"
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
|
368
|
+
# The :stats_filter is basically any configured stats filter that you've added to this same
|
369
|
+
# URI. This lets the status handler print out statistics on how Mongrel is doing.
|
370
|
+
class StatusHandler < HttpHandler
|
371
|
+
def initialize(ops={})
|
372
|
+
@stats = ops[:stats_filter]
|
373
|
+
end
|
374
|
+
|
375
|
+
def table(title, rows)
|
376
|
+
results = "<table border=\"1\"><tr><th colspan=\"#{rows[0].length}\">#{title}</th></tr>"
|
377
|
+
rows.each do |cols|
|
378
|
+
results << "<tr>"
|
379
|
+
cols.each {|col| results << "<td>#{col}</td>" }
|
380
|
+
results << "</tr>"
|
381
|
+
end
|
382
|
+
results + "</table>"
|
383
|
+
end
|
384
|
+
|
385
|
+
def describe_listener
|
386
|
+
results = ""
|
387
|
+
results << "<h1>Listener #{listener.host}:#{listener.port}</h1>"
|
388
|
+
results << table("settings", [
|
389
|
+
["host",listener.host],
|
390
|
+
["port",listener.port],
|
391
|
+
["throttle",listener.throttle],
|
392
|
+
["timeout",listener.timeout],
|
393
|
+
["workers max",listener.num_processors],
|
394
|
+
])
|
395
|
+
|
396
|
+
if @stats
|
397
|
+
results << "<h2>Statistics</h2><p>N means the number of samples, pay attention to MEAN, SD, MIN and MAX."
|
398
|
+
results << "<pre>#{@stats.dump}</pre>"
|
399
|
+
end
|
400
|
+
|
401
|
+
results << "<h2>Registered Handlers</h2>"
|
402
|
+
handler_map = listener.classifier.handler_map
|
403
|
+
results << table("handlers", handler_map.map {|uri,handlers|
|
404
|
+
[uri,
|
405
|
+
"<pre>" +
|
406
|
+
handlers.map {|h| h.class.to_s }.join("\n") +
|
407
|
+
"</pre>"
|
408
|
+
]
|
409
|
+
})
|
410
|
+
|
411
|
+
results
|
412
|
+
end
|
413
|
+
|
414
|
+
def process(request, response)
|
415
|
+
response.start do |head,out|
|
416
|
+
out.write <<-END
|
417
|
+
<html><body><title>Mongrel Server Status</title>
|
418
|
+
#{describe_listener}
|
419
|
+
</body></html>
|
420
|
+
END
|
421
|
+
end
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
# This handler allows you to redirect one url to another.
|
426
|
+
# You can use it like String#gsub, where the string is the REQUEST_URI.
|
427
|
+
# REQUEST_URI is the full path with GET parameters.
|
428
|
+
#
|
429
|
+
# Eg. /test/something?help=true&disclaimer=false
|
430
|
+
#
|
431
|
+
# == Examples
|
432
|
+
#
|
433
|
+
# h = Mongrel::HttpServer.new('0.0.0.0')
|
434
|
+
# h.register '/test', Mongrel::RedirectHandler.new('/to/there') # simple
|
435
|
+
# h.register '/to', Mongrel::RedirectHandler.new(/t/, 'w') # regexp
|
436
|
+
# # and with a block
|
437
|
+
# h.register '/hey', Mongrel::RedirectHandler.new(/(\w+)/) { |match| ... }
|
438
|
+
#
|
439
|
+
class RedirectHandler < Mongrel::HttpHandler
|
440
|
+
# You set the rewrite rules when building the object.
|
441
|
+
#
|
442
|
+
# pattern => What to look for or replacement if used alone
|
443
|
+
#
|
444
|
+
# replacement, block => One of them is used to replace the found text
|
445
|
+
|
446
|
+
def initialize(pattern, replacement = nil, &block)
|
447
|
+
unless replacement or block
|
448
|
+
@pattern, @replacement = nil, pattern
|
449
|
+
else
|
450
|
+
@pattern, @replacement, @block = pattern, replacement, block
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
# Process the request and return a redirect response
|
455
|
+
def process(request, response)
|
456
|
+
unless @pattern
|
457
|
+
response.socket.write(Mongrel::Const::REDIRECT % @replacement)
|
458
|
+
else
|
459
|
+
if @block
|
460
|
+
new_path = request.params['REQUEST_URI'].gsub(@pattern, &@block)
|
461
|
+
else
|
462
|
+
new_path = request.params['REQUEST_URI'].gsub(@pattern, @replacement)
|
463
|
+
end
|
464
|
+
response.socket.write(Mongrel::Const::REDIRECT % new_path)
|
465
|
+
end
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|