rack 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rack might be problematic. Click here for more details.

Files changed (82) hide show
  1. data/COPYING +1 -1
  2. data/KNOWN-ISSUES +3 -0
  3. data/RDOX +0 -428
  4. data/README +61 -26
  5. data/SPEC +8 -1
  6. data/bin/rackup +2 -174
  7. data/lib/rack.rb +10 -8
  8. data/lib/rack/builder.rb +17 -0
  9. data/lib/rack/cascade.rb +17 -12
  10. data/lib/rack/chunked.rb +2 -2
  11. data/lib/rack/commonlogger.rb +31 -43
  12. data/lib/rack/config.rb +15 -0
  13. data/lib/rack/content_type.rb +1 -1
  14. data/lib/rack/directory.rb +6 -2
  15. data/lib/rack/etag.rb +23 -0
  16. data/lib/rack/file.rb +4 -2
  17. data/lib/rack/handler.rb +19 -0
  18. data/lib/rack/handler/cgi.rb +1 -1
  19. data/lib/rack/handler/fastcgi.rb +2 -3
  20. data/lib/rack/handler/lsws.rb +4 -1
  21. data/lib/rack/handler/mongrel.rb +8 -5
  22. data/lib/rack/handler/scgi.rb +4 -4
  23. data/lib/rack/handler/webrick.rb +2 -4
  24. data/lib/rack/lint.rb +44 -15
  25. data/lib/rack/logger.rb +20 -0
  26. data/lib/rack/mime.rb +3 -1
  27. data/lib/rack/mock.rb +30 -4
  28. data/lib/rack/nulllogger.rb +18 -0
  29. data/lib/rack/reloader.rb +4 -1
  30. data/lib/rack/request.rb +40 -15
  31. data/lib/rack/response.rb +5 -39
  32. data/lib/rack/runtime.rb +27 -0
  33. data/lib/rack/sendfile.rb +142 -0
  34. data/lib/rack/server.rb +212 -0
  35. data/lib/rack/session/abstract/id.rb +3 -5
  36. data/lib/rack/session/cookie.rb +3 -4
  37. data/lib/rack/session/memcache.rb +53 -43
  38. data/lib/rack/session/pool.rb +1 -1
  39. data/lib/rack/urlmap.rb +9 -8
  40. data/lib/rack/utils.rb +230 -11
  41. data/rack.gemspec +33 -49
  42. data/test/spec_rack_cascade.rb +3 -5
  43. data/test/spec_rack_cgi.rb +3 -3
  44. data/test/spec_rack_commonlogger.rb +39 -10
  45. data/test/spec_rack_config.rb +24 -0
  46. data/test/spec_rack_directory.rb +1 -1
  47. data/test/spec_rack_etag.rb +17 -0
  48. data/test/spec_rack_fastcgi.rb +2 -2
  49. data/test/spec_rack_file.rb +1 -1
  50. data/test/spec_rack_lint.rb +26 -19
  51. data/test/spec_rack_logger.rb +21 -0
  52. data/test/spec_rack_mock.rb +87 -1
  53. data/test/spec_rack_mongrel.rb +4 -4
  54. data/test/spec_rack_nulllogger.rb +13 -0
  55. data/test/spec_rack_request.rb +47 -6
  56. data/test/spec_rack_response.rb +3 -0
  57. data/test/spec_rack_runtime.rb +35 -0
  58. data/test/spec_rack_sendfile.rb +86 -0
  59. data/test/spec_rack_session_cookie.rb +1 -10
  60. data/test/spec_rack_session_memcache.rb +53 -20
  61. data/test/spec_rack_urlmap.rb +30 -0
  62. data/test/spec_rack_utils.rb +171 -6
  63. data/test/spec_rack_webrick.rb +4 -4
  64. data/test/spec_rackup.rb +154 -0
  65. metadata +37 -79
  66. data/Rakefile +0 -164
  67. data/lib/rack/auth/openid.rb +0 -480
  68. data/test/cgi/lighttpd.conf +0 -20
  69. data/test/cgi/test +0 -9
  70. data/test/cgi/test.fcgi +0 -8
  71. data/test/cgi/test.ru +0 -7
  72. data/test/multipart/binary +0 -0
  73. data/test/multipart/empty +0 -10
  74. data/test/multipart/ie +0 -6
  75. data/test/multipart/nested +0 -10
  76. data/test/multipart/none +0 -9
  77. data/test/multipart/semicolon +0 -6
  78. data/test/multipart/text +0 -10
  79. data/test/spec_rack_auth_openid.rb +0 -84
  80. data/test/testrequest.rb +0 -57
  81. data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
  82. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
