Alex Ciminian
Alex Ciminian

Reputation: 11508

Rails: do not allow access to static asset directory without a trailing /

I would like to serve up a generated static website (containing documentation) from the Rails public/ directory. My directory structure looks like this:

`-- rails-root
    |   [...]
    |-- public
    |   |-- assets
    |   |   |-- favicon.png
    |   |   |-- fonts
    |   |   |   `-- ...
    |   |   |-- images
    |   |   |   `-- ...
    |   |   |-- javascripts
    |   |   |   `-- ...
    |   |   `-- stylesheets
    |   |       `-- ...
    |   |-- documentation
    |   |   |-- GLOSSARY.html
    |   |   |-- index.html
    |   |   `-- ...

I am copying my generated files to rails-root/public/documentation and everything is fine as long as I request it as http://<site>/documentation/ or http://<site>/documentation/index.html.

However, when I request http://<site>/documentation rails tries to be helpful and serves up my index.html page. All the links in the page are relative so serving it up from a different base (root) instead of its directory (documentation/) makes it impossible for it to load any assets, looks broken and prevents navigation from all links.

I've tried setting up a route to do a redirect in case someone requests http://<site>/documentation (without a trailing slash):

  get '/documentation', :to => redirect('/documentation/index.html')

This does not work, I'm guessing because the public files take precedence over routing? I did try a bogus path just to validate that my routing code is correct and it worked:

  get '/xxx', :to => redirect('/documentation/index.html')

I would like to either prevent rails from being helpful (I'm fine with a 404 for http://<site>/documentation as long as trailing / works; 404 is better than a broken page) or make the redirect. I am using Rails 4.2. Any ideas welcome!

Upvotes: 1

Views: 381

Answers (1)

Ivo Anjo
Ivo Anjo

Reputation: 124

The ActionDispatch::Static middleware is responsible for serving static content, and for this behavior of serving a directory even when accessed without trailing slash.

There doesn't seem to be a nice way to change this behavior without monkey patching or forking this middleware.

But, if you're ok with the overhead, a workaround is to observe that the request path/path_info is changed by ActionDispatch::Static and use this information to write a middleware that intercepts it and redirects directories without trailing slash:

module Rack
  class RedirectStaticWithoutTrailingSlash
    def initialize(app)
      @app = app
    end

    def call(env)
      request = Rack::Request.new(env)
      original_path_info = request.path_info

      response = @app.call(env)

      updated_path_info = request.path_info

      if serving_index_for_path_without_trailing_slash?(original_path_info, updated_path_info)
        redirect_using_trailing_slash(original_path_info)
      else
        response
      end
    end

    def serving_index_for_path_without_trailing_slash?(original_path_info, updated_path_info)
      updated_path_info.end_with?('index.html') &&
        original_path_info != updated_path_info &&
        !original_path_info.end_with?('/')
    end

    def redirect_using_trailing_slash(path)
      redirect("#{path}/")
    end

    def redirect(url)
      [301, {'Location' => url, 'Content-Type' => 'text/html'}, ['Moved Permanently']]
    end
  end
end

This middleware will need to be configured to act before ActionDispatch in your rails configuration:

config.middleware.insert_before('ActionDispatch::Static', 'Rack::RedirectStaticWithoutTrailingSlash')

The disadvantage of this approach is that you still do all the work of reading the file and preparing the request, and since it's not using any kind of document behavior, it may break in future rails versions.

Upvotes: 1

Related Questions