summaryrefslogtreecommitdiff
path: root/lib/webrick/httpservlet/filehandler.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/webrick/httpservlet/filehandler.rb')
-rw-r--r--lib/webrick/httpservlet/filehandler.rb224
1 files changed, 165 insertions, 59 deletions
diff --git a/lib/webrick/httpservlet/filehandler.rb b/lib/webrick/httpservlet/filehandler.rb
index 24f59d7142..7cac05d818 100644
--- a/lib/webrick/httpservlet/filehandler.rb
+++ b/lib/webrick/httpservlet/filehandler.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
#
# filehandler.rb -- FileHandler Module
#
@@ -8,22 +9,38 @@
#
# $IPR: filehandler.rb,v 1.44 2003/06/07 01:34:51 gotoyuzo Exp $
-require 'thread'
require 'time'
-require 'webrick/htmlutils'
-require 'webrick/httputils'
-require 'webrick/httpstatus'
+require_relative '../htmlutils'
+require_relative '../httputils'
+require_relative '../httpstatus'
module WEBrick
module HTTPServlet
+ ##
+ # Servlet for serving a single file. You probably want to use the
+ # FileHandler servlet instead as it handles directories and fancy indexes.
+ #
+ # Example:
+ #
+ # server.mount('/my_page.txt', WEBrick::HTTPServlet::DefaultFileHandler,
+ # '/path/to/my_page.txt')
+ #
+ # This servlet handles If-Modified-Since and Range requests.
+
class DefaultFileHandler < AbstractServlet
+
+ ##
+ # Creates a DefaultFileHandler instance for the file at +local_path+.
+
def initialize(server, local_path)
- super
+ super(server, local_path)
@local_path = local_path
end
+ # :stopdoc:
+
def do_GET(req, res)
st = File::stat(@local_path)
mtime = st.mtime
@@ -32,15 +49,15 @@ module WEBrick
if not_modified?(req, res, mtime, res['etag'])
res.body = ''
raise HTTPStatus::NotModified
- elsif req['range']
+ elsif req['range']
make_partial_content(req, res, @local_path, st.size)
raise HTTPStatus::PartialContent
else
mtype = HTTPUtils::mime_type(@local_path, @config[:MimeTypes])
res['content-type'] = mtype
- res['content-length'] = st.size
+ res['content-length'] = st.size.to_s
res['last-modified'] = mtime.httpdate
- res.body = open(@local_path, "rb")
+ res.body = File.open(@local_path, "rb")
end
end
@@ -69,47 +86,66 @@ module WEBrick
return false
end
+ # returns a lambda for webrick/httpresponse.rb send_body_proc
+ def multipart_body(body, parts, boundary, mtype, filesize)
+ lambda do |socket|
+ begin
+ begin
+ first = parts.shift
+ last = parts.shift
+ socket.write(
+ "--#{boundary}#{CRLF}" \
+ "Content-Type: #{mtype}#{CRLF}" \
+ "Content-Range: bytes #{first}-#{last}/#{filesize}#{CRLF}" \
+ "#{CRLF}"
+ )
+
+ begin
+ IO.copy_stream(body, socket, last - first + 1, first)
+ rescue NotImplementedError
+ body.seek(first, IO::SEEK_SET)
+ IO.copy_stream(body, socket, last - first + 1)
+ end
+ socket.write(CRLF)
+ end while parts[0]
+ socket.write("--#{boundary}--#{CRLF}")
+ ensure
+ body.close
+ end
+ end
+ end
+
def make_partial_content(req, res, filename, filesize)
mtype = HTTPUtils::mime_type(filename, @config[:MimeTypes])
unless ranges = HTTPUtils::parse_range_header(req['range'])
raise HTTPStatus::BadRequest,
"Unrecognized range-spec: \"#{req['range']}\""
end
- open(filename, "rb"){|io|
+ File.open(filename, "rb"){|io|
if ranges.size > 1
time = Time.now
boundary = "#{time.sec}_#{time.usec}_#{Process::pid}"
- body = ''
- ranges.each{|range|
- first, last = prepare_range(range, filesize)
- next if first < 0
- io.pos = first
- content = io.read(last-first+1)
- body << "--" << boundary << CRLF
- body << "Content-Type: #{mtype}" << CRLF
- body << "Content-Range: #{first}-#{last}/#{filesize}" << CRLF
- body << CRLF
- body << content
- body << CRLF
+ parts = []
+ ranges.each {|range|
+ prange = prepare_range(range, filesize)
+ next if prange[0] < 0
+ parts.concat(prange)
}
- raise HTTPStatus::RequestRangeNotSatisfiable if body.empty?
- body << "--" << boundary << "--" << CRLF
+ raise HTTPStatus::RequestRangeNotSatisfiable if parts.empty?
res["content-type"] = "multipart/byteranges; boundary=#{boundary}"
- res.body = body
+ if req.http_version < '1.1'
+ res['connection'] = 'close'
+ else
+ res.chunked = true
+ end
+ res.body = multipart_body(io.dup, parts, boundary, mtype, filesize)
elsif range = ranges[0]
first, last = prepare_range(range, filesize)
raise HTTPStatus::RequestRangeNotSatisfiable if first < 0
- if last == filesize - 1
- content = io.dup
- content.pos = first
- else
- io.pos = first
- content = io.read(last-first+1)
- end
res['content-type'] = mtype
- res['content-range'] = "#{first}-#{last}/#{filesize}"
- res['content-length'] = last - first + 1
- res.body = content
+ res['content-range'] = "bytes #{first}-#{last}/#{filesize}"
+ res['content-length'] = (last - first + 1).to_s
+ res.body = io.dup
else
raise HTTPStatus::BadRequest
end
@@ -123,19 +159,47 @@ module WEBrick
last = filesize - 1 if last >= filesize
return first, last
end
+
+ # :startdoc:
end
+ ##
+ # Serves a directory including fancy indexing and a variety of other
+ # options.
+ #
+ # Example:
+ #
+ # server.mount('/assets', WEBrick::HTTPServlet::FileHandler,
+ # '/path/to/assets')
+
class FileHandler < AbstractServlet
- HandlerTable = Hash.new
+ HandlerTable = Hash.new # :nodoc:
+
+ ##
+ # Allow custom handling of requests for files with +suffix+ by class
+ # +handler+
def self.add_handler(suffix, handler)
HandlerTable[suffix] = handler
end
+ ##
+ # Remove custom handling of requests for files with +suffix+
+
def self.remove_handler(suffix)
HandlerTable.delete(suffix)
end
+ ##
+ # Creates a FileHandler servlet on +server+ that serves files starting
+ # at directory +root+
+ #
+ # +options+ may be a Hash containing keys from
+ # WEBrick::Config::FileHandler or +true+ or +false+.
+ #
+ # If +options+ is true or false then +:FancyIndexing+ is enabled or
+ # disabled respectively.
+
def initialize(server, root, options={}, default=Config::FileHandler)
@config = server.config
@logger = @config[:Logger]
@@ -146,9 +210,11 @@ module WEBrick
@options = default.dup.update(options)
end
+ # :stopdoc:
+
def service(req, res)
# if this class is mounted on "/" and /~username is requested.
- # we're going to override path informations before invoking service.
+ # 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
@@ -214,16 +280,20 @@ module WEBrick
# 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(req.path_info + "x")
+ expanded = File.expand_path(path + "x")
expanded.chop! # remove trailing "x"
else
- expanded = File.expand_path(req.path_info)
+ expanded = File.expand_path(path)
end
+ expanded.force_encoding(req.path_info.encoding)
req.path_info = expanded
end
@@ -374,11 +444,18 @@ module WEBrick
}
list.compact!
- if d0 = req.query["N"]; idx = 0
- elsif d0 = req.query["M"]; idx = 1
- elsif d0 = req.query["S"]; idx = 2
- else d0 = "A" ; idx = 0
+ 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"
@@ -387,40 +464,68 @@ module WEBrick
list.sort!{|a,b| b[idx] <=> a[idx] }
end
- res['content-type'] = "text/html"
+ 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>Index of #{HTMLUtils::escape(req.path)}</TITLE></HEAD>
+ <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>Index of #{HTMLUtils::escape(req.path)}</H1>
+ <H1>#{title}</H1>
_end_of_html_
- res.body << "<PRE>\n"
- res.body << " <A HREF=\"?N=#{d1}\">Name</A> "
- res.body << "<A HREF=\"?M=#{d1}\">Last modified</A> "
- res.body << "<A HREF=\"?S=#{d1}\">Size</A>\n"
- res.body << "<HR>\n"
-
+ 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 name.size > 25
- dname = name.sub(/^(.{23})(.*)/){ $1 + ".." }
+ elsif namewidth and name.size > namewidth
+ dname = name[0...(namewidth - 2)] << '..'
else
dname = name
end
- s = " <A HREF=\"#{HTTPUtils::escape(name)}\">#{dname}</A>"
- s << " " * (30 - dname.size)
- s << (time ? time.strftime("%Y/%m/%d %H:%M ") : " " * 22)
- s << (size >= 0 ? size.to_s : "-") << "\n"
+ 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 << "</PRE><HR>"
+ res.body << "</TBODY></TABLE>"
+ res.body << "<HR>"
- res.body << <<-_end_of_html_
+ res.body << <<-_end_of_html_
<ADDRESS>
#{HTMLUtils::escape(@config[:ServerSoftware])}<BR>
at #{req.host}:#{req.port}
@@ -430,6 +535,7 @@ module WEBrick
_end_of_html_
end
+ # :startdoc:
end
end
end