rack 0.4.0 → 0.9.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 (59) hide show
  1. data/RDOX +61 -3
  2. data/README +52 -37
  3. data/Rakefile +9 -0
  4. data/SPEC +6 -3
  5. data/bin/rackup +0 -0
  6. data/lib/rack.rb +7 -2
  7. data/lib/rack/adapter/camping.rb +1 -1
  8. data/lib/rack/auth/openid.rb +4 -3
  9. data/lib/rack/builder.rb +12 -1
  10. data/lib/rack/conditionalget.rb +43 -0
  11. data/lib/rack/content_length.rb +25 -0
  12. data/lib/rack/deflater.rb +29 -5
  13. data/lib/rack/directory.rb +82 -91
  14. data/lib/rack/file.rb +45 -76
  15. data/lib/rack/handler.rb +4 -0
  16. data/lib/rack/handler/evented_mongrel.rb +1 -1
  17. data/lib/rack/handler/fastcgi.rb +2 -0
  18. data/lib/rack/handler/mongrel.rb +6 -2
  19. data/lib/rack/handler/swiftiplied_mongrel.rb +8 -0
  20. data/lib/rack/handler/thin.rb +15 -0
  21. data/lib/rack/handler/webrick.rb +8 -4
  22. data/lib/rack/head.rb +19 -0
  23. data/lib/rack/lint.rb +74 -10
  24. data/lib/rack/lobster.rb +13 -13
  25. data/lib/rack/methodoverride.rb +27 -0
  26. data/lib/rack/mime.rb +204 -0
  27. data/lib/rack/request.rb +10 -1
  28. data/lib/rack/response.rb +7 -2
  29. data/lib/rack/session/abstract/id.rb +14 -1
  30. data/lib/rack/session/cookie.rb +19 -1
  31. data/lib/rack/session/memcache.rb +1 -1
  32. data/lib/rack/session/pool.rb +1 -1
  33. data/lib/rack/showexceptions.rb +5 -1
  34. data/lib/rack/showstatus.rb +3 -2
  35. data/lib/rack/urlmap.rb +1 -1
  36. data/lib/rack/utils.rb +42 -13
  37. data/test/cgi/lighttpd.conf +1 -1
  38. data/test/cgi/test +0 -0
  39. data/test/cgi/test.fcgi +0 -0
  40. data/test/cgi/test.ru +0 -0
  41. data/test/spec_rack_builder.rb +34 -0
  42. data/test/spec_rack_conditionalget.rb +41 -0
  43. data/test/spec_rack_content_length.rb +43 -0
  44. data/test/spec_rack_deflater.rb +49 -14
  45. data/test/spec_rack_file.rb +7 -0
  46. data/test/spec_rack_handler.rb +3 -3
  47. data/test/spec_rack_head.rb +30 -0
  48. data/test/spec_rack_lint.rb +79 -2
  49. data/test/spec_rack_methodoverride.rb +60 -0
  50. data/test/spec_rack_mock.rb +1 -1
  51. data/test/spec_rack_mongrel.rb +20 -1
  52. data/test/spec_rack_request.rb +46 -1
  53. data/test/spec_rack_response.rb +10 -3
  54. data/test/spec_rack_session_cookie.rb +33 -0
  55. data/test/spec_rack_thin.rb +90 -0
  56. data/test/spec_rack_utils.rb +20 -18
  57. data/test/spec_rack_webrick.rb +17 -0
  58. data/test/testrequest.rb +12 -0
  59. metadata +91 -5
data/lib/rack/builder.rb CHANGED
@@ -13,6 +13,13 @@ module Rack
13
13
  # end
14
14
  # }
15
15
  #
16
+ # Or
17
+ #
18
+ # app = Rack::Builder.app do
19
+ # use Rack::CommonLogger
20
+ # lambda { |env| [200, {'Content-Type' => 'text/plain'}, 'OK'] }
21
+ # end
22
+ #
16
23
  # +use+ adds a middleware to the stack, +run+ dispatches to an application.
