rack-rack-contrib 0.9.1

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.
Files changed (55) hide show
  1. data/COPYING +18 -0
  2. data/README.rdoc +74 -0
  3. data/Rakefile +97 -0
  4. data/lib/rack/contrib.rb +32 -0
  5. data/lib/rack/contrib/accept_format.rb +44 -0
  6. data/lib/rack/contrib/backstage.rb +20 -0
  7. data/lib/rack/contrib/bounce_favicon.rb +16 -0
  8. data/lib/rack/contrib/callbacks.rb +37 -0
  9. data/lib/rack/contrib/config.rb +16 -0
  10. data/lib/rack/contrib/csshttprequest.rb +39 -0
  11. data/lib/rack/contrib/deflect.rb +137 -0
  12. data/lib/rack/contrib/etag.rb +20 -0
  13. data/lib/rack/contrib/evil.rb +12 -0
  14. data/lib/rack/contrib/garbagecollector.rb +14 -0
  15. data/lib/rack/contrib/jsonp.rb +41 -0
  16. data/lib/rack/contrib/lighttpd_script_name_fix.rb +16 -0
  17. data/lib/rack/contrib/locale.rb +31 -0
  18. data/lib/rack/contrib/mailexceptions.rb +120 -0
  19. data/lib/rack/contrib/nested_params.rb +143 -0
  20. data/lib/rack/contrib/not_found.rb +18 -0
  21. data/lib/rack/contrib/post_body_content_type_parser.rb +40 -0
  22. data/lib/rack/contrib/proctitle.rb +30 -0
  23. data/lib/rack/contrib/profiler.rb +106 -0
  24. data/lib/rack/contrib/relative_redirect.rb +44 -0
  25. data/lib/rack/contrib/response_cache.rb +59 -0
  26. data/lib/rack/contrib/route_exceptions.rb +48 -0
  27. data/lib/rack/contrib/sendfile.rb +142 -0
  28. data/lib/rack/contrib/signals.rb +63 -0
  29. data/lib/rack/contrib/time_zone.rb +25 -0
  30. data/rack-contrib.gemspec +87 -0
  31. data/test/404.html +1 -0
  32. data/test/Maintenance.html +1 -0
  33. data/test/mail_settings.rb +12 -0
  34. data/test/spec_rack_accept_format.rb +41 -0
  35. data/test/spec_rack_backstage.rb +26 -0
  36. data/test/spec_rack_callbacks.rb +65 -0
  37. data/test/spec_rack_config.rb +22 -0
  38. data/test/spec_rack_contrib.rb +8 -0
  39. data/test/spec_rack_csshttprequest.rb +66 -0
  40. data/test/spec_rack_deflect.rb +107 -0
  41. data/test/spec_rack_etag.rb +23 -0
  42. data/test/spec_rack_evil.rb +19 -0
  43. data/test/spec_rack_garbagecollector.rb +13 -0
  44. data/test/spec_rack_jsonp.rb +34 -0
  45. data/test/spec_rack_lighttpd_script_name_fix.rb +16 -0
  46. data/test/spec_rack_mailexceptions.rb +97 -0
  47. data/test/spec_rack_nested_params.rb +46 -0
  48. data/test/spec_rack_not_found.rb +17 -0
  49. data/test/spec_rack_post_body_content_type_parser.rb +32 -0
  50. data/test/spec_rack_proctitle.rb +26 -0
  51. data/test/spec_rack_profiler.rb +37 -0
  52. data/test/spec_rack_relative_redirect.rb +78 -0
  53. data/test/spec_rack_response_cache.rb +137 -0
  54. data/test/spec_rack_sendfile.rb +86 -0
  55. metadata +171 -0
