rack 0.3.0 → 0.4.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 (50) hide show
  1. data/AUTHORS +1 -0
  2. data/RDOX +61 -3
  3. data/README +94 -9
  4. data/Rakefile +36 -32
  5. data/SPEC +1 -7
  6. data/bin/rackup +31 -13
  7. data/lib/rack.rb +8 -19
  8. data/lib/rack/auth/digest/params.rb +2 -2
  9. data/lib/rack/auth/openid.rb +406 -80
  10. data/lib/rack/builder.rb +1 -1
  11. data/lib/rack/cascade.rb +10 -0
  12. data/lib/rack/commonlogger.rb +6 -1
  13. data/lib/rack/deflater.rb +63 -0
  14. data/lib/rack/directory.rb +158 -0
  15. data/lib/rack/file.rb +11 -5
  16. data/lib/rack/handler.rb +44 -0
  17. data/lib/rack/handler/evented_mongrel.rb +8 -0
  18. data/lib/rack/handler/fastcgi.rb +1 -0
  19. data/lib/rack/handler/mongrel.rb +21 -1
  20. data/lib/rack/lint.rb +20 -13
  21. data/lib/rack/mock.rb +1 -0
  22. data/lib/rack/request.rb +69 -2
  23. data/lib/rack/session/abstract/id.rb +140 -0
  24. data/lib/rack/session/memcache.rb +97 -0
  25. data/lib/rack/session/pool.rb +50 -59
  26. data/lib/rack/showstatus.rb +3 -1
  27. data/lib/rack/urlmap.rb +12 -12
  28. data/lib/rack/utils.rb +88 -9
  29. data/test/cgi/lighttpd.conf +1 -1
  30. data/test/cgi/test.fcgi +1 -2
  31. data/test/cgi/test.ru +2 -2
  32. data/test/spec_rack_auth_openid.rb +137 -0
  33. data/test/spec_rack_camping.rb +37 -33
  34. data/test/spec_rack_cascade.rb +15 -0
  35. data/test/spec_rack_cgi.rb +4 -3
  36. data/test/spec_rack_deflater.rb +70 -0
  37. data/test/spec_rack_directory.rb +56 -0
  38. data/test/spec_rack_fastcgi.rb +4 -3
  39. data/test/spec_rack_file.rb +11 -1
  40. data/test/spec_rack_handler.rb +24 -0
  41. data/test/spec_rack_lint.rb +19 -33
  42. data/test/spec_rack_mongrel.rb +71 -0
  43. data/test/spec_rack_request.rb +91 -1
  44. data/test/spec_rack_session_memcache.rb +132 -0
  45. data/test/spec_rack_session_pool.rb +48 -1
  46. data/test/spec_rack_showstatus.rb +5 -4
  47. data/test/spec_rack_urlmap.rb +60 -25
  48. data/test/spec_rack_utils.rb +118 -1
  49. data/test/testrequest.rb +3 -1
  50. metadata +67 -44
@@ -19,7 +19,7 @@ module Rack
19
19
  class Builder
20
20
  def initialize(&block)
21
21
  @ins = []
22
- instance_eval(&block)
22
+ instance_eval(&block) if block_given?
23
23
  end
24
24
 
25
25
  def use(middleware, *args, &block)
@@ -22,5 +22,15 @@ module Rack
22
22
  }
23
23
  [status, headers, body]
24
24
  end
25
+
26
+ def add app
27
+ @apps << app
28
+ end
29
+
30
+ def include? app
31
+ @apps.include? app
32
+ end
33
+
34
+ alias_method :<<, :add
25
35
  end
26
36
  end
@@ -21,6 +21,10 @@ module Rack
21
21
  [@status, @header, self]
22
22
  end
23
23
 
24
+ def close
25
+ @body.close if @body.respond_to? :close
26
+ end
27
+
24
28
  # By default, log to rack.errors.
25
29
  def <<(str)
26
30
  @env["rack.errors"].write(str)
@@ -40,7 +44,8 @@ module Rack
40
44
  # lilith.local - - [07/Aug/2006 23:58:02] "GET / HTTP/1.1" 500 -
41
45
  # %{%s - %s [%s] "%s %s%s %s" %d %s\n} %
42
46
  @logger << %{%s - %s [%s] "%s %s%s %s" %d %s %0.4f\n} %