@@ -40,7 +40,7 @@ module Rack
40
40
  end
41
41
 
42
42
  DEFAULT_ENV = {
43
- "rack.version" => [1,0],
43
+ "rack.version" => [1,1],
44
44
  "rack.input" => StringIO.new,
45
45
  "rack.errors" => StringIO.new,
46
46
  "rack.multithread" => true,
@@ -73,14 +73,17 @@ module Rack
73
73
  # Return the Rack environment used for a request to +uri+.
74
74
  def self.env_for(uri="", opts={})
75
75
  uri = URI(uri)
76
+ uri.path = "/#{uri.path}" unless uri.path[0] == ?/
77
+
76
78
  env = DEFAULT_ENV.dup
77
79
 
78
- env["REQUEST_METHOD"] = opts[:method] || "GET"
80
+ env["REQUEST_METHOD"] = opts[:method] ? opts[:method].to_s.upcase : "GET"
79
81
  env["SERVER_NAME"] = uri.host || "example.org"
80
82
  env["SERVER_PORT"] = uri.port ? uri.port.to_s : "80"
81
83
  env["QUERY_STRING"] = uri.query.to_s
82
84
  env["PATH_INFO"] = (!uri.path || uri.path.empty?) ? "/" : uri.path
83
85
  env["rack.url_scheme"] = uri.scheme || "http"
86
+ env["HTTPS"] = env["rack.url_scheme"] == "https" ? "on" : "off"
84
87
 
85
88
  env["SCRIPT_NAME"] = opts[:script_name] || ""
86
89
 
@@ -90,7 +93,30 @@ module Rack
90
93
  env["rack.errors"] = StringIO.new
91
94
  end
92
95
 
93
- opts[:input] ||= ""
96
+ if params = opts[:params]
97
+ if env["REQUEST_METHOD"] == "GET"
98
+ params = Utils.parse_nested_query(params) if params.is_a?(String)
99
+ params.update(Utils.parse_nested_query(env["QUERY_STRING"]))
100
+ env["QUERY_STRING"] = Utils.build_nested_query(params)
101
+ elsif !opts.has_key?(:input)
102
+ opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
103
+ if params.is_a?(Hash)
104
+ if data = Utils::Multipart.build_multipart(params)
105
+ opts[:input] = data
106
+ opts["CONTENT_LENGTH"] ||= data.length.to_s
107
+ opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Utils::Multipart::MULTIPART_BOUNDARY}"
108
+ else
109
+ opts[:input] = Utils.build_nested_query(params)
110
+ end
111
+ else
112
+ opts[:input] = params
113
+ end
114
+ end
115
+ end
116
+
117
+ empty_str = ""
118
+ empty_str.force_encoding("ASCII-8BIT") if empty_str.respond_to? :force_encoding
119
+ opts[:input] ||= empty_str
94
120
  if String === opts[:input]
95
121
  rack_input = StringIO.new(opts[:input])
96
122
  else
@@ -128,7 +154,7 @@ module Rack
128
154
  @body = ""
129
155
  body.each { |part| @body << part }
130
156
 
131
- @errors = errors.string
157
+ @errors = errors.string if errors.respond_to?(:string)
132
158
  end
133
159
 
134
160
  # Status
@@ -0,0 +1,18 @@
1
+ module Rack
2
+ class NullLogger
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ env['rack.logger'] = self
9
+ @app.call(env)
10
+ end
11
+
12
+ def info(progname = nil, &block); end
13
+ def debug(progname = nil, &block); end
14
+ def warn(progname = nil, &block); end
15
+ def error(progname = nil, &block); end
16
+ def fatal(progname = nil, &block); end
17
+ end
18
+ end
@@ -1,5 +1,6 @@
1
1
  # Copyright (c) 2009 Michael Fellinger m.fellinger@gmail.com
2
- # All files in this distribution are subject to the terms of the Ruby license.
2
+ # Rack::Reloader is subject to the terms of an MIT-style license.
3
+ # See COPYING or http://www.opensource.org/licenses/mit-license.php.
3
4
 
4
5
  require 'pathname'
5
6
 
@@ -92,6 +93,8 @@ module Rack
92
93
  found, stat = safe_stat(path)
93
94
  return ::File.expand_path(found), stat if found
94
95
  end
96
+
97
+ return false, false
95
98
  end
96
99
 
97
100
  def safe_stat(file)
@@ -32,6 +32,7 @@ module Rack
32
32
  def content_type; @env['CONTENT_TYPE'] end
33
33
  def session; @env['rack.session'] ||= {} end
34
34
  def session_options; @env['rack.session.options'] ||= {} end
35
+ def logger; @env['rack.logger'] end
35
36
 
