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: #{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
|
---|
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 | super(req, res)
|
---|
167 | end
|
---|
168 |
|
---|
169 | def do_GET(req, res)
|
---|
170 | unless exec_handler(req, res)
|
---|
171 | set_dir_list(req, res)
|
---|
172 | end
|
---|
173 | end
|
---|
174 |
|
---|
175 | def do_POST(req, res)
|
---|
176 | unless exec_handler(req, res)
|
---|
177 | raise HTTPStatus::NotFound, "`#{req.path}' not found."
|
---|
178 | end
|
---|
179 | end
|
---|
180 |
|
---|
181 | def do_OPTIONS(req, res)
|
---|
182 | unless exec_handler(req, res)
|
---|
183 | super(req, res)
|
---|
184 | end
|
---|
185 | end
|
---|
186 |
|
---|
187 | # ToDo
|
---|
188 | # RFC2518: HTTP Extensions for Distributed Authoring -- WEBDAV
|
---|
189 | #
|
---|
190 | # PROPFIND PROPPATCH MKCOL DELETE PUT COPY MOVE
|
---|
191 | # LOCK UNLOCK
|
---|
192 |
|
---|
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
|
---|
198 |
|
---|
199 | private
|
---|
200 |
|
---|
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
|
---|
213 |
|
---|
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
|
---|
222 |
|
---|
223 | def set_filename(req, res)
|
---|
224 | res.filename = @root.dup
|
---|
225 | path_info = req.path_info.scan(%r|/[^/]*|)
|
---|
226 |
|
---|
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
|
---|
235 |
|
---|
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
|
---|
253 |
|
---|
254 | return false
|
---|
255 | end
|
---|
256 |
|
---|
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
|
---|
265 |
|
---|
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
|
---|
273 |
|
---|
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
|
---|
282 |
|
---|
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
|
---|
304 |
|
---|
305 | def call_callback(callback_name, req, res)
|
---|
306 | if cb = @options[callback_name]
|
---|
307 | cb.call(req, res)
|
---|
308 | end
|
---|
309 | end
|
---|
310 |
|
---|
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
|
---|
319 |
|
---|
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!
|
---|
339 |
|
---|
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"
|
---|
346 |
|
---|
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
|
---|
352 |
|
---|
353 | res['content-type'] = "text/html"
|
---|
354 |
|
---|
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_
|
---|
362 |
|
---|
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>"
|
---|
385 |
|
---|
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
|
---|
395 |
|
---|
396 | end
|
---|
397 | end
|
---|
398 | end
|
---|