File: webrick/httpservlet/filehandler.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: WEBrick#18
  module: HTTPServlet#19
  class: DefaultFileHandler#21
inherits from
  AbstractServlet ( WEBrick::HTTPServlet )
has properties
method: initialize / 2 #22
method: do_GET / 2 #27
method: not_modified? / 4 #47
method: make_partial_content / 4 #72
method: prepare_range / 2 #119
  class: FileHandler#128
inherits from
  AbstractServlet ( WEBrick::HTTPServlet )
has properties
constant: HandlerTable #129
class method: add_handler / 2 #131
class method: remove_handler / 1 #135
method: initialize / 4 #139
method: service / 2 #149
method: do_GET / 2 #170
method: do_POST / 2 #176
method: do_OPTIONS / 2 #182
method: trailing_pathsep? / 1 #202
method: prevent_directory_traversal / 2 #211
method: exec_handler / 2 #230
method: get_handler / 2 #243
method: set_filename / 2 #256
method: check_filename / 3 #288
method: shift_path_info / 4 #295
method: search_index_file / 2 #304
method: search_file / 3 #313
method: call_callback / 3 #335
method: windows_ambiguous_name? / 1 #341
method: nondisclosure_name? / 1 #347
method: set_dir_list / 2 #356

Class Hierarchy

