rack-contrib 0.9.0 → 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of rack-contrib might be problematic. Click here for more details.
- data/README.rdoc +16 -0
- data/Rakefile +6 -1
- data/lib/rack/contrib.rb +10 -1
- data/lib/rack/contrib/accept_format.rb +46 -0
- data/lib/rack/contrib/backstage.rb +20 -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/jsonp.rb +18 -15
- data/lib/rack/contrib/not_found.rb +18 -0
- data/lib/rack/contrib/post_body_content_type_parser.rb +10 -10
- 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 +20 -19
- data/lib/rack/contrib/signals.rb +63 -0
- data/rack-contrib.gemspec +22 -3
- data/test/404.html +1 -0
- data/test/Maintenance.html +1 -0
- data/test/spec_rack_accept_format.rb +72 -0
- data/test/spec_rack_backstage.rb +26 -0
- data/test/spec_rack_callbacks.rb +2 -2
- data/test/spec_rack_config.rb +22 -0
- data/test/spec_rack_csshttprequest.rb +66 -0
- data/test/spec_rack_deflect.rb +107 -0
- data/test/spec_rack_jsonp.rb +23 -10
- data/test/spec_rack_lighttpd_script_name_fix.rb +1 -1
- data/test/spec_rack_not_found.rb +17 -0
- data/test/spec_rack_post_body_content_type_parser.rb +5 -5
- data/test/spec_rack_profiler.rb +31 -26
- data/test/spec_rack_relative_redirect.rb +78 -0
- data/test/spec_rack_response_cache.rb +137 -0
- metadata +30 -3
data/README.rdoc
CHANGED
@@ -23,10 +23,26 @@ interface:
|
|
23
23
|
* Rack::Profiler - Uses ruby-prof to measure request time.
|
24
24
|
* Rack::Sendfile - Enables X-Sendfile support for bodies that can be served
|
25
25
|
from file.
|
26
|
+
* Rack::Signals - Installs signal handlers that are safely processed after
|
27
|
+
a request
|
26
28
|
* Rack::TimeZone - Detects the clients timezone using JavaScript and sets
|
27
29
|
a variable in Rack's environment with the offset from UTC.
|
28
30
|
* Rack::Evil - Lets the rack application return a response to the client from any place.
|
29
31
|
* Rack::Callbacks - Implements DLS for pure before/after filter like Middlewares.
|
32
|
+
* Rack::Config - Shared configuration for cooperative middleware.
|
33
|
+
* Rack::NotFound - A default 404 application.
|
34
|
+
* Rack::CSSHTTPRequest - Adds CSSHTTPRequest support by encoding responses as
|
35
|
+
CSS for cross-site AJAX-style data loading
|
36
|
+
* Rack::Deflect - Helps protect against DoS attacks.
|
37
|
+
* Rack::ResponseCache - Caches responses to requests without query strings
|
38
|
+
to Disk or a user provider Ruby object. Similar to Rails' page caching.
|
39
|
+
* Rack::RelativeRedirect - Transforms relative paths in redirects to
|
40
|
+
absolute URLs.
|
41
|
+
* Rack::Backstage - Returns content of specified file if it exists, which makes
|
42
|
+
it convenient for putting up maintenance pages.
|
43
|
+
* Rack::AcceptFormat - Adds a format extension at the end of the URI when there is none, corresponding to the mime-type given in the Accept HTTP header.
|
44
|
+
* Rack::HostMeta - Configures /host-meta using a block
|
45
|
+
* Rack::Cookies - Adds simple cookie jar hash to env
|
30
46
|
|
31
47
|
=== Use
|
32
48
|
|
data/Rakefile
CHANGED
@@ -10,11 +10,16 @@ task "RDOX" do
|
|
10
10
|
sh "specrb -Ilib:test -a --rdox >RDOX"
|
11
11
|
end
|
12
12
|
|
13
|
-
desc "Run
|
13
|
+
desc "Run specs with test/unit style output"
|
14
14
|
task :test do
|
15
15
|
sh "specrb -Ilib:test -w #{ENV['TEST'] || '-a'} #{ENV['TESTOPTS']}"
|
16
16
|
end
|
17
17
|
|
18
|
+
desc "Run specs with specdoc style output"
|
19
|
+
task :spec do
|
20
|
+
sh "specrb -Ilib:test -s -w #{ENV['TEST'] || '-a'} #{ENV['TESTOPTS']}"
|
21
|
+
end
|
22
|
+
|
18
23
|
desc "Run all the tests"
|
19
24
|
task :fulltest do
|
20
25
|
sh "specrb -Ilib:test -w #{ENV['TEST'] || '-a'} #{ENV['TESTOPTS']}"
|
data/lib/rack/contrib.rb
CHANGED
@@ -3,11 +3,15 @@ require 'rack'
|
|
3
3
|
module Rack
|
4
4
|
module Contrib
|
5
5
|
def self.release
|
6
|
-
"0.9"
|
6
|
+
"0.9.1"
|
7
7
|
end
|
8
8
|
end
|
9
9
|
|
10
|
+
autoload :AcceptFormat, "rack/contrib/accept_format"
|
10
11
|
autoload :BounceFavicon, "rack/contrib/bounce_favicon"
|
12
|
+
autoload :Cookies, "rack/contrib/cookies"
|
13
|
+
autoload :CSSHTTPRequest, "rack/contrib/csshttprequest"
|
14
|
+
autoload :Deflect, "rack/contrib/deflect"
|
11
15
|
autoload :ETag, "rack/contrib/etag"
|
12
16
|
autoload :GarbageCollector, "rack/contrib/garbagecollector"
|
13
17
|
autoload :JSONP, "rack/contrib/jsonp"
|
@@ -18,8 +22,13 @@ module Rack
|
|
18
22
|
autoload :ProcTitle, "rack/contrib/proctitle"
|
19
23
|
autoload :Profiler, "rack/contrib/profiler"
|
20
24
|
autoload :Sendfile, "rack/contrib/sendfile"
|
25
|
+
autoload :Signals, "rack/contrib/signals"
|
21
26
|
autoload :TimeZone, "rack/contrib/time_zone"
|
22
27
|
autoload :Evil, "rack/contrib/evil"
|
23
28
|
autoload :Callbacks, "rack/contrib/callbacks"
|
24
29
|
autoload :NestedParams, "rack/contrib/nested_params"
|
30
|
+
autoload :Config, "rack/contrib/config"
|
31
|
+
autoload :NotFound, "rack/contrib/not_found"
|
32
|
+
autoload :ResponseCache, "rack/contrib/response_cache"
|
33
|
+
autoload :RelativeRedirect, "rack/contrib/relative_redirect"
|
25
34
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Rack
|
2
|
+
#
|
3
|
+
# A Rack middleware for automatically adding a <tt>format</tt> token at the end of the request path
|
4
|
+
# when there is none. It can detect formats passed in the HTTP_ACCEPT header to populate this token.
|
5
|
+
#
|
6
|
+
# e.g.:
|
7
|
+
# GET /some/resource HTTP/1.1
|
8
|
+
# Accept: application/json
|
9
|
+
# ->
|
10
|
+
# GET /some/resource.json HTTP/1.1
|
11
|
+
# Accept: application/json
|
12
|
+
#
|
13
|
+
# You can add custom types with this kind of function (taken from sinatra):
|
14
|
+
# def mime(ext, type)
|
15
|
+
# ext = ".#{ext}" unless ext.to_s[0] == ?.
|
16
|
+
# Rack::Mime::MIME_TYPES[ext.to_s] = type
|
17
|
+
# end
|
18
|
+
# and then:
|
19
|
+
# mime :json, 'application/json'
|
20
|
+
#
|
21
|
+
# Note: it does not take into account multiple media types in the Accept header.
|
22
|
+
# The first media type takes precedence over all the others.
|
23
|
+
#
|
24
|
+
# MIT-License - Cyril Rohr
|
25
|
+
#
|
26
|
+
class AcceptFormat
|
27
|
+
|
28
|
+
def initialize(app, default_extention = '.html')
|
29
|
+
@ext = default_extention.to_s.strip
|
30
|
+
@ext = ".#{@ext}" unless @ext[0] == ?.
|
31
|
+
@app = app
|
32
|
+
end
|
33
|
+
|
34
|
+
def call(env)
|
35
|
+
req = Rack::Request.new(env)
|
36
|
+
|
37
|
+
if ::File.extname(req.path_info).empty?
|
38
|
+
accept = env['HTTP_ACCEPT'].to_s.scan(/[^;,\s]*\/[^;,\s]*/)[0].to_s
|
39
|
+
extension = Rack::Mime::MIME_TYPES.invert[accept] || @ext
|
40
|
+
req.path_info = req.path_info+"#{extension}"
|
41
|
+
end
|
42
|
+
|
43
|
+
@app.call(env)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Rack
|
2
|
+
class Backstage
|
3
|
+
File = ::File
|
4
|
+
|
5
|
+
def initialize(app, path)
|
6
|
+
@app = app
|
7
|
+
@file = File.expand_path(path)
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
if File.exists?(@file)
|
12
|
+
content = File.read(@file)
|
13
|
+
length = "".respond_to?(:bytesize) ? content.bytesize.to_s : content.size.to_s
|
14
|
+
[503, {'Content-Type' => 'text/html', 'Content-Length' => length}, [content]]
|
15
|
+
else
|
16
|
+
@app.call(env)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Rack
|
2
|
+
|
3
|
+
# Rack::Config modifies the environment using the block given during
|
4
|
+
# initialization.
|
5
|
+
class Config
|
6
|
+
def initialize(app, &block)
|
7
|
+
@app = app
|
8
|
+
@block = block
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
@block.call(env)
|
13
|
+
@app.call(env)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'csshttprequest'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
|
5
|
+
# A Rack middleware for providing CSSHTTPRequest responses.
|
6
|
+
class CSSHTTPRequest
|
7
|
+
|
8
|
+
def initialize(app)
|
9
|
+
@app = app
|
10
|
+
end
|
11
|
+
|
12
|
+
# Proxies the request to the application then encodes the response with
|
13
|
+
# the CSSHTTPRequest encoder
|
14
|
+
def call(env)
|
15
|
+
status, headers, response = @app.call(env)
|
16
|
+
if chr_request?(env)
|
17
|
+
response = encode(response)
|
18
|
+
modify_headers!(headers, response)
|
19
|
+
end
|
20
|
+
[status, headers, response]
|
21
|
+
end
|
22
|
+
|
23
|
+
def chr_request?(env)
|
24
|
+
env['csshttprequest.chr'] ||=
|
25
|
+
!(/\.chr$/.match(env['PATH_INFO'])).nil? || Rack::Request.new(env).params['_format'] == 'chr'
|
26
|
+
end
|
27
|
+
|
28
|
+
def encode(response, assembled_body="")
|
29
|
+
response.each { |s| assembled_body << s.to_s } # call down the stack
|
30
|
+
return ::CSSHTTPRequest.encode(assembled_body)
|
31
|
+
end
|
32
|
+
|
33
|
+
def modify_headers!(headers, encoded_response)
|
34
|
+
headers['Content-Length'] = encoded_response.length.to_s
|
35
|
+
headers['Content-Type'] = 'text/css'
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
# TODO: optional stats
|
4
|
+
# TODO: performance
|
5
|
+
# TODO: clean up tests
|
6
|
+
|
7
|
+
module Rack
|
8
|
+
|
9
|
+
##
|
10
|
+
# Rack middleware for protecting against Denial-of-service attacks
|
11
|
+
# http://en.wikipedia.org/wiki/Denial-of-service_attack.
|
12
|
+
#
|
13
|
+
# This middleware is designed for small deployments, which most likely
|
14
|
+
# are not utilizing load balancing from other software or hardware. Deflect
|
15
|
+
# current supports the following functionality:
|
16
|
+
#
|
17
|
+
# * Saturation prevention (small DoS attacks, or request abuse)
|
18
|
+
# * Blacklisting of remote addresses
|
19
|
+
# * Whitelisting of remote addresses
|
20
|
+
# * Logging
|
21
|
+
#
|
22
|
+
# === Options:
|
23
|
+
#
|
24
|
+
# :log When false logging will be bypassed, otherwise pass an object responding to #puts
|
25
|
+
# :log_format Alter the logging format
|
26
|
+
# :log_date_format Alter the logging date format
|
27
|
+
# :request_threshold Number of requests allowed within the set :interval. Defaults to 100
|
28
|
+
# :interval Duration in seconds until the request counter is reset. Defaults to 5
|
29
|
+
# :block_duration Duration in seconds that a remote address will be blocked. Defaults to 900 (15 minutes)
|
30
|
+
# :whitelist Array of remote addresses which bypass Deflect. NOTE: this does not block others
|
31
|
+
# :blacklist Array of remote addresses immediately considered malicious
|
32
|
+
#
|
33
|
+
# === Examples:
|
34
|
+
#
|
35
|
+
# use Rack::Deflect, :log => $stdout, :request_threshold => 20, :interval => 2, :block_duration => 60
|
36
|
+
#
|
37
|
+
# CREDIT: TJ Holowaychuk <tj@vision-media.ca>
|
38
|
+
#
|
39
|
+
|
40
|
+
class Deflect
|
41
|
+
|
42
|
+
attr_reader :options
|
43
|
+
|
44
|
+
def initialize app, options = {}
|
45
|
+
@mutex = Mutex.new
|
46
|
+
@remote_addr_map = {}
|
47
|
+
@app, @options = app, {
|
48
|
+
:log => false,
|
49
|
+
:log_format => 'deflect(%s): %s',
|
50
|
+
:log_date_format => '%m/%d/%Y',
|
51
|
+
:request_threshold => 100,
|
52
|
+
:interval => 5,
|
53
|
+
:block_duration => 900,
|
54
|
+
:whitelist => [],
|
55
|
+
:blacklist => []
|
56
|
+
}.merge(options)
|
57
|
+
end
|
58
|
+
|
59
|
+
def call env
|
60
|
+
return deflect! if deflect? env
|
61
|
+
status, headers, body = @app.call env
|
62
|
+
[status, headers, body]
|
63
|
+
end
|
64
|
+
|
65
|
+
def deflect!
|
66
|
+
[403, { 'Content-Type' => 'text/html', 'Content-Length' => '0' }, '']
|
67
|
+
end
|
68
|
+
|
69
|
+
def deflect? env
|
70
|
+
@remote_addr = env['REMOTE_ADDR']
|
71
|
+
return false if options[:whitelist].include? @remote_addr
|
72
|
+
return true if options[:blacklist].include? @remote_addr
|
73
|
+
sync { watch }
|
74
|
+
end
|
75
|
+
|
76
|
+
def log message
|
77
|
+
return unless options[:log]
|
78
|
+
options[:log].puts(options[:log_format] % [Time.now.strftime(options[:log_date_format]), message])
|
79
|
+
end
|
80
|
+
|
81
|
+
def sync &block
|
82
|
+
@mutex.synchronize(&block)
|
83
|
+
end
|
84
|
+
|
85
|
+
def map
|
86
|
+
@remote_addr_map[@remote_addr] ||= {
|
87
|
+
:expires => Time.now + options[:interval],
|
88
|
+
:requests => 0
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
def watch
|
93
|
+
increment_requests
|
94
|
+
clear! if watch_expired? and not blocked?
|
95
|
+
clear! if blocked? and block_expired?
|
96
|
+
block! if watching? and exceeded_request_threshold?
|
97
|
+
blocked?
|
98
|
+
end
|
99
|
+
|
100
|
+
def block!
|
101
|
+
return if blocked?
|
102
|
+
log "blocked #{@remote_addr}"
|
103
|
+
map[:block_expires] = Time.now + options[:block_duration]
|
104
|
+
end
|
105
|
+
|
106
|
+
def blocked?
|
107
|
+
map.has_key? :block_expires
|
108
|
+
end
|
109
|
+
|
110
|
+
def block_expired?
|
111
|
+
map[:block_expires] < Time.now rescue false
|
112
|
+
end
|
113
|
+
|
114
|
+
def watching?
|
115
|
+
@remote_addr_map.has_key? @remote_addr
|
116
|
+
end
|
117
|
+
|
118
|
+
def clear!
|
119
|
+
return unless watching?
|
120
|
+
log "released #{@remote_addr}" if blocked?
|
121
|
+
@remote_addr_map.delete @remote_addr
|
122
|
+
end
|
123
|
+
|
124
|
+
def increment_requests
|
125
|
+
map[:requests] += 1
|
126
|
+
end
|
127
|
+
|
128
|
+
def exceeded_request_threshold?
|
129
|
+
map[:requests] > options[:request_threshold]
|
130
|
+
end
|
131
|
+
|
132
|
+
def watch_expired?
|
133
|
+
map[:expires] <= Time.now
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
end
|
data/lib/rack/contrib/jsonp.rb
CHANGED
@@ -1,38 +1,41 @@
|
|
1
1
|
module Rack
|
2
|
-
|
2
|
+
|
3
3
|
# A Rack middleware for providing JSON-P support.
|
4
|
-
#
|
4
|
+
#
|
5
5
|
# Full credit to Flinn Mueller (http://actsasflinn.com/) for this contribution.
|
6
|
-
#
|
6
|
+
#
|
7
7
|
class JSONP
|
8
|
-
|
8
|
+
|
9
9
|
def initialize(app)
|
10
10
|
@app = app
|
11
11
|
end
|
12
|
-
|
12
|
+
|
13
13
|
# Proxies the request to the application, stripping out the JSON-P callback
|
14
14
|
# method and padding the response with the appropriate callback format.
|
15
|
-
#
|
15
|
+
#
|
16
16
|
# Changes nothing if no <tt>callback</tt> param is specified.
|
17
|
-
#
|
17
|
+
#
|
18
18
|
def call(env)
|
19
19
|
status, headers, response = @app.call(env)
|
20
20
|
request = Rack::Request.new(env)
|
21
|
-
|
22
|
-
|
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]]
|
23
26
|
end
|
24
|
-
|
27
|
+
|
25
28
|
# Pads the response with the appropriate callback format according to the
|
26
29
|
# JSON-P spec/requirements.
|
27
|
-
#
|
30
|
+
#
|
28
31
|
# The Rack response spec indicates that it should be enumerable. The method
|
29
|
-
# of combining all of the data into a
|
32
|
+
# of combining all of the data into a single string makes sense since JSON
|
30
33
|
# is returned as a full string.
|
31
|
-
#
|
34
|
+
#
|
32
35
|
def pad(callback, response, body = "")
|
33
|
-
response.each{ |s| body << s }
|
36
|
+
response.each{ |s| body << s.to_s }
|
34
37
|
"#{callback}(#{body})"
|
35
38
|
end
|
36
|
-
|
39
|
+
|
37
40
|
end
|
38
41
|
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
|
@@ -5,29 +5,29 @@ rescue LoadError => e
|
|
5
5
|
end
|
6
6
|
|
7
7
|
module Rack
|
8
|
-
|
8
|
+
|
9
9
|
# A Rack middleware for parsing POST/PUT body data when Content-Type is
|
10
10
|
# not one of the standard supported types, like <tt>application/json</tt>.
|
11
|
-
#
|
11
|
+
#
|
12
12
|
# TODO: Find a better name.
|
13
|
-
#
|
13
|
+
#
|
14
14
|
class PostBodyContentTypeParser
|
15
|
-
|
15
|
+
|
16
16
|
# Constants
|
17
|
-
#
|
17
|
+
#
|
18
18
|
CONTENT_TYPE = 'CONTENT_TYPE'.freeze
|
19
19
|
POST_BODY = 'rack.input'.freeze
|
20
20
|
FORM_INPUT = 'rack.request.form_input'.freeze
|
21
21
|
FORM_HASH = 'rack.request.form_hash'.freeze
|
22
|
-
|
22
|
+
|
23
23
|
# Supported Content-Types
|
24
|
-
#
|
24
|
+
#
|
25
25
|
APPLICATION_JSON = 'application/json'.freeze
|
26
|
-
|
26
|
+
|
27
27
|
def initialize(app)
|
28
28
|
@app = app
|
29
29
|
end
|
30
|
-
|
30
|
+
|
31
31
|
def call(env)
|
32
32
|
case env[CONTENT_TYPE]
|
33
33
|
when APPLICATION_JSON
|
@@ -35,6 +35,6 @@ module Rack
|
|
35
35
|
end
|
36
36
|
@app.call(env)
|
37
37
|
end
|
38
|
-
|
38
|
+
|
39
39
|
end
|
40
40
|
end
|