43
- [@env["REMOTE_ADDR"] || "-",
47
+ [
48
+ @env['HTTP_X_FORWARDED_FOR'] || @env["REMOTE_ADDR"] || "-",
44
49
  @env["REMOTE_USER"] || "-",
45
50
  @now.strftime("%d/%b/%Y %H:%M:%S"),
46
51
  @env["REQUEST_METHOD"],
@@ -0,0 +1,63 @@
1
+ require "zlib"
2
+ require "stringio"
3
+
4
+ module Rack
5
+
6
+ class Deflater
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ status, headers, body = @app.call(env)
13
+
14
+ request = Request.new(env)
15
+
16
+ encoding = Utils.select_best_encoding(%w(gzip deflate identity), request.accept_encoding)
17
+
18
+ case encoding
19
+ when "gzip"
20
+ mtime = headers["Last-Modified"] || Time.now
21
+ [status, headers.merge("Content-Encoding" => "gzip"), self.class.gzip(body, mtime)]
22
+ when "deflate"
23
+ [status, headers.merge("Content-Encoding" => "deflate"), self.class.deflate(body)]
24
+ when "identity"
25
+ [status, headers, body]
26
+ when nil
27
+ message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
28
+ [406, {"Content-Type" => "text/plain"}, message]
29
+ end
30
+ end
31
+
32
+ def self.gzip(body, mtime)
33
+ io = StringIO.new
34
+ gzip = Zlib::GzipWriter.new(io)
35
+ gzip.mtime = mtime
36
+
37
+ # TODO: Add streaming
38
+ body.each { |part| gzip << part }
39
+
40
+ gzip.close
41
+ return io.string
42
+ end
43
+
44
+ DEFLATE_ARGS = [
45
+ Zlib::DEFAULT_COMPRESSION,
46
+ # drop the zlib header which causes both Safari and IE to choke
47
+ -Zlib::MAX_WBITS,
48
+ Zlib::DEF_MEM_LEVEL,
49
+ Zlib::DEFAULT_STRATEGY
50
+ ]
51
+
52
+ # Loosely based on Mongrel's Deflate handler
53
+ def self.deflate(body)
54
+ deflater = Zlib::Deflate.new(*DEFLATE_ARGS)
55
+
56
+ # TODO: Add streaming
57
+ body.each { |part| deflater << part }
58
+
59
+ return deflater.finish
60
+ end
61
+ end
62
+
63
+ end
@@ -0,0 +1,158 @@
1
+ require 'time'
2
+
3
+ module Rack
4
+ # Rack::Directory serves entries below the +root+ given, according to the
5
+ # path info of the Rack request. If a directory is found, the file's contents
6
+ # will be presented in an html based index. If a file is found, the env will
7
+ # be passed to the specified +app+.
8
+ #
9
+ # If +app+ is not specified, a Rack::File of the same +root+ will be used.
10
+
11
+ class Directory
12
+ DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>"
13
+ DIR_PAGE = <<-PAGE
14
+ <html><head>
15
+ <title>%s</title>
16
+ <style type='text/css'>
17
+ table { width:100%%; }
18
+ .name { text-align:left; }
19
+ .size, .mtime { text-align:right; }
20
+ </style>
21
+ </head><body>
22
+ <h1>%s</h1>
23
+ <hr />
24
+ <table>
25
+ <tr>
26
+ <th class='name'>Name</th>
27
+ <th class='size'>Size</th>
28
+ <th class='type'>Type</th>
29
+ <th class='mtime'>Last Modified</th>
30
+ </tr>
31
+ %s
32
+ </table>
33
+ <hr />
34
+ </body></html>
35
+ PAGE
36
+
37
+ attr_reader :files
38
+ attr_accessor :root, :path
39
+
40
+ def initialize(root, app=nil)
41
+ @root = root
42
+ @app = app
43
+ unless defined? @app
44
+ @app = Rack::File.new(@root)
45
+ end
46
+ end
47
+
48
+ def call(env)
49
+ dup._call(env)
50
+ end
51
+
52
+ F = ::File
53
+
54
+ 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]]
59
+ end
60
+
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
82
+ end
83
+
84
+ body = "Entity not found: #{env["PATH_INFO"]}\n"
85
+ size = body.respond_to?(:bytesize) ? body.bytesize : body.size
86
+ return [404, {"Content-Type" => "text/plain", "Content-Length" => size.to_s}, [body]]
87
+ end
88
+
89
+ def each
90
+ show_path = @path.sub(/^#{@root}/,'')
91
+ files = @files.map{|f| DIR_FILE % f }*"\n"
92
+ page = DIR_PAGE % [ show_path, show_path , files ]
93
+ page.each_line{|l| yield l }
94
+ end
95
+
96
+ def each_entry
97
+ @files.each{|e| yield e }
98
+ end
99
+
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
+ }
157
+ end
158
+ end
@@ -1,3 +1,5 @@
1
+ require 'time'
2
+
1
3
  module Rack
2
4
  # Rack::File serves files below the +root+ given, according to the
3
5
  # path info of the Rack request.
@@ -21,7 +23,9 @@ module Rack
21
23
 
22
24
  def _call(env)
23
25
  if env["PATH_INFO"].include? ".."
