rack-contrib 0.9.0 → 0.9.2
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.
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
|