freels-mongrel 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/CHANGELOG +12 -0
  2. data/COPYING +55 -0
  3. data/LICENSE +55 -0
  4. data/Manifest +70 -0
  5. data/README +74 -0
  6. data/Rakefile +237 -0
  7. data/TODO +4 -0
  8. data/bin/mongrel_rails +284 -0
  9. data/examples/builder.rb +29 -0
  10. data/examples/camping/README +3 -0
  11. data/examples/camping/blog.rb +294 -0
  12. data/examples/camping/tepee.rb +149 -0
  13. data/examples/httpd.conf +474 -0
  14. data/examples/mime.yaml +3 -0
  15. data/examples/mongrel.conf +9 -0
  16. data/examples/mongrel_simple_ctrl.rb +92 -0
  17. data/examples/mongrel_simple_service.rb +116 -0
  18. data/examples/monitrc +57 -0
  19. data/examples/random_thrash.rb +19 -0
  20. data/examples/simpletest.rb +52 -0
  21. data/examples/webrick_compare.rb +20 -0
  22. data/ext/http11/ext_help.h +15 -0
  23. data/ext/http11/extconf.rb +6 -0
  24. data/ext/http11/http11.c +527 -0
  25. data/ext/http11/http11_parser.c +1216 -0
  26. data/ext/http11/http11_parser.h +49 -0
  27. data/ext/http11/http11_parser.java.rl +171 -0
  28. data/ext/http11/http11_parser.rl +165 -0
  29. data/ext/http11/http11_parser_common.rl +55 -0
  30. data/ext/http11_java/Http11Service.java +13 -0
  31. data/ext/http11_java/org/jruby/mongrel/Http11.java +266 -0
  32. data/ext/http11_java/org/jruby/mongrel/Http11Parser.java +572 -0
  33. data/lib/mongrel.rb +359 -0
  34. data/lib/mongrel/camping.rb +107 -0
  35. data/lib/mongrel/cgi.rb +182 -0
  36. data/lib/mongrel/command.rb +220 -0
  37. data/lib/mongrel/configurator.rb +389 -0
  38. data/lib/mongrel/const.rb +114 -0
  39. data/lib/mongrel/debug.rb +203 -0
  40. data/lib/mongrel/gems.rb +22 -0
  41. data/lib/mongrel/handlers.rb +472 -0
  42. data/lib/mongrel/header_out.rb +28 -0
  43. data/lib/mongrel/http_request.rb +155 -0
  44. data/lib/mongrel/http_response.rb +163 -0
  45. data/lib/mongrel/init.rb +10 -0
  46. data/lib/mongrel/logger.rb +74 -0
  47. data/lib/mongrel/mime_types.yml +616 -0
  48. data/lib/mongrel/rails.rb +185 -0
  49. data/lib/mongrel/stats.rb +89 -0
  50. data/lib/mongrel/tcphack.rb +18 -0
  51. data/lib/mongrel/uri_classifier.rb +76 -0
  52. data/mongrel-public_cert.pem +20 -0
  53. data/mongrel.gemspec +47 -0
  54. data/setup.rb +1585 -0
  55. data/test/mime.yaml +3 -0
  56. data/test/mongrel.conf +1 -0
  57. data/test/test_cgi_wrapper.rb +26 -0
  58. data/test/test_command.rb +86 -0
  59. data/test/test_conditional.rb +107 -0
  60. data/test/test_configurator.rb +88 -0
  61. data/test/test_debug.rb +25 -0
  62. data/test/test_handlers.rb +104 -0
  63. data/test/test_http11.rb +272 -0
  64. data/test/test_redirect_handler.rb +44 -0
  65. data/test/test_request_progress.rb +100 -0
  66. data/test/test_response.rb +127 -0
  67. data/test/test_stats.rb +35 -0
  68. data/test/test_uriclassifier.rb +261 -0
  69. data/test/test_ws.rb +116 -0
  70. data/test/testhelp.rb +74 -0
  71. data/tools/trickletest.rb +45 -0
  72. metadata +202 -0
