rack-contrib 0.9.2 → 1.0.0

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/AUTHORS ADDED
@@ -0,0 +1,26 @@
1
+ Ryan Tomayko <rtomayko@gmail.com>
2
+ Joshua Peek <josh@joshpeek.com>
3
+ Jeremy Kemper <jeremy@bitsweat.net>
4
+ mynyml <mynyml@gmail.com>
5
+ Cameron Walters <cameron.walters@gmail.com>
6
+ Jon Crosby <jon@joncrosby.me>
7
+ Matt Todd <chiology@gmail.com>
8
+ Pirmin Kalberer <pka@sourcepole.ch>
9
+ Rune Botten <rbotten@gmail.com>
10
+ Pratik Naik <pratiknaik@gmail.com>
11
+ Paul Sadauskas <psadauskas@gmail.com>
12
+ Jeremy Evans <code@jeremyevans.net>
13
+ Michael Fellinger <m.fellinger@gmail.com>
14
+ Geoff Buesing <gbuesing@gmail.com>
15
+ Nicolas Mérouze <nicolas.merouze@gmail.com>
16
+ Cyril Rohr <cyril.rohr@irisa.fr>
17
+ Harry Vangberg <harry@vangberg.name>
18
+ James Rosen <jrosen@mitre.org>
19
+ Mislav Marohnić <mislav.marohnic@gmail.com>
20
+ Ben Brinckerhoff <ben@devver.net>
21
+ Rafael Souza <rafael.ssouza@gmail.com>
22
+ Stephen Delano <sdelano@sdelano-air.(none)>
23
+ TJ Holowaychuk <tj@vision-media.ca>
24
+ anupom syam <anupom.syam@gmail.com>
25
+ ichverstehe <ichverstehe@gmail.com>
26
+ kubicek <jiri@kubicek.cz>
@@ -3,7 +3,6 @@
3
3
  This package includes a variety of add-on components for Rack, a Ruby web server
4
4
  interface:
5
5
 
6
- * Rack::ETag - Automatically sets the ETag header on all String bodies.
7
6
  * Rack::JSONP - Adds JSON-P support by stripping out the callback param
8
7
  and padding the response with the appropriate callback format.
9
8
  * Rack::LighttpdScriptNameFix - Fixes how lighttpd sets the SCRIPT_NAME
@@ -25,6 +24,8 @@ interface:
25
24
  from file.
26
25
  * Rack::Signals - Installs signal handlers that are safely processed after
27
26
  a request
27
+ * Rack::StaticCache - Modifies the response headers to facilitiate client and proxy caching for
28
+ static files that minimizes http requests and improves overall load times for second time visitors.
28
29
  * Rack::TimeZone - Detects the clients timezone using JavaScript and sets
29
30
  a variable in Rack's environment with the offset from UTC.
30
31
  * Rack::Evil - Lets the rack application return a response to the client from any place.
@@ -43,6 +44,9 @@ interface:
43
44
  * 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
45
  * Rack::HostMeta - Configures /host-meta using a block
45
46
  * Rack::Cookies - Adds simple cookie jar hash to env
47
+ * Rack::Access - Limit access based on IP address
48
+ * Rack::ResponseHeaders - Manipulate response headers object at runtime
49
+ * Rack::SimpleEndpoint - Create simple endpoints with routing rules, similar to Sinatra actions
46
50
 
47
51
  === Use
48
52
 
@@ -50,9 +54,9 @@ Git is the quickest way to the rack-contrib sources:
50
54
 
51
55
  git clone git://github.com/rack/rack-contrib.git
52
56
 
53
- Gems are currently available from GitHub clones:
57
+ Gems are available too:
54
58
 
55
- gem install rack-rack-contrib --source=http://gems.github.com/
59
+ gem install rack-contrib
56
60
 
57
61
  Requiring 'rack/contrib' will add autoloads to the Rack modules for all of the
58
62
  components included. The following example shows what a simple rackup
data/Rakefile CHANGED
@@ -42,8 +42,8 @@ task :rdoc => ["RDOX"]
42
42
 
43
43
  # PACKAGING =================================================================
44
44
 
45
- # load gemspec like github's gem builder to surface any SAFE issues.
46
- require 'rubygems/specification'
45
+ require 'rubygems'
46
+ # load gemspec
47
47
  $spec = eval(File.read('rack-contrib.gemspec'))
48
48
 
49
49
  def package(ext='')
@@ -69,14 +69,6 @@ file package('.tar.gz') => %w[pkg/] + $spec.files do |f|
69
69
  sh "git archive --format=tar HEAD | gzip > #{f.name}"
70
70
  end
71
71
 