17
24
  # You can use +map+ to construct a Rack::URLMap in a convenient way.
18
25
 
@@ -22,6 +29,10 @@ module Rack
22
29
  instance_eval(&block) if block_given?
23
30
  end
24
31
 
32
+ def self.app(&block)
33
+ self.new(&block).to_app
34
+ end
35
+
25
36
  def use(middleware, *args, &block)
26
37
  @ins << if block_given?
27
38
  lambda { |app| middleware.new(app, *args, &block) }
@@ -36,7 +47,7 @@ module Rack
36
47
 
37
48
  def map(path, &block)
38
49
  if @ins.last.kind_of? Hash
39
- @ins.last[path] = Rack::Builder.new(&block).to_app
50
+ @ins.last[path] = self.class.new(&block).to_app
40
51
  else
41
52
  @ins << {}
42
53
  map(path, &block)
@@ -0,0 +1,43 @@
1
+ module Rack
2
+
3
+ # Middleware that enables conditional GET using If-None-Match and
4
+ # If-Modified-Since. The application should set either or both of the
5
+ # Last-Modified or Etag response headers according to RFC 2616. When
6
+ # either of the conditions is met, the response body is set to be zero
7
+ # length and the response status is set to 304 Not Modified.
8
+ #
9
+ # Applications that defer response body generation until the body's each
10
+ # message is received will avoid response body generation completely when
11
+ # a conditional GET matches.
12
+ #
13
+ # Adapted from Michael Klishin's Merb implementation:
14
+ # http://github.com/wycats/merb-core/tree/master/lib/merb-core/rack/middleware/conditional_get.rb
15
+ class ConditionalGet
16
+ def initialize(app)
17
+ @app = app
18
+ end
19
+
20
+ def call(env)
21
+ return @app.call(env) unless %w[GET HEAD].include?(env['REQUEST_METHOD'])
22
+
23
+ status, headers, body = @app.call(env)
24
+ headers = Utils::HeaderHash.new(headers)
25
+ if etag_matches?(env, headers) || modified_since?(env, headers)
26
+ status = 304
27
+ body = []
28
+ end
29
+ [status, headers, body]
30
+ end
31
+
32
+ private
33
+ def etag_matches?(env, headers)
34
+ etag = headers['Etag'] and etag == env['HTTP_IF_NONE_MATCH']
35
+ end
36
+
37
+ def modified_since?(env, headers)
38
+ last_modified = headers['Last-Modified'] and
39
+ last_modified == env['HTTP_IF_MODIFIED_SINCE']
40
+ end
41
+ end
42
+
43
+ end
@@ -0,0 +1,25 @@
1
+ module Rack
2
+ # Sets the Content-Length header on responses with fixed-length bodies.
3
+ class ContentLength
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ status, headers, body = @app.call(env)
10
+ headers = Utils::HeaderHash.new(headers)
11
+
12
+ if !Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) &&
13
+ !headers['Content-Length'] &&
14
+ !headers['Transfer-Encoding'] &&
15
+ (body.respond_to?(:to_ary) || body.respond_to?(:to_str))
16
+
17
+ body = [body] if body.respond_to?(:to_str) # rack 0.4 compat
18
+ length = body.to_ary.inject(0) { |len, part| len + part.length }
19
+ headers['Content-Length'] = length.to_s
20
+ end
21
+
22
+ [status, headers, body]
23
+ end
24
+ end
25
+ end
data/lib/rack/deflater.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "zlib"
2
2
  require "stringio"
3
+ require "time" # for Time.httpdate
3
4
 
4
5
  module Rack
5
6
 
@@ -10,21 +11,44 @@ class Deflater
10
11
 
11
12
  def call(env)
12
13
  status, headers, body = @app.call(env)
14
+ headers = Utils::HeaderHash.new(headers)
15
+
16
+ # Skip compressing empty entity body responses and responses with
17
+ # no-transform set.
18
+ if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
19
+ headers['Cache-Control'].to_s =~ /\bno-transform\b/
20
+ return [status, headers, body]
21
+ end
13
22
 
