123456789_123456789_123456789_123456789_123456789_

Class: WEBrick::HTTPServlet::FileHandler

Relationships & Source Files
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
Instance Chain:
Inherits: WEBrick::HTTPServlet::AbstractServlet
Defined in: lib/webrick/httpservlet/filehandler.rb

Overview

Serves a directory including fancy indexing and a variety of other options.

Example:

server.mount('/assets', WEBrick::HTTPServlet::FileHandler,
             '/path/to/assets')

Constant Summary

Class Method Summary

AbstractServlet - Inherited

.get_instance

Factory for servlet instances that will handle a request from server using options from the mount point.

.new

Initializes a new servlet for server using options which are stored as-is in @options.

Instance Method Summary

AbstractServlet - Inherited

#do_GET

Raises a NotFound exception.

#do_HEAD

Dispatches to do_GET.

#do_OPTIONS

Returns the allowed HTTP request methods.

#service

Dispatches to a do_ method based on req if such a method is available.

#redirect_to_directory_uri

Redirects to a path ending in /.

Constructor Details

.new(server, root, options = {}, default = Config::FileHandler) ⇒ FileHandler

Creates a FileHandler servlet on server that serves files starting at directory ::

options may be a Hash containing keys from Config::FileHandler or true or false.

If options is true or false then :FancyIndexing is enabled or disabled respectively.

[ GitHub ]

  
# File 'lib/webrick/httpservlet/filehandler.rb', line 203

def initialize(server, root, options={}, default=Config::FileHandler)
  @config = server.config
  @logger = @config[:Logger]
  @root = File.expand_path(root)
  if options == true || options == false
    options = { :FancyIndexing => options }
  end
  @options = default.dup.update(options)
end

Class Method Details

.add_handler(suffix, handler)

Allow custom handling of requests for files with suffix by class handler

[ GitHub ]

  
# File 'lib/webrick/httpservlet/filehandler.rb', line 182

def self.add_handler(suffix, handler)
  HandlerTable[suffix] = handler
end

.remove_handler(suffix)

Remove custom handling of requests for files with suffix

[ GitHub ]

  
# File 'lib/webrick/httpservlet/filehandler.rb', line 189

def self.remove_handler(suffix)
  HandlerTable.delete(suffix)
end

Instance Method Details

#call_callback(callback_name, req, res) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpservlet/filehandler.rb', line 405

def call_callback(callback_name, req, res)
  if cb = @options[callback_name]
    cb.call(req, res)
  end
end

#check_filename(req, res, name) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpservlet/filehandler.rb', line 358

def check_filename(req, res, name)
  if nondisclosure_name?(name) || windows_ambiguous_name?(name)
    @logger.warn("the request refers nondisclosure name `#{name}'.")
    raise HTTPStatus::NotFound, "`#{req.path}' not found."
  end
end

#do_GET(req, res)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpservlet/filehandler.rb', line 236

def do_GET(req, res)
  unless exec_handler(req, res)
    set_dir_list(req, res)
  end
end

#do_OPTIONS(req, res)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpservlet/filehandler.rb', line 248

def do_OPTIONS(req, res)
  unless exec_handler(req, res)
    super(req, res)
  end
end

#do_POST(req, res)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpservlet/filehandler.rb', line 242

def do_POST(req, res)
  unless exec_handler(req, res)
    raise HTTPStatus::NotFound, "`#{req.path}' not found."
  end
end

#exec_handler(req, res) (private)

This method is for internal use only.

Raises:

  • (HTTPStatus::NotFound)
[ GitHub ]

  
# File 'lib/webrick/httpservlet/filehandler.rb', line 300

def exec_handler(req, res)
  raise HTTPStatus::NotFound, "`#{req.path}' not found" unless @root
  if set_filename(req, res)
    handler = get_handler(req, res)
    call_callback(:HandlerCallback, req, res)
    h = handler.get_instance(@config, res.filename)
    h.service(req, res)
    return true
  end
  call_callback(:HandlerCallback, req, res)
  return false
end

#get_handler(req, res) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpservlet/filehandler.rb', line 313

def get_handler(req, res)
  suffix1 = (/\.(\w+)\z/ =~ res.filename) && $1.downcase
  if /\.(\w)\.([\w\-])\z/ =~ res.filename
    if @options[:AcceptableLanguages].include?($2.downcase)
      suffix2 = $1.downcase
    end
  end
  handler_table = @options[:HandlerTable]
  return handler_table[suffix1] || handler_table[suffix2] ||
         HandlerTable[suffix1] || HandlerTable[suffix2] ||
         DefaultFileHandler
end