72
- desc 'Publish gem and tarball to rubyforge'
73
- task 'publish:gem' => [package('.gem'), package('.tar.gz')] do |t|
74
- sh <<-end
75
- rubyforge add_release rack rack-contrib #{$spec.version} #{package('.gem')} &&
76
- rubyforge add_file rack rack-contrib #{$spec.version} #{package('.tar.gz')}
77
- end
78
- end
79
-
80
72
  # GEMSPEC ===================================================================
81
73
 
82
74
  file 'rack-contrib.gemspec' => FileList['{lib,test}/**','Rakefile', 'README.rdoc'] do |f|
@@ -8,11 +8,13 @@ module Rack
8
8
  end
9
9
 
10
10
  autoload :AcceptFormat, "rack/contrib/accept_format"
11
+ autoload :Access, "rack/contrib/access"
11
12
  autoload :BounceFavicon, "rack/contrib/bounce_favicon"
12
13
  autoload :Cookies, "rack/contrib/cookies"
13
14
  autoload :CSSHTTPRequest, "rack/contrib/csshttprequest"
14
15
  autoload :Deflect, "rack/contrib/deflect"
15
16
  autoload :ETag, "rack/contrib/etag"
17
+ autoload :ExpectationCascade, "rack/contrib/expectation_cascade"
16
18
  autoload :GarbageCollector, "rack/contrib/garbagecollector"
17
19
  autoload :JSONP, "rack/contrib/jsonp"
18
20
  autoload :LighttpdScriptNameFix, "rack/contrib/lighttpd_script_name_fix"
@@ -21,8 +23,11 @@ module Rack
21
23
  autoload :PostBodyContentTypeParser, "rack/contrib/post_body_content_type_parser"
22
24
  autoload :ProcTitle, "rack/contrib/proctitle"
23
25
  autoload :Profiler, "rack/contrib/profiler"
26
+ autoload :ResponseHeaders, "rack/contrib/response_headers"
27
+ autoload :Runtime, "rack/contrib/runtime"
24
28
  autoload :Sendfile, "rack/contrib/sendfile"
25
29
  autoload :Signals, "rack/contrib/signals"
30
+ autoload :SimpleEndpoint, "rack/contrib/simple_endpoint"
26
31
  autoload :TimeZone, "rack/contrib/time_zone"
27
32
  autoload :Evil, "rack/contrib/evil"
28
33
  autoload :Callbacks, "rack/contrib/callbacks"
@@ -31,4 +36,5 @@ module Rack
31
36
  autoload :NotFound, "rack/contrib/not_found"
32
37
  autoload :ResponseCache, "rack/contrib/response_cache"
33
38
  autoload :RelativeRedirect, "rack/contrib/relative_redirect"
39
+ autoload :StaticCache, "rack/contrib/static_cache"
34
40
  end