14
23
  request = Request.new(env)
15
24
 
16
- encoding = Utils.select_best_encoding(%w(gzip deflate identity), request.accept_encoding)
25
+ encoding = Utils.select_best_encoding(%w(gzip deflate identity),
26
+ request.accept_encoding)
27
+
28
+ # Set the Vary HTTP header.
29
+ vary = headers["Vary"].to_s.split(",").map { |v| v.strip }
30
+ unless vary.include?("*") || vary.include?("Accept-Encoding")
31
+ headers["Vary"] = vary.push("Accept-Encoding").join(",")
32
+ end
17
33
 
18
34
  case encoding
19
35
  when "gzip"
20
- mtime = headers["Last-Modified"] || Time.now
21
- [status, headers.merge("Content-Encoding" => "gzip"), self.class.gzip(body, mtime)]
36
+ mtime = if headers.key?("Last-Modified")
37
+ Time.httpdate(headers["Last-Modified"])
38
+ else
39
+ Time.now
40
+ end
41
+ [status,
42
+ headers.merge("Content-Encoding" => "gzip"),
43
+ self.class.gzip(body, mtime)]
22
44
  when "deflate"
23
- [status, headers.merge("Content-Encoding" => "deflate"), self.class.deflate(body)]
45
+ [status,
46
+ headers.merge("Content-Encoding" => "deflate"),
47
+ self.class.deflate(body)]
24
48
  when "identity"
25
49
  [status, headers, body]
26
50
  when nil
27
- message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
51
+ message = ["An acceptable encoding for the requested resource #{request.fullpath} could not be found."]
28
52
  [406, {"Content-Type" => "text/plain"}, message]
29
53
  end
30
54
  end
@@ -1,4 +1,5 @@
1
1
  require 'time'
2
+ require 'rack/mime'
2
3
 
3
4
  module Rack
4
5
  # Rack::Directory serves entries below the +root+ given, according to the
@@ -13,10 +14,13 @@ module Rack
13
14
  DIR_PAGE = <<-PAGE
14
15
  <html><head>
15
16
  <title>%s</title>
17
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
16
18
  <style type='text/css'>
17
19
  table { width:100%%; }
18
20
  .name { text-align:left; }
19
21
  .size, .mtime { text-align:right; }
22
+ .type { width:11em; }
23
+ .mtime { width:15em; }
20
24
  </style>
21
25
  </head><body>
22
26
  <h1>%s</h1>
@@ -38,11 +42,8 @@ table { width:100%%; }
38
42
  attr_accessor :root, :path
39
43
 
40
44
  def initialize(root, app=nil)
41
- @root = root
42
- @app = app
43
- unless defined? @app
44
- @app = Rack::File.new(@root)
45
- end
45
+ @root = F.expand_path(root)
46
+ @app = app || Rack::File.new(@root)
46
47
  end
47
48
 
48
49
  def call(env)
@@ -52,36 +53,71 @@ table { width:100%%; }
52
53
  F = ::File
53
54
 
54
55
  def _call(env)
55
- if env["PATH_INFO"].include? ".."
56
- body = "Forbidden\n"
57
- size = body.respond_to?(:bytesize) ? body.bytesize : body.size
58
- return [403, {"Content-Type" => "text/plain","Content-Length" => size.to_s}, [body]]
56
+ @env = env
57
+ @path_info, @script_name = env.values_at('PATH_INFO', 'SCRIPT_NAME')
58
+
59
+ if forbidden = check_forbidden
60
+ forbidden
61
+ else
62
+ @path = F.join(@root, Utils.unescape(@path_info))
63
+ list_path
64
+ end
65
+ end
66
+
67
+ def check_forbidden
68
+ return unless @path_info.include? ".."
69
+
70
+ body = "Forbidden\n"
71
+ size = body.respond_to?(:bytesize) ? body.bytesize : body.size
72
+ return [403, {"Content-Type" => "text/plain","Content-Length" => size.to_s}, [body]]
73
+ end
74
+
75
+ def list_directory
76
+ @files = [['../','Parent Directory','','','']]
77
+ glob = F.join(@path, '*')
78
+
79
+ Dir[glob].sort.each do |node|
80
+ stat = stat(node)
81
+ next unless stat
82
+ basename = F.basename(node)
83
+ ext = F.extname(node)
84
+
85
+ url = F.join(@script_name, @path_info, basename)
86
+ size = stat.size
87
+ type = stat.directory? ? 'directory' : Mime.mime_type(ext)
88
+ size = stat.directory? ? '-' : filesize_format(size)
89
+ mtime = stat.mtime.httpdate
90
+
91
+ @files << [ url, basename, size, type, mtime ]
59
92
  end
