rtomayko-rack-contrib 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/COPYING ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2008 The Committers
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,54 @@
1
+ = Contributed Rack Middleware and Utilities
2
+
3
+ This package includes a variety of add-on components for Rack, a Ruby web server
4
+ interface:
5
+
6
+ * Rack::ETag - Automatically sets the ETag header on all String bodies.
7
+ * Rack::JSONP - Adds JSON-P support by stripping out the callback param
8
+ and padding the response with the appropriate callback format.
9
+ * Rack::LighttpdScriptNameFix - Fixes how lighttpd sets the SCRIPT_NAME
10
+ and PATH_INFO variables in certain configurations.
11
+ * Rack::Locale - Detects the client locale using the Accept-Language request
12
+ header and sets a rack.locale variable in the environment.
13
+ * Rack::MailExceptions - Rescues exceptions raised from the app and
14
+ sends a useful email with the exception, stacktrace, and contents of the
15
+ environment.
16
+ * Rack::PostBodyContentTypeParser - Adds support for JSON request bodies. The
17
+ Rack parameter hash is populated by deserializing the JSON data provided in
18
+ the request body when the Content-Type is application/json.
19
+ * Rack::Profiler - Uses ruby-prof to measure request time.
20
+ * Rack::Sendfile - Enables X-Sendfile support for bodies that can be served
21
+ from file.
22
+ * Rack::TimeZone - Detects the clients timezone using JavaScript and sets
23
+ a variable in Rack's environment with the offset from UTC.
24
+
25
+ === Use
26
+
27
+ Git is the quickest way to the rack-contrib sources:
28
+
29
+ git clone git://github.com/rtomayko/rack-contrib.git
30
+
31
+ Gems are currently available from GitHub clones:
32
+
33
+ gem install rtomayko-rack-contrib --source=http://gems.github.com/
34
+
35
+ Requiring 'rack/contrib' will add autoloads to the Rack modules for all of the
36
+ components included. The following example shows what a simple rackup
37
+ (+config.ru+) file might look like:
38
+
39
+ require 'rack'
40
+ require 'rack/contrib'
41
+
42
+ use Rack::Profiler if ENV['RACK_ENV'] == 'development'
43
+
44
+ use Rack::ETag
45
+ use Rack::MailExceptions
46
+
47
+ run theapp
48
+
49
+ === Links
50
+
51
+ rack-contrib on GitHub:: <http://github.com/rtomayko/rack-contrib>
52
+ Rack:: <http://rack.rubyforge.org/>
53
+ Rack On GitHub:: <http://github.org/chneukirchen/rack>
54
+ rack-devel mailing list:: <http://groups.google.com/group/rack-devel>
@@ -0,0 +1,84 @@
1
+ # Rakefile for Rack::Contrib. -*-ruby-*-
2
+ require 'rake/rdoctask'
3
+ require 'rake/testtask'
4
+
5
+ desc "Run all the tests"
6
+ task :default => [:test]
7
+
8
+ desc "Generate RDox"
9
+ task "RDOX" do
10
+ sh "specrb -Ilib:test -a --rdox >RDOX"
11
+ end
12
+
13
+ desc "Run all the fast tests"
14
+ task :test do
15
+ sh "specrb -Ilib:test -w #{ENV['TEST'] || '-a'} #{ENV['TESTOPTS']}"
16
+ end
17
+
18
+ desc "Run all the tests"
19
+ task :fulltest do
20
+ sh "specrb -Ilib:test -w #{ENV['TEST'] || '-a'} #{ENV['TESTOPTS']}"
21
+ end
22
+
23
+ desc "Generate RDoc documentation"
24
+ Rake::RDocTask.new(:rdoc) do |rdoc|
25
+ rdoc.options << '--line-numbers' << '--inline-source' <<
26
+ '--main' << 'README' <<
27
+ '--title' << 'Rack Contrib Documentation' <<
28
+ '--charset' << 'utf-8'
29
+ rdoc.rdoc_dir = "doc"
30
+ rdoc.rdoc_files.include 'README.rdoc'
31
+ rdoc.rdoc_files.include 'RDOX'
32
+ rdoc.rdoc_files.include('lib/rack/*.rb')
33
+ rdoc.rdoc_files.include('lib/rack/*/*.rb')
34
+ end
35
+ task :rdoc => ["RDOX"]
36
+
37
+
38
+ # PACKAGING =================================================================
39
+
40
+ # load gemspec like github's gem builder to surface any SAFE issues.
41
+ require 'rubygems/specification'
42
+ $spec = eval(File.read('rack-contrib.gemspec'))
43
+
44
+ def package(ext='')
45
+ "pkg/rack-contrib-#{$spec.version}" + ext
46
+ end
47
+
48
+ desc 'Build packages'
49
+ task :package => %w[.gem .tar.gz].map {|e| package(e)}
50
+
51
+ desc 'Build and install as local gem'
52
+ task :install => package('.gem') do
53
+ sh "gem install #{package('.gem')}"
54
+ end
55
+
56
+ directory 'pkg/'
57
+
58
+ file package('.gem') => %w[pkg/ rack-contrib.gemspec] + $spec.files do |f|
59
+ sh "gem build rack-contrib.gemspec"
60
+ mv File.basename(f.name), f.name
61
+ end
62
+
63
+ file package('.tar.gz') => %w[pkg/] + $spec.files do |f|
64
+ sh "git archive --format=tar HEAD | gzip > #{f.name}"
65
+ end
66
+
67
+ # GEMSPEC ===================================================================
68
+
69
+ file 'rack-contrib.gemspec' => FileList['{lib,test}/**','Rakefile', 'README.rdoc'] do |f|
70
+ # read spec file and split out manifest section
71
+ spec = File.read(f.name)
72
+ parts = spec.split(" # = MANIFEST =\n")
73
+ fail 'bad spec' if parts.length != 3
74
+ # determine file list from git ls-files
75
+ files = `git ls-files`.
76
+ split("\n").sort.reject{ |file| file =~ /^\./ }.
77
+ map{ |file| " #{file}" }.join("\n")
78
+ # piece file back together and write...
79
+ parts[1] = " s.files = %w[\n#{files}\n ]\n"
80
+ spec = parts.join(" # = MANIFEST =\n")
81
+ spec.sub!(/s.date = '.*'/, "s.date = '#{Time.now.strftime("%Y-%m-%d")}'")
82
+ File.open(f.name, 'w') { |io| io.write(spec) }
83
+ puts "updated #{f.name}"
84
+ end
@@ -0,0 +1,18 @@
1
+ require 'rack'
2
+ module Rack
3
+ module Contrib
4
+ def self.release
5
+ "0.4"
6
+ end
7
+ end
8
+
9
+ autoload :ETag, "rack/etag"
10
+ autoload :JSONP, "rack/jsonp"
11
+ autoload :LighttpdScriptNameFix, "rack/lighttpd_script_name_fix"
12
+ autoload :Locale, "rack/locale"
13
+ autoload :MailExceptions, "rack/mailexceptions"
14
+ autoload :PostBodyContentTypeParser, "rack/post_body_content_type_parser"
15
+ autoload :Profiler, "rack/profiler"
16
+ autoload :Sendfile, "rack/sendfile"
17
+ autoload :TimeZone, "rack/time_zone"
18
+ end
@@ -0,0 +1,20 @@
1
+ require 'digest/md5'
2
+
3
+ module Rack
4
+ # Automatically sets the ETag header on all String bodies
5
+ class ETag
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ status, headers, body = @app.call(env)
12
+
13
+ if !headers.has_key?('ETag') && body.is_a?(String)
14
+ headers['ETag'] = %("#{Digest::MD5.hexdigest(body)}")
15
+ end
16
+
17
+ [status, headers, body]
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,38 @@
1
+ module Rack
2
+
3
+ # A Rack middleware for providing JSON-P support.
4
+ #
5
+ # Full credit to Flinn Mueller (http://actsasflinn.com/) for this contribution.
6
+ #
7
+ class JSONP
8
+
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ # Proxies the request to the application, stripping out the JSON-P callback
14
+ # method and padding the response with the appropriate callback format.
15
+ #
16
+ # Changes nothing if no <tt>callback</tt> param is specified.
17
+ #
18
+ def call(env)
19
+ status, headers, response = @app.call(env)
20
+ request = Rack::Request.new(env)
21
+ response = pad(request.params.delete('callback'), response) if request.params.include?('callback')
22
+ [status, headers, response]
23
+ end
24
+
25
+ # Pads the response with the appropriate callback format according to the
26
+ # JSON-P spec/requirements.
27
+ #
28
+ # The Rack response spec indicates that it should be enumerable. The method
29
+ # of combining all of the data into a sinle string makes sense since JSON
30
+ # is returned as a full string.
31
+ #
32
+ def pad(callback, response, body = "")
33
+ response.each{ |s| body << s }
34
+ "#{callback}(#{body})"
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,16 @@
1
+ module Rack
2
+ # Lighttpd sets the wrong SCRIPT_NAME and PATH_INFO if you mount your
3
+ # FastCGI app at "/". This middleware fixes this issue.
4
+
5
+ class LighttpdScriptNameFix
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ env["PATH_INFO"] = env["SCRIPT_NAME"].to_s + env["PATH_INFO"].to_s
12
+ env["SCRIPT_NAME"] = ""
13
+ @app.call(env)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,31 @@
1
+ require 'i18n'
2
+
3
+ module Rack
4
+ class Locale
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ old_locale = I18n.locale
11
+ locale = nil
12
+
13
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
14
+ if lang = env["HTTP_ACCEPT_LANGUAGE"]
15
+ lang = lang.split(",").map { |l|
16
+ l += ';q=1.0' unless l =~ /;q=\d+\.\d+$/
17
+ l.split(';q=')
18
+ }.first
19
+ locale = lang.first.split("-").first
20
+ else
21
+ locale = I18n.default_locale
22
+ end
23
+
24
+ locale = env['rack.locale'] = I18n.locale = locale.to_s
25
+ status, headers, body = @app.call(env)
26
+ headers['Content-Language'] = locale
27
+ I18n.locale = old_locale
28
+ [status, headers, body]
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,120 @@
1
+ require 'net/smtp'
2
+ require 'tmail'
3
+ require 'erb'
4
+
5
+ module Rack
6
+ # Catches all exceptions raised from the app it wraps and
7
+ # sends a useful email with the exception, stacktrace, and
8
+ # contents of the environment.
9
+
10
+ class MailExceptions
11
+ attr_reader :config
12
+
13
+ def initialize(app)
14
+ @app = app
15
+ @config = {
16
+ :to => nil,
17
+ :from => ENV['USER'] || 'rack',
18
+ :subject => '[exception] %s',
19
+ :smtp => {
20
+ :server => 'localhost',
21
+ :domain => 'localhost',
22
+ :port => 25,
23
+ :authentication => :login,
24
+ :user_name => nil,
25
+ :password => nil
26
+ }
27
+ }
28
+ @template = ERB.new(TEMPLATE)
29
+ yield self if block_given?
30
+ end
31
+
32
+ def call(env)
33
+ status, headers, body =
34
+ begin
35
+ @app.call(env)
36
+ rescue => boom
37
+ # TODO don't allow exceptions from send_notification to
38
+ # propogate
39
+ send_notification boom, env
40
+ raise
41
+ end
42
+ send_notification env['mail.exception'], env if env['mail.exception']
43
+ [status, headers, body]
44
+ end
45
+
46
+ %w[to from subject].each do |meth|
47
+ define_method(meth) { |value| @config[meth.to_sym] = value }
48
+ end
49
+
50
+ def smtp(settings={})
51
+ @config[:smtp].merge! settings
52
+ end
53
+
54
+ private
55
+ def generate_mail(exception, env)
56
+ mail = TMail::Mail.new
57
+ mail.to = Array(config[:to])
58
+ mail.from = config[:from]
59
+ mail.subject = config[:subject] % [exception.to_s]
60
+ mail.date = Time.now
61
+ mail.set_content_type 'text/plain'
62
+ mail.charset = 'UTF-8'
63
+ mail.body = @template.result(binding)
64
+ mail
65
+ end
66
+
67
+ def send_notification(exception, env)
68
+ mail = generate_mail(exception, env)
69
+ smtp = config[:smtp]
70
+ env['mail.sent'] = true
71
+ return if smtp[:server] == 'example.com'
72
+
73
+ Net::SMTP.start smtp[:address], smtp[:port], smtp[:domain], smtp[:user_name], smtp[:password], smtp[:authentication] do |server|
74
+ mail.to.each do |recipient|
75
+ server.send_message mail.to_s, mail.from, recipient
76
+ end
77
+ end
78
+ end
79
+
80
+ def extract_body(env)
81
+ if io = env['rack.input']
82
+ io.rewind if io.respond_to?(:rewind)
83
+ io.read
84
+ end
85
+ end
86
+
87
+ TEMPLATE = (<<-'EMAIL').gsub(/^ {4}/, '')
88
+ A <%= exception.class.to_s %> occured: <%= exception.to_s %>
89
+ <% if body = extract_body(env) %>
90
+
91
+ ===================================================================
92
+ Request Body:
93
+ ===================================================================
94
+
95
+ <%= body.gsub(/^/, ' ') %>
96
+ <% end %>
97
+
98
+ ===================================================================
99
+ Rack Environment:
100
+ ===================================================================
101
+
102
+ PID: <%= $$ %>
103
+ PWD: <%= Dir.getwd %>
104
+
105
+ <%= env.to_a.
106
+ sort{|a,b| a.first <=> b.first}.
107
+ map{ |k,v| "%-25s%p" % [k+':', v] }.
108
+ join("\n ") %>
109
+
110
+ <% if exception.respond_to?(:backtrace) %>
111
+ ===================================================================
112
+ Backtrace:
113
+ ===================================================================
114
+
115
+ <%= exception.backtrace.join("\n ") %>
116
+ <% end %>
117
+ EMAIL
118
+
119
+ end
120
+ end
@@ -0,0 +1,40 @@
1
+ begin
2
+ require 'json'
3
+ rescue LoadError => e
4
+ require 'json/pure'
5
+ end
6
+
7
+ module Rack
8
+
9
+ # A Rack middleware for parsing POST/PUT body data when Content-Type is
10
+ # not one of the standard supported types, like <tt>application/json</tt>.
11
+ #
12
+ # TODO: Find a better name.
13
+ #
14
+ class PostBodyContentTypeParser
15
+
16
+ # Constants
17
+ #
18
+ CONTENT_TYPE = 'CONTENT_TYPE'.freeze
19
+ POST_BODY = 'rack.input'.freeze
20
+ FORM_INPUT = 'rack.request.form_input'.freeze
21
+ FORM_HASH = 'rack.request.form_hash'.freeze
22
+
23
+ # Supported Content-Types
24
+ #
25
+ APPLICATION_JSON = 'application/json'.freeze
26
+
27
+ def initialize(app)
28
+ @app = app
29
+ end
30
+
31
+ def call(env)
32
+ case env[CONTENT_TYPE]
33
+ when APPLICATION_JSON
34
+ env.update(FORM_HASH => JSON.parse(env[POST_BODY].read), FORM_INPUT => env[POST_BODY])
35
+ end
36
+ @app.call(env)
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,80 @@
1
+ gem 'ruby-prof', '>= 0.7.1'
2
+ require 'ruby-prof'
3
+ require 'set'
4
+
5
+ module Rack
6
+ # Set the profile=process_time query parameter to download a
7
+ # calltree profile of the request.
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
+ ).to_set
18
+
19
+ PRINTER_CONTENT_TYPE = {
20
+ RubyProf::FlatPrinter => 'text/plain',
21
+ RubyProf::GraphPrinter => 'text/plain',
22
+ RubyProf::GraphHtmlPrinter => 'text/html',
23
+ RubyProf::CallTreePrinter => 'application/octet-stream'
24
+ }
25
+
26
+ def initialize(app, printer = RubyProf::GraphHtmlPrinter)
27
+ @app = app
28
+ @printer = printer
29
+ end
30
+
31
+ def call(env)
32
+ if mode = profiling?(env)
33
+ profile(env, mode)
34
+ else
35
+ @app.call(env)
36
+ end
37
+ end
38
+
39
+ private
40
+ def profiling?(env)
41
+ unless RubyProf.running?
42
+ request = Rack::Request.new(env)
43
+ if mode = request.params.delete('profile')
44
+ if MODES.include?(mode)
45
+ mode
46
+ else
47
+ env['rack.errors'].write "Invalid RubyProf measure_mode: " +
48
+ "#{mode}. Use one of #{MODES.to_a.join(', ')}"
49
+ false
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ def profile(env, mode)
56
+ RubyProf.measure_mode = RubyProf.const_get(mode.upcase)
57
+ result = RubyProf.profile { @app.call(env) }
58
+ headers = headers(@printer, env, mode)
59
+ body = print(@printer, result)
60
+ [200, headers, body]
61
+ end
62
+
63
+ def print(printer, result)
64
+ body = StringIO.new
65
+ printer.new(result).print(body, :min_percent => 0.01)
66
+ body.rewind
67
+ body
68
+ end
69
+
70
+ def headers(printer, env, mode)
71
+ headers = { 'Content-Type' => PRINTER_CONTENT_TYPE[printer] }
72
+ if printer == RubyProf::CallTreePrinter
73
+ filename = ::File.basename(env['PATH_INFO'])
74
+ headers['Content-Disposition'] = "attachment; " +
75
+ "filename=\"#{filename}.#{mode}.tree\")"
76
+ end
77
+ headers
78
+ end
79
+ end
80
+ 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,54 @@
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.4.0'
7
+ s.date = '2008-12-09'
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/etag.rb
22
+ lib/rack/jsonp.rb
23
+ lib/rack/lighttpd_script_name_fix.rb
24
+ lib/rack/locale.rb
25
+ lib/rack/mailexceptions.rb
26
+ lib/rack/post_body_content_type_parser.rb
27
+ lib/rack/profiler.rb
28
+ lib/rack/sendfile.rb
29
+ lib/rack/time_zone.rb
30
+ rack-contrib.gemspec
31
+ test/mail_settings.rb
32
+ test/spec_rack_contrib.rb
33
+ test/spec_rack_etag.rb
34
+ test/spec_rack_jsonp.rb
35
+ test/spec_rack_lighttpd_script_name_fix.rb
36
+ test/spec_rack_mailexceptions.rb
37
+ test/spec_rack_post_body_content_type_parser.rb
38
+ test/spec_rack_sendfile.rb
39
+ ]
40
+ # = MANIFEST =
41
+
42
+ s.test_files = s.files.select {|path| path =~ /^test\/spec_.*\.rb/}
43
+
44
+ s.extra_rdoc_files = %w[README.rdoc COPYING]
45
+ s.add_dependency 'rack', '~> 0.4'
46
+ s.add_dependency 'tmail', '>= 1.2'
47
+ s.add_dependency 'json', '>= 1.1'
48
+
49
+ s.has_rdoc = true
50
+ s.homepage = "http://github.com/rtomayko/rack-contrib/"
51
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "rack-contrib", "--main", "README"]
52
+ s.require_paths = %w[lib]
53
+ s.rubygems_version = '1.1.1'
54
+ 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,7 @@
1
+ require 'rack/contrib'
2
+
3
+ context "Rack::Contrib" do
4
+ specify "should expose release" do
5
+ Rack::Contrib.should.respond_to :release
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ require 'rack/mock'
2
+ require 'rack/etag'
3
+
4
+ context "Rack::ETag" do
5
+ specify "sets ETag if none is set" do
6
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, World!"] }
7
+ response = Rack::ETag.new(app).call({})
8
+ response[1]['ETag'].should.equal "\"65a8e27d8879283831b664bd8b7f0ad4\""
9
+ end
10
+
11
+ specify "does not change ETag if it is already set" do
12
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'ETag' => '"abc"'}, "Hello, World!"] }
13
+ response = Rack::ETag.new(app).call({})
14
+ response[1]['ETag'].should.equal "\"abc\""
15
+ end
16
+
17
+ specify "does not set ETag if steaming body" do
18
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello", "World"]] }
19
+ response = Rack::ETag.new(app).call({})
20
+ response[1]['ETag'].should.equal nil
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ require 'rack/mock'
2
+ require 'rack/jsonp'
3
+
4
+ context "Rack::JSONP" do
5
+
6
+ specify "should wrap the response body in the Javascript callback when provided" do
7
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, '{"bar":"foo"}'] }
8
+ request = Rack::MockRequest.env_for("/", :input => "foo=bar&callback=foo")
9
+ body = Rack::JSONP.new(app).call(request).last
10
+ body.should == 'foo({"bar":"foo"})'
11
+ end
12
+
13
+ specify "should not change anything if no :callback param is provided" do
14
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, '{"bar":"foo"}'] }
15
+ request = Rack::MockRequest.env_for("/", :input => "foo=bar")
16
+ body = Rack::JSONP.new(app).call(request).last
17
+ body.should == '{"bar":"foo"}'
18
+ end
19
+
20
+ end
@@ -0,0 +1,15 @@
1
+ require 'rack/mock'
2
+ require 'rack/lighttpd_script_name_fix'
3
+
4
+ context "Rack::LighttpdScriptNameFix" do
5
+ specify "corrects SCRIPT_NAME and PATH_INFO set by lighttpd " do
6
+ env = {
7
+ "PATH_INFO" => "/foo/bar/baz",
8
+ "SCRIPT_NAME" => "/hello"
9
+ }
10
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, World!"] }
11
+ response = Rack::LighttpdScriptNameFix.new(app).call(env)
12
+ env['SCRIPT_NAME'].should.be.empty
13
+ env['PATH_INFO'].should.equal '/hello/foo/bar/baz'
14
+ end
15
+ end
@@ -0,0 +1,96 @@
1
+ require 'rack/mock'
2
+
3
+ begin
4
+ require 'tmail'
5
+ require 'rack/mailexceptions'
6
+
7
+ require File.dirname(__FILE__) + '/mail_settings.rb'
8
+
9
+ class TestError < RuntimeError
10
+ end
11
+
12
+ def test_exception
13
+ raise TestError, 'Suffering Succotash!'
14
+ rescue => boom
15
+ return boom
16
+ end
17
+
18
+ context 'Rack::MailExceptions' do
19
+
20
+ setup do
21
+ @app = lambda { |env| raise TestError, 'Why, I say' }
22
+ @env = Rack::MockRequest.env_for("/foo",
23
+ 'FOO' => 'BAR',
24
+ :method => 'GET',
25
+ :input => 'THE BODY'
26
+ )
27
+ @smtp_settings = {
28
+ :server => 'example.com',
29
+ :domain => 'example.com',
30
+ :port => 500,
31
+ :authentication => :login,
32
+ :user_name => 'joe',
33
+ :password => 'secret'
34
+ }
35
+ end
36
+
37
+ specify 'yields a configuration object to the block when created' do
38
+ called = false
39
+ mailer =
40
+ Rack::MailExceptions.new(@app) do |mail|
41
+ called = true
42
+ mail.to 'foo@example.org'
43
+ mail.from 'bar@example.org'
44
+ mail.subject '[ERROR] %s'
45
+ mail.smtp @smtp_settings
46
+ end
47
+ called.should.be == true
48
+ end
49
+
50
+ specify 'generates a TMail object with configured settings' do
51
+ mailer =
52
+ Rack::MailExceptions.new(@app) do |mail|
53
+ mail.to 'foo@example.org'
54
+ mail.from 'bar@example.org'
55
+ mail.subject '[ERROR] %s'
56
+ mail.smtp @smtp_settings
57
+ end
58
+
59
+ tmail = mailer.send(:generate_mail, test_exception, @env)
60
+ tmail.to.should.equal ['foo@example.org']
61
+ tmail.from.should.equal ['bar@example.org']
62
+ tmail.subject.should.equal '[ERROR] Suffering Succotash!'
63
+ tmail.body.should.not.be.nil
64
+ tmail.body.should.be =~ /FOO:\s+"BAR"/
65
+ tmail.body.should.be =~ /^\s*THE BODY\s*$/
66
+ end
67
+
68
+ specify 'catches exceptions raised from app, sends mail, and re-raises' do
69
+ mailer =
70
+ Rack::MailExceptions.new(@app) do |mail|
71
+ mail.to 'foo@example.org'
72
+ mail.from 'bar@example.org'
73
+ mail.subject '[ERROR] %s'
74
+ mail.smtp @smtp_settings
75
+ end
76
+ lambda { mailer.call(@env) }.should.raise(TestError)
77
+ @env['mail.sent'].should.be == true
78
+ end
79
+
80
+ if TEST_SMTP && ! TEST_SMTP.empty?
81
+ specify 'sends mail' do
82
+ mailer =
83
+ Rack::MailExceptions.new(@app) do |mail|
84
+ mail.config.merge! TEST_SMTP
85
+ end
86
+ lambda { mailer.call(@env) }.should.raise(TestError)
87
+ @env['mail.sent'].should.be == true
88
+ end
89
+ else
90
+ STDERR.puts 'WARN: Skipping SMTP tests (edit test/mail_settings.rb to enable)'
91
+ end
92
+
93
+ end
94
+ rescue LoadError => boom
95
+ STDERR.puts "WARN: Skipping Rack::MailExceptions tests (tmail not installed)"
96
+ end
@@ -0,0 +1,31 @@
1
+ require 'rack/mock'
2
+
3
+ begin
4
+ require 'rack/post_body_content_type_parser'
5
+
6
+ context "Rack::PostBodyContentTypeParser" do
7
+
8
+ specify "should handle requests with POST body Content-Type of application/json" do
9
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, Rack::Request.new(env).POST] }
10
+ env = env_for_post_with_headers('/', {'Content_Type'.upcase => 'application/json'}, {:body => "asdf", :status => "12"}.to_json)
11
+ body = Rack::PostBodyContentTypeParser.new(app).call(env).last
12
+ body['body'].should.equal "asdf"
13
+ body['status'].should.equal "12"
14
+ end
15
+
16
+ specify "should change nothing when the POST body content type isn't application/json" do
17
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, Rack::Request.new(env).POST] }
18
+ body = app.call(Rack::MockRequest.env_for("/", :input => "body=asdf&status=12")).last
19
+ body['body'].should.equal "asdf"
20
+ body['status'].should.equal "12"
21
+ end
22
+
23
+ end
24
+
25
+ def env_for_post_with_headers(path, headers, body)
26
+ Rack::MockRequest.env_for(path, {:method => "POST", :input => body}.merge(headers))
27
+ end
28
+ rescue LoadError => e
29
+ # Missing dependency JSON, skipping tests.
30
+ STDERR.puts "WARN: Skipping Rack::PostBodyContentTypeParser tests (json not installed)"
31
+ end
@@ -0,0 +1,85 @@
1
+ require 'rack/mock'
2
+ require 'rack/sendfile'
3
+
4
+ context "Rack::File" do
5
+ specify "should respond to #to_path" do
6
+ Rack::File.new(Dir.pwd).should.respond_to :to_path
7
+ end
8
+ end
9
+
10
+ context "Rack::Sendfile" do
11
+ def sendfile_body
12
+ res = ['Hello World']
13
+ def res.to_path ; "/tmp/hello.txt" ; end
14
+ res
15
+ end
16
+
17
+ def simple_app(body=sendfile_body)
18
+ lambda { |env| [200, {'Content-Type' => 'text/plain'}, body] }
19
+ end
20
+
21
+ def sendfile_app(body=sendfile_body)
22
+ Rack::Sendfile.new(simple_app(body))
23
+ end
24
+
25
+ setup do
26
+ @request = Rack::MockRequest.new(sendfile_app)
27
+ end
28
+
29
+ def request(headers={})
30
+ yield @request.get('/', headers)
31
+ end
32
+
33
+ specify "does nothing when no X-Sendfile-Type header present" do
34
+ request do |response|
35
+ response.should.be.ok
36
+ response.body.should.equal 'Hello World'
37
+ response.headers.should.not.include 'X-Sendfile'
38
+ end
39
+ end
40
+
41
+ specify "sets X-Sendfile response header and discards body" do
42
+ request 'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile' do |response|
43
+ response.should.be.ok
44
+ response.body.should.be.empty
45
+ response.headers['X-Sendfile'].should.equal '/tmp/hello.txt'
46
+ end
47
+ end
48
+
49
+ specify "sets X-Lighttpd-Send-File response header and discards body" do
50
+ request 'HTTP_X_SENDFILE_TYPE' => 'X-Lighttpd-Send-File' do |response|
51
+ response.should.be.ok
52
+ response.body.should.be.empty
53
+ response.headers['X-Lighttpd-Send-File'].should.equal '/tmp/hello.txt'
54
+ end
55
+ end
56
+
57
+ specify "sets X-Accel-Redirect response header and discards body" do
58
+ headers = {
59
+ 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect',
60
+ 'HTTP_X_ACCEL_MAPPING' => '/tmp/=/foo/bar/'
61
+ }
62
+ request headers do |response|
63
+ response.should.be.ok
64
+ response.body.should.be.empty
65
+ response.headers['X-Accel-Redirect'].should.equal '/foo/bar/hello.txt'
66
+ end
67
+ end
68
+
69
+ specify 'writes to rack.error when no X-Accel-Mapping is specified' do
70
+ request 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect' do |response|
71
+ response.should.be.ok
72
+ response.body.should.equal 'Hello World'
73
+ response.headers.should.not.include 'X-Accel-Redirect'
74
+ response.errors.should.include 'X-Accel-Mapping'
75
+ end
76
+ end
77
+
78
+ specify 'does nothing when body does not respond to #to_path' do
79
+ @request = Rack::MockRequest.new(sendfile_app(['Not a file...']))
80
+ request 'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile' do |response|
81
+ response.body.should.equal 'Not a file...'
82
+ response.headers.should.not.include 'X-Sendfile'
83
+ end
84
+ end
85
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rtomayko-rack-contrib
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - rack-devel
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-12-09 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rack
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: "0.4"
23
+ version:
24
+ - !ruby/object:Gem::Dependency
25
+ name: tmail
26
+ version_requirement:
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: "1.2"
32
+ version:
33
+ - !ruby/object:Gem::Dependency
34
+ name: json
35
+ version_requirement:
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: "1.1"
41
+ version:
42
+ description: Contributed Rack Middleware and Utilities
43
+ email: rack-devel@googlegroups.com
44
+ executables: []
45
+
46
+ extensions: []
47
+
48
+ extra_rdoc_files:
49
+ - README.rdoc
50
+ - COPYING
51
+ files:
52
+ - COPYING
53
+ - README.rdoc
54
+ - Rakefile
55
+ - lib/rack/contrib.rb
56
+ - lib/rack/etag.rb
57
+ - lib/rack/jsonp.rb
58
+ - lib/rack/lighttpd_script_name_fix.rb
59
+ - lib/rack/locale.rb
60
+ - lib/rack/mailexceptions.rb
61
+ - lib/rack/post_body_content_type_parser.rb
62
+ - lib/rack/profiler.rb
63
+ - lib/rack/sendfile.rb
64
+ - lib/rack/time_zone.rb
65
+ - rack-contrib.gemspec
66
+ - test/mail_settings.rb
67
+ - test/spec_rack_contrib.rb
68
+ - test/spec_rack_etag.rb
69
+ - test/spec_rack_jsonp.rb
70
+ - test/spec_rack_lighttpd_script_name_fix.rb
71
+ - test/spec_rack_mailexceptions.rb
72
+ - test/spec_rack_post_body_content_type_parser.rb
73
+ - test/spec_rack_sendfile.rb
74
+ has_rdoc: true
75
+ homepage: http://github.com/rtomayko/rack-contrib/
76
+ post_install_message:
77
+ rdoc_options:
78
+ - --line-numbers
79
+ - --inline-source
80
+ - --title
81
+ - rack-contrib
82
+ - --main
83
+ - README
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: "0"
91
+ version:
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: "0"
97
+ version:
98
+ requirements: []
99
+
100
+ rubyforge_project:
101
+ rubygems_version: 1.2.0
102
+ signing_key:
103
+ specification_version: 2
104
+ summary: Contributed Rack Middleware and Utilities
105
+ test_files:
106
+ - test/spec_rack_contrib.rb
107
+ - test/spec_rack_etag.rb
108
+ - test/spec_rack_jsonp.rb
109
+ - test/spec_rack_lighttpd_script_name_fix.rb
110
+ - test/spec_rack_mailexceptions.rb
111
+ - test/spec_rack_post_body_content_type_parser.rb
112
+ - test/spec_rack_sendfile.rb