36
37
  # The media type (type/subtype) portion of the CONTENT_TYPE header
37
38
  # without any media type parameters. e.g., when CONTENT_TYPE is
@@ -63,9 +64,17 @@ module Rack
63
64
  media_type_params['charset']
64
65
  end
65
66
 
67
+ def host_with_port
68
+ if forwarded = @env["HTTP_X_FORWARDED_HOST"]
69
+ forwarded.split(/,\s?/).last
70
+ else
71
+ @env['HTTP_HOST'] || "#{@env['SERVER_NAME'] || @env['SERVER_ADDR']}:#{@env['SERVER_PORT']}"
72
+ end
73
+ end
74
+
66
75
  def host
67
76
  # Remove port number.
68
- (@env["HTTP_HOST"] || @env["SERVER_NAME"]).to_s.gsub(/:\d+\z/, '')
77
+ host_with_port.to_s.gsub(/:\d+\z/, '')
69
78
  end
70
79
 
71
80
  def script_name=(s); @env["SCRIPT_NAME"] = s.to_s end
@@ -81,7 +90,6 @@ module Rack
81
90
  # one of the media types presents in this list will not be eligible
82
91
  # for form-data / param parsing.
83
92
  FORM_DATA_MEDIA_TYPES = [
84
- nil,
85
93
  'application/x-www-form-urlencoded',
86
94
  'multipart/form-data'
87
95
  ]
@@ -92,15 +100,20 @@ module Rack
92
100
  PARSEABLE_DATA_MEDIA_TYPES = [
93
101
  'multipart/related',
94
102
  'multipart/mixed'
95
- ]
103
+ ]
96
104
 
97
105
  # Determine whether the request body contains form-data by checking
98
- # the request media_type against registered form-data media-types:
99
- # "application/x-www-form-urlencoded" and "multipart/form-data". The
106
+ # the request Content-Type for one of the media-types:
107
+ # "application/x-www-form-urlencoded" or "multipart/form-data". The
100
108
  # list of form-data media types can be modified through the
101
109
  # +FORM_DATA_MEDIA_TYPES+ array.
110
+ #
111
+ # A request body is also assumed to contain form-data when no
112
+ # Content-Type header is provided and the request_method is POST.
102
113
  def form_data?
103
- FORM_DATA_MEDIA_TYPES.include?(media_type)
114
+ type = media_type
115
+ meth = env["rack.methodoverride.original_method"] || env['REQUEST_METHOD']
116
+ (meth == 'POST' && type.nil?) || FORM_DATA_MEDIA_TYPES.include?(type)
104
117
  end
105
118
 
106
119
  # Determine whether the request body contains data by checking
@@ -115,8 +128,7 @@ module Rack
115
128
  @env["rack.request.query_hash"]
116
129
  else
117
130
  @env["rack.request.query_string"] = query_string
118
- @env["rack.request.query_hash"] =
119
- Utils.parse_nested_query(query_string)
131
+ @env["rack.request.query_hash"] = parse_query(query_string)
120
132
  end
121
133
  end
122
134
 
@@ -125,19 +137,20 @@ module Rack
125
137
  # This method support both application/x-www-form-urlencoded and
126
138
  # multipart/form-data.
127
139
  def POST
128
- if @env["rack.request.form_input"].eql? @env["rack.input"]
140
+ if @env["rack.input"].nil?
141
+ raise "Missing rack.input"
142
+ elsif @env["rack.request.form_input"].eql? @env["rack.input"]
129
143
  @env["rack.request.form_hash"]
130
144
  elsif form_data? || parseable_data?
131
145
  @env["rack.request.form_input"] = @env["rack.input"]
132
- unless @env["rack.request.form_hash"] =
133
- Utils::Multipart.parse_multipart(env)
146
+ unless @env["rack.request.form_hash"] = parse_multipart(env)
134
147
  form_vars = @env["rack.input"].read
135
148
 
136
149
  # Fix for Safari Ajax postings that always append \0
137
150
  form_vars.sub!(/\0\z/, '')
138
151
 
139
152
  @env["rack.request.form_vars"] = form_vars
140
- @env["rack.request.form_hash"] = Utils.parse_nested_query(form_vars)
153
+ @env["rack.request.form_hash"] = parse_query(form_vars)
141
154
 
142
155
  @env["rack.input"].rewind
143
156
  end
@@ -149,7 +162,7 @@ module Rack
149
162
 
150
163
  # The union of GET and POST data.
151
164
  def params
152
- self.put? ? self.GET : self.GET.update(self.POST)
165
+ self.GET.update(self.POST)
153
166
  rescue EOFError => e
154
167
  self.GET
