diff options
author | Eric Wong <normalperson@yhbt.net> | 2013-11-04 05:33:31 +0000 |
---|---|---|
committer | Eric Wong <normalperson@yhbt.net> | 2013-11-05 21:55:39 +0000 |
commit | 52487c2dac216944543a823ac0f921471b685d60 (patch) | |
tree | 95eeba1cad0535914c285ffab2b8bc286788572b /extras | |
parent | 12f8394fc46aecb19616fc54969c7ab87cccd208 (diff) | |
download | yahns-52487c2dac216944543a823ac0f921471b685d60.tar.gz |
Unlike Rack::Directory, this this also avoids tables and CSS for preformatted HTML. This is meant to resemble nginx autoindex and index functionality (combined).
Diffstat (limited to 'extras')
-rw-r--r-- | extras/autoindex.rb | 151 |
1 files changed, 151 insertions, 0 deletions
diff --git a/extras/autoindex.rb b/extras/autoindex.rb new file mode 100644 index 0000000..b868a5c --- /dev/null +++ b/extras/autoindex.rb @@ -0,0 +1,151 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require 'time' +require 'rack/utils' +require 'rack/request' + +# this is middleware meant to behave like "index" and "autoindex" in nginx +# No CSS or JS to avoid potential security bugs +# Only basic pre-formatted HTML, not even tables, should look good in lynx +# all bikeshedding here :> +class Autoindex + FN = %{<a href="%s">%s</a>} + TFMT = "%Y-%m-%d %H:%M" + + def initialize(app, index = %w(index.html)) + app.respond_to?(:root) or raise ArgumentError, + "wrapped app #{app.inspect} does not respond to :root" + @app = app + @root = app.root + @index = index + end + + def redirect_slash(env) + req = Rack::Request.new(env) + location = "#{req.url}/" + body = "Redirecting to #{location}\n" + [ 302, + { + "Content-Type" => "text/plain", + "Location" => location, + "Content-Length" => body.size.to_s + }, + [ body ] ] + end + + def call(env) + case env["REQUEST_METHOD"] + when "GET", "HEAD" + # try to serve the static file, first + status, headers, body = res = @app.call(env) + return res if status.to_i != 404 + + path_info = env["PATH_INFO"] + path_info_ue = Rack::Utils.unescape(path_info) + + # reject requests to go up a level (browser takes care of it) + path_info_ue =~ /\.\./ and return r(403) + + # cleanup the path + path_info_ue.squeeze!('/') + + # will raise ENOENT/ENOTDIR + pfx = "#@root#{path_info_ue}" + dir = Dir.open(pfx) + + return redirect_slash(env) unless path_info =~ %r{/\z} + + # try index.html and friends + tryenv = env.dup + @index.each do |base| + tryenv["PATH_INFO"] = "#{path_info}#{base}" + status, headers, body = res = @app.call(tryenv) + return res if status.to_i != 404 + end + + # generate the index, show directories first + dirs = [] + files = [] + dir.each do |base| + case base + when "." + next + end + + begin + st = File.stat("#{pfx}#{base}") + rescue + next + end + + url = Rack::Utils.escape_html(Rack::Utils.escape(base)) + name = Rack::Utils.escape_html(base) + if st.directory? + name << "/" + url << "/" + end + entry = sprintf(FN, url, name) + pad = 52 - name.size + entry << (" " * pad) if pad > 0 + entry << st.mtime.strftime(TFMT) + entry << sprintf("% 8s", human_size(st)) + + (st.directory? ? dirs : files) << [ name, entry ] + end + + dirs.sort! { |(a,_),(b)| a <=> b }.map! { |(_,ent)| ent } + files.sort! { |(a,_),(b)| a <=> b }.map! { |(_,ent)| ent } + + path_info_html = path_info_ue.split(%r{/}, -1).map! do |part| + Rack::Utils.escape_html(part) + end.join("/") + body = "<html><head><title>Index of #{path_info_html}</title></head>" \ + "<body><h1>Index of #{path_info_html}</h1><hr><pre>\n" \ + "#{dirs.concat(files).join("\n")}" \ + "</pre><hr></body></html>\n" + h = { "Content-Type" => "text/html", "Content-Length" => body.size.to_s } + [ 200, h, [ body ] ] + else + r(405) + end + rescue Errno::ENOENT, Errno::ENOTDIR # from Dir.open + r(404) + rescue => e + r(500, e.message, env) + ensure + dir.close if dir + end + + def r(code, msg = nil, env = nil) + if env && logger = env["rack.logger"] + logger.warn("#{env['REQUEST_METHOD']} #{env['PATH_INFO']} " \ + "#{code} #{msg.inspect}") + end + + if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(code) + [ code, {}, [] ] + else + h = { 'Content-Type' => 'text/plain', 'Content-Length' => '0' } + [ code, h, [] ] + end + end + + def human_size(st) + if st.file? + size = st.size + suffix = "" + %w(K M G T).each do |s| + break if size < 1024 + size /= 1024.0 + if size <= 1024 + suffix = s + break + end + end + "#{size.round}#{suffix}" + else + "-" + end + end +end |