rack-rack-contrib 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/COPYING +18 -0
- data/README.rdoc +74 -0
- data/Rakefile +97 -0
- data/lib/rack/contrib.rb +32 -0
- data/lib/rack/contrib/accept_format.rb +44 -0
- data/lib/rack/contrib/backstage.rb +20 -0
- data/lib/rack/contrib/bounce_favicon.rb +16 -0
- data/lib/rack/contrib/callbacks.rb +37 -0
- data/lib/rack/contrib/config.rb +16 -0
- data/lib/rack/contrib/csshttprequest.rb +39 -0
- data/lib/rack/contrib/deflect.rb +137 -0
- data/lib/rack/contrib/etag.rb +20 -0
- data/lib/rack/contrib/evil.rb +12 -0
- data/lib/rack/contrib/garbagecollector.rb +14 -0
- data/lib/rack/contrib/jsonp.rb +41 -0
- data/lib/rack/contrib/lighttpd_script_name_fix.rb +16 -0
- data/lib/rack/contrib/locale.rb +31 -0
- data/lib/rack/contrib/mailexceptions.rb +120 -0
- data/lib/rack/contrib/nested_params.rb +143 -0
- data/lib/rack/contrib/not_found.rb +18 -0
- data/lib/rack/contrib/post_body_content_type_parser.rb +40 -0
- data/lib/rack/contrib/proctitle.rb +30 -0
- data/lib/rack/contrib/profiler.rb +106 -0
- data/lib/rack/contrib/relative_redirect.rb +44 -0
- data/lib/rack/contrib/response_cache.rb +59 -0
- data/lib/rack/contrib/route_exceptions.rb +48 -0
- data/lib/rack/contrib/sendfile.rb +142 -0
- data/lib/rack/contrib/signals.rb +63 -0
- data/lib/rack/contrib/time_zone.rb +25 -0
- data/rack-contrib.gemspec +87 -0
- data/test/404.html +1 -0
- data/test/Maintenance.html +1 -0
- data/test/mail_settings.rb +12 -0
- data/test/spec_rack_accept_format.rb +41 -0
- data/test/spec_rack_backstage.rb +26 -0
- data/test/spec_rack_callbacks.rb +65 -0
- data/test/spec_rack_config.rb +22 -0
- data/test/spec_rack_contrib.rb +8 -0
- data/test/spec_rack_csshttprequest.rb +66 -0
- data/test/spec_rack_deflect.rb +107 -0
- data/test/spec_rack_etag.rb +23 -0
- data/test/spec_rack_evil.rb +19 -0
- data/test/spec_rack_garbagecollector.rb +13 -0
- data/test/spec_rack_jsonp.rb +34 -0
- data/test/spec_rack_lighttpd_script_name_fix.rb +16 -0
- data/test/spec_rack_mailexceptions.rb +97 -0
- data/test/spec_rack_nested_params.rb +46 -0
- data/test/spec_rack_not_found.rb +17 -0
- data/test/spec_rack_post_body_content_type_parser.rb +32 -0
- data/test/spec_rack_proctitle.rb +26 -0
- data/test/spec_rack_profiler.rb +37 -0
- data/test/spec_rack_relative_redirect.rb +78 -0
- data/test/spec_rack_response_cache.rb +137 -0
- data/test/spec_rack_sendfile.rb +86 -0
- metadata +171 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
# Automatically sets the ETag header on all String bodies
|
5
|
+
class ETag
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
status, headers, body = @app.call(env)
|
12
|
+
|
13
|
+
if !headers.has_key?('ETag') && body.is_a?(String)
|
14
|
+
headers['ETag'] = %("#{Digest::MD5.hexdigest(body)}")
|
15
|
+
end
|
16
|
+
|
17
|
+
[status, headers, body]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Rack
|
2
|
+
|
3
|
+
# A Rack middleware for providing JSON-P support.
|
4
|
+
#
|
5
|
+
# Full credit to Flinn Mueller (http://actsasflinn.com/) for this contribution.
|
6
|
+
#
|
7
|
+
class JSONP
|
8
|
+
|
9
|
+
def initialize(app)
|
10
|
+
@app = app
|
11
|
+
end
|
12
|
+
|
13
|
+
# Proxies the request to the application, stripping out the JSON-P callback
|
14
|
+
# method and padding the response with the appropriate callback format.
|
15
|
+
#
|
16
|
+
# Changes nothing if no <tt>callback</tt> param is specified.
|
17
|
+
#
|
18
|
+
def call(env)
|
19
|
+
status, headers, response = @app.call(env)
|
20
|
+
request = Rack::Request.new(env)
|
21
|
+
if request.params.include?('callback')
|
22
|
+
response = pad(request.params.delete('callback'), response)
|
23
|
+
headers['Content-Length'] = response.length.to_s
|
24
|
+
end
|
25
|
+
[status, headers, [response]]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Pads the response with the appropriate callback format according to the
|
29
|
+
# JSON-P spec/requirements.
|
30
|
+
#
|
31
|
+
# The Rack response spec indicates that it should be enumerable. The method
|
32
|
+
# of combining all of the data into a single string makes sense since JSON
|
33
|
+
# is returned as a full string.
|
34
|
+
#
|
35
|
+
def pad(callback, response, body = "")
|
36
|
+
response.each{ |s| body << s }
|
37
|
+
"#{callback}(#{body})"
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Rack
|
2
|
+
# Lighttpd sets the wrong SCRIPT_NAME and PATH_INFO if you mount your
|
3
|
+
# FastCGI app at "/". This middleware fixes this issue.
|
4
|
+
|
5
|
+
class LighttpdScriptNameFix
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
env["PATH_INFO"] = env["SCRIPT_NAME"].to_s + env["PATH_INFO"].to_s
|
12
|
+
env["SCRIPT_NAME"] = ""
|
13
|
+
@app.call(env)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'i18n'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class Locale
|
5
|
+
def initialize(app)
|
6
|
+
@app = app
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
old_locale = I18n.locale
|
11
|
+
locale = nil
|
12
|
+
|
13
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
|
14
|
+
if lang = env["HTTP_ACCEPT_LANGUAGE"]
|
15
|
+
lang = lang.split(",").map { |l|
|
16
|
+
l += ';q=1.0' unless l =~ /;q=\d+\.\d+$/
|
17
|
+
l.split(';q=')
|
18
|
+
}.first
|
19
|
+
locale = lang.first.split("-").first
|
20
|
+
else
|
21
|
+
locale = I18n.default_locale
|
22
|
+
end
|
23
|
+
|
24
|
+
locale = env['rack.locale'] = I18n.locale = locale.to_s
|
25
|
+
status, headers, body = @app.call(env)
|
26
|
+
headers['Content-Language'] = locale
|
27
|
+
I18n.locale = old_locale
|
28
|
+
[status, headers, body]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'net/smtp'
|
2
|
+
require 'tmail'
|
3
|
+
require 'erb'
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
# Catches all exceptions raised from the app it wraps and
|
7
|
+
# sends a useful email with the exception, stacktrace, and
|
8
|
+
# contents of the environment.
|
9
|
+
|
10
|
+
class MailExceptions
|
11
|
+
attr_reader :config
|
12
|
+
|
13
|
+
def initialize(app)
|
14
|
+
@app = app
|
15
|
+
@config = {
|
16
|
+
:to => nil,
|
17
|
+
:from => ENV['USER'] || 'rack',
|
18
|
+
:subject => '[exception] %s',
|
19
|
+
:smtp => {
|
20
|
+
:server => 'localhost',
|
21
|
+
:domain => 'localhost',
|
22
|
+
:port => 25,
|
23
|
+
:authentication => :login,
|
24
|
+
:user_name => nil,
|
25
|
+
:password => nil
|
26
|
+
}
|
27
|
+
}
|
28
|
+
@template = ERB.new(TEMPLATE)
|
29
|
+
yield self if block_given?
|
30
|
+
end
|
31
|
+
|
32
|
+
def call(env)
|
33
|
+
status, headers, body =
|
34
|
+
begin
|
35
|
+
@app.call(env)
|
36
|
+
rescue => boom
|
37
|
+
# TODO don't allow exceptions from send_notification to
|
38
|
+
# propogate
|
39
|
+
send_notification boom, env
|
40
|
+
raise
|
41
|
+
end
|
42
|
+
send_notification env['mail.exception'], env if env['mail.exception']
|
43
|
+
[status, headers, body]
|
44
|
+
end
|
45
|
+
|
46
|
+
%w[to from subject].each do |meth|
|
47
|
+
define_method(meth) { |value| @config[meth.to_sym] = value }
|
48
|
+
end
|
49
|
+
|
50
|
+
def smtp(settings={})
|
51
|
+
@config[:smtp].merge! settings
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
def generate_mail(exception, env)
|
56
|
+
mail = TMail::Mail.new
|
57
|
+
mail.to = Array(config[:to])
|
58
|
+
mail.from = config[:from]
|
59
|
+
mail.subject = config[:subject] % [exception.to_s]
|
60
|
+
mail.date = Time.now
|
61
|
+
mail.set_content_type 'text/plain'
|
62
|
+
mail.charset = 'UTF-8'
|
63
|
+
mail.body = @template.result(binding)
|
64
|
+
mail
|
65
|
+
end
|
66
|
+
|
67
|
+
def send_notification(exception, env)
|
68
|
+
mail = generate_mail(exception, env)
|
69
|
+
smtp = config[:smtp]
|
70
|
+
env['mail.sent'] = true
|
71
|
+
return if smtp[:server] == 'example.com'
|
72
|
+
|
73
|
+
Net::SMTP.start smtp[:address], smtp[:port], smtp[:domain], smtp[:user_name], smtp[:password], smtp[:authentication] do |server|
|
74
|
+
mail.to.each do |recipient|
|
75
|
+
server.send_message mail.to_s, mail.from, recipient
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def extract_body(env)
|
81
|
+
if io = env['rack.input']
|
82
|
+
io.rewind if io.respond_to?(:rewind)
|
83
|
+
io.read
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
TEMPLATE = (<<-'EMAIL').gsub(/^ {4}/, '')
|
88
|
+
A <%= exception.class.to_s %> occured: <%= exception.to_s %>
|
89
|
+
<% if body = extract_body(env) %>
|
90
|
+
|
91
|
+
===================================================================
|
92
|
+
Request Body:
|
93
|
+
===================================================================
|
94
|
+
|
95
|
+
<%= body.gsub(/^/, ' ') %>
|
96
|
+
<% end %>
|
97
|
+
|
98
|
+
===================================================================
|
99
|
+
Rack Environment:
|
100
|
+
===================================================================
|
101
|
+
|
102
|
+
PID: <%= $$ %>
|
103
|
+
PWD: <%= Dir.getwd %>
|
104
|
+
|
105
|
+
<%= env.to_a.
|
106
|
+
sort{|a,b| a.first <=> b.first}.
|
107
|
+
map{ |k,v| "%-25s%p" % [k+':', v] }.
|
108
|
+
join("\n ") %>
|
109
|
+
|
110
|
+
<% if exception.respond_to?(:backtrace) %>
|
111
|
+
===================================================================
|
112
|
+
Backtrace:
|
113
|
+
===================================================================
|
114
|
+
|
115
|
+
<%= exception.backtrace.join("\n ") %>
|
116
|
+
<% end %>
|
117
|
+
EMAIL
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'strscan'
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
# Rack middleware for parsing POST/PUT body data into nested parameters
|
6
|
+
class NestedParams
|
7
|
+
|
8
|
+
CONTENT_TYPE = 'CONTENT_TYPE'.freeze
|
9
|
+
POST_BODY = 'rack.input'.freeze
|
10
|
+
FORM_INPUT = 'rack.request.form_input'.freeze
|
11
|
+
FORM_HASH = 'rack.request.form_hash'.freeze
|
12
|
+
FORM_VARS = 'rack.request.form_vars'.freeze
|
13
|
+
|
14
|
+
# supported content type
|
15
|
+
URL_ENCODED = 'application/x-www-form-urlencoded'.freeze
|
16
|
+
|
17
|
+
def initialize(app)
|
18
|
+
@app = app
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(env)
|
22
|
+
if form_vars = env[FORM_VARS]
|
23
|
+
env[FORM_HASH] = parse_query_parameters(form_vars)
|
24
|
+
elsif env[CONTENT_TYPE] == URL_ENCODED
|
25
|
+
post_body = env[POST_BODY]
|
26
|
+
env[FORM_INPUT] = post_body
|
27
|
+
env[FORM_HASH] = parse_query_parameters(post_body.read)
|
28
|
+
post_body.rewind if post_body.respond_to?(:rewind)
|
29
|
+
end
|
30
|
+
@app.call(env)
|
31
|
+
end
|
32
|
+
|
33
|
+
## the rest is nabbed from Rails ##
|
34
|
+
|
35
|
+
def parse_query_parameters(query_string)
|
36
|
+
return {} if query_string.nil? or query_string.empty?
|
37
|
+
|
38
|
+
pairs = query_string.split('&').collect do |chunk|
|
39
|
+
next if chunk.empty?
|
40
|
+
key, value = chunk.split('=', 2)
|
41
|
+
next if key.empty?
|
42
|
+
value = value.nil? ? nil : CGI.unescape(value)
|
43
|
+
[ CGI.unescape(key), value ]
|
44
|
+
end.compact
|
45
|
+
|
46
|
+
UrlEncodedPairParser.new(pairs).result
|
47
|
+
end
|
48
|
+
|
49
|
+
class UrlEncodedPairParser < StringScanner
|
50
|
+
attr_reader :top, :parent, :result
|
51
|
+
|
52
|
+
def initialize(pairs = [])
|
53
|
+
super('')
|
54
|
+
@result = {}
|
55
|
+
pairs.each { |key, value| parse(key, value) }
|
56
|
+
end
|
57
|
+
|
58
|
+
KEY_REGEXP = %r{([^\[\]=&]+)}
|
59
|
+
BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]}
|
60
|
+
|
61
|
+
# Parse the query string
|
62
|
+
def parse(key, value)
|
63
|
+
self.string = key
|
64
|
+
@top, @parent = result, nil
|
65
|
+
|
66
|
+
# First scan the bare key
|
67
|
+
key = scan(KEY_REGEXP) or return
|
68
|
+
key = post_key_check(key)
|
69
|
+
|
70
|
+
# Then scan as many nestings as present
|
71
|
+
until eos?
|
72
|
+
r = scan(BRACKETED_KEY_REGEXP) or return
|
73
|
+
key = self[1]
|
74
|
+
key = post_key_check(key)
|
75
|
+
end
|
76
|
+
|
77
|
+
bind(key, value)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
# After we see a key, we must look ahead to determine our next action. Cases:
|
82
|
+
#
|
83
|
+
# [] follows the key. Then the value must be an array.
|
84
|
+
# = follows the key. (A value comes next)
|
85
|
+
# & or the end of string follows the key. Then the key is a flag.
|
86
|
+
# otherwise, a hash follows the key.
|
87
|
+
def post_key_check(key)
|
88
|
+
if scan(/\[\]/) # a[b][] indicates that b is an array
|
89
|
+
container(key, Array)
|
90
|
+
nil
|
91
|
+
elsif check(/\[[^\]]/) # a[b] indicates that a is a hash
|
92
|
+
container(key, Hash)
|
93
|
+
nil
|
94
|
+
else # End of key? We do nothing.
|
95
|
+
key
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Add a container to the stack.
|
100
|
+
def container(key, klass)
|
101
|
+
type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass)
|
102
|
+
value = bind(key, klass.new)
|
103
|
+
type_conflict! klass, value unless value.is_a?(klass)
|
104
|
+
push(value)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Push a value onto the 'stack', which is actually only the top 2 items.
|
108
|
+
def push(value)
|
109
|
+
@parent, @top = @top, value
|
110
|
+
end
|
111
|
+
|
112
|
+
# Bind a key (which may be nil for items in an array) to the provided value.
|
113
|
+
def bind(key, value)
|
114
|
+
if top.is_a? Array
|
115
|
+
if key
|
116
|
+
if top[-1].is_a?(Hash) && ! top[-1].key?(key)
|
117
|
+
top[-1][key] = value
|
118
|
+
else
|
119
|
+
top << {key => value}
|
120
|
+
end
|
121
|
+
push top.last
|
122
|
+
return top[key]
|
123
|
+
else
|
124
|
+
top << value
|
125
|
+
return value
|
126
|
+
end
|
127
|
+
elsif top.is_a? Hash
|
128
|
+
key = CGI.unescape(key)
|
129
|
+
parent << (@top = {}) if top.key?(key) && parent.is_a?(Array)
|
130
|
+
top[key] ||= value
|
131
|
+
return top[key]
|
132
|
+
else
|
133
|
+
raise ArgumentError, "Don't know what to do: top is #{top.inspect}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def type_conflict!(klass, value)
|
138
|
+
raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value. (The parameters received were #{value.inspect}.)"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Rack
|
2
|
+
# Rack::NotFound is a default endpoint. Initialize with the path to
|
3
|
+
# your 404 page.
|
4
|
+
|
5
|
+
class NotFound
|
6
|
+
F = ::File
|
7
|
+
|
8
|
+
def initialize(path)
|
9
|
+
file = F.expand_path(path)
|
10
|
+
@content = F.read(file)
|
11
|
+
@length = @content.size.to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
[404, {'Content-Type' => 'text/html', 'Content-Length' => @length}, [@content]]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
begin
|
2
|
+
require 'json'
|
3
|
+
rescue LoadError => e
|
4
|
+
require 'json/pure'
|
5
|
+
end
|
6
|
+
|
7
|
+
module Rack
|
8
|
+
|
9
|
+
# A Rack middleware for parsing POST/PUT body data when Content-Type is
|
10
|
+
# not one of the standard supported types, like <tt>application/json</tt>.
|
11
|
+
#
|
12
|
+
# TODO: Find a better name.
|
13
|
+
#
|
14
|
+
class PostBodyContentTypeParser
|
15
|
+
|
16
|
+
# Constants
|
17
|
+
#
|
18
|
+
CONTENT_TYPE = 'CONTENT_TYPE'.freeze
|
19
|
+
POST_BODY = 'rack.input'.freeze
|
20
|
+
FORM_INPUT = 'rack.request.form_input'.freeze
|
21
|
+
FORM_HASH = 'rack.request.form_hash'.freeze
|
22
|
+
|
23
|
+
# Supported Content-Types
|
24
|
+
#
|
25
|
+
APPLICATION_JSON = 'application/json'.freeze
|
26
|
+
|
27
|
+
def initialize(app)
|
28
|
+
@app = app
|
29
|
+
end
|
30
|
+
|
31
|
+
def call(env)
|
32
|
+
case env[CONTENT_TYPE]
|
33
|
+
when APPLICATION_JSON
|
34
|
+
env.update(FORM_HASH => JSON.parse(env[POST_BODY].read), FORM_INPUT => env[POST_BODY])
|
35
|
+
end
|
36
|
+
@app.call(env)
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|