Re-enable spec/library for full CI runs.
[rbx.git] / lib / webrick / httpservlet / filehandler.rb
blob410cc6f9a95e05ef27f6416b1fa8b4860030631b
2 # filehandler.rb -- FileHandler Module
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.
9 # $IPR: filehandler.rb,v 1.44 2003/06/07 01:34:51 gotoyuzo Exp $
11 require 'thread'
12 require 'time'
14 require 'webrick/htmlutils'
15 require 'webrick/httputils'
16 require 'webrick/httpstatus'
18 module WEBrick
19   module HTTPServlet
21     class DefaultFileHandler < AbstractServlet
22       def initialize(server, local_path)
23         super
24         @local_path = local_path
25       end
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)
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
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
60         if (ims = req['if-modified-since']) && Time.parse(ims) >= mtime
61           return true
62         end
64         if (inm = req['if-none-match']) &&
65            HTTPUtils::split_header_value(inm).member?(res['etag'])
66           return true
67         end
69         return false
70       end
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: #{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'] = "#{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
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
128     class FileHandler < AbstractServlet
129       HandlerTable = Hash.new
131       def self.add_handler(suffix, handler)
132         HandlerTable[suffix] = handler
133       end
135       def self.remove_handler(suffix)
136         HandlerTable.delete(suffix)
137       end
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
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         super(req, res)
167       end
169       def do_GET(req, res)
170         unless exec_handler(req, res)
171           set_dir_list(req, res)
172         end
173       end
175       def do_POST(req, res)
176         unless exec_handler(req, res)
177           raise HTTPStatus::NotFound, "`#{req.path}' not found."
178         end
179       end
181       def do_OPTIONS(req, res)
182         unless exec_handler(req, res)
183           super(req, res)
184         end
185       end
187       # ToDo
188       # RFC2518: HTTP Extensions for Distributed Authoring -- WEBDAV
189       #
190       # PROPFIND PROPPATCH MKCOL DELETE PUT COPY MOVE
191       # LOCK UNLOCK
193       # RFC3253: Versioning Extensions to WebDAV
194       #          (Web Distributed Authoring and Versioning)
195       #
196       # VERSION-CONTROL REPORT CHECKOUT CHECK_IN UNCHECKOUT
197       # MKWORKSPACE UPDATE LABEL MERGE ACTIVITY
199       private
201       def exec_handler(req, res)
202         raise HTTPStatus::NotFound, "`#{req.path}' not found" unless @root
203         if set_filename(req, res)
204           handler = get_handler(req)
205           call_callback(:HandlerCallback, req, res)
206           h = handler.get_instance(@config, res.filename)
207           h.service(req, res)
208           return true
209         end
210         call_callback(:HandlerCallback, req, res)
211         return false
212       end
214       def get_handler(req)
215         suffix1 = (/\.(\w+)$/ =~ req.script_name) && $1.downcase
216         suffix2 = (/\.(\w+)\.[\w\-]+$/ =~ req.script_name) && $1.downcase
217         handler_table = @options[:HandlerTable]
218         return handler_table[suffix1] || handler_table[suffix2] ||
219                HandlerTable[suffix1] || HandlerTable[suffix2] ||
220                DefaultFileHandler
221       end
223       def set_filename(req, res)
224         res.filename = @root.dup
225         path_info = req.path_info.scan(%r|/[^/]*|)
227         path_info.unshift("")  # dummy for checking @root dir
228         while base = path_info.first
229           check_filename(req, res, base)
230           break if base == "/"
231           break unless File.directory?(res.filename + base)
232           shift_path_info(req, res, path_info)
233           call_callback(:DirectoryCallback, req, res)
234         end
236         if base = path_info.first
237           check_filename(req, res, base)
238           if base == "/"
239             if file = search_index_file(req, res)
240               shift_path_info(req, res, path_info, file)
241               call_callback(:FileCallback, req, res)
242               return true
243             end
244             shift_path_info(req, res, path_info)
245           elsif file = search_file(req, res, base)
246             shift_path_info(req, res, path_info, file)
247             call_callback(:FileCallback, req, res)
248             return true
249           else
250             raise HTTPStatus::NotFound, "`#{req.path}' not found."
251           end
252         end
254         return false
255       end
257       def check_filename(req, res, name)
258         @options[:NondisclosureName].each{|pattern|
259           if File.fnmatch("/#{pattern}", name)
260             @logger.warn("the request refers nondisclosure name `#{name}'.")
261             raise HTTPStatus::NotFound, "`#{req.path}' not found."
262           end
263         }
264       end
266       def shift_path_info(req, res, path_info, base=nil)
267         tmp = path_info.shift
268         base = base || tmp
269         req.path_info = path_info.join
270         req.script_name << base
271         res.filename << base
272       end
274       def search_index_file(req, res)
275         @config[:DirectoryIndex].each{|index|
276           if file = search_file(req, res, "/"+index)
277             return file
278           end
279         }
280         return nil
281       end
283       def search_file(req, res, basename)
284         langs = @options[:AcceptableLanguages]
285         path = res.filename + basename
286         if File.file?(path)
287           return basename
288         elsif langs.size > 0
289           req.accept_language.each{|lang|
290             path_with_lang = path + ".#{lang}"
291             if langs.member?(lang) && File.file?(path_with_lang)
292               return basename + ".#{lang}"
293             end
294           }
295           (langs - req.accept_language).each{|lang|
296             path_with_lang = path + ".#{lang}"
297             if File.file?(path_with_lang)
298               return basename + ".#{lang}"
299             end
300           }
301         end
302         return nil
303       end
305       def call_callback(callback_name, req, res)
306         if cb = @options[callback_name]
307           cb.call(req, res)
308         end
309       end
311       def nondisclosure_name?(name)
312         @options[:NondisclosureName].each{|pattern|
313           if File.fnmatch(pattern, name)
314             return true
315           end
316         }
317         return false
318       end
320       def set_dir_list(req, res)
321         redirect_to_directory_uri(req, res)
322         unless @options[:FancyIndexing]
323           raise HTTPStatus::Forbidden, "no access permission to `#{req.path}'"
324         end
325         local_path = res.filename
326         list = Dir::entries(local_path).collect{|name|
327           next if name == "." || name == ".."
328           next if nondisclosure_name?(name)
329           st = (File::stat(local_path + name) rescue nil)
330           if st.nil?
331             [ name, nil, -1 ]
332           elsif st.directory?
333             [ name + "/", st.mtime, -1 ]
334           else
335             [ name, st.mtime, st.size ]
336           end
337         }
338         list.compact!
340         if    d0 = req.query["N"]; idx = 0
341         elsif d0 = req.query["M"]; idx = 1
342         elsif d0 = req.query["S"]; idx = 2
343         else  d0 = "A"           ; idx = 0
344         end
345         d1 = (d0 == "A") ? "D" : "A"
347         if d0 == "A"
348           list.sort!{|a,b| a[idx] <=> b[idx] }
349         else
350           list.sort!{|a,b| b[idx] <=> a[idx] }
351         end
353         res['content-type'] = "text/html"
355         res.body = <<-_end_of_html_
356 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
357 <HTML>
358   <HEAD><TITLE>Index of #{HTMLUtils::escape(req.path)}</TITLE></HEAD>
359   <BODY>
360     <H1>Index of #{HTMLUtils::escape(req.path)}</H1>
361         _end_of_html_
363         res.body << "<PRE>\n"
364         res.body << " <A HREF=\"?N=#{d1}\">Name</A>                          "
365         res.body << "<A HREF=\"?M=#{d1}\">Last modified</A>         "
366         res.body << "<A HREF=\"?S=#{d1}\">Size</A>\n"
367         res.body << "<HR>\n"
368        
369         list.unshift [ "..", File::mtime(local_path+".."), -1 ]
370         list.each{ |name, time, size|
371           if name == ".."
372             dname = "Parent Directory"
373           elsif name.size > 25
374             dname = name.sub(/^(.{23})(.*)/){ $1 + ".." }
375           else
376             dname = name
377           end
378           s =  " <A HREF=\"#{HTTPUtils::escape(name)}\">#{dname}</A>"
379           s << " " * (30 - dname.size)
380           s << (time ? time.strftime("%Y/%m/%d %H:%M      ") : " " * 22)
381           s << (size >= 0 ? size.to_s : "-") << "\n"
382           res.body << s
383         }
384         res.body << "</PRE><HR>"
386         res.body << <<-_end_of_html_    
387     <ADDRESS>
388      #{HTMLUtils::escape(@config[:ServerSoftware])}<BR>
389      at #{req.host}:#{req.port}
390     </ADDRESS>
391   </BODY>
392 </HTML>
393         _end_of_html_
394       end
396     end
397   end