corntrace-rack-contrib 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +26 -0
- data/COPYING +18 -0
- data/README.rdoc +87 -0
- data/Rakefile +89 -0
- data/lib/rack/contrib/accept_format.rb +46 -0
- data/lib/rack/contrib/access.rb +85 -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/cookies.rb +50 -0
- data/lib/rack/contrib/csshttprequest.rb +39 -0
- data/lib/rack/contrib/deflect.rb +137 -0
- data/lib/rack/contrib/evil.rb +12 -0
- data/lib/rack/contrib/expectation_cascade.rb +32 -0
- data/lib/rack/contrib/garbagecollector.rb +14 -0
- data/lib/rack/contrib/host_meta.rb +47 -0
- data/lib/rack/contrib/jsonp.rb +78 -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 +108 -0
- data/lib/rack/contrib/relative_redirect.rb +44 -0
- data/lib/rack/contrib/response_cache.rb +59 -0
- data/lib/rack/contrib/response_headers.rb +24 -0
- data/lib/rack/contrib/route_exceptions.rb +49 -0
- data/lib/rack/contrib/runtime.rb +31 -0
- data/lib/rack/contrib/sendfile.rb +142 -0
- data/lib/rack/contrib/signals.rb +63 -0
- data/lib/rack/contrib/simple_endpoint.rb +81 -0
- data/lib/rack/contrib/static_cache.rb +93 -0
- data/lib/rack/contrib/time_zone.rb +25 -0
- data/lib/rack/contrib.rb +39 -0
- data/rack-contrib.gemspec +104 -0
- data/test/404.html +1 -0
- data/test/Maintenance.html +1 -0
- data/test/documents/test +1 -0
- data/test/mail_settings.rb +12 -0
- data/test/spec_rack_accept_format.rb +72 -0
- data/test/spec_rack_access.rb +154 -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_cookies.rb +56 -0
- data/test/spec_rack_csshttprequest.rb +66 -0
- data/test/spec_rack_deflect.rb +107 -0
- data/test/spec_rack_evil.rb +19 -0
- data/test/spec_rack_expectation_cascade.rb +72 -0
- data/test/spec_rack_garbagecollector.rb +13 -0
- data/test/spec_rack_host_meta.rb +50 -0
- data/test/spec_rack_jsonp.rb +83 -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_response_headers.rb +35 -0
- data/test/spec_rack_runtime.rb +35 -0
- data/test/spec_rack_sendfile.rb +86 -0
- data/test/spec_rack_simple_endpoint.rb +95 -0
- data/test/spec_rack_static_cache.rb +91 -0
- data/test/statics/test +1 -0
- metadata +234 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
module Rack
|
2
|
+
class ExpectationCascade
|
3
|
+
Expect = "Expect".freeze
|
4
|
+
ContinueExpectation = "100-continue".freeze
|
5
|
+
|
6
|
+
ExpectationFailed = [417, {"Content-Type" => "text/html"}, []].freeze
|
7
|
+
NotFound = [404, {"Content-Type" => "text/html"}, []].freeze
|
8
|
+
|
9
|
+
attr_reader :apps
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@apps = []
|
13
|
+
yield self if block_given?
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(env)
|
17
|
+
set_expectation = env[Expect] != ContinueExpectation
|
18
|
+
env[Expect] = ContinueExpectation if set_expectation
|
19
|
+
@apps.each do |app|
|
20
|
+
result = app.call(env)
|
21
|
+
return result unless result[0].to_i == 417
|
22
|
+
end
|
23
|
+
set_expectation ? NotFound : ExpectationFailed
|
24
|
+
ensure
|
25
|
+
env.delete(Expect) if set_expectation
|
26
|
+
end
|
27
|
+
|
28
|
+
def <<(app)
|
29
|
+
@apps << app
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Rack
|
2
|
+
|
3
|
+
# Rack middleware implementing the IETF draft: "Host Metadata for the Web"
|
4
|
+
# including support for Link-Pattern elements as described in the IETF draft:
|
5
|
+
# "Link-based Resource Descriptor Discovery."
|
6
|
+
#
|
7
|
+
# Usage:
|
8
|
+
# use Rack::HostMeta do
|
9
|
+
# link :uri => '/robots.txt', :rel => 'robots'
|
10
|
+
# link :uri => '/w3c/p3p.xml', :rel => 'privacy', :type => 'application/p3p.xml'
|
11
|
+
# link :pattern => '{uri};json_schema', :rel => 'describedby', :type => 'application/x-schema+json'
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# See also:
|
15
|
+
# http://tools.ietf.org/html/draft-nottingham-site-meta
|
16
|
+
# http://tools.ietf.org/html/draft-hammer-discovery
|
17
|
+
#
|
18
|
+
# TODO:
|
19
|
+
# Accept POST operations allowing downstream services to register themselves
|
20
|
+
#
|
21
|
+
class HostMeta
|
22
|
+
def initialize(app, &block)
|
23
|
+
@app = app
|
24
|
+
@lines = []
|
25
|
+
instance_eval(&block)
|
26
|
+
@response = @lines.join("\n")
|
27
|
+
end
|
28
|
+
|
29
|
+
def call(env)
|
30
|
+
if env['PATH_INFO'] == '/host-meta'
|
31
|
+
[200, {'Content-Type' => 'application/host-meta'}, [@response]]
|
32
|
+
else
|
33
|
+
@app.call(env)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
def link(config)
|
40
|
+
line = config[:uri] ? "Link: <#{config[:uri]}>;" : "Link-Pattern: <#{config[:pattern]}>;"
|
41
|
+
fragments = []
|
42
|
+
fragments << "rel=\"#{config[:rel]}\"" if config[:rel]
|
43
|
+
fragments << "type=\"#{config[:type]}\"" if config[:type]
|
44
|
+
@lines << "#{line} #{fragments.join("; ")}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,78 @@
|
|
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
|
+
include Rack::Utils
|
9
|
+
|
10
|
+
def initialize(app)
|
11
|
+
@app = app
|
12
|
+
end
|
13
|
+
|
14
|
+
# Proxies the request to the application, stripping out the JSON-P callback
|
15
|
+
# method and padding the response with the appropriate callback format if
|
16
|
+
# the returned body is application/json
|
17
|
+
#
|
18
|
+
# Changes nothing if no <tt>callback</tt> param is specified.
|
19
|
+
#
|
20
|
+
def call(env)
|
21
|
+
status, headers, response = @app.call(env)
|
22
|
+
|
23
|
+
headers = HeaderHash.new(headers)
|
24
|
+
request = Rack::Request.new(env)
|
25
|
+
|
26
|
+
if is_json?(headers) && has_callback?(request)
|
27
|
+
status = translate_error_code!(status, response)
|
28
|
+
response = pad(request.params.delete('callback'), response)
|
29
|
+
|
30
|
+
# No longer json, its javascript!
|
31
|
+
headers['Content-Type'].gsub!('json', 'javascript')
|
32
|
+
|
33
|
+
# Set new Content-Length, if it was set before we mutated the response body
|
34
|
+
if headers['Content-Length']
|
35
|
+
length = response.to_ary.inject(0) { |len, part| len + bytesize(part) }
|
36
|
+
headers['Content-Length'] = length.to_s
|
37
|
+
end
|
38
|
+
end
|
39
|
+
[status, headers, response]
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def is_json?(headers)
|
45
|
+
headers['Content-Type'].include?('application/json')
|
46
|
+
end
|
47
|
+
|
48
|
+
def has_callback?(request)
|
49
|
+
request.params.include?('callback')
|
50
|
+
end
|
51
|
+
|
52
|
+
# Pads the response with the appropriate callback format according to the
|
53
|
+
# JSON-P spec/requirements.
|
54
|
+
#
|
55
|
+
# The Rack response spec indicates that it should be enumerable. The
|
56
|
+
# method of combining all of the data into a single string makes sense
|
57
|
+
# since JSON is returned as a full string.
|
58
|
+
#
|
59
|
+
def pad(callback, response, body = "")
|
60
|
+
response.each{ |s| body << s.to_s }
|
61
|
+
["#{callback}(#{body})"]
|
62
|
+
end
|
63
|
+
|
64
|
+
# Translate status code 40x and 50x to a json string in response, and change
|
65
|
+
# status code to 200. That because the jsonp's implemetation could not handler
|
66
|
+
# http status code when error occured, which could be meaningful in webservice.
|
67
|
+
def translate_error_code!(status, response)
|
68
|
+
if status % 400 == 4 || status % 500 == 5
|
69
|
+
response.slice(1..-1) # keep the first element, delete the others
|
70
|
+
response[0] = %Q|{"errorCode":#{status}}|
|
71
|
+
status = 200
|
72
|
+
else
|
73
|
+
status
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
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[:server], 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
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Rack
|
2
|
+
# Middleware to update the process title ($0) with information about the
|
3
|
+
# current request. Based loosely on:
|
4
|
+
# - http://purefiction.net/mongrel_proctitle/
|
5
|
+
# - http://github.com/grempe/thin-proctitle/tree/master
|
6
|
+
#
|
7
|
+
# NOTE: This will not work properly in a multi-threaded environment.
|
8
|
+
class ProcTitle
|
9
|
+
F = ::File
|
10
|
+
PROGNAME = F.basename($0)
|
11
|
+
|
12
|
+
def initialize(app)
|
13
|
+
@app = app
|
14
|
+
@appname = Dir.pwd.split('/').reverse.
|
15
|
+
find { |name| name !~ /^(\d+|current|releases)$/ } || PROGNAME
|
16
|
+
@requests = 0
|
17
|
+
$0 = "#{PROGNAME} [#{@appname}] init ..."
|
18
|
+
end
|
19
|
+
|
20
|
+
def call(env)
|
21
|
+
host, port = env['SERVER_NAME'], env['SERVER_PORT']
|
22
|
+
meth, path = env['REQUEST_METHOD'], env['PATH_INFO']
|
23
|
+
@requests += 1
|
24
|
+
$0 = "#{PROGNAME} [#{@appname}/#{port}] (#{@requests}) " \
|
25
|
+
"#{meth} #{path}"
|
26
|
+
|
27
|
+
@app.call(env)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'ruby-prof'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
# Set the profile=process_time query parameter to download a
|
5
|
+
# calltree profile of the request.
|
6
|
+
#
|
7
|
+
# Pass the :printer option to pick a different result format.
|
8
|
+
class Profiler
|
9
|
+
MODES = %w(
|
10
|
+
process_time
|
11
|
+
wall_time
|
12
|
+
cpu_time
|
13
|
+
allocations
|
14
|
+
memory
|
15
|
+
gc_runs
|
16
|
+
gc_time
|
17
|
+
)
|
18
|
+
|
19
|
+
DEFAULT_PRINTER = RubyProf::CallTreePrinter
|
20
|
+
DEFAULT_CONTENT_TYPE = 'application/octet-stream'
|
21
|
+
|
22
|
+
PRINTER_CONTENT_TYPE = {
|
23
|
+
RubyProf::FlatPrinter => 'text/plain',
|
24
|
+
RubyProf::GraphPrinter => 'text/plain',
|
25
|
+
RubyProf::GraphHtmlPrinter => 'text/html'
|
26
|
+
}
|
27
|
+
|
28
|
+
# Accepts a :printer => [:call_tree|:graph_html|:graph|:flat] option
|
29
|
+
# defaulting to :call_tree.
|
30
|
+
def initialize(app, options = {})
|
31
|
+
@app = app
|
32
|
+
@printer = parse_printer(options[:printer])
|
33
|
+
@times = (options[:times] || 1).to_i
|
34
|
+
end
|
35
|
+
|
36
|
+
def call(env)
|
37
|
+
if mode = profiling?(env)
|
38
|
+
profile(env, mode)
|
39
|
+
else
|
40
|
+
@app.call(env)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def profiling?(env)
|
46
|
+
unless RubyProf.running?
|
47
|
+
request = Rack::Request.new(env.clone)
|
48
|
+
if mode = request.params.delete('profile')
|
49
|
+
if RubyProf.const_defined?(mode.upcase)
|
50
|
+
mode
|
51
|
+
else
|
52
|
+
env['rack.errors'].write "Invalid RubyProf measure_mode: " +
|
53
|
+
"#{mode}. Use one of #{MODES.to_a.join(', ')}"
|
54
|
+
false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def profile(env, mode)
|
61
|
+
RubyProf.measure_mode = RubyProf.const_get(mode.upcase)
|
62
|
+
|
63
|
+
GC.enable_stats if GC.respond_to?(:enable_stats)
|
64
|
+
result = RubyProf.profile do
|
65
|
+
@times.times { @app.call(env) }
|
66
|
+
end
|
67
|
+
GC.disable_stats if GC.respond_to?(:disable_stats)
|
68
|
+
|
69
|
+
[200, headers(@printer, env, mode), print(@printer, result)]
|
70
|
+
end
|
71
|
+
|
72
|
+
def print(printer, result)
|
73
|
+
body = StringIO.new
|
74
|
+
printer.new(result).print(body, :min_percent => 0.01)
|
75
|
+
body.rewind
|
76
|
+
body
|
77
|
+
end
|
78
|
+
|
79
|
+
def headers(printer, env, mode)
|
80
|
+
headers = { 'Content-Type' => PRINTER_CONTENT_TYPE[printer] || DEFAULT_CONTENT_TYPE }
|
81
|
+
if printer == RubyProf::CallTreePrinter
|
82
|
+
filename = ::File.basename(env['PATH_INFO'])
|
83
|
+
headers['Content-Disposition'] =
|
84
|
+
%(attachment; filename="#{filename}.#{mode}.tree")
|
85
|
+
end
|
86
|
+
headers
|
87
|
+
end
|
88
|
+
|
89
|
+
def parse_printer(printer)
|
90
|
+
if printer.nil?
|
91
|
+
DEFAULT_PRINTER
|
92
|
+
elsif printer.is_a?(Class)
|
93
|
+
printer
|
94
|
+
else
|
95
|
+
name = "#{camel_case(printer)}Printer"
|
96
|
+
if RubyProf.const_defined?(name)
|
97
|
+
RubyProf.const_get(name)
|
98
|
+
else
|
99
|
+
DEFAULT_PRINTER
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def camel_case(word)
|
105
|
+
word.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|