class WEBrick::HTTPServlet::FileHandler
def self.add_handler(suffix, handler)
def self.add_handler(suffix, handler) HandlerTable[suffix] = handler end
def self.remove_handler(suffix)
def self.remove_handler(suffix) HandlerTable.delete(suffix) end
def call_callback(callback_name, req, res)
def call_callback(callback_name, req, res) if cb = @options[callback_name] cb.call(req, res) end end
def check_filename(req, res, name)
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
def do_GET(req, res)
def do_GET(req, res) unless exec_handler(req, res) set_dir_list(req, res) end end
def do_OPTIONS(req, res)
def do_OPTIONS(req, res) unless exec_handler(req, res) super(req, res) end end
def do_POST(req, res)
def do_POST(req, res) unless exec_handler(req, res) raise HTTPStatus::NotFound, "`#{req.path}' not found." end end
def exec_handler(req, res)
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
def get_handler(req, res)
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
def initialize(server, root, options={}, default=Config::FileHandler)
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
def nondisclosure_name?(name)
def nondisclosure_name?(name) @options[:NondisclosureName].each{|pattern| if File.fnmatch(pattern, name, File::FNM_CASEFOLD) return true end } return false end
def prevent_directory_traversal(req, res)
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
def search_file(req, res, basename)
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
def search_index_file(req, res)
def search_index_file(req, res) @config[:DirectoryIndex].each{|index| if file = search_file(req, res, "/"+index) return file end } return nil end
def service(req, res)
def service(req, res) # if this class is mounted on "/" and /~username is requested. # we're going to override path information 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
def set_dir_list(req, res)
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}")}.dup 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_ YPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> D> ITLE>#{title}</TITLE> tyle type="text/css"> -- ame, .mtime { text-align: left; } ize { text-align: right; } { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; } ble { border-collapse: collapse; } th { border-bottom: 2px groove; } --> style> AD> Y> 1>#{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_ DDRESS> {HTMLUtils::escape(@config[:ServerSoftware])}<BR> t #{req.host}:#{req.port} ADDRESS> DY> > _end_of_html_ end
def set_filename(req, res)
def set_filename(req, res) res.filename = @root path_info = req.path_info.scan(%r|/[^/]*|) path_info.unshift("") # dummy for checking @root dir while base = path_info.first base = set_filesystem_encoding(base) 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 base = set_filesystem_encoding(base) 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
def set_filesystem_encoding(str)
def set_filesystem_encoding(str) enc = Encoding.find('filesystem') if enc == Encoding::US_ASCII str.b else str.dup.force_encoding(enc) end end
def shift_path_info(req, res, path_info, base=nil)
def shift_path_info(req, res, path_info, base=nil) tmp = path_info.shift base = base || set_filesystem_encoding(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
def trailing_pathsep?(path)
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
def windows_ambiguous_name?(name)
def windows_ambiguous_name?(name) return true if /[. ]+\z/ =~ name return true if /::\$DATA\z/ =~ name return false end