@@ -0,0 +1,114 @@
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.2".freeze
69
+
70
+ MONGREL_TMP_BASE="mongrel".freeze
71
+
72
+ # A standard 400 response for a request which generates a http parse exception
73
+ ERROR_400_RESPONSE="HTTP/1.1 400 Bad Request\r\nConnection: close\r\nServer: Mongrel #{MONGREL_VERSION}\r\n\r\nBAD REQUEST".freeze
74
+
75
+ # The standard empty 404 response for bad requests. Use Error4040Handler for custom stuff.
76
+ ERROR_404_RESPONSE="HTTP/1.1 404 Not Found\r\nConnection: close\r\nServer: Mongrel #{MONGREL_VERSION}\r\n\r\nNOT FOUND".freeze
77
+
78
+ CONTENT_LENGTH="CONTENT_LENGTH".freeze
79
+
80
+ # A common header for indicating the server is too busy. Not used yet.
81
+ ERROR_503_RESPONSE="HTTP/1.1 503 Service Unavailable\r\n\r\nBUSY".freeze
82
+
83
+ # The basic max request size we'll try to read.
84
+ CHUNK_SIZE=(16 * 1024)
85
+
86
+ # This is the maximum header that is allowed before a client is booted. The parser detects
87
+ # this, but we'd also like to do this as well.
88
+ MAX_HEADER=1024 * (80 + 32)
89
+
90
+ # Maximum request body size before it is moved out of memory and into a tempfile for reading.
91
+ MAX_BODY=MAX_HEADER
92
+
93
+ # A frozen format for this is about 15% faster
94
+ STATUS_FORMAT = "HTTP/1.1 %d %s\r\nConnection: close\r\n".freeze
95
+ CONTENT_TYPE = "Content-Type".freeze
96
+ LAST_MODIFIED = "Last-Modified".freeze
97
+ ETAG = "ETag".freeze
98
+ SLASH = "/".freeze
99
+ REQUEST_METHOD="REQUEST_METHOD".freeze
100
+ GET="GET".freeze
101
+ HEAD="HEAD".freeze
102
+ # ETag is based on the apache standard of hex mtime-size-inode (inode is 0 on win32)
103
+ ETAG_FORMAT="\"%x-%x-%x\"".freeze
104
+ HEADER_FORMAT="%s: %s\r\n".freeze
105
+ LINE_END="\r\n".freeze
106
+ REMOTE_ADDR="REMOTE_ADDR".freeze
107
+ HTTP_X_FORWARDED_FOR="HTTP_X_FORWARDED_FOR".freeze
108
+ HTTP_IF_MODIFIED_SINCE="HTTP_IF_MODIFIED_SINCE".freeze
109
+ HTTP_IF_NONE_MATCH="HTTP_IF_NONE_MATCH".freeze
110
+ REDIRECT = "HTTP/1.1 302 Found\r\nLocation: %s\r\nConnection: close\r\n\r\n".freeze
111
+ HOST = "HOST".freeze
112
+ end
113
+
114
+ 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, "#{Time.now.httpdate}: TRACING ON")
39
+ end
40
+
41
+ def MongrelDbg::end_trace(target)
42
+ SETTINGS[:tracing][target] = false
43
+ MongrelDbg::trace(target, "#{Time.now.httpdate}: TRACING OFF")
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
+ module RequestLog
93
+
94
+ class Access < GemPlugin::Plugin "/handlers"
95
+ include Mongrel::HttpHandlerPlugin
96
+
97
+ def process(request,response)
98
+ p = request.params
99
+ Mongrel.log("#{p['REMOTE_ADDR']} \"#{p['REQUEST_METHOD']} #{p["REQUEST_URI"]} HTTP/1.1\"")
100
+ end
101
+ end
102
+
103
+
104
+ class Files < GemPlugin::Plugin "/handlers"
105
+ include Mongrel::HttpHandlerPlugin
106
+
107
+ def process(request, response)
108
+ MongrelDbg::trace(:files, "#{Time.now.httpdate}: FILES OPEN BEFORE REQUEST #{request.params['PATH_INFO']}")
109
+ log_open_files
110
+ end
111
+
112
+ end
113
+
114
+
115
+ # stolen from Robert Klemme
116
+ class Objects < GemPlugin::Plugin "/handlers"
117
+ include Mongrel::HttpHandlerPlugin
118
+
119
+ def process(request,response)
120
+ begin
121
+ stats = Hash.new(0)
122
+ lengths = {}
123
+ begin
124
+ ObjectSpace.each_object do |o|
125
+ begin
126
+ if o.respond_to?(:length)
127
+ len = o.length
128
+ lengths[o.class] ||= Mongrel::Stats.new(o.class)
129
+ lengths[o.class].sample(len)
130
+ end
131
+ rescue Object
132
+ end
133
+
134
+ stats[o.class] += 1
135
+ end
136
+ rescue Object # Ignore since ObjectSpace might not be loaded on JRuby
137
+ end
138
+
139
+ stats.sort {|(k1,v1),(k2,v2)| v2 <=> v1}.each do |k,v|
140
+ if $last_stat
141
+ delta = v - $last_stat[k]
142
+ if v > 10 and delta != 0
143
+ if lengths[k]
144
+ $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
145
+ else
146
+ $objects_out.printf "%d,%s,%d,%d,%d,,,\n", $run_count, k, $last_stat[k], v, delta
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ $run_count += 1
153
+ $last_stat = stats
154
+ rescue Object
155
+ STDERR.puts "#{Time.now.httpdate}: object.log ERROR: #$!"
156
+ end
157
+ end
158
+ end
159
+
160
+
161
+ class Params < GemPlugin::Plugin "/handlers"
162
+ include Mongrel::HttpHandlerPlugin
163
+
164
+ def process(request, response)
165
+ MongrelDbg::trace(:rails, "#{Time.now.httpdate}: REQUEST #{request.params['PATH_INFO']}")
166
+ MongrelDbg::trace(:rails, request.params.to_yaml)
167
+ end
168
+
169
+ end
170
+
171
+
172
+ class Threads < GemPlugin::Plugin "/handlers"
173
+ include Mongrel::HttpHandlerPlugin
174
+
175
+ def process(request, response)
176
+ MongrelDbg::trace(:threads, "#{Time.now.httpdate}: 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, "#{Time.now.httpdate}: #{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, "#{Time.now.httpdate}: FILES OPEN AT EXIT")
202
+ log_open_files
203
+ }
@@ -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,472 @@
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
+ # do not remove the check for @path at the beginning, it's what prevents
137
+ # the serving of arbitrary files (and good programmer Rule #1 Says: If
138
+ # you don't understand something, it's not because I'm stupid, it's
139
+ # because you are).
140
+ if req_path.index(@path) == 0 and File.exist? req_path
141
+ # It exists and it's in the right location
142
+ if File.directory? req_path
143
+ # The request is for a directory
144
+ index = File.join(req_path, @index_html)
145
+ if File.exist? index
146
+ # Serve the index
147
+ return index
148
+ elsif @listing_allowed
149
+ # Serve the directory
150
+ return req_path
151
+ else
152
+ # Do not serve anything
153
+ return nil
154
+ end
155
+ else
156
+ # It's a file and it's there
157
+ return req_path
158
+ end
159
+ else
160
+ # does not exist or isn't in the right spot or isn't valid because not start with @path
161
+ return nil
162
+ end
163
+ end
164
+
165
+
166
+ # Returns a simplistic directory listing if they're enabled, otherwise a 403.
167
+ # Base is the base URI from the REQUEST_URI, dir is the directory to serve
168
+ # on the file system (comes from can_serve()), and response is the HttpResponse
169
+ # object to send the results on.
170
+ def send_dir_listing(base, dir, response)
171
+ # take off any trailing / so the links come out right
172
+ base = HttpRequest.unescape(base)
173
+ base.chop! if base[-1] == "/"[-1]
174
+
175
+ if @listing_allowed
176
+ response.start(200) do |head,out|
177
+ head[Const::CONTENT_TYPE] = "text/html"
178
+ out << "<html><head><title>Directory Listing</title></head><body>"
179
+ Dir.entries(dir).each do |child|
180
+ next if child == "."
181
+ out << "<a href=\"#{base}/#{ HttpRequest.escape(child)}\">"
182
+ out << (child == ".." ? "Up to parent.." : child)
183
+ out << "</a><br/>"
184
+ end
185
+ out << "</body></html>"
186
+ end
187
+ else
188
+ response.start(403) do |head,out|
189
+ out.write("Directory listings not allowed")
190
+ end
191
+ end
192
+ end
193
+
194
+
195
+ # Sends the contents of a file back to the user. Not terribly efficient since it's
196
+ # opening and closing the file for each read.
197
+ def send_file(req_path, request, response, header_only=false)
198
+
199
+ stat = File.stat(req_path)
200
+
201
+ # Set the last modified times as well and etag for all files
202
+ mtime = stat.mtime
203
+ # Calculated the same as apache, not sure how well the works on win32
204
+ etag = Const::ETAG_FORMAT % [mtime.to_i, stat.size, stat.ino]
205
+
206
+ modified_since = request.params[Const::HTTP_IF_MODIFIED_SINCE]
207
+ none_match = request.params[Const::HTTP_IF_NONE_MATCH]
208
+
209
+ # test to see if this is a conditional request, and test if
210
+ # the response would be identical to the last response
211
+ same_response = case
212
+ when modified_since && !last_response_time = Time.httpdate(modified_since) rescue nil then false
213
+ when modified_since && last_response_time > Time.now then false
214
+ when modified_since && mtime > last_response_time then false
215
+ when none_match && none_match == '*' then false
216
+ when none_match && !none_match.strip.split(/\s*,\s*/).include?(etag) then false
217
+ else modified_since || none_match # validation successful if we get this far and at least one of the header exists
218
+ end
219
+
220
+ header = response.header
221
+ header[Const::ETAG] = etag
222
+
223
+ if same_response
224
+ response.start(304) {}
225
+ else
226
+
227
+ # First we setup the headers and status then we do a very fast send on the socket directly
228
+
229
+ # Support custom responses except 404, which is the default. A little awkward.
230
+ response.status = 200 if response.status == 404
231
+ header[Const::LAST_MODIFIED] = mtime.httpdate
232
+
233
+ # Set the mime type from our map based on the ending
234
+ dot_at = req_path.rindex('.')
235
+ if dot_at
236
+ header[Const::CONTENT_TYPE] = MIME_TYPES[req_path[dot_at .. -1]] || @default_content_type
237
+ else
238
+ header[Const::CONTENT_TYPE] = @default_content_type
239
+ end
240
+
241
+ # send a status with out content length
242
+ response.send_status(stat.size)
243
+ response.send_header
244
+
245
+ if not header_only
246
+ response.send_file(req_path, stat.size < Const::CHUNK_SIZE * 2)
247
+ end
248
+ end
249
+ end
250
+
251
+ # Process the request to either serve a file or a directory listing
252
+ # if allowed (based on the listing_allowed parameter to the constructor).
253
+ def process(request, response)
254
+ req_method = request.params[Const::REQUEST_METHOD] || Const::GET
255
+ req_path = can_serve request.params[Const::PATH_INFO]
256
+ if not req_path
257
+ # not found, return a 404
258
+ response.start(404) do |head,out|
259
+ out << "File not found"
260
+ end
261
+ else
262
+ begin
263
+ if File.directory? req_path
264
+ send_dir_listing(request.params[Const::REQUEST_URI], req_path, response)
265
+ elsif req_method == Const::HEAD
266
+ send_file(req_path, request, response, true)
267
+ elsif req_method == Const::GET
268
+ send_file(req_path, request, response, false)
269
+ else
270
+ response.start(403) {|head,out| out.write(ONLY_HEAD_GET) }
271
+ end
272
+ rescue => details
273
+ STDERR.puts "#{Time.now.httpdate}: Error sending file #{req_path}: #{details}"
274
+ end
275
+ end
276
+ end
277
+
278
+ # There is a small number of default mime types for extensions, but
279
+ # this lets you add any others you'll need when serving content.
280
+ def DirHandler::add_mime_type(extension, type)
281
+ MIME_TYPES[extension] = type
282
+ end
283
+
284
+ end
285
+
286
+
287
+ # When added to a config script (-S in mongrel_rails) it will
288
+ # look at the client's allowed response types and then gzip
289
+ # compress anything that is going out.
290
+ #
291
+ # Valid option is :always_deflate => false which tells the handler to
292
+ # deflate everything even if the client can't handle it.
293
+ class DeflateFilter < HttpHandler
294
+ include Zlib
295
+ HTTP_ACCEPT_ENCODING = "HTTP_ACCEPT_ENCODING"
296
+
297
+ def initialize(ops={})
298
+ @options = ops
299
+ @always_deflate = ops[:always_deflate] || false
300
+ end
301
+
302
+ def process(request, response)
303
+ accepts = request.params[HTTP_ACCEPT_ENCODING]
304
+ # only process if they support compression
305
+ if @always_deflate or (accepts and (accepts.include? "deflate" and not response.body_sent))
306
+ response.header["Content-Encoding"] = "deflate"
307
+ response.body = deflate(response.body)
308
+ end
309
+ end
310
+
311
+ private
312
+ def deflate(stream)
313
+ deflater = Deflate.new(
314
+ DEFAULT_COMPRESSION,
315
+ # drop the zlib header which causes both Safari and IE to choke
316
+ -MAX_WBITS,
317
+ DEF_MEM_LEVEL,
318
+ DEFAULT_STRATEGY)
319
+
320
+ stream.rewind
321
+ gzout = StringIO.new(deflater.deflate(stream.read, FINISH))
322
+ stream.close
323
+ gzout.rewind
324
+ gzout
325
+ end
326
+ end
327
+
328
+
329
+ # Implements a few basic statistics for a particular URI. Register it anywhere
330
+ # you want in the request chain and it'll quickly gather some numbers for you
331
+ # to analyze. It is pretty fast, but don't put it out in production.
332
+ #
333
+ # You should pass the filter to StatusHandler as StatusHandler.new(:stats_filter => stats).
334
+ # This lets you then hit the status URI you want and get these stats from a browser.
335
+ #
336
+ # StatisticsFilter takes an option of :sample_rate. This is a number that's passed to
337
+ # rand and if that number gets hit then a sample is taken. This helps reduce the load
338
+ # and keeps the statistics valid (since sampling is a part of how they work).
339
+ #
340
+ # The exception to :sample_rate is that inter-request time is sampled on every request.
341
+ # If this wasn't done then it wouldn't be accurate as a measure of time between requests.
342
+ class StatisticsFilter < HttpHandler
343
+ attr_reader :stats
344
+
345
+ def initialize(ops={})
346
+ @sample_rate = ops[:sample_rate] || 300
347
+
348
+ @processors = Mongrel::Stats.new("processors")
349
+ @reqsize = Mongrel::Stats.new("request Kb")
350
+ @headcount = Mongrel::Stats.new("req param count")
351
+ @respsize = Mongrel::Stats.new("response Kb")
352
+ @interreq = Mongrel::Stats.new("inter-request time")
353
+ end
354
+
355
+
356
+ def process(request, response)
357
+ if rand(@sample_rate)+1 == @sample_rate
358
+ @processors.sample(listener.workers.list.length)
359
+ @headcount.sample(request.params.length)
360
+ @reqsize.sample(request.body.length / 1024.0)
361
+ @respsize.sample((response.body.length + response.header.out.length) / 1024.0)
362
+ end
363
+ @interreq.tick
364
+ end
365
+
366
+ def dump
367
+ "#{@processors.to_s}\n#{@reqsize.to_s}\n#{@headcount.to_s}\n#{@respsize.to_s}\n#{@interreq.to_s}"
368
+ end
369
+ end
370
+
371
+
372
+ # The :stats_filter is basically any configured stats filter that you've added to this same
373
+ # URI. This lets the status handler print out statistics on how Mongrel is doing.
374
+ class StatusHandler < HttpHandler
375
+ def initialize(ops={})
376
+ @stats = ops[:stats_filter]
377
+ end
378
+
379
+ def table(title, rows)
380
+ results = "<table border=\"1\"><tr><th colspan=\"#{rows[0].length}\">#{title}</th></tr>"
381
+ rows.each do |cols|
382
+ results << "<tr>"
383
+ cols.each {|col| results << "<td>#{col}</td>" }
384
+ results << "</tr>"
385
+ end
386
+ results + "</table>"
387
+ end
388
+
389
+ def describe_listener
390
+ results = ""
391
+ results << "<h1>Listener #{listener.host}:#{listener.port}</h1>"
392
+ results << table("settings", [
393
+ ["host",listener.host],
394
+ ["port",listener.port],
395
+ ["throttle",listener.throttle],
396
+ ["timeout",listener.timeout],
397
+ ["workers max",listener.num_processors],
398
+ ])
399
+
400
+ if @stats
401
+ results << "<h2>Statistics</h2><p>N means the number of samples, pay attention to MEAN, SD, MIN and MAX."
402
+ results << "<pre>#{@stats.dump}</pre>"
403
+ end
404
+
405
+ results << "<h2>Registered Handlers</h2>"
406
+ handler_map = listener.classifier.handler_map
407
+ results << table("handlers", handler_map.map {|uri,handlers|
408
+ [uri,
409
+ "<pre>" +
410
+ handlers.map {|h| h.class.to_s }.join("\n") +
411
+ "</pre>"
412
+ ]
413
+ })
414
+
415
+ results
416
+ end
417
+
418
+ def process(request, response)
419
+ response.start do |head,out|
420
+ out.write <<-END
421
+ <html><body><title>Mongrel Server Status</title>
422
+ #{describe_listener}
423
+ </body></html>
424
+ END
425
+ end
426
+ end
427
+ end
428
+
429
+ # This handler allows you to redirect one url to another.
430
+ # You can use it like String#gsub, where the string is the REQUEST_URI.
431
+ # REQUEST_URI is the full path with GET parameters.
432
+ #
433
+ # Eg. /test/something?help=true&disclaimer=false
434
+ #
435
+ # == Examples
436
+ #
437
+ # h = Mongrel::HttpServer.new('0.0.0.0')
438
+ # h.register '/test', Mongrel::RedirectHandler.new('/to/there') # simple
439
+ # h.register '/to', Mongrel::RedirectHandler.new(/t/, 'w') # regexp
440
+ # # and with a block
441
+ # h.register '/hey', Mongrel::RedirectHandler.new(/(\w+)/) { |match| ... }
442
+ #
443
+ class RedirectHandler < Mongrel::HttpHandler
444
+ # You set the rewrite rules when building the object.
445
+ #
446
+ # pattern => What to look for or replacement if used alone
447
+ #
448
+ # replacement, block => One of them is used to replace the found text
449
+
450
+ def initialize(pattern, replacement = nil, &block)
451
+ unless replacement or block
452
+ @pattern, @replacement = nil, pattern
453
+ else
454
+ @pattern, @replacement, @block = pattern, replacement, block
455
+ end
456
+ end
457
+
458
+ # Process the request and return a redirect response
459
+ def process(request, response)
460
+ unless @pattern
461
+ response.socket.write(Mongrel::Const::REDIRECT % @replacement)
462
+ else
463
+ if @block
464
+ new_path = request.params['REQUEST_URI'].gsub(@pattern, &@block)
465
+ else
466
+ new_path = request.params['REQUEST_URI'].gsub(@pattern, @replacement)
467
+ end
468
+ response.socket.write(Mongrel::Const::REDIRECT % new_path)
469
+ end
470
+ end
471
+ end
472
+ end