60
93
 
61
- @path = F.join(@root, Utils.unescape(env['PATH_INFO']))
62
-
63
- if F.exist?(@path) and F.readable?(@path)
64
- if F.file?(@path)
65
- return @app.call(env)
66
- elsif F.directory?(@path)
67
- @files = [['../','Parent Directory','','','']]
68
- sName, pInfo = env.values_at('SCRIPT_NAME', 'PATH_INFO')
69
- Dir.entries(@path).sort.each do |file|
70
- next if file[0] == ?.
71
- fl = F.join(@path, file)
72
- sz = F.size(fl)
73
- url = F.join(sName, pInfo, file)
74
- type = F.directory?(fl) ? 'directory' :
75
- MIME_TYPES.fetch(F.extname(file)[1..-1],'unknown')
76
- size = (type!='directory' ? (sz<10240 ? "#{sz}B" : "#{sz/1024}KB") : '-')
77
- mtime = F.mtime(fl).httpdate
78
- @files << [ url, file, size, type, mtime ]
79
- end
80
- return [ 200, {'Content-Type'=>'text/html'}, self ]
81
- end
94
+ return [ 200, {'Content-Type'=>'text/html; charset=utf-8'}, self ]
95
+ end
96
+
97
+ def stat(node, max = 10)
98
+ F.stat(node)
99
+ rescue Errno::ENOENT, Errno::ELOOP
100
+ return nil
101
+ end
102
+
103
+ # TODO: add correct response if not readable, not sure if 404 is the best
104
+ # option
105
+ def list_path
106
+ @stat = F.stat(@path)
107
+
108
+ if @stat.readable?
109
+ return @app.call(@env) if @stat.file?
110
+ return list_directory if @stat.directory?
111
+ else
112
+ raise Errno::ENOENT, 'No such file or directory'
82
113
  end
83
114
 
84
- body = "Entity not found: #{env["PATH_INFO"]}\n"
115
+ rescue Errno::ENOENT, Errno::ELOOP
116
+ return entity_not_found
117
+ end
118
+
119
+ def entity_not_found
120
+ body = "Entity not found: #{@path_info}\n"
85
121
  size = body.respond_to?(:bytesize) ? body.bytesize : body.size
86
122
  return [404, {"Content-Type" => "text/plain", "Content-Length" => size.to_s}, [body]]
87
123
  end
@@ -93,66 +129,21 @@ table { width:100%%; }
93
129
  page.each_line{|l| yield l }
94
130
  end
95
131
 
96
- def each_entry
97
- @files.each{|e| yield e }
98
- end
132
+ # Stolen from Ramaze
133
+
134
+ FILESIZE_FORMAT = [
135
+ ['%.1fT', 1 << 40],
136
+ ['%.1fG', 1 << 30],
137
+ ['%.1fM', 1 << 20],
138
+ ['%.1fK', 1 << 10],
139
+ ]
99
140
 