24
- return [403, {"Content-Type" => "text/plain"}, ["Forbidden\n"]]
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]]
25
29
  end
26
30
 
27
31
  @path = F.join(@root, Utils.unescape(env["PATH_INFO"]))
@@ -29,13 +33,14 @@ module Rack
29
33
 
30
34
  if F.file?(@path) && F.readable?(@path)
31
35
  [200, {
32
- "Last-Modified" => F.mtime(@path).rfc822,
36
+ "Last-Modified" => F.mtime(@path).httpdate,
33
37
  "Content-Type" => MIME_TYPES[ext] || "text/plain",
34
38
  "Content-Length" => F.size(@path).to_s
35
39
  }, self]
36
40
  else
37
- return [404, {"Content-Type" => "text/plain"},
38
- ["File not found: #{env["PATH_INFO"]}\n"]]
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]]
39
44
  end
40
45
  end
41
46
 
@@ -48,7 +53,7 @@ module Rack
48
53
  end
49
54
 
50
55
  # :stopdoc:
51
- # From WEBrick.
56
+ # From WEBrick with some additions.
52
57
  MIME_TYPES = {
53
58
  "ai" => "application/postscript",
54
59
  "asc" => "text/plain",
@@ -77,6 +82,7 @@ module Rack
77
82
  "lha" => "application/octet-stream",
78
83
  "lzh" => "application/octet-stream",
79
84
  "mov" => "video/quicktime",
85
+ "mp3" => "audio/mpeg",
80
86
  "mpe" => "video/mpeg",
81
87
  "mpeg" => "video/mpeg",
82
88
  "mpg" => "video/mpeg",
@@ -0,0 +1,44 @@
1
+ module Rack
2
+ # *Handlers* connect web servers with Rack.
3
+ #
4
+ # Rack includes Handlers for Mongrel, WEBrick, FastCGI, CGI, SCGI
5
+ # and LiteSpeed.
6
+ #
7
+ # Handlers usually are activated by calling <tt>MyHandler.run(myapp)</tt>.
8
+ # A second optional hash can be passed to include server-specific
9
+ # configuration.
10
+ module Handler
11
+ def self.get(server)
12
+ return unless server
13
+
14
+ if klass = @handlers[server]
15
+ obj = Object
16
+ klass.split("::").each { |x| obj = obj.const_get(x) }
17
+ obj
18
+ else
19
+ Rack::Handler.const_get(server.capitalize)
20
+ end
21
+ end
22
+
23
+ def self.register(server, klass)
24
+ @handlers ||= {}
25
+ @handlers[server] = klass
26
+ end
27
+
28
+ autoload :CGI, "rack/handler/cgi"
29
+ autoload :FastCGI, "rack/handler/fastcgi"
30
+ autoload :Mongrel, "rack/handler/mongrel"
31
+ autoload :EventedMongrel, "rack/handler/evented_mongrel"
32
+ autoload :WEBrick, "rack/handler/webrick"
33
+ autoload :LSWS, "rack/handler/lsws"
34
+ autoload :SCGI, "rack/handler/scgi"
35
+
36
+ register 'cgi', 'Rack::Handler::CGI'
37
+ register 'fastcgi', 'Rack::Handler::FastCGI'
38
+ register 'mongrel', 'Rack::Handler::Mongrel'
39
+ register 'emongrel', 'Rack::Handler::EventedMongrel'
40
+ register 'webrick', 'Rack::Handler::WEBrick'
41
+ register 'lsws', 'Rack::Handler::LSWS'
42
+ register 'scgi', 'Rack::Handler::SCGI'
43
+ end
44
+ end
@@ -0,0 +1,8 @@
1
+ require 'swiftcore/evented_mongrel'
2
+
3
+ module Rack
4
+ module Handler
5
+ class EventedMongrel < Mongrel
6
+ end
7
+ end
8
+ end
@@ -1,4 +1,5 @@
1
1
  require 'fcgi'
2
+ require 'socket'
2
3
 
3
4
  module Rack
4
5
  module Handler
@@ -7,7 +7,27 @@ module Rack
7
7
  def self.run(app, options={})
8
8
  server = ::Mongrel::HttpServer.new(options[:Host] || '0.0.0.0',
9
9
  options[:Port] || 8080)
10
- server.register('/', Rack::Handler::Mongrel.new(app))
10
+ # Acts like Rack::URLMap, utilizing Mongrel's own path finding methods.
11
+ # Use is similar to #run, replacing the app argument with a hash of
12
+ # { path=>app, ... } or an instance of Rack::URLMap.
13
+ if options[:map]
14
+ if app.is_a? Hash
15
+ app.each do |path, appl|
16
+ path = '/'+path unless path[0] == ?/
17
+ server.register(path, Rack::Handler::Mongrel.new(appl))
18
+ end
19
+ elsif app.is_a? URLMap
20
+ app.instance_variable_get(:@mapping).each do |(host, path, appl)|
21
+ next if !host.nil? && !options[:Host].nil? && options[:Host] != host
22
+ path = '/'+path unless path[0] == ?/
23
+ server.register(path, Rack::Handler::Mongrel.new(appl))
24
+ end
25
+ else
26
+ raise ArgumentError, "first argument should be a Hash or URLMap"
27
+ end
28
+ else
29
+ server.register('/', Rack::Handler::Mongrel.new(app))
30
+ end
11
31
  yield server if block_given?
12
32
  server.run.join
13
33
  end
@@ -29,7 +29,11 @@ module Rack
29
29
 
30
30
  ## A Rack application is an Ruby object (not a class) that
31
31
  ## responds to +call+.
32
- def call(env=nil)
32
+ def call(env=nil)
33
+ dup._call(env)
34
+ end
35
+
36
+ def _call(env)
33
37
  ## It takes exactly one argument, the *environment*
34
38
  assert("No env given") { env }
35
39
  check_env env
@@ -57,7 +61,7 @@ module Rack
57
61
  env.instance_of? Hash
58
62
  }
59
63
 
60
- ##
64
+ ##
61
65
  ## The environment is required to include these variables
62
66
  ## (adopted from PEP333), except when they'd be empty, but see
63
67
  ## below.
@@ -115,7 +119,7 @@ module Rack
115
119
  ## and should be prefixed uniquely. The prefix <tt>rack.</tt>
116
120
  ## is reserved for use with the Rack core distribution and must
117
121
  ## not be used otherwise.
118
- ##
122
+ ##
119
123
 
120
124
  %w[REQUEST_METHOD SERVER_NAME SERVER_PORT
121
125
  QUERY_STRING
@@ -141,7 +145,7 @@ module Rack
141
145
  }
142
146
  }
143
147
 
144
- ##
148
+ ##
145
149
  ## There are the following restrictions:
146
150
 
147
151
  ## * <tt>rack.version</tt> must be an array of Integers.
@@ -301,14 +305,16 @@ module Rack
301
305
 
302
306
  ## === The Status
303
307
  def check_status(status)
304
- ## The status, if parsed as integer (+to_i+), must be bigger than 100.
305
- assert("Status must be >100 seen as integer") { status.to_i > 100 }
308
+ ## The status, if parsed as integer (+to_i+), must be greater than or equal to 100.
309
+ assert("Status must be >=100 seen as integer") { status.to_i >= 100 }
306
310
  end
307
311
 
308
312
  ## === The Headers
309
313
  def check_headers(header)
310
314
  ## The header must respond to each, and yield values of key and value.
311
- assert("header should respond to #each") { header.respond_to? :each }
315
+ assert("headers object should respond to #each, but doesn't (got #{header.class} as headers)") {
316
+ header.respond_to? :each
317
+ }
312
318
  header.each { |key, value|
313
319
  ## The header keys must be Strings.
314
320
  assert("header key must be a string, was #{key.class}") {
@@ -323,12 +329,13 @@ module Rack
323
329
  ## but only contain keys that consist of
324
330
  ## letters, digits, <tt>_</tt> or <tt>-</tt> and start with a letter.
325
331
  assert("invalid header name: #{key}") { key =~ /\A[a-zA-Z][a-zA-Z0-9_-]*\z/ }
326
- ##
332
+ ##
327
333
  ## The values of the header must respond to #each.
328
- assert("header values must respond to #each") { value.respond_to? :each }
334
+ assert("header values must respond to #each, but the value of " +
335
+ "'#{key}' doesn't (is #{value.class})") { value.respond_to? :each }
329
336
  value.each { |item|
330
337
  ## The values passed on #each must be Strings
331
- assert("header values must consist of Strings") {
338
+ assert("header values must consist of Strings, but '#{key}' also contains a #{item.class}") {
332
339
  item.instance_of?(String)
333
340
  }
334
341
  ## and not contain characters below 037.
@@ -353,7 +360,7 @@ module Rack
353
360
  end
354
361
  }
355
362
  assert("No Content-Type header found") {
356
- [201, 204, 304].include? status.to_i
363
+ [204, 304].include? status.to_i
357
364
  }
358
365
  end
359
366
 
@@ -368,11 +375,11 @@ module Rack
368
375
  }
369
376
  yield part
370
377
  }
371
- ##
378
+ ##
372
379
  ## If the Body responds to #close, it will be called after iteration.
373
380
  # XXX howto: assert("Body has not been closed") { @closed }
374
381
 
375
- ##
382
+ ##
376
383
  ## The Body commonly is an Array of Strings, the application
377
384
  ## instance itself, or a File-like object.
378
385
  end