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

7rans James Tucker Patrick Mulder Aaron Patterson
7rans

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.

James Tucker 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
James Tucker Owner
raggi added a note

Why the addition of strip?

7rans
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.

Aaron Patterson 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.

7rans
trans added a note

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

James Tucker 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.

7rans
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.

James Tucker 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.

7rans
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
James Tucker 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))
James Tucker 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
James Tucker 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)
James Tucker 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
James Tucker 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)
James Tucker Owner
raggi added a note

route matching may want an unescaped representation of the path.

7rans
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
James Tucker
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.

7rans

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.

James Tucker
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.

7rans

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!

7rans

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)?

Patrick Mulder

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/")
7rans

@mulderp Yes, that's the idea.

Would be nice if this would make it into Rack.

7rans

:frowning: Unfortunate this feature never made it in.

James Tucker
Owner

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

7rans

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.

7rans 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. 7rans
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
49 lib/rack/static.rb
View
@@ -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)
James Tucker Owner
raggi added a note

route matching may want an unescaped representation of the path.

7rans
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
James Tucker Owner
raggi added a note

Why the addition of strip?

7rans
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.

Aaron Patterson 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.

7rans
trans added a note

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

James Tucker 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.

7rans
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.

James Tucker 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.

7rans
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)
James Tucker 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))
James Tucker 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
13 test/spec_static.rb
View
@@ -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.