@@ -0,0 +1,85 @@
1
+ require "ipaddr"
2
+
3
+ module Rack
4
+
5
+ ##
6
+ # Rack middleware for limiting access based on IP address
7
+ #
8
+ #
9
+ # === Options:
10
+ #
11
+ # path => ipmasks ipmasks: Array of remote addresses which are allowed to access
12
+ #
13
+ # === Examples:
14
+ #
15
+ # use Rack::Access, '/backend' => [ '127.0.0.1', '192.168.1.0/24' ]
16
+ #
17
+ #
18
+
19
+ class Access
20
+
21
+ attr_reader :options
22
+
23
+ def initialize(app, options = {})
24
+ @app = app
25
+ mapping = options.empty? ? {"/" => ["127.0.0.1"]} : options
26
+ @mapping = remap(mapping)
27
+ end
28
+
29
+ def remap(mapping)
30
+ mapping.map { |location, ipmasks|
31
+ if location =~ %r{\Ahttps?://(.*?)(/.*)}
32
+ host, location = $1, $2
33
+ else
34
+ host = nil
35
+ end
36
+
37
+ unless location[0] == ?/
38
+ raise ArgumentError, "paths need to start with /"
39
+ end
40
+ location = location.chomp('/')
41
+ match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", nil, 'n')
42
+
43
+ ipmasks.collect! do |ipmask|
44
+ ipmask.is_a?(IPAddr) ? ipmask : IPAddr.new(ipmask)
45
+ end
46
+ [host, location, match, ipmasks]
47
+ }.sort_by { |(h, l, m, a)| [h ? -h.size : (-1.0 / 0.0), -l.size] } # Longest path first
48
+ end
49
+
50
+ def call(env)
51
+ @original_request = Request.new(env)
52
+ ipmasks = ipmasks_for_path(env)
53
+ return forbidden! unless ip_authorized?(ipmasks)
54
+ status, headers, body = @app.call(env)
55
+ [status, headers, body]
56
+ end
57
+
58
+ def ipmasks_for_path(env)
59
+ path = env["PATH_INFO"].to_s
60
+ hHost, sName, sPort = env.values_at('HTTP_HOST','SERVER_NAME','SERVER_PORT')
61
+ @mapping.each do |host, location, match, ipmasks|
62
+ next unless (hHost == host || sName == host \
63
+ || (host.nil? && (hHost == sName || hHost == sName+':'+sPort)))
64
+ next unless path =~ match && rest = $1
65
+ next unless rest.empty? || rest[0] == ?/
66
+
67
+ return ipmasks
68
+ end
69
+ nil
70
+ end
71
+
72
+ def forbidden!
73
+ [403, { 'Content-Type' => 'text/html', 'Content-Length' => '0' }, '']
74
+ end
75
+
76
+ def ip_authorized?(ipmasks)
77
+ return true if ipmasks.nil?
78
+
79
+ ipmasks.any? do |ip_mask|
80
+ ip_mask.include?(IPAddr.new(@original_request.ip))
81
+ end
82
+ end
83
+
84
+ end
85
+ end
@@ -0,0 +1,50 @@
1
+ module Rack
2
+ class Cookies
3
+ class CookieJar < Hash
4
+ def initialize(cookies)
5
+ @set_cookies = {}
6
+ @delete_cookies = {}
7
+ super()
8
+ update(cookies)
9
+ end
10
+
11
+ def [](name)
12
+ super(name.to_s)
13
+ end
14
+
15
+ def []=(key, options)
16
+ unless options.is_a?(Hash)
17
+ options = { :value => options }
18
+ end
19
+
20
+ options[:path] ||= '/'
21
+ @set_cookies[key] = options
22
+ super(key.to_s, options[:value])
23
+ end
24
+
25
+ def delete(key, options = {})
26
+ options[:path] ||= '/'
27
+ @delete_cookies[key] = options
28
+ super(key.to_s)
29
+ end
30
+
31
+ def finish!(resp)
32
+ @set_cookies.each { |k, v| resp.set_cookie(k, v) }
33
+ @delete_cookies.each { |k, v| resp.delete_cookie(k, v) }
34
+ end
35
+ end
36
+
37
+ def initialize(app)
38
+ @app = app
39
+ end
40
+
41
+ def call(env)
42
+ req = Request.new(env)
43
+ env['rack.cookies'] = cookies = CookieJar.new(req.cookies)
44
+ status, headers, body = @app.call(env)
45
+ resp = Response.new(body, status, headers)
46
+ cookies.finish!(resp)
47
+ resp.to_a
48
+ end
49
+ end
50
+ end
@@ -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
@@ -22,7 +22,7 @@ module Rack
22
22
  response = pad(request.params.delete('callback'), response)
23
23
  headers['Content-Length'] = response.length.to_s
24
24
  end
25
- [status, headers, [response]]
25
+ [status, headers, response]
26
26
  end
27
27
 
28
28
  # Pads the response with the appropriate callback format according to the
@@ -70,7 +70,7 @@ module Rack
70
70
  env['mail.sent'] = true
71
71
  return if smtp[:server] == 'example.com'
72
72
 
73
- Net::SMTP.start smtp[:address], smtp[:port], smtp[:domain], smtp[:user_name], smtp[:password], smtp[:authentication] do |server|
73
+ Net::SMTP.start smtp[:server], smtp[:port], smtp[:domain], smtp[:user_name], smtp[:password], smtp[:authentication] do |server|
74
74
  mail.to.each do |recipient|
75
75
  server.send_message mail.to_s, mail.from, recipient
76
76
  end
@@ -44,7 +44,7 @@ module Rack
44
44
  private
45
45
  def profiling?(env)
46
46
  unless RubyProf.running?
47
- request = Rack::Request.new(env)
47
+ request = Rack::Request.new(env.clone)
48
48
  if mode = request.params.delete('profile')
49
49
  if RubyProf.const_defined?(mode.upcase)
50
50
  mode
@@ -60,9 +60,11 @@ module Rack
60
60
  def profile(env, mode)
61
61
  RubyProf.measure_mode = RubyProf.const_get(mode.upcase)
62
62
 
63
+ GC.enable_stats if GC.respond_to?(:enable_stats)
63
64
  result = RubyProf.profile do
64
65
  @times.times { @app.call(env) }
65
66
  end
67
+ GC.disable_stats if GC.respond_to?(:disable_stats)
66
68
 
67
69
  [200, headers(@printer, env, mode), print(@printer, result)]
68
70
  end
@@ -0,0 +1,24 @@
1
+ module Rack
2
+ # Allows you to tap into the response headers. Yields a Rack::Utils::HeaderHash
3
+ # of current response headers to the block. Example:
4
+ #
5
+ # use Rack::ResponseHeaders do |headers|
6
+ # headers['X-Foo'] = 'bar'
7
+ # headers.delete('X-Baz')
8
+ # end
9
+ #
10
+ class ResponseHeaders
11
+ def initialize(app, &block)
12
+ @app = app
13
+ @block = block
14
+ end
15
+
16
+ def call(env)
17
+ response = @app.call(env)
18
+ headers = Utils::HeaderHash.new(response[1])
19
+ @block.call(headers)
20
+ response[1] = headers
21
+ response
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,31 @@
1
+
2
+ module Rack
3
+ # Sets an "X-Runtime" response header, indicating the response
4
+ # time of the request, in seconds
5
+ #
6
+ # You can put it right before the application to see the processing
7
+ # time, or before all the other middlewares to include time for them,
8
+ # too.
9
+ class Runtime
10
+ def initialize(app, name = nil)
11
+ @app = app
12
+ @header_name = "X-Runtime"
13
+ @header_name << "-#{name}" if name
14
+ end
15
+
16
+ def call(env)
17
+ start_time = Time.now
18
+ status, headers, body = @app.call(env)
19
+ request_time = Time.now - start_time
20
+
21
+ if !headers.has_key?(@header_name)
22
+ headers[@header_name] = "%0.6f" % request_time
23
+ end
24
+
25
+ [status, headers, body]
26
+ end
27
+ end
28
+ end
29
+
30
+
31
+
@@ -0,0 +1,81 @@
1
+ module Rack
2
+ # Create simple endpoints with routing rules, similar to Sinatra actions.
3
+ #
4
+ # Simplest example:
5
+ #
6
+ # use Rack::SimpleEndpoint, '/ping_monitor' do
7
+ # 'pong'
8
+ # end
9
+ #
10
+ # The value returned from the block will be written to the response body, so
11
+ # the above example will return "pong" when the request path is /ping_monitor.
12
+ #
13
+ # HTTP verb requirements can optionally be specified:
14
+ #
15
+ # use Rack::SimpleEndpoint, '/foo' => :get do
16
+ # 'only GET requests will match'
17
+ # end
18
+ #
19
+ # use Rack::SimpleEndpoint, '/bar' => [:get, :post] do
20
+ # 'only GET and POST requests will match'
21
+ # end
22
+ #
23
+ # Rack::Request and Rack::Response objects are yielded to block:
24
+ #
25
+ # use Rack::SimpleEndpoint, '/json' do |req, res|
26
+ # res['Content-Type'] = 'application/json'
27
+ # %({"foo": "#{req[:foo]}"})
28
+ # end
29
+ #
30
+ # When path is a Regexp, match data object is yielded as third argument to block
31
+ #
32
+ # use Rack::SimpleEndpoint, %r{^/(john|paul|george|ringo)} do |req, res, match|
33
+ # "Hello, #{match[1]}"
34
+ # end
35
+ #
36
+ # A :pass symbol returned from block will not return a response; control will continue down the
37
+ # Rack stack:
38
+ #
39
+ # use Rack::SimpleEndpoint, '/api_key' do |req, res|
40
+ # req.env['myapp.user'].authorized? ? '12345' : :pass
41
+ # end
42
+ #
43
+ # # Unauthorized access to /api_key will be handled by PublicApp
44
+ # run PublicApp
45
+ class SimpleEndpoint
46
+ def initialize(app, arg, &block)
47
+ @app = app
48
+ @path = extract_path(arg)
49
+ @verbs = extract_verbs(arg)
50
+ @block = block
51
+ end
52
+
53
+ def call(env)
54
+ match = match_path(env['PATH_INFO'])
55
+ if match && valid_method?(env['REQUEST_METHOD'])
56
+ req, res = Request.new(env), Response.new
57
+ body = @block.call(req, res, (match unless match == true))
58
+ body == :pass ? @app.call(env) : (res.write(body); res.finish)
59
+ else
60
+ @app.call(env)
61
+ end
62
+ end
63
+
64
+ private
65
+ def extract_path(arg)
66
+ arg.is_a?(Hash) ? arg.keys.first : arg
67
+ end
68
+
69
+ def extract_verbs(arg)
70
+ arg.is_a?(Hash) ? [arg.values.first].flatten.map {|verb| verb.to_s.upcase} : []
71
+ end
72
+
73
+ def match_path(path)
74
+ @path.is_a?(Regexp) ? @path.match(path.to_s) : @path == path.to_s
75
+ end
76
+
77
+ def valid_method?(method)
78
+ @verbs.empty? || @verbs.include?(method)
79
+ end
80
+ end
81
+ end