#nondisclosure_name?(name) ⇒ Boolean (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpservlet/filehandler.rb', line 417

def nondisclosure_name?(name)
  @options[:NondisclosureName].each{|pattern|
    if File.fnmatch(pattern, name, File::FNM_CASEFOLD)
      return true
    end
  }
  return false
end

#prevent_directory_traversal(req, res) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpservlet/filehandler.rb', line 277

def prevent_directory_traversal(req, res)
  # Preventing directory traversal on Windows platforms;
  # Backslashes (0x5c) in path_info are not interpreted as special
  # character in URI notation. So the value of path_info should be
  # normalize before accessing to the filesystem.

  # dirty hack for filesystem encoding; in nature, File.expand_path
  # should not be used for path normalization.  [Bug #3345]
  path = req.path_info.dup.force_encoding(Encoding.find("filesystem"))
  if trailing_pathsep?(req.path_info)
    # File.expand_path removes the trailing path separator.
    # Adding a character is a workaround to save it.
    #  File.expand_path("/aaa/")        #=> "/aaa"
    #  File.expand_path("/aaa/" + "x")  #=> "/aaa/x"
    expanded = File.expand_path(path + "x")
    expanded.chop!  # remove trailing "x"
  else
    expanded = File.expand_path(path)
  end
  expanded.force_encoding(req.path_info.encoding)
  req.path_info = expanded
end

#search_file(req, res, basename) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpservlet/filehandler.rb', line 383

def search_file(req, res, basename)
  langs = @options[:AcceptableLanguages]
  path = res.filename + basename
  if File.file?(path)
    return basename
  elsif langs.size > 0
    req.accept_language.each{|lang|
      path_with_lang = path + ".#{lang}"
      if langs.member?(lang) && File.file?(path_with_lang)
        return basename + ".#{lang}"
      end
    }
    (langs - req.accept_language).each{|lang|
      path_with_lang = path + ".#{lang}"
      if File.file?(path_with_lang)
        return basename + ".#{lang}"
      end
    }
  end
  return nil
end

#search_index_file(req, res) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpservlet/filehandler.rb', line 374

def search_index_file(req, res)
  @config[:DirectoryIndex].each{|index|
    if file = search_file(req, res, "/"+index)
      return file
    end
  }
  return nil
end

#service(req, res)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpservlet/filehandler.rb', line 215

def service(req, res)
  # if this class is mounted on "/" and /~username is requested.
  # we're going to override path informations before invoking service.
  if defined?(Etc) && @options[:UserDir] && req.script_name.empty?
    if %r|^(/~([^/]+))| =~ req.path_info
      script_name, user = $1, $2
      path_info = $'
      begin
        passwd = Etc::getpwnam(user)
        @root = File::join(passwd.dir, @options[:UserDir])
        req.script_name = script_name
        req.path_info = path_info
      rescue
        @logger.debug "#{self.class}#do_GET: getpwnam(#{user}) failed"
      end
    end
  end
  prevent_directory_traversal(req, res)
  super(req, res)
end

#set_dir_list(req, res) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpservlet/filehandler.rb', line 426

