rack-contrib 0.9.0

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