155
168
  end
@@ -175,6 +188,9 @@ module Rack
175
188
  end
176
189
  alias referrer referer
177
190
 
191
+ def user_agent
192
+ @env['HTTP_USER_AGENT']
193
+ end
178
194
 
179
195
  def cookies
180
196
  return {} unless @env["HTTP_COOKIE"]
@@ -214,11 +230,11 @@ module Rack
214
230
 
215
231
  url
216
232
  end
217
-
233
+
218
234
  def path
219
235
  script_name + path_info
220
236
  end
221
-
237
+
222
238
  def fullpath
223
239
  query_string.empty? ? path : "#{path}?#{query_string}"
224
240
  end
@@ -242,5 +258,14 @@ module Rack
242
258
  @env['REMOTE_ADDR']
243
259
  end
244
260
  end
261
+
262
+ protected
263
+ def parse_query(qs)
264
+ Utils.parse_nested_query(qs)
265
+ end
266
+
267
+ def parse_multipart(env)
268
+ Utils::Multipart.parse_multipart(env)
269
+ end
245
270
  end
246
271
  end
@@ -19,7 +19,7 @@ module Rack
19
19
  attr_accessor :length
20
20
 
21
21
  def initialize(body=[], status=200, header={}, &block)
22
- @status = status
22
+ @status = status.to_i
23
23
  @header = Utils::HeaderHash.new({"Content-Type" => "text/html"}.
24
24
  merge(header))
25
25
 
@@ -54,45 +54,11 @@ module Rack
54
54
  end
55
55
 
56
56
  def set_cookie(key, value)
57
- case value
58
- when Hash
59
- domain = "; domain=" + value[:domain] if value[:domain]
60
- path = "; path=" + value[:path] if value[:path]
61
- # According to RFC 2109, we need dashes here.
62
- # N.B.: cgi.rb uses spaces...
63
- expires = "; expires=" + value[:expires].clone.gmtime.
64
- strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
65
- secure = "; secure" if value[:secure]
66
- httponly = "; HttpOnly" if value[:httponly]
67
- value = value[:value]
68
- end
69
- value = [value] unless Array === value
70
- cookie = Utils.escape(key) + "=" +
71
- value.map { |v| Utils.escape v }.join("&") +
72
- "#{domain}#{path}#{expires}#{secure}#{httponly}"
73
-
74
- case self["Set-Cookie"]
75
- when Array
76
- self["Set-Cookie"] << cookie
77
- when String
78
- self["Set-Cookie"] = [self["Set-Cookie"], cookie]
79
- when nil
80
- self["Set-Cookie"] = cookie
81
- end
57
+ Utils.set_cookie_header!(header, key, value)
82
58
  end
83
59
 
84
60
  def delete_cookie(key, value={})
85
- unless Array === self["Set-Cookie"]
86
- self["Set-Cookie"] = [self["Set-Cookie"]].compact
87
- end
88
-
89
- self["Set-Cookie"].reject! { |cookie|
90
- cookie =~ /\A#{Utils.escape(key)}=/
91
- }
92
-
93
- set_cookie(key,
94
- {:value => '', :path => nil, :domain => nil,
95
- :expires => Time.at(0) }.merge(value))
61
+ Utils.delete_cookie_header!(header, key, value)
96
62
  end
97
63
 
98
64
  def redirect(target, status=302)
@@ -105,9 +71,9 @@ module Rack
105
71
 
106
72
  if [204, 304].include?(status.to_i)
107
73
  header.delete "Content-Type"
108
- [status.to_i, header.to_hash, []]
74
+ [status.to_i, header, []]
109
75
  else
110
- [status.to_i, header.to_hash, self]
76
+ [status.to_i, header, self]
111
77
  end
112
78
  end
113
79
  alias to_a finish # For *response
@@ -0,0 +1,27 @@
1
+ module Rack
2
+ # Sets an "X-Runtime" response header, indicating the response
3
+ # time of the request, in seconds
4
+ #
5
+ # You can put it right before the application to see the processing
6
+ # time, or before all the other middlewares to include time for them,
7
+ # too.
8
+ class Runtime
9
+ def initialize(app, name = nil)
10
+ @app = app
11
+ @header_name = "X-Runtime"
12
+ @header_name << "-#{name}" if name
13
+ end
14
+
15
+ def call(env)
16
+ start_time = Time.now
17
+ status, headers, body = @app.call(env)
18
+ request_time = Time.now - start_time
19
+
20
+ if !headers.has_key?(@header_name)
21
+ headers[@header_name] = "%0.6f" % request_time
22
+ end
23
+
24
+ [status, headers, body]
25
+ end
26
+ end
27
+ 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