about summary refs log tree commit homepage
path: root/extras
diff options
context:
space:
mode:
authorEric Wong <normalperson@yhbt.net>2013-11-04 05:33:31 +0000
committerEric Wong <normalperson@yhbt.net>2013-11-05 21:55:39 +0000
commit52487c2dac216944543a823ac0f921471b685d60 (patch)
tree95eeba1cad0535914c285ffab2b8bc286788572b /extras
parent12f8394fc46aecb19616fc54969c7ab87cccd208 (diff)
downloadyahns-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.rb151
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