Code

   1  #
   2  # filehandler.rb -- FileHandler Module
   3  #
   4  # Author: IPR -- Internet Programming with Ruby -- writers
   5  # Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
   6  # Copyright (c) 2003 Internet Programming with Ruby writers. All rights
   7  # reserved.
   8  #
   9  # $IPR: filehandler.rb,v 1.44 2003/06/07 01:34:51 gotoyuzo Exp $
  10 
  11  require 'thread'
  12  require 'time'
  13 
  14  require 'webrick/htmlutils'
  15  require 'webrick/httputils'
  16  require 'webrick/httpstatus'
  17 
  18  module WEBrick
  19    module HTTPServlet
  20 
  21      class DefaultFileHandler < AbstractServlet
  22        def initialize(server, local_path)
  23          super
  24          @local_path = local_path
  25        end
  26 
  27        def do_GET(req, res)
  28          st = File::stat(@local_path)
  29          mtime = st.mtime
  30          res['etag'] = sprintf("%x-%x-%x", st.ino, st.size, st.mtime.to_i)
  31 
  32          if not_modified?(req, res, mtime, res['etag'])
  33            res.body = ''
  34            raise HTTPStatus::NotModified
  35          elsif req['range'] 
  36            make_partial_content(req, res, @local_path, st.size)
  37            raise HTTPStatus::PartialContent
  38          else
  39            mtype = HTTPUtils::mime_type(@local_path, @config[:MimeTypes])
  40            res['content-type'] = mtype
  41            res['content-length'] = st.size
  42            res['last-modified'] = mtime.httpdate
  43            res.body = open(@local_path, "rb")
  44          end
  45        end
  46 
  47        def not_modified?(req, res, mtime, etag)
  48          if ir = req['if-range']
  49            begin
  50              if Time.httpdate(ir) >= mtime
  51                return true
  52              end
  53            rescue
  54              if HTTPUtils::split_header_value(ir).member?(res['etag'])
  55                return true
  56              end
  57            end
  58          end
  59 
  60          if (ims = req['if-modified-since']) && Time.parse(ims) >= mtime
  61            return true
  62          end
  63 
  64          if (inm = req['if-none-match']) &&
  65             HTTPUtils::split_header_value(inm).member?(res['etag'])
  66            return true
  67          end
  68 
  69          return false
  70        end
  71 
  72        def make_partial_content(req, res, filename, filesize)
  73          mtype = HTTPUtils::mime_type(filename, @config[:MimeTypes])
  74          unless ranges = HTTPUtils::parse_range_header(req['range'])
  75            raise HTTPStatus::BadRequest,
  76              "Unrecognized range-spec: \"#{req['range']}\""
  77          end
  78          open(filename, "rb"){|io|
  79            if ranges.size > 1
  80              time = Time.now
  81              boundary = "#{time.sec}_#{time.usec}_#{Process::pid}"
  82              body = ''
  83              ranges.each{|range|
  84                first, last = prepare_range(range, filesize)
  85                next if first < 0
  86                io.pos = first
  87                content = io.read(last-first+1)
  88                body << "--" << boundary << CRLF
  89                body << "Content-Type: #{mtype}" << CRLF
  90                body << "Content-Range: bytes #{first}-#{last}/#{filesize}" << CRLF
  91                body << CRLF
  92                body << content
  93                body << CRLF
  94              }
  95              raise HTTPStatus::RequestRangeNotSatisfiable if body.empty?
  96              body << "--" << boundary << "--" << CRLF
  97              res["content-type"] = "multipart/byteranges; boundary=#{boundary}"
  98              res.body = body
  99            elsif range = ranges[0]
 100              first, last = prepare_range(range, filesize)
 101              raise HTTPStatus::RequestRangeNotSatisfiable if first < 0
 102              if last == filesize - 1
 103                content = io.dup
 104                content.pos = first
 105              else
 106                io.pos = first
 107                content = io.read(last-first+1)
 108              end
 109              res['content-type'] = mtype
 110              res['content-range'] = "bytes #{first}-#{last}/#{filesize}"
 111              res['content-length'] = last - first + 1
 112              res.body = content
 113            else
 114              raise HTTPStatus::BadRequest
 115            end
 116          }
 117        end
 118 
 119        def prepare_range(range, filesize)
 120          first = range.first < 0 ? filesize + range.first : range.first
 121          return -1, -1 if first < 0 || first >= filesize
 122          last = range.last < 0 ? filesize + range.last : range.last
 123          last = filesize - 1 if last >= filesize
 124          return first, last
 125        end
 126      end
 127 
 128      class FileHandler < AbstractServlet
 129        HandlerTable = Hash.new
 130 
 131        def self.add_handler(suffix, handler)
 132          HandlerTable[suffix] = handler
 133        end
 134 
 135        def self.remove_handler(suffix)
 136          HandlerTable.delete(suffix)
 137        end
 138 
 139        def initialize(server, root, options={}, default=Config::FileHandler)
 140          @config = server.config
 141          @logger = @config[:Logger]
 142          @root = File.expand_path(root)
 143          if options == true || options == false
 144            options = { :FancyIndexing => options }
 145          end
 146          @options = default.dup.update(options)
 147        end
 148 
 149        def service(req, res)
 150          # if this class is mounted on "/" and /~username is requested.
 151          # we're going to override path informations before invoking service.
 152          if defined?(Etc) && @options[:UserDir] && req.script_name.empty?
 153            if %r|^(/~([^/]+))| =~ req.path_info
 154              script_name, user = $1, $2
 155              path_info = $'
 156              begin
 157                passwd = Etc::getpwnam(user)
 158                @root = File::join(passwd.dir, @options[:UserDir])
 159                req.script_name = script_name
 160                req.path_info = path_info
 161              rescue
 162                @logger.debug "#{self.class}#do_GET: getpwnam(#{user}) failed"
 163              end
 164            end
 165          end
 166          prevent_directory_traversal(req, res)
 167          super(req, res)
 168        end
 169 
 170        def do_GET(req, res)
 171          unless exec_handler(req, res)
 172            set_dir_list(req, res)
 173          end
 174        end
 175 
 176        def do_POST(req, res)
 177          unless exec_handler(req, res)
 178            raise HTTPStatus::NotFound, "`#{req.path}' not found."
 179          end
 180        end
 181 
 182        def do_OPTIONS(req, res)
 183          unless exec_handler(req, res)
 184            super(req, res)
 185          end
 186        end
 187 
 188        # ToDo
 189        # RFC2518: HTTP Extensions for Distributed Authoring -- WEBDAV
 190        #
 191        # PROPFIND PROPPATCH MKCOL DELETE PUT COPY MOVE
 192        # LOCK UNLOCK
 193 
 194        # RFC3253: Versioning Extensions to WebDAV
 195        #          (Web Distributed Authoring and Versioning)
 196        #
 197        # VERSION-CONTROL REPORT CHECKOUT CHECK_IN UNCHECKOUT
 198        # MKWORKSPACE UPDATE LABEL MERGE ACTIVITY
 199 
 200        private
 201 
 202        def trailing_pathsep?(path)
 203          # check for trailing path separator:
 204          #   File.dirname("/aaaa/bbbb/")      #=> "/aaaa")
 205          #   File.dirname("/aaaa/bbbb/x")     #=> "/aaaa/bbbb")
 206          #   File.dirname("/aaaa/bbbb")       #=> "/aaaa")
 207          #   File.dirname("/aaaa/bbbbx")      #=> "/aaaa")
 208          return File.dirname(path) != File.dirname(path+"x")
 209        end
 210 
 211        def prevent_directory_traversal(req, res)
 212          # Preventing directory traversal on Windows platforms;
 213          # Backslashes (0x5c) in path_info are not interpreted as special
 214          # character in URI notation. So the value of path_info should be
 215          # normalize before accessing to the filesystem.
 216 
 217          if trailing_pathsep?(req.path_info)
 218            # File.expand_path removes the trailing path separator.
 219            # Adding a character is a workaround to save it.
 220            #  File.expand_path("/aaa/")        #=> "/aaa"
 221            #  File.expand_path("/aaa/" + "x")  #=> "/aaa/x"
 222            expanded = File.expand_path(req.path_info + "x")
 223            expanded.chop!  # remove trailing "x"
 224          else
 225            expanded = File.expand_path(req.path_info)
 226          end
 227          req.path_info = expanded
 228        end
 229 
 230        def exec_handler(req, res)
 231          raise HTTPStatus::NotFound, "`#{req.path}' not found" unless @root
 232          if set_filename(req, res)
 233            handler = get_handler(req, res)
 234            call_callback(:HandlerCallback, req, res)
 235            h = handler.get_instance(@config, res.filename)
 236            h.service(req, res)
 237            return true
 238          end
 239          call_callback(:HandlerCallback, req, res)
 240          return false
 241        end
 242 
 243        def get_handler(req, res)
 244          suffix1 = (/\.(\w+)\z/ =~ res.filename) && $1.downcase
 245          if /\.(\w+)\.([\w\-]+)\z/ =~ res.filename
 246            if @options[:AcceptableLanguages].include?($2.downcase)
 247              suffix2 = $1.downcase
 248            end
 249          end
 250          handler_table = @options[:HandlerTable]
 251          return handler_table[suffix1] || handler_table[suffix2] ||
 252                 HandlerTable[suffix1] || HandlerTable[suffix2] ||
 253                 DefaultFileHandler
 254        end
 255 
 256        def set_filename(req, res)
 257          res.filename = @root.dup
 258          path_info = req.path_info.scan(%r|/[^/]*|)
 259 
 260          path_info.unshift("")  # dummy for checking @root dir
 261          while base = path_info.first
 262            break if base == "/"
 263            break unless File.directory?(File.expand_path(res.filename + base))
 264            shift_path_info(req, res, path_info)
 265            call_callback(:DirectoryCallback, req, res)
 266          end
 267 
 268          if base = path_info.first
 269            if base == "/"
 270              if file = search_index_file(req, res)
 271                shift_path_info(req, res, path_info, file)
 272                call_callback(:FileCallback, req, res)
 273                return true
 274              end
 275              shift_path_info(req, res, path_info)
 276            elsif file = search_file(req, res, base)
 277              shift_path_info(req, res, path_info, file)
 278              call_callback(:FileCallback, req, res)
 279              return true
 280            else
 281              raise HTTPStatus::NotFound, "`#{req.path}' not found."
 282            end
 283          end
 284 
 285          return false
 286        end
 287 
 288        def check_filename(req, res, name)
 289          if nondisclosure_name?(name) || windows_ambiguous_name?(name)
 290            @logger.warn("the request refers nondisclosure name `#{name}'.")
 291            raise HTTPStatus::NotFound, "`#{req.path}' not found."
 292          end
 293        end
 294 
 295        def shift_path_info(req, res, path_info, base=nil)
 296          tmp = path_info.shift
 297          base = base || tmp
 298          req.path_info = path_info.join
 299          req.script_name << base
 300          res.filename = File.expand_path(res.filename + base)
 301          check_filename(req, res, File.basename(res.filename))
 302        end
 303 
 304        def search_index_file(req, res)
 305          @config[:DirectoryIndex].each{|index|
 306            if file = search_file(req, res, "/"+index)
 307              return file
 308            end
 309          }
 310          return nil
 311        end
 312 
 313        def search_file(req, res, basename)
 314          langs = @options[:AcceptableLanguages]
 315          path = res.filename + basename
 316          if File.file?(path)
 317            return basename
 318          elsif langs.size > 0
 319            req.accept_language.each{|lang|
 320              path_with_lang = path + ".#{lang}"
 321              if langs.member?(lang) && File.file?(path_with_lang)
 322                return basename + ".#{lang}"
 323              end
 324            }
 325            (langs - req.accept_language).each{|lang|
 326              path_with_lang = path + ".#{lang}"
 327              if File.file?(path_with_lang)
 328                return basename + ".#{lang}"
 329              end
 330            }
 331          end
 332          return nil
 333        end
 334 
 335        def call_callback(callback_name, req, res)
 336          if cb = @options[callback_name]
 337            cb.call(req, res)
 338          end
 339        end
 340 
 341        def windows_ambiguous_name?(name)
 342          return true if /[. ]+\z/ =~ name
 343          return true if /::\$DATA\z/ =~ name
 344          return false
 345        end
 346 
 347        def nondisclosure_name?(name)
 348          @options[:NondisclosureName].each{|pattern|
 349            if File.fnmatch(pattern, name, File::FNM_CASEFOLD)
 350              return true
 351            end
 352          }
 353          return false
 354        end
 355 
 356        def set_dir_list(req, res)
 357          redirect_to_directory_uri(req, res)
 358          unless @options[:FancyIndexing]
 359            raise HTTPStatus::Forbidden, "no access permission to `#{req.path}'"
 360          end
 361          local_path = res.filename
 362          list = Dir::entries(local_path).collect{|name|
 363            next if name == "." || name == ".."
 364            next if nondisclosure_name?(name)
 365            next if windows_ambiguous_name?(name)
 366            st = (File::stat(File.join(local_path, name)) rescue nil)
 367            if st.nil?
 368              [ name, nil, -1 ]
 369            elsif st.directory?
 370              [ name + "/", st.mtime, -1 ]
 371            else
 372              [ name, st.mtime, st.size ]
 373            end
 374          }
 375          list.compact!
 376 
 377          if    d0 = req.query["N"]; idx = 0
 378          elsif d0 = req.query["M"]; idx = 1
 379          elsif d0 = req.query["S"]; idx = 2
 380          else  d0 = "A"           ; idx = 0
 381          end
 382          d1 = (d0 == "A") ? "D" : "A"
 383 
 384          if d0 == "A"
 385            list.sort!{|a,b| a[idx] <=> b[idx] }
 386          else
 387            list.sort!{|a,b| b[idx] <=> a[idx] }
 388          end
 389 
 390          res['content-type'] = "text/html"
 391 
 392          res.body = <<-_end_of_html_
 393  <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
 394  <HTML>
 395    <HEAD><TITLE>Index of #{HTMLUtils::escape(req.path)}</TITLE></HEAD>
 396    <BODY>
 397      <H1>Index of #{HTMLUtils::escape(req.path)}</H1>
 398          _end_of_html_
 399 
 400          res.body << "<PRE>\n"
 401          res.body << " <A HREF=\"?N=#{d1}\">Name</A>                          "
 402          res.body << "<A HREF=\"?M=#{d1}\">Last modified</A>         "
 403          res.body << "<A HREF=\"?S=#{d1}\">Size</A>\n"
 404          res.body << "<HR>\n"
 405         
 406          list.unshift [ "..", File::mtime(local_path+"/.."), -1 ]
 407          list.each{ |name, time, size|
 408            if name == ".."
 409              dname = "Parent Directory"
 410            elsif name.size > 25
 411              dname = name.sub(/^(.{23})(.*)/){ $1 + ".." }
 412            else
 413              dname = name
 414            end
 415            s =  " <A HREF=\"#{HTTPUtils::escape(name)}\">#{dname}</A>"
 416            s << " " * (30 - dname.size)
 417            s << (time ? time.strftime("%Y/%m/%d %H:%M      ") : " " * 22)
 418            s << (size >= 0 ? size.to_s : "-") << "\n"
 419            res.body << s
 420          }
 421          res.body << "</PRE><HR>"
 422 
 423          res.body << <<-_end_of_html_    
 424      <ADDRESS>
 425       #{HTMLUtils::escape(@config[:ServerSoftware])}<BR>
 426       at #{req.host}:#{req.port}
 427      </ADDRESS>
 428    </BODY>
 429  </HTML>
 430          _end_of_html_
 431        end
 432 
 433      end
 434    end
 435  end