rack-contrib 0.9.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.

@@ -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,106 @@
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)
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
+ result = RubyProf.profile do
64
+ @times.times { @app.call(env) }
65
+ end
66
+
67
+ [200, headers(@printer, env, mode), print(@printer, result)]
68
+ end
69
+
70
+ def print(printer, result)
71
+ body = StringIO.new
72
+ printer.new(result).print(body, :min_percent => 0.01)
73
+ body.rewind
74
+ body
75
+ end
76
+
77
+ def headers(printer, env, mode)
78
+ headers = { 'Content-Type' => PRINTER_CONTENT_TYPE[printer] || DEFAULT_CONTENT_TYPE }
79
+ if printer == RubyProf::CallTreePrinter
80
+ filename = ::File.basename(env['PATH_INFO'])
81
+ headers['Content-Disposition'] =
82
+ %(attachment; filename="#{filename}.#{mode}.tree")
83
+ end
84
+ headers
85
+ end
86
+
87
+ def parse_printer(printer)
88
+ if printer.nil?
89
+ DEFAULT_PRINTER
90
+ elsif printer.is_a?(Class)
91
+ printer
92
+ else
93
+ name = "#{camel_case(printer)}Printer"
94
+ if RubyProf.const_defined?(name)
95
+ RubyProf.const_get(name)
96
+ else
97
+ DEFAULT_PRINTER
98
+ end
99
+ end
100
+ end
101
+
102
+ def camel_case(word)
103
+ word.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,48 @@
1
+ module Rack
2
+ class RouteExceptions
3
+ ROUTES = [
4
+ [Exception, '/error/internal']
5
+ ]
6
+
7
+ ROUTE_EXCEPTIONS_PATH_INFO = 'rack.route_exceptions.path_info'.freeze
8
+ ROUTE_EXCEPTIONS_EXCEPTION = 'rack.route_exceptions.exception'.freeze
9
+ ROUTE_EXCEPTIONS_RESPONSE = 'rack.route_exceptions.response'.freeze
10
+
11
+ def initialize(app)
12
+ @app = app
13
+ end
14
+
15
+ def call(env, try_again = true)
16
+ status, header, body = response = @app.call(env)
17
+
18
+ response
19
+ rescue Exception => exception
20
+ raise(exception) unless try_again
21
+
22
+ ROUTES.each do |klass, to|
23
+ next unless klass === exception
24
+ return route(to, env, response, exception)
25
+ end
26
+
27
+ raise(exception)
28
+ end
29
+
30
+ def route(to, env, response, exception)
31
+ hash = {
32
+ ROUTE_EXCEPTIONS_PATH_INFO => env['PATH_INFO'],
33
+ ROUTE_EXCEPTIONS_EXCEPTION => exception,
34
+ ROUTE_EXCEPTIONS_RESPONSE => response
35
+ }
36
+ env.merge!(hash)
37
+
38
+ env['PATH_INFO'] = to
39
+
40
+ call(env, try_again = false)
41
+ end
42
+
43
+ def self.route(exception, to)
44
+ ROUTES.delete_if{|k,v| k == exception }
45
+ ROUTES << [exception, to]
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,142 @@
1
+ require 'rack/file'
2
+
3
+ module Rack
4
+ class File #:nodoc:
5
+ alias :to_path :path
6
+ end
7
+
8
+ # = Sendfile
9
+ #
10
+ # The Sendfile middleware intercepts responses whose body is being
11
+ # served from a file and replaces it with a server specific X-Sendfile
12
+ # header. The web server is then responsible for writing the file contents
13
+ # to the client. This can dramatically reduce the amount of work required
14
+ # by the Ruby backend and takes advantage of the web servers optimized file
15
+ # delivery code.
16
+ #
17
+ # In order to take advantage of this middleware, the response body must
18
+ # respond to +to_path+ and the request must include an X-Sendfile-Type
19
+ # header. Rack::File and other components implement +to_path+ so there's
20
+ # rarely anything you need to do in your application. The X-Sendfile-Type
21
+ # header is typically set in your web servers configuration. The following
22
+ # sections attempt to document
23
+ #
24
+ # === Nginx
25
+ #
26
+ # Nginx supports the X-Accel-Redirect header. This is similar to X-Sendfile
27
+ # but requires parts of the filesystem to be mapped into a private URL
28
+ # hierarachy.
29
+ #
30
+ # The following example shows the Nginx configuration required to create
31
+ # a private "/files/" area, enable X-Accel-Redirect, and pass the special
32
+ # X-Sendfile-Type and X-Accel-Mapping headers to the backend:
33
+ #
34
+ # location /files/ {
35
+ # internal;
36
+ # alias /var/www/;
37
+ # }
38
+ #
39
+ # location / {
40
+ # proxy_redirect false;
41
+ #
42
+ # proxy_set_header Host $host;
43
+ # proxy_set_header X-Real-IP $remote_addr;
44
+ # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
45
+ #
46
+ # proxy_set_header X-Sendfile-Type X-Accel-Redirect
47
+ # proxy_set_header X-Accel-Mapping /files/=/var/www/;
48
+ #
49
+ # proxy_pass http://127.0.0.1:8080/;
50
+ # }
51
+ #
52
+ # Note that the X-Sendfile-Type header must be set exactly as shown above. The
53
+ # X-Accel-Mapping header should specify the name of the private URL pattern,
54
+ # followed by an equals sign (=), followed by the location on the file system
55
+ # that it maps to. The middleware performs a simple substitution on the
56
+ # resulting path.
57
+ #
58
+ # See Also: http://wiki.codemongers.com/NginxXSendfile
59
+ #
60
+ # === lighttpd
61
+ #
62
+ # Lighttpd has supported some variation of the X-Sendfile header for some
63
+ # time, although only recent version support X-Sendfile in a reverse proxy
64
+ # configuration.
65
+ #
66
+ # $HTTP["host"] == "example.com" {
67
+ # proxy-core.protocol = "http"
68
+ # proxy-core.balancer = "round-robin"
69
+ # proxy-core.backends = (
70
+ # "127.0.0.1:8000",
71
+ # "127.0.0.1:8001",
72
+ # ...
73
+ # )
74
+ #
75
+ # proxy-core.allow-x-sendfile = "enable"
76
+ # proxy-core.rewrite-request = (
77
+ # "X-Sendfile-Type" => (".*" => "X-Sendfile")
78
+ # )
79
+ # }
80
+ #
81
+ # See Also: http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModProxyCore
82
+ #
83
+ # === Apache
84
+ #
85
+ # X-Sendfile is supported under Apache 2.x using a separate module:
86
+ #
87
+ # http://tn123.ath.cx/mod_xsendfile/
88
+ #
89
+ # Once the module is compiled and installed, you can enable it using
90
+ # XSendFile config directive:
91
+ #
92
+ # RequestHeader Set X-Sendfile-Type X-Sendfile
93
+ # ProxyPassReverse / http://localhost:8001/
94
+ # XSendFile on
95
+
96
+ class Sendfile
97
+ F = ::File
98
+
99
+ def initialize(app, variation=nil)
100
+ @app = app
101
+ @variation = variation
102
+ end
103
+
104
+ def call(env)
105
+ status, headers, body = @app.call(env)
106
+ if body.respond_to?(:to_path)
107
+ case type = variation(env)
108
+ when 'X-Accel-Redirect'
109
+ path = F.expand_path(body.to_path)
110
+ if url = map_accel_path(env, path)
111
+ headers[type] = url
112
+ body = []
113
+ else
114
+ env['rack.errors'] << "X-Accel-Mapping header missing"
115
+ end
116
+ when 'X-Sendfile', 'X-Lighttpd-Send-File'
117
+ path = F.expand_path(body.to_path)
118
+ headers[type] = path
119
+ body = []
120
+ when '', nil
121
+ else
122
+ env['rack.errors'] << "Unknown x-sendfile variation: '#{variation}'.\n"
123
+ end
124
+ end
125
+ [status, headers, body]
126
+ end
127
+
128
+ private
129
+ def variation(env)
130
+ @variation ||
131
+ env['sendfile.type'] ||
132
+ env['HTTP_X_SENDFILE_TYPE']
133
+ end
134
+
135
+ def map_accel_path(env, file)
136
+ if mapping = env['HTTP_X_ACCEL_MAPPING']
137
+ internal, external = mapping.split('=', 2).map{ |p| p.strip }
138
+ file.sub(/^#{internal}/i, external)
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,25 @@
1
+ module Rack
2
+ class TimeZone
3
+ Javascript = <<-EOJ
4
+ function setTimezoneCookie() {
5
+ var offset = (new Date()).getTimezoneOffset()
6
+ var date = new Date();
7
+ date.setTime(date.getTime()+3600000);
8
+ document.cookie = "utc_offset="+offset+"; expires="+date.toGMTString();+"; path=/";
9
+ }
10
+ EOJ
11
+
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ def call(env)
17
+ request = Rack::Request.new(env)
18
+ if utc_offset = request.cookies["utc_offset"]
19
+ env["rack.timezone.utc_offset"] = -(utc_offset.to_i * 60)
20
+ end
21
+
22
+ @app.call(env)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ require 'rack'
2
+
3
+ module Rack
4
+ module Contrib
5
+ def self.release
6
+ "0.9"
7
+ end
8
+ end
9
+
10
+ autoload :BounceFavicon, "rack/contrib/bounce_favicon"
11
+ autoload :ETag, "rack/contrib/etag"
12
+ autoload :GarbageCollector, "rack/contrib/garbagecollector"
13
+ autoload :JSONP, "rack/contrib/jsonp"
14
+ autoload :LighttpdScriptNameFix, "rack/contrib/lighttpd_script_name_fix"
15
+ autoload :Locale, "rack/contrib/locale"
16
+ autoload :MailExceptions, "rack/contrib/mailexceptions"
17
+ autoload :PostBodyContentTypeParser, "rack/contrib/post_body_content_type_parser"
18
+ autoload :ProcTitle, "rack/contrib/proctitle"
19
+ autoload :Profiler, "rack/contrib/profiler"
20
+ autoload :Sendfile, "rack/contrib/sendfile"
21
+ autoload :TimeZone, "rack/contrib/time_zone"
22
+ autoload :Evil, "rack/contrib/evil"
23
+ autoload :Callbacks, "rack/contrib/callbacks"
24
+ autoload :NestedParams, "rack/contrib/nested_params"
25
+ end
@@ -0,0 +1,68 @@
1
+ Gem::Specification.new do |s|
2
+ s.specification_version = 2 if s.respond_to? :specification_version=
3
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
4
+
5
+ s.name = 'rack-contrib'
6
+ s.version = '0.9.0'
7
+ s.date = '2009-01-23'
8
+
9
+ s.description = "Contributed Rack Middleware and Utilities"
10
+ s.summary = "Contributed Rack Middleware and Utilities"
11
+
12
+ s.authors = ["rack-devel"]
13
+ s.email = "rack-devel@googlegroups.com"
14
+
15
+ # = MANIFEST =
16
+ s.files = %w[
17
+ COPYING
18
+ README.rdoc
19
+ Rakefile
20
+ lib/rack/contrib.rb
21
+ lib/rack/contrib/bounce_favicon.rb
22
+ lib/rack/contrib/callbacks.rb
23
+ lib/rack/contrib/etag.rb
24
+ lib/rack/contrib/evil.rb
25
+ lib/rack/contrib/garbagecollector.rb
26
+ lib/rack/contrib/jsonp.rb
27
+ lib/rack/contrib/lighttpd_script_name_fix.rb
28
+ lib/rack/contrib/locale.rb
29
+ lib/rack/contrib/mailexceptions.rb
30
+ lib/rack/contrib/nested_params.rb
31
+ lib/rack/contrib/post_body_content_type_parser.rb
32
+ lib/rack/contrib/proctitle.rb
33
+ lib/rack/contrib/profiler.rb
34
+ lib/rack/contrib/route_exceptions.rb
35
+ lib/rack/contrib/sendfile.rb
36
+ lib/rack/contrib/time_zone.rb
37
+ rack-contrib.gemspec
38
+ test/mail_settings.rb
39
+ test/spec_rack_callbacks.rb
40
+ test/spec_rack_contrib.rb
41
+ test/spec_rack_etag.rb
42
+ test/spec_rack_evil.rb
43
+ test/spec_rack_garbagecollector.rb
44
+ test/spec_rack_jsonp.rb
45
+ test/spec_rack_lighttpd_script_name_fix.rb
46
+ test/spec_rack_mailexceptions.rb
47
+ test/spec_rack_nested_params.rb
48
+ test/spec_rack_post_body_content_type_parser.rb
49
+ test/spec_rack_proctitle.rb
50
+ test/spec_rack_profiler.rb
51
+ test/spec_rack_sendfile.rb
52
+ ]
53
+ # = MANIFEST =
54
+
55
+ s.test_files = s.files.select {|path| path =~ /^test\/spec_.*\.rb/}
56
+
57
+ s.extra_rdoc_files = %w[README.rdoc COPYING]
58
+ s.add_dependency 'rack', '~> 0.9.1'
59
+ s.add_dependency 'test-spec', '~> 0.9.0'
60
+ s.add_development_dependency 'tmail', '>= 1.2'
61
+ s.add_development_dependency 'json', '>= 1.1'
62
+
63
+ s.has_rdoc = true
64
+ s.homepage = "http://github.com/rack/rack-contrib/"
65
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "rack-contrib", "--main", "README"]
66
+ s.require_paths = %w[lib]
67
+ s.rubygems_version = '1.1.1'
68
+ end
@@ -0,0 +1,12 @@
1
+ TEST_SMTP = nil
2
+
3
+ # Enable SMTP tests by providing the following for your SMTP server.
4
+ #
5
+ # TEST_SMTP = {
6
+ # :server => 'localhost',
7
+ # :domain => 'localhost',
8
+ # :port => 25,
9
+ # :authentication => :login,
10
+ # :user_name => nil,
11
+ # :password => nil
12
+ # }
@@ -0,0 +1,65 @@
1
+ require 'test/spec'
2
+ require 'rack/mock'
3
+
4
+ class Flame
5
+ def call(env)
6
+ env['flame'] = 'F Lifo..'
7
+ end
8
+ end
9
+
10
+ class Pacify
11
+ def initialize(with)
12
+ @with = with
13
+ end
14
+
15
+ def call(env)
16
+ env['peace'] = @with
17
+ end
18
+ end
19
+
20
+ class Finale
21
+ def call(response)
22
+ status, headers, body = response
23
+
24
+ headers['last'] = 'Finale'
25
+ $old_status = status
26
+
27
+ [201, headers, body]
28
+ end
29
+ end
30
+
31
+ class TheEnd
32
+ def call(response)
33
+ status, headers, body = response
34
+
35
+ headers['last'] = 'TheEnd'
36
+ [201, headers, body]
37
+ end
38
+ end
39
+
40
+ context "Rack::Callbacks" do
41
+ specify "works for love and small stack trace" do
42
+ callback_app = Rack::Callbacks.new do
43
+ before Flame
44
+ before Pacify, "with love"
45
+
46
+ run lambda {|env| [200, {}, env['flame'] + env['peace']] }
47
+
48
+ after Finale
49
+ after TheEnd
50
+ end
51
+
52
+ app = Rack::Builder.new do
53
+ run callback_app
54
+ end.to_app
55
+
56
+ response = Rack::MockRequest.new(app).get("/")
57
+
58
+ response.body.should.equal 'F Lifo..with love'
59
+
60
+ $old_status.should.equal 200
61
+ response.status.should.equal 201
62
+
63
+ response.headers['last'].should.equal 'TheEnd'
64
+ end
65
+ end
@@ -0,0 +1,8 @@
1
+ require 'test/spec'
2
+ require 'rack/contrib'
3
+
4
+ context "Rack::Contrib" do
5
+ specify "should expose release" do
6
+ Rack::Contrib.should.respond_to :release
7
+ end
8
+ end
@@ -0,0 +1,23 @@
1
+ require 'test/spec'
2
+ require 'rack/mock'
3
+ require 'rack/contrib/etag'
4
+
5
+ context "Rack::ETag" do
6
+ specify "sets ETag if none is set" do
7
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, World!"] }
8
+ response = Rack::ETag.new(app).call({})
9
+ response[1]['ETag'].should.equal "\"65a8e27d8879283831b664bd8b7f0ad4\""
10
+ end
11
+
12
+ specify "does not change ETag if it is already set" do
13
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'ETag' => '"abc"'}, "Hello, World!"] }
14
+ response = Rack::ETag.new(app).call({})
15
+ response[1]['ETag'].should.equal "\"abc\""
16
+ end
17
+
18
+ specify "does not set ETag if steaming body" do
19
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello", "World"]] }
20
+ response = Rack::ETag.new(app).call({})
21
+ response[1]['ETag'].should.equal nil
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ require 'test/spec'
2
+ require 'rack/mock'
3
+ require 'rack/contrib/evil'
4
+ require 'erb'
5
+
6
+ context "Rack::Evil" do
7
+ app = lambda do |env|
8
+ template = ERB.new("<%= throw :response, [404, {'Content-Type' => 'text/html'}, 'Never know where it comes from'] %>")
9
+ [200, {'Content-Type' => 'text/plain'}, template.result(binding)]
10
+ end
11
+
12
+ specify "should enable the app to return the response from anywhere" do
13
+ status, headers, body = Rack::Evil.new(app).call({})
14
+
15
+ status.should.equal 404
16
+ headers['Content-Type'].should.equal 'text/html'
17
+ body.should.equal 'Never know where it comes from'
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ require 'test/spec'
2
+ require 'rack/mock'
3
+ require 'rack/contrib/garbagecollector'
4
+
5
+ context 'Rack::GarbageCollector' do
6
+
7
+ specify 'starts the garbage collector after each request' do
8
+ app = lambda { |env|
9
+ [200, {'Content-Type'=>'text/plain'}, ['Hello World']] }
10
+ Rack::GarbageCollector.new(app).call({})
11
+ end
12
+
13
+ end
@@ -0,0 +1,21 @@
1
+ require 'test/spec'
2
+ require 'rack/mock'
3
+ require 'rack/contrib/jsonp'
4
+
5
+ context "Rack::JSONP" do
6
+
7
+ specify "should wrap the response body in the Javascript callback when provided" do
8
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, '{"bar":"foo"}'] }
9
+ request = Rack::MockRequest.env_for("/", :input => "foo=bar&callback=foo")
10
+ body = Rack::JSONP.new(app).call(request).last
11
+ body.should == 'foo({"bar":"foo"})'
12
+ end
13
+
14
+ specify "should not change anything if no :callback param is provided" do
15
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, '{"bar":"foo"}'] }
16
+ request = Rack::MockRequest.env_for("/", :input => "foo=bar")
17
+ body = Rack::JSONP.new(app).call(request).last
18
+ body.should == '{"bar":"foo"}'
19
+ end
20
+
21
+ end
@@ -0,0 +1,16 @@
1
+ require 'test/spec'
2
+ require 'rack/mock'
3
+ require 'rack/contrib/lighttpd_script_name_fix'
4
+
5
+ context "Rack::LighttpdScriptNameFix" do
6
+ specify "corrects SCRIPT_NAME and PATH_INFO set by lighttpd " do
7
+ env = {
8
+ "PATH_INFO" => "/foo/bar/baz",
9
+ "SCRIPT_NAME" => "/hello"
10
+ }
11
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, World!"] }
12
+ response = Rack::LighttpdScriptNameFix.new(app).call(env)
13
+ env['SCRIPT_NAME'].should.be.empty
14
+ env['PATH_INFO'].should.equal '/hello/foo/bar/baz'
15
+ end
16
+ end