mongrel 1.1.2-java
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of mongrel might be problematic. Click here for more details.
- data.tar.gz.sig +1 -0
- data/CHANGELOG +12 -0
- data/COPYING +55 -0
- data/LICENSE +55 -0
- data/Manifest +69 -0
- data/README +74 -0
- data/TODO +5 -0
- data/bin/mongrel_rails +283 -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/mongrel_simple_ctrl.rb +92 -0
- data/examples/mongrel_simple_service.rb +116 -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/ext_help.h +14 -0
- data/ext/http11/extconf.rb +6 -0
- data/ext/http11/http11.c +402 -0
- data/ext/http11/http11_parser.c +1221 -0
- data/ext/http11/http11_parser.h +49 -0
- data/ext/http11/http11_parser.java.rl +170 -0
- data/ext/http11/http11_parser.rl +152 -0
- data/ext/http11/http11_parser_common.rl +54 -0
- data/ext/http11_java/Http11Service.java +13 -0
- data/ext/http11_java/org/jruby/mongrel/Http11.java +266 -0
- data/ext/http11_java/org/jruby/mongrel/Http11Parser.java +572 -0
- data/lib/http11.jar +0 -0
- data/lib/mongrel.rb +355 -0
- data/lib/mongrel/camping.rb +107 -0
- data/lib/mongrel/cgi.rb +181 -0
- data/lib/mongrel/command.rb +222 -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 +163 -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/mongrel-public_cert.pem +20 -0
- data/mongrel.gemspec +263 -0
- data/setup.rb +1585 -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 +103 -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 +115 -0
- data/test/testhelp.rb +66 -0
- data/tools/trickletest.rb +45 -0
- metadata +186 -0
- metadata.gz.sig +4 -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="1.1.2".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
|
+
|
12
|
+
module Mongrel
|
13
|
+
|
14
|
+
# You implement your application handler with this. It's very light giving
|
15
|
+
# just the minimum necessary for you to handle a request and shoot back
|
16
|
+
# a response. Look at the HttpRequest and HttpResponse objects for how
|
17
|
+
# to use them.
|
18
|
+
#
|
19
|
+
# This is used for very simple handlers that don't require much to operate.
|
20
|
+
# More extensive plugins or those you intend to distribute as GemPlugins
|
21
|
+
# should be implemented using the HttpHandlerPlugin mixin.
|
22
|
+
#
|
23
|
+
class HttpHandler
|
24
|
+
attr_reader :request_notify
|
25
|
+
attr_accessor :listener
|
26
|
+
|
27
|
+
# This will be called by Mongrel if HttpHandler.request_notify set to *true*.
|
28
|
+
# You only get the parameters for the request, with the idea that you'd "bound"
|
29
|
+
# the beginning of the request processing and the first call to process.
|
30
|
+
def request_begins(params)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Called by Mongrel for each IO chunk that is received on the request socket
|
34
|
+
# from the client, allowing you to track the progress of the IO and monitor
|
35
|
+
# the input. This will be called by Mongrel only if HttpHandler.request_notify
|
36
|
+
# set to *true*.
|
37
|
+
def request_progress(params, clen, total)
|
38
|
+
end
|
39
|
+
|
40
|
+
def process(request, response)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
# This is used when your handler is implemented as a GemPlugin.
|
47
|
+
# The plugin always takes an options hash which you can modify
|
48
|
+
# and then access later. They are stored by default for
|
49
|
+
# the process method later.
|
50
|
+
module HttpHandlerPlugin
|
51
|
+
attr_reader :options
|
52
|
+
attr_reader :request_notify
|
53
|
+
attr_accessor :listener
|
54
|
+
|
55
|
+
def request_begins(params)
|
56
|
+
end
|
57
|
+
|
58
|
+
def request_progress(params, clen, total)
|
59
|
+
end
|
60
|
+
|
61
|
+
def initialize(options={})
|
62
|
+
@options = options
|
63
|
+
@header_only = false
|
64
|
+
end
|
65
|
+
|
66
|
+
def process(request, response)
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
#
|
73
|
+
# The server normally returns a 404 response if an unknown URI is requested, but it
|
74
|
+
# also returns a lame empty message. This lets you do a 404 response
|
75
|
+
# with a custom message for special URIs.
|
76
|
+
#
|
77
|
+
class Error404Handler < HttpHandler
|
78
|
+
|
79
|
+
# Sets the message to return. This is constructed once for the handler
|
80
|
+
# so it's pretty efficient.
|
81
|
+
def initialize(msg)
|
82
|
+
@response = Const::ERROR_404_RESPONSE + msg
|
83
|
+
end
|
84
|
+
|
85
|
+
# Just kicks back the standard 404 response with your special message.
|
86
|
+
def process(request, response)
|
87
|
+
response.socket.write(@response)
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# Serves the contents of a directory. You give it the path to the root
|
94
|
+
# where the files are located, and it tries to find the files based on
|
95
|
+
# the PATH_INFO inside the directory. If the requested path is a
|
96
|
+
# directory then it returns a simple directory listing.
|
97
|
+
#
|
98
|
+
# It does a simple protection against going outside it's root path by
|
99
|
+
# converting all paths to an absolute expanded path, and then making
|
100
|
+
# sure that the final expanded path includes the root path. If it doesn't
|
101
|
+
# than it simply gives a 404.
|
102
|
+
#
|
103
|
+
# If you pass nil as the root path, it will not check any locations or
|
104
|
+
# expand any paths. This lets you serve files from multiple drives
|
105
|
+
# on win32.
|
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
|
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 : false
|
209
|
+
when modified_since && last_response_time > Time.now : false
|
210
|
+
when modified_since && mtime > last_response_time : false
|
211
|
+
when none_match && none_match == '*' : false
|
212
|
+
when none_match && !none_match.strip.split(/\s*,\s*/).include?(etag) : 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
|