@@ -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,12 @@
1
+ module Rack
2
+ class Evil
3
+ # Lets you return a response to the client immediately from anywhere ( M V or C ) in the code.
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ catch(:response) { @app.call(env) }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ module Rack
2
+ # Forces garbage collection after each request.
3
+ class GarbageCollector
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ @app.call(env)
10
+ ensure
11
+ GC.start
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,41 @@
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
+ if request.params.include?('callback')
22
+ response = pad(request.params.delete('callback'), response)
23
+ headers['Content-Length'] = response.length.to_s
24
+ end
25
+ [status, headers, [response]]
26
+ end
27
+
28
+ # Pads the response with the appropriate callback format according to the
29
+ # JSON-P spec/requirements.
30
+ #
31
+ # The Rack response spec indicates that it should be enumerable. The method
32
+ # of combining all of the data into a single string makes sense since JSON
33
+ # is returned as a full string.
34
+ #
35
+ def pad(callback, response, body = "")
36
+ response.each{ |s| body << s }
37
+ "#{callback}(#{body})"
38
+ end
39
+
40
+ end
41
+ 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,143 @@
1
+ require 'cgi'
2
+ require 'strscan'
3
+
4
+ module Rack
5
+ # Rack middleware for parsing POST/PUT body data into nested parameters
6
+ class NestedParams
7
+
8
+ CONTENT_TYPE = 'CONTENT_TYPE'.freeze
9
+ POST_BODY = 'rack.input'.freeze
10
+ FORM_INPUT = 'rack.request.form_input'.freeze
11
+ FORM_HASH = 'rack.request.form_hash'.freeze
12
+ FORM_VARS = 'rack.request.form_vars'.freeze
13
+
14
+ # supported content type
15
+ URL_ENCODED = 'application/x-www-form-urlencoded'.freeze
16
+
17
+ def initialize(app)
18
+ @app = app
19
+ end
20
+
21
+ def call(env)
22
+ if form_vars = env[FORM_VARS]
23
+ env[FORM_HASH] = parse_query_parameters(form_vars)
24
+ elsif env[CONTENT_TYPE] == URL_ENCODED
25
+ post_body = env[POST_BODY]
26
+ env[FORM_INPUT] = post_body
27
+ env[FORM_HASH] = parse_query_parameters(post_body.read)
28
+ post_body.rewind if post_body.respond_to?(:rewind)
29
+ end
30
+ @app.call(env)
31
+ end
32
+
33
+ ## the rest is nabbed from Rails ##
34
+
35
+ def parse_query_parameters(query_string)
36
+ return {} if query_string.nil? or query_string.empty?
37
+
38
+ pairs = query_string.split('&').collect do |chunk|
39
+ next if chunk.empty?
40
+ key, value = chunk.split('=', 2)
41
+ next if key.empty?
42
+ value = value.nil? ? nil : CGI.unescape(value)
43
+ [ CGI.unescape(key), value ]
44
+ end.compact
45
+
46
+ UrlEncodedPairParser.new(pairs).result
47
+ end
48
+
49
+ class UrlEncodedPairParser < StringScanner
50
+ attr_reader :top, :parent, :result
51
+
52
+ def initialize(pairs = [])
53
+ super('')
54
+ @result = {}
55
+ pairs.each { |key, value| parse(key, value) }
56
+ end
57
+
58
+ KEY_REGEXP = %r{([^\[\]=&]+)}
59
+ BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]}
60
+
61
+ # Parse the query string
62
+ def parse(key, value)
63
+ self.string = key
64
+ @top, @parent = result, nil
65
+
66
+ # First scan the bare key
67
+ key = scan(KEY_REGEXP) or return
68
+ key = post_key_check(key)
69
+
70
+ # Then scan as many nestings as present
71
+ until eos?
72
+ r = scan(BRACKETED_KEY_REGEXP) or return
73
+ key = self[1]
74
+ key = post_key_check(key)
75
+ end
76
+
77
+ bind(key, value)
78
+ end
79
+
80
+ private
81
+ # After we see a key, we must look ahead to determine our next action. Cases:
82
+ #
83
+ # [] follows the key. Then the value must be an array.
84
+ # = follows the key. (A value comes next)
85
+ # & or the end of string follows the key. Then the key is a flag.
86
+ # otherwise, a hash follows the key.
87
+ def post_key_check(key)
88
+ if scan(/\[\]/) # a[b][] indicates that b is an array
89
+ container(key, Array)
90
+ nil
91
+ elsif check(/\[[^\]]/) # a[b] indicates that a is a hash
92
+ container(key, Hash)
93
+ nil
94
+ else # End of key? We do nothing.
95
+ key
96
+ end
97
+ end
98
+
99
+ # Add a container to the stack.
100
+ def container(key, klass)
101
+ type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass)
102
+ value = bind(key, klass.new)
103
+ type_conflict! klass, value unless value.is_a?(klass)
104
+ push(value)
105
+ end
106
+
107
+ # Push a value onto the 'stack', which is actually only the top 2 items.
108
+ def push(value)
109
+ @parent, @top = @top, value
110
+ end
111
+
112
+ # Bind a key (which may be nil for items in an array) to the provided value.
113
+ def bind(key, value)
114
+ if top.is_a? Array
115
+ if key
116
+ if top[-1].is_a?(Hash) && ! top[-1].key?(key)
117
+ top[-1][key] = value
118
+ else
119
+ top << {key => value}
120
+ end
121
+ push top.last
122
+ return top[key]
123
+ else
124
+ top << value
125
+ return value
126
+ end
127
+ elsif top.is_a? Hash
128
+ key = CGI.unescape(key)
129
+ parent << (@top = {}) if top.key?(key) && parent.is_a?(Array)
130
+ top[key] ||= value
131
+ return top[key]
132
+ else
133
+ raise ArgumentError, "Don't know what to do: top is #{top.inspect}"
134
+ end
135
+ end
136
+
137
+ def type_conflict!(klass, value)
138
+ raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value. (The parameters received were #{value.inspect}.)"
139
+ end
140
+ end
141
+
142
+ end
143
+ end
@@ -0,0 +1,18 @@
1
+ module Rack
2
+ # Rack::NotFound is a default endpoint. Initialize with the path to
3
+ # your 404 page.
4
+
5
+ class NotFound
6
+ F = ::File
7
+
8
+ def initialize(path)
9
+ file = F.expand_path(path)
10
+ @content = F.read(file)
11
+ @length = @content.size.to_s
12
+ end
13
+
14
+ def call(env)
15
+ [404, {'Content-Type' => 'text/html', 'Content-Length' => @length}, [@content]]
16
+ end
17
+ end
18
+ 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