def set_dir_list(req, res)
  redirect_to_directory_uri(req, res)
  unless @options[:FancyIndexing]
    raise HTTPStatus::Forbidden, "no access permission to `#{req.path}'"
  end
  local_path = res.filename
  list = Dir::entries(local_path).collect{|name|
    next if name == "." || name == ".."
    next if nondisclosure_name?(name)
    next if windows_ambiguous_name?(name)
    st = (File::stat(File.join(local_path, name)) rescue nil)
    if st.nil?
      [ name, nil, -1 ]
    elsif st.directory?
      [ name + "/", st.mtime, -1 ]
    else
      [ name, st.mtime, st.size ]
    end
  }
  list.compact!

  query = req.query

  d0 = nil
  idx = nil
  %w[N M S].each_with_index do |q, i|
    if d = query.delete(q)
      idx ||= i
      d0 ||= d
    end
  end
  d0 ||= "A"
  idx ||= 0
  d1 = (d0 == "A") ? "D" : "A"

  if d0 == "A"
    list.sort!{|a,b| a[idx] <=> b[idx] }
  else
    list.sort!{|a,b| b[idx] <=> a[idx] }
  end

  namewidth = query["NameWidth"]
  if namewidth == "*"
    namewidth = nil
  elsif !namewidth or (namewidth = namewidth.to_i) < 2
    namewidth = 25
  end
  query = query.inject('') {|s, (k, v)| s << '&' << HTMLUtils::escape("#{k}=#{v}")}

  type = "text/html"
  case enc = Encoding.find('filesystem')
  when Encoding::US_ASCII, Encoding::ASCII_8BIT
  else
    type << "; charset=\"#{enc.name}\""
  end
  res['content-type'] = type

  title = "Index of #{HTMLUtils::escape(req.path)}"
  res.body = <<-_end_of_html_
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<HTML>
  <HEAD>
    <TITLE>#{title}</TITLE>
    <style type="text/css">
    <!--
    .name, .mtime { text-align: left; }
    .size { text-align: right; }
    td { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; }
    table { border-collapse: collapse; }
    tr th { border-bottom: 2px groove; }
    //-->
    </style>
  </HEAD>
  <BODY>
    <H1>#{title}</H1>
  _end_of_html_

  res.body << "<TABLE width=\"100%\"><THEAD><TR>\n"
  res.body << "<TH class=\"name\"><A HREF=\"?N=#{d1}#{query}\">Name</A></TH>"
  res.body << "<TH class=\"mtime\"><A HREF=\"?M=#{d1}#{query}\">Last modified</A></TH>"
  res.body << "<TH class=\"size\"><A HREF=\"?S=#{d1}#{query}\">Size</A></TH>\n"
  res.body << "</TR></THEAD>\n"
  res.body << "<TBODY>\n"

  query.sub!(/\A&/, '?')
  list.unshift [ "..", File::mtime(local_path+"/.."), -1 ]
  list.each{ |name, time, size|
    if name == ".."
      dname = "Parent Directory"
    elsif namewidth and name.size > namewidth
      dname = name[0...(namewidth - 2)] << '..'
    else
      dname = name
    end
    s =  "<TR><TD class=\"name\"><A HREF=\"#{HTTPUtils::escape(name)}#{query if name.end_with?('/')}\">#{HTMLUtils::escape(dname)}</A></TD>"
    s << "<TD class=\"mtime\">" << (time ? time.strftime("%Y/%m/%d %H:%M") : "") << "</TD>"
    s << "<TD class=\"size\">" << (size >= 0 ? size.to_s : "-") << "</TD></TR>\n"
    res.body << s
  }
  res.body << "</TBODY></TABLE>"
  res.body << "<HR>"

  res.body << <<-_end_of_html_
    <ADDRESS>
     #{HTMLUtils::escape(@config[:ServerSoftware])}<BR>
     at #{req.host}:#{req.port}
    </ADDRESS>
  </BODY>
</HTML>
  _end_of_html_
end

#set_filename(req, res) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpservlet/filehandler.rb', line 326

def set_filename(req, res)
  res.filename = @root.dup
  path_info = req.path_info.scan(%r|/[^/]*|)

  path_info.unshift("")  # dummy for checking @root dir
  while base = path_info.first
    break if base == "/"
    break unless File.directory?(File.expand_path(res.filename + base))
    shift_path_info(req, res, path_info)
    call_callback(:DirectoryCallback, req, res)
  end

  if base = path_info.first
    if base == "/"
      if file = search_index_file(req, res)
        shift_path_info(req, res, path_info, file)
        call_callback(:FileCallback, req, res)
        return true
      end
      shift_path_info(req, res, path_info)
    elsif file = search_file(req, res, base)
      shift_path_info(req, res, path_info, file)
      call_callback(:FileCallback, req, res)
      return true
    else
      raise HTTPStatus::NotFound, "`#{req.path}' not found."
    end
  end

  return false
end

#shift_path_info(req, res, path_info, base = nil) (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpservlet/filehandler.rb', line 365

def shift_path_info(req, res, path_info, base=nil)
  tmp = path_info.shift
  base = base || tmp
  req.path_info = path_info.join
  req.script_name << base
  res.filename = File.expand_path(res.filename + base)
  check_filename(req, res, File.basename(res.filename))
end

#trailing_pathsep?(path) ⇒ Boolean (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpservlet/filehandler.rb', line 268

def trailing_pathsep?(path)
  # check for trailing path separator:
  #   File.dirname("/aaaa/bbbb/")      #=> "/aaaa")
  #   File.dirname("/aaaa/bbbb/x")     #=> "/aaaa/bbbb")
  #   File.dirname("/aaaa/bbbb")       #=> "/aaaa")
  #   File.dirname("/aaaa/bbbbx")      #=> "/aaaa")
  return File.dirname(path) != File.dirname(path+"x")
end

#windows_ambiguous_name?(name) ⇒ Boolean (private)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/webrick/httpservlet/filehandler.rb', line 411

def windows_ambiguous_name?(name)
  return true if /[. ]+\z/ =~ name
  return true if /::\$DATA\z/ =~ name
  return false
end