100
- # From WEBrick.
101
- MIME_TYPES = {
102
- "ai" => "application/postscript",
103
- "asc" => "text/plain",
104
- "avi" => "video/x-msvideo",
105
- "bin" => "application/octet-stream",
106
- "bmp" => "image/bmp",
107
- "class" => "application/octet-stream",
108
- "cer" => "application/pkix-cert",
109
- "crl" => "application/pkix-crl",
110
- "crt" => "application/x-x509-ca-cert",
111
- #"crl" => "application/x-pkcs7-crl",
112
- "css" => "text/css",
113
- "dms" => "application/octet-stream",
114
- "doc" => "application/msword",
115
- "dvi" => "application/x-dvi",
116
- "eps" => "application/postscript",
117
- "etx" => "text/x-setext",
118
- "exe" => "application/octet-stream",
119
- "gif" => "image/gif",
120
- "htm" => "text/html",
121
- "html" => "text/html",
122
- "jpe" => "image/jpeg",
123
- "jpeg" => "image/jpeg",
124
- "jpg" => "image/jpeg",
125
- "js" => "text/javascript",
126
- "lha" => "application/octet-stream",
127
- "lzh" => "application/octet-stream",
128
- "mov" => "video/quicktime",
129
- "mpe" => "video/mpeg",
130
- "mpeg" => "video/mpeg",
131
- "mpg" => "video/mpeg",
132
- "pbm" => "image/x-portable-bitmap",
133
- "pdf" => "application/pdf",
134
- "pgm" => "image/x-portable-graymap",
135
- "png" => "image/png",
136
- "pnm" => "image/x-portable-anymap",
137
- "ppm" => "image/x-portable-pixmap",
138
- "ppt" => "application/vnd.ms-powerpoint",
139
- "ps" => "application/postscript",
140
- "qt" => "video/quicktime",
141
- "ras" => "image/x-cmu-raster",
142
- "rb" => "text/plain",
143
- "rd" => "text/plain",
144
- "rtf" => "application/rtf",
145
- "sgm" => "text/sgml",
146
- "sgml" => "text/sgml",
147
- "tif" => "image/tiff",
148
- "tiff" => "image/tiff",
149
- "txt" => "text/plain",
150
- "xbm" => "image/x-xbitmap",
151
- "xls" => "application/vnd.ms-excel",
152
- "xml" => "text/xml",
153
- "xpm" => "image/x-xpixmap",
154
- "xwd" => "image/x-xwindowdump",
155
- "zip" => "application/zip",
156
- }
141
+ def filesize_format(int)
142
+ FILESIZE_FORMAT.each do |format, size|
143
+ return format % (int.to_f / size) if int >= size
144
+ end
145
+
146
+ int.to_s + 'B'
147
+ end
157
148
  end
158
149
  end
data/lib/rack/file.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'time'
2
+ require 'rack/mime'
2
3
 
3
4
  module Rack
4
5
  # Rack::File serves files below the +root+ given, according to the
@@ -22,26 +23,55 @@ module Rack
22
23
  F = ::File
23
24
 
24
25
  def _call(env)
25
- if env["PATH_INFO"].include? ".."
26
- body = "Forbidden\n"
27
- size = body.respond_to?(:bytesize) ? body.bytesize : body.size
28
- return [403, {"Content-Type" => "text/plain","Content-Length" => size.to_s}, [body]]
26
+ return forbidden if env["PATH_INFO"].include? ".."
27
+
28
+ @path_info = Utils.unescape(env["PATH_INFO"])
29
+ @path = F.join(@root, @path_info)
30
+
31
+ begin
32
+ if F.file?(@path) && F.readable?(@path)
33
+ serving
34
+ else
35
+ raise Errno::EPERM
36
+ end
37
+ rescue SystemCallError
38
+ not_found
29
39
  end
40
+ end
41
+
42
+ def forbidden
43
+ body = "Forbidden\n"
44
+ [403, {"Content-Type" => "text/plain",
45
+ "Content-Length" => body.size.to_s},
46
+ [body]]
47
+ end
30
48
 
31
- @path = F.join(@root, Utils.unescape(env["PATH_INFO"]))
32
- ext = F.extname(@path)[1..-1]
49
+ # NOTE:
50
+ # We check via File::size? whether this file provides size info
51
+ # via stat (e.g. /proc files often don't), otherwise we have to
52
+ # figure it out by reading the whole file into memory. And while
53
+ # we're at it we also use this as body then.
33
54
 
