rack.git  about / heads / tags
a modular Ruby webserver interface
blob b08f5949096f8a5690e3feb8d34de127e5fe801a 5043 bytes (raw)
$ git show no-unicorn:lib/rack/directory.rb	# shows this blob on the CLI

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
 
# frozen_string_literal: true

require 'time'
require 'rack/utils'
require 'rack/mime'
require 'rack/files'

module Rack
  # Rack::Directory serves entries below the +root+ given, according to the
  # path info of the Rack request. If a directory is found, the file's contents
  # will be presented in an html based index. If a file is found, the env will
  # be passed to the specified +app+.
  #
  # If +app+ is not specified, a Rack::Files of the same +root+ will be used.

  class Directory
    DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>"
    DIR_PAGE = <<-PAGE
<html><head>
  <title>%s</title>
  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  <style type='text/css'>
table { width:100%%; }
.name { text-align:left; }
.size, .mtime { text-align:right; }
.type { width:11em; }
.mtime { width:15em; }
  </style>
</head><body>
<h1>%s</h1>
<hr />
<table>
  <tr>
    <th class='name'>Name</th>
    <th class='size'>Size</th>
    <th class='type'>Type</th>
    <th class='mtime'>Last Modified</th>
  </tr>
%s
</table>
<hr />
</body></html>
    PAGE

    class DirectoryBody < Struct.new(:root, :path, :files)
      def each
        show_path = Rack::Utils.escape_html(path.sub(/^#{root}/, ''))
        listings = files.map{|f| DIR_FILE % DIR_FILE_escape(*f) } * "\n"
        page = DIR_PAGE % [ show_path, show_path, listings ]
        page.each_line{|l| yield l }
      end

      private
      # Assumes url is already escaped.
      def DIR_FILE_escape url, *html
        [url, *html.map { |e| Utils.escape_html(e) }]
      end
    end

    attr_reader :root, :path

    def initialize(root, app = nil)
      @root = ::File.expand_path(root)
      @app = app || Rack::Files.new(@root)
      @head = Rack::Head.new(lambda { |env| get env })
    end

    def call(env)
      # strip body if this is a HEAD call
      @head.call env
    end

    def get(env)
      script_name = env[SCRIPT_NAME]
      path_info = Utils.unescape_path(env[PATH_INFO])

      if bad_request = check_bad_request(path_info)
        bad_request
      elsif forbidden = check_forbidden(path_info)
        forbidden
      else
        path = ::File.join(@root, path_info)
        list_path(env, path, path_info, script_name)
      end
    end

    def check_bad_request(path_info)
      return if Utils.valid_path?(path_info)

      body = "Bad Request\n"
      size = body.bytesize
      return [400, { CONTENT_TYPE => "text/plain",
        CONTENT_LENGTH => size.to_s,
        "X-Cascade" => "pass" }, [body]]
    end

    def check_forbidden(path_info)
      return unless path_info.include? ".."

      body = "Forbidden\n"
      size = body.bytesize
      return [403, { CONTENT_TYPE => "text/plain",
        CONTENT_LENGTH => size.to_s,
        "X-Cascade" => "pass" }, [body]]
    end

    def list_directory(path_info, path, script_name)
      files = [['../', 'Parent Directory', '', '', '']]
      glob = ::File.join(path, '*')

      url_head = (script_name.split('/') + path_info.split('/')).map do |part|
        Rack::Utils.escape_path part
      end

      Dir[glob].sort.each do |node|
        stat = stat(node)
        next unless stat
        basename = ::File.basename(node)
        ext = ::File.extname(node)

        url = ::File.join(*url_head + [Rack::Utils.escape_path(basename)])
        size = stat.size
        type = stat.directory? ? 'directory' : Mime.mime_type(ext)
        size = stat.directory? ? '-' : filesize_format(size)
        mtime = stat.mtime.httpdate
        url << '/'  if stat.directory?
        basename << '/'  if stat.directory?

        files << [ url, basename, size, type, mtime ]
      end

      return [ 200, { CONTENT_TYPE => 'text/html; charset=utf-8' }, DirectoryBody.new(@root, path, files) ]
    end

    def stat(node)
      ::File.stat(node)
    rescue Errno::ENOENT, Errno::ELOOP
      return nil
    end

    # TODO: add correct response if not readable, not sure if 404 is the best
    #       option
    def list_path(env, path, path_info, script_name)
      stat = ::File.stat(path)

      if stat.readable?
        return @app.call(env) if stat.file?
        return list_directory(path_info, path, script_name) if stat.directory?
      else
        raise Errno::ENOENT, 'No such file or directory'
      end

    rescue Errno::ENOENT, Errno::ELOOP
      return entity_not_found(path_info)
    end

    def entity_not_found(path_info)
      body = "Entity not found: #{path_info}\n"
      size = body.bytesize
      return [404, { CONTENT_TYPE => "text/plain",
        CONTENT_LENGTH => size.to_s,
        "X-Cascade" => "pass" }, [body]]
    end

    # Stolen from Ramaze

    FILESIZE_FORMAT = [
      ['%.1fT', 1 << 40],
      ['%.1fG', 1 << 30],
      ['%.1fM', 1 << 20],
      ['%.1fK', 1 << 10],
    ]

    def filesize_format(int)
      FILESIZE_FORMAT.each do |format, size|
        return format % (int.to_f / size) if int >= size
      end

      "#{int}B"
    end
  end
end

git clone https://yhbt.net/rack.git