Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Refactor Rack::Static and improve directory index support #477

Closed
wants to merge 1 commit into from

4 participants

@trans

This is a reissue of #403, rebased and with test.

This patch makes the the Rack::Static code a little more efficient and readable. At the same time it adds support for existent directory index support --instead of just checking for a trailing /, failing that it will check to see if the the path is an actual directory on disk. This is indispensable in some use cases where a trailing slash can not be insured, but the directories index file still needs to be served.

If necessary we can add a new configuration option to conditionally support this new directory behaviour, but I did not do so presently b/c I suspect it would be a YAGNI.

@raggi raggi commented on the diff
lib/rack/static.rb
((30 lines not shown))
end
def call(env)
- path = env["PATH_INFO"]
+ path = env["PATH_INFO"].strip
@raggi Owner
raggi added a note

Why the addition of strip?

@trans
trans added a note

Don't recall at this point. That's what I had from before. I suppose it was just in case trailing white space ended up in the path --it could mess up the route matching.

@tenderlove Owner

This middleware is for serving up static files. It's legitimate for a static file to end in a space, so strip should not be used.

@trans
trans added a note

Sounds technically correct, OTOH who in their right mind would do that and is it wise to facilitate them?

@raggi Owner
raggi added a note

The point is, your code is incorrect if it disregards correct use cases. This shouldn't be an argument, so please don't make it one.

@trans
trans added a note

Spaces aren't valid in URLs, trialing spaces aren't valid in Windows, and even if they are possible in Linux it's a world of headache. Here's some learning for you: http://www.dwheeler.com/essays/fixing-unix-linux-filenames.html. But hey if you still think my argument doesn't have a point, have at it.

@raggi Owner
raggi added a note

Congratulations, you just sent me an essay about how the shell is broken, incorrectly reporting that the filesystem is broken, as a justification for an invalid patch.

If you want to force your opinions on people in your own libraries and frameworks, that's totally fine, but I neither agree with you, nor can force such opinions on rack users. Please stop making this difficult, it's making me upset.

@trans
trans added a note

After two years of nothing. No problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@raggi raggi commented on the diff
lib/rack/static.rb
@@ -149,5 +160,11 @@ def set_headers(headers)
headers.each { |field, content| @headers[field] = content }
end
+ # Determine if a path is a directory by looking for
+ # trailing `/` or, failing that, checking the file system.
+ def directory?(path)
+ path.end_with?('/') || ::File.directory?(::File.join(@root, path.to_s))
@raggi Owner
raggi added a note

I think we need to extract utility code out of Rack::File into Rack::Utils for path depth checks and readability checks, then use that here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@raggi raggi commented on the diff
lib/rack/static.rb
@@ -149,5 +160,11 @@ def set_headers(headers)
headers.each { |field, content| @headers[field] = content }
end
+ # Determine if a path is a directory by looking for
+ # trailing `/` or, failing that, checking the file system.
+ def directory?(path)
@raggi Owner
raggi added a note

directory matching may want an unescaped path. previously this was handled by Rack::File as static didn't need to care.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@raggi raggi commented on the diff
lib/rack/static.rb
((10 lines not shown))
end
+ # Look up `path` in urls.
+ # If urls is an Array, return `path` if there is a match, otherwise nil.
+ # If urls is a Hash, return path "overwrite" if there is a match.
+ # Otherwise return nil.
def route_file(path)
@raggi Owner
raggi added a note

route matching may want an unescaped representation of the path.

@trans
trans added a note

That makes sense. I wasn't cognisant of the escape vs unescaped paths.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@raggi
Owner

Thanks for taking the time to rebase this and add the test case. I left a few line comments. I'll be happy to do the security refactors, but please comment if I've missed anything.

Thanks again.

@trans

I don't see anything else off hand. You've covered some points I'd not readily notice, as I am not familiar enough with the Rack code as a whole. So if you can make these adjustments, that would be great.

@raggi
Owner

Due to the work that needs doing to this, I don't have time to cover it before the 1.5.0 release. I have rescheduled this for the 1.6 milestone.

@trans

Ok. But I hope 1.6 doesn't take too long to be released. I've been wanting to use this feature for a very long time!

@trans

Hmm... besides unescaping the path, what has to be done? Seems like the rest is just code structure improvements. Or am I overlooking something (e.g. moving some code to Utils)?

@mulderp

I was wondering if this refactoring could help to introduce a similar behavior as with Python's SimpleHTTPServer:

When I go to a directory with an index.html and I do:

python -m SimpleHTTPServer
Serving HTTP on 0.0.0.0 port 8000 ...

I automatically get the index.html served from the 0.0.0.0:8000 root path.
Could this be done in a similar way with Rack?
Right now, I need to specify index.html to get the same effect with the following config.ru:

run Rack::Directory.new("./public/")
@trans

@mulderp Yes, that's the idea.

Would be nice if this would make it into Rack.

@trans

:frowning: Unfortunate this feature never made it in.

@raggi
Owner

Oh, I was supposed to finish it? :tongue:

@trans

I think it is finished. You mentioned refactoring a bit, moving some code into Utils, but I don't know what you want exactly in that regard, so that's your call.

@trans trans closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Dec 30, 2012
  1. @trans
