Skip to content
This repository

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

Open
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 December 30, 2012
lib/rack/static.rb
((30 lines not shown))
108 111
     end
109 112
 
110 113
     def call(env)
111  
-      path = env["PATH_INFO"]
  114
+      path = env["PATH_INFO"].strip
3
James Tucker Owner
raggi added a note December 30, 2012

Why the addition of strip?

7rans
trans added a note December 30, 2012

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 Collaborator

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.

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 December 30, 2012
lib/rack/static.rb
@@ -149,5 +160,11 @@ def set_headers(headers)
149 160
       headers.each { |field, content| @headers[field] = content }
150 161
     end
151 162
 
  163
+    # Determine if a path is a directory by looking for
  164
+    # trailing `/` or, failing that, checking the file system.
  165
+    def directory?(path)
  166
+      path.end_with?('/') || ::File.directory?(::File.join(@root, path.to_s))
1
James Tucker Owner
raggi added a note December 30, 2012

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 December 30, 2012
lib/rack/static.rb
@@ -149,5 +160,11 @@ def set_headers(headers)
149 160
       headers.each { |field, content| @headers[field] = content }
150 161
     end
151 162
 
  163
+    # Determine if a path is a directory by looking for
  164
+    # trailing `/` or, failing that, checking the file system.
  165
+    def directory?(path)
1
James Tucker Owner
raggi added a note December 30, 2012

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 December 30, 2012
lib/rack/static.rb
((10 lines not shown))
100 96
     end
101 97
 
  98
+    # Look up `path` in urls.
  99
+    # If urls is an Array, return `path` if there is a match, otherwise nil.
  100
+    # If urls is a Hash, return path "overwrite" if there is a match.
  101
+    # Otherwise return nil.
102 102
     def route_file(path)
2
James Tucker Owner
raggi added a note December 30, 2012

route matching may want an unescaped representation of the path.

7rans
trans added a note December 30, 2012

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
trans commented April 04, 2014

@mulderp Yes, that's the idea.

Would be nice if this would make it into Rack.

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

Showing 1 unique commit by 1 author.

Dec 30, 2012
7rans Refactor Static and add existing dir index support. 8615262
This page is out of date. Refresh to see the latest.
49  lib/rack/static.rb
@@ -84,7 +84,7 @@ def initialize(app, options={})
84 84
       @app = app
85 85
       @urls = options[:urls] || ["/favicon.ico"]
86 86
       @index = options[:index]
87  
-      root = options[:root] || Dir.pwd
  87
+      @root = options[:root] || Dir.pwd
88 88
 
89 89
       # HTTP Headers
90 90
       @header_rules = options[:header_rules] || []
@@ -92,35 +92,46 @@ def initialize(app, options={})
92 92
       @header_rules.insert(0, [:all, {'Cache-Control' => options[:cache_control]}]) if options[:cache_control]
93 93
       @headers = {}
94 94
 
95  
-      @file_server = Rack::File.new(root, @headers)
96  
-    end
97  
-
98  
-    def overwrite_file_path(path)
99  
-      @urls.kind_of?(Hash) && @urls.key?(path) || @index && path =~ /\/$/
  95
+      @file_server = Rack::File.new(@root, @headers)
100 96
     end
101 97
 
  98
+    # Look up `path` in urls.
  99
+    # If urls is an Array, return `path` if there is a match, otherwise nil.
  100
+    # If urls is a Hash, return path "overwrite" if there is a match.
  101
+    # Otherwise return nil.
102 102
     def route_file(path)
103  
-      @urls.kind_of?(Array) && @urls.any? { |url| path.index(url) == 0 }
104  
-    end
105  
-
106  
-    def can_serve(path)
107  
-      route_file(path) || overwrite_file_path(path)
  103
+      case @urls
  104
+      when Array
  105
+        @urls.any?{ |url| path.index(url) == 0 } ? path : nil
  106
+      when Hash
  107
+        @urls[path]
  108
+      else
  109
+        nil
  110
+      end
108 111
     end
109 112
 
110 113
     def call(env)
111  
-      path = env["PATH_INFO"]
  114
+      path = env["PATH_INFO"].strip
  115
+      route = route_file(path)
112 116
 
113  
-      if can_serve(path)
114  
-        env["PATH_INFO"] = (path =~ /\/$/ ? path + @index : @urls[path]) if overwrite_file_path(path)
115  
-        @path = env["PATH_INFO"]
  117
+      if route
  118
+        if @index && directory?(route)
  119
+          route = route.chomp('/') + '/' + @index
  120
+        end
  121
+        @path = env["PATH_INFO"] = route
  122
+        apply_header_rules
  123
+        @file_server.call(env)
  124
+      elsif @index && directory?(path)
  125
+        @path = env["PATH_INFO"] = path.chomp('/') + '/' + @index
116 126
         apply_header_rules
117 127
         @file_server.call(env)
118 128
       else
  129
+        @path = path
119 130
         @app.call(env)
120 131
       end
121 132
     end
122 133
 
123  
-    # Convert HTTP header rules to HTTP headers
  134
+    # Convert HTTP header rules to HTTP headers.
124 135
     def apply_header_rules
125 136
       @header_rules.each do |rule, headers|
126 137
         apply_rule(rule, headers)
@@ -149,5 +160,11 @@ def set_headers(headers)
149 160
       headers.each { |field, content| @headers[field] = content }
150 161
     end
151 162
 
  163
+    # Determine if a path is a directory by looking for
  164
+    # trailing `/` or, failing that, checking the file system.
  165
+    def directory?(path)
  166
+      path.end_with?('/') || ::File.directory?(::File.join(@root, path.to_s))
  167
+    end
  168
+
152 169
   end
153 170
 end
13  test/spec_static.rb
@@ -53,6 +53,19 @@ def static(app, *args)
53 53
     res.body.should =~ /another index!/
54 54
   end
55 55
 
  56
+  it "calls index file for existing folders without specific root request" do
  57
+    res = @static_request.get("")
  58
+    res.should.be.ok
  59
+    res.body.should =~ /index!/
  60
+
  61
+    res = @static_request.get("/other")
  62
+    res.should.be.not_found
  63
+
  64
+    res = @static_request.get("/another")
  65
+    res.should.be.ok
  66
+    res.body.should =~ /another index!/
  67
+  end
  68
+
56 69
   it "doesn't call index file if :index option was omitted" do
57 70
     res = @request.get("/")
58 71
     res.body.should == "Hello World"
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.