34
- if F.file?(@path) && F.readable?(@path)
35
- [200, {
36
- "Last-Modified" => F.mtime(@path).httpdate,
37
- "Content-Type" => MIME_TYPES[ext] || "text/plain",
38
- "Content-Length" => F.size(@path).to_s
39
- }, self]
55
+ def serving
56
+ if size = F.size?(@path)
57
+ body = self
40
58
  else
41
- body = "File not found: #{env["PATH_INFO"]}\n"
42
- size = body.respond_to?(:bytesize) ? body.bytesize : body.size
43
- [404, {"Content-Type" => "text/plain", "Content-Length" => size.to_s}, [body]]
59
+ body = [F.read(@path)]
60
+ size = body.first.size
44
61
  end
62
+
63
+ [200, {
64
+ "Last-Modified" => F.mtime(@path).httpdate,
65
+ "Content-Type" => Mime.mime_type(F.extname(@path), 'text/plain'),
66
+ "Content-Length" => size.to_s
67
+ }, body]
68
+ end
69
+
70
+ def not_found
71
+ body = "File not found: #{@path_info}\n"
72
+ [404, {"Content-Type" => "text/plain",
73
+ "Content-Length" => body.size.to_s},
74
+ [body]]
45
75
  end
46
76
 
47
77
  def each
@@ -51,66 +81,5 @@ module Rack
51
81
  end
52
82
  }
53
83
  end
54
-
55
- # :stopdoc:
56
- # From WEBrick with some additions.
57
- MIME_TYPES = {
58
- "ai" => "application/postscript",
59
- "asc" => "text/plain",
60
- "avi" => "video/x-msvideo",
61
- "bin" => "application/octet-stream",
62
- "bmp" => "image/bmp",
63
- "class" => "application/octet-stream",
64
- "cer" => "application/pkix-cert",
65
- "crl" => "application/pkix-crl",
66
- "crt" => "application/x-x509-ca-cert",
67
- #"crl" => "application/x-pkcs7-crl",
68
- "css" => "text/css",
69
- "dms" => "application/octet-stream",
70
- "doc" => "application/msword",
71
- "dvi" => "application/x-dvi",
72
- "eps" => "application/postscript",
73
- "etx" => "text/x-setext",
74
- "exe" => "application/octet-stream",
75
- "gif" => "image/gif",
76
- "htm" => "text/html",
77
- "html" => "text/html",
78
- "jpe" => "image/jpeg",
79
- "jpeg" => "image/jpeg",
80
- "jpg" => "image/jpeg",
81
- "js" => "text/javascript",
82
- "lha" => "application/octet-stream",
83
- "lzh" => "application/octet-stream",
84
- "mov" => "video/quicktime",
85
- "mp3" => "audio/mpeg",
86
- "mpe" => "video/mpeg",
87
- "mpeg" => "video/mpeg",
88
- "mpg" => "video/mpeg",
89
- "pbm" => "image/x-portable-bitmap",
90
- "pdf" => "application/pdf",
91
- "pgm" => "image/x-portable-graymap",
92
- "png" => "image/png",
93
- "pnm" => "image/x-portable-anymap",
94
- "ppm" => "image/x-portable-pixmap",
95
- "ppt" => "application/vnd.ms-powerpoint",
96
- "ps" => "application/postscript",
97
- "qt" => "video/quicktime",
98
- "ras" => "image/x-cmu-raster",
99
- "rb" => "text/plain",
100
- "rd" => "text/plain",
101
- "rtf" => "application/rtf",
102
- "sgm" => "text/sgml",
103
- "sgml" => "text/sgml",
104
- "tif" => "image/tiff",
105
- "tiff" => "image/tiff",
106
- "txt" => "text/plain",
107
- "xbm" => "image/x-xbitmap",
108
- "xls" => "application/vnd.ms-excel",
109
- "xml" => "text/xml",
110
- "xpm" => "image/x-xpixmap",
111
- "xwd" => "image/x-xwindowdump",
112
- "zip" => "application/zip",
113
- }
114
- # :startdoc:
115
84
  end
116
85
  end