This page is out of date. Refresh to see the latest.
Showing with 46 additions and 16 deletions.
  1. +33 −16 lib/rack/static.rb
  2. +13 −0 test/spec_static.rb
View
49 lib/rack/static.rb
@@ -84,7 +84,7 @@ def initialize(app, options={})
@app = app
@urls = options[:urls] || ["/favicon.ico"]
@index = options[:index]
- root = options[:root] || Dir.pwd
+ @root = options[:root] || Dir.pwd
# HTTP Headers
@header_rules = options[:header_rules] || []
@@ -92,35 +92,46 @@ def initialize(app, options={})
@header_rules.insert(0, [:all, {'Cache-Control' => options[:cache_control]}]) if options[:cache_control]
@headers = {}
- @file_server = Rack::File.new(root, @headers)
- end
-
- def overwrite_file_path(path)
- @urls.kind_of?(Hash) && @urls.key?(path) || @index && path =~ /\/$/
+ @file_server = Rack::File.new(@root, @headers)
end
+ # Look up `path` in urls.
+ # If urls is an Array, return `path` if there is a match, otherwise nil.
+ # If urls is a Hash, return path "overwrite" if there is a match.
+ # Otherwise return nil.
def route_file(path)
@raggi Owner
raggi added a note

route matching may want an unescaped representation of the path.

@trans
trans added a note

That makes sense. I wasn't cognisant of the escape vs unescaped paths.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
- @urls.kind_of?(Array) && @urls.any? { |url| path.index(url) == 0 }
- end
-
- def can_serve(path)
- route_file(path) || overwrite_file_path(path)
+ case @urls
+ when Array
+ @urls.any?{ |url| path.index(url) == 0 } ? path : nil
+ when Hash
+ @urls[path]
+ else
+ nil
+ end
end
def call(env)
- path = env["PATH_INFO"]
+ path = env["PATH_INFO"].strip
@raggi Owner
raggi added a note

Why the addition of strip?

@trans
trans added a note

Don't recall at this point. That's what I had from before. I suppose it was just in case trailing white space ended up in the path --it could mess up the route matching.

@tenderlove Owner

This middleware is for serving up static files. It's legitimate for a static file to end in a space, so strip should not be used.

@trans
trans added a note

Sounds technically correct, OTOH who in their right mind would do that and is it wise to facilitate them?

@raggi Owner
raggi added a note

The point is, your code is incorrect if it disregards correct use cases. This shouldn't be an argument, so please don't make it one.

@trans
trans added a note

Spaces aren't valid in URLs, trialing spaces aren't valid in Windows, and even if they are possible in Linux it's a world of headache. Here's some learning for you: http://www.dwheeler.com/essays/fixing-unix-linux-filenames.html. But hey if you still think my argument doesn't have a point, have at it.

@raggi Owner
raggi added a note

Congratulations, you just sent me an essay about how the shell is broken, incorrectly reporting that the filesystem is broken, as a justification for an invalid patch.

If you want to force your opinions on people in your own libraries and frameworks, that's totally fine, but I neither agree with you, nor can force such opinions on rack users. Please stop making this difficult, it's making me upset.

@trans
trans added a note

After two years of nothing. No problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ route = route_file(path)
- if can_serve(path)
- env["PATH_INFO"] = (path =~ /\/$/ ? path + @index : @urls[path]) if overwrite_file_path(path)
- @path = env["PATH_INFO"]
+ if route
+ if @index && directory?(route)
+ route = route.chomp('/') + '/' + @index
+ end
+ @path = env["PATH_INFO"] = route
+ apply_header_rules
+ @file_server.call(env)
+ elsif @index && directory?(path)
+ @path = env["PATH_INFO"] = path.chomp('/') + '/' + @index
apply_header_rules
@file_server.call(env)
else
+ @path = path
@app.call(env)
end
end
- # Convert HTTP header rules to HTTP headers
+ # Convert HTTP header rules to HTTP headers.
def apply_header_rules
@header_rules.each do |rule, headers|
apply_rule(rule, headers)
@@ -149,5 +160,11 @@ def set_headers(headers)
headers.each { |field, content| @headers[field] = content }
end
+ # Determine if a path is a directory by looking for
+ # trailing `/` or, failing that, checking the file system.
+ def directory?(path)
@raggi Owner
raggi added a note

directory matching may want an unescaped path. previously this was handled by Rack::File as static didn't need to care.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ path.end_with?('/') || ::File.directory?(::File.join(@root, path.to_s))
@raggi Owner
raggi added a note

I think we need to extract utility code out of Rack::File into Rack::Utils for path depth checks and readability checks, then use that here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ end
+
end
end
View
13 test/spec_static.rb
@@ -53,6 +53,19 @@ def static(app, *args)
res.body.should =~ /another index!/
end
+ it "calls index file for existing folders without specific root request" do
+ res = @static_request.get("")
+ res.should.be.ok
+ res.body.should =~ /index!/
+
+ res = @static_request.get("/other")
+ res.should.be.not_found
+
+ res = @static_request.get("/another")
+ res.should.be.ok
+ res.body.should =~ /another index!/
+ end
+
it "doesn't call index file if :index option was omitted" do
res = @request.get("/")
res.body.should == "Hello World"
Something went wrong with that request. Please try again.