rack-contrib 0.9.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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