webrick-webdav 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,583 @@
1
+ #
2
+ # webdavhandler.rb - WEBrick WebDAV handler
3
+ #
4
+ # Author: Tatsuki Sugiura <sugi@nemui.org>
5
+ # License: Ruby's
6
+ #
7
+
8
+ require 'time'
9
+ require 'fileutils.rb'
10
+ require 'rexml/document'
11
+ require 'webrick/httpservlet/filehandler'
12
+ require 'iconv'
13
+
14
+ module WEBrick
15
+ class HTTPRequest
16
+ # buffer is too small to transport huge files...
17
+ if BUFSIZE < 512 * 1024
18
+ remove_const :BUFSIZE
19
+ BUFSIZE = 512 * 1024
20
+ end
21
+ end
22
+
23
+ module Config
24
+ webdavconf = {
25
+ :FileSystemCoding => "UTF-8",
26
+ :DefaultClientCoding => "UTF-8",
27
+ :DefaultClientCodingWin => "CP932",
28
+ :DefaultClientCodingMacx => "UTF-8",
29
+ :DefaultClientCodingUnix => "EUC-JP",
30
+ :NotInListName => %w(.*),
31
+ :NondisclosureName => %w(.ht*),
32
+ }
33
+ WebDAVHandler = FileHandler.merge(webdavconf)
34
+ end
35
+
36
+ module HTTPStatus
37
+ new_StatusMessage = {
38
+ 102, 'Processing',
39
+ 207, 'Multi-Status',
40
+ 422, 'Unprocessable Entity',
41
+ 423, 'Locked',
42
+ 424, 'Failed Dependency',
43
+ 507, 'Insufficient Storage',
44
+ }
45
+ StatusMessage.each_key {|k| new_StatusMessage.delete(k)}
46
+ StatusMessage.update new_StatusMessage
47
+
48
+ new_StatusMessage.each{|code, message|
49
+ var_name = message.gsub(/[ \-]/,'_').upcase
50
+ err_name = message.gsub(/[ \-]/,'')
51
+
52
+ case code
53
+ when 100...200; parent = Info
54
+ when 200...300; parent = Success
55
+ when 300...400; parent = Redirect
56
+ when 400...500; parent = ClientError
57
+ when 500...600; parent = ServerError
58
+ end
59
+
60
+ eval %-
61
+ RC_#{var_name} = #{code}
62
+ class #{err_name} < #{parent}
63
+ def self.code() RC_#{var_name} end
64
+ def self.reason_phrase() StatusMessage[code] end
65
+ def code() self::class::code end
66
+ def reason_phrase() self::class::reason_phrase end
67
+ alias to_i code
68
+ end
69
+ -
70
+
71
+ CodeToError[code] = const_get(err_name)
72
+ }
73
+ end # HTTPStatus
74
+ end # WEBrick
75
+
76
+ module WEBrick; module HTTPServlet;
77
+ class WebDAVHandler < FileHandler
78
+ class Unsupported < NotImplementedError; end
79
+ class IgnoreProp < StandardError; end
80
+
81
+ class CodeConvFilter
82
+ module Detector
83
+ def dav_ua(req)
84
+ case req["USER-AGENT"]
85
+ when /Microsoft Data Access Internet Publishing/
86
+ {@options[:DefaultClientCodingWin] => 70, "UTF-8" => 30}
87
+ when /^gnome-vfs/
88
+ {"UTF-8" => 90}
89
+ when /^WebDAVFS/
90
+ {@options[:DefaultClientCodingMacx] => 80}
91
+ when /Konqueror/
92
+ {@options[:DefaultClientCodingUnix] => 60, "UTF-8" => 40}
93
+ else
94
+ {}
95
+ end
96
+ end
97
+
98
+ def chk_utf8(req)
99
+ begin
100
+ Iconv.iconv("UTF-8", "UTF-8", req.path, req.path_info)
101
+ {"UTF-8" => 40}
102
+ rescue Iconv::IllegalSequence
103
+ {"UTF-8" => -500}
104
+ end
105
+ end
106
+
107
+ def chk_os(req)
108
+ case req["USER-AGENT"]
109
+ when /Microsoft|Windows/i
110
+ {@options[:DefaultClientCodingWin] => 10}
111
+ when /UNIX|X11/i
112
+ {@options[:DefaultClientCodingUnix] => 10}
113
+ when /darwin|MacOSX/
114
+ {"UTF-8" => 20}
115
+ else
116
+ {}
117
+ end
118
+ end
119
+
120
+ def default(req)
121
+ {@options[:DefaultClientCoding] => 20}
122
+ end
123
+ end # Detector
124
+
125
+ def initialize(options={}, default=Config::WebDAVHandler)
126
+ @options = default.merge(options)
127
+ @detect_meth = [:default, :chk_utf8, :dav_ua, :chk_os]
128
+ @enc_score = Hash.new(0)
129
+ end
130
+ attr_accessor :detect_meth
131
+
132
+ def detect(req)
133
+ self.extend Detector
134
+ detect_meth.each { |meth|
135
+ score = self.__send__ meth, req
136
+ @enc_score.update(score) {|enc, cur, new| cur + new}
137
+ }
138
+ #$DEBUG and $stderr.puts "code detection score ===> #{@enc_score.inspect}"
139
+ platform_codename(@enc_score.keys.sort_by{|k| @enc_score[k] }.last)
140
+ end
141
+
142
+ def conv(req, from=nil, to="UTF-8")
143
+ from ||= detect(req)
144
+ #$DEBUG and $stderr.puts "=== CONVERT === #{from} -> #{to}"
145
+ return true if from == to
146
+ req.path_info = Iconv.iconv(to, from, req.path_info).first
147
+ req.instance_variable_set :@path, Iconv.iconv(to, from, req.path).first
148
+ req["destination"].nil? or req.instance_eval {
149
+ @header["destination"][0] = HTTPUtils.escape(
150
+ Iconv.iconv(to, from,
151
+ HTTPUtils.unescape(@header["destination"][0])).first)
152
+ }
153
+ true
154
+ end
155
+
156
+ def conv2fscode!(req)
157
+ conv(req, nil, @options[:FileSystemCoding])
158
+ end
159
+
160
+ def platform_codename(name)
161
+ case RUBY_PLATFORM
162
+ when /linux/
163
+ name
164
+ when /solaris|sunos/
165
+ {
166
+ "CP932" => "MS932",
167
+ "EUC-JP" => "eucJP"
168
+ }[name]
169
+ when /aix/
170
+ {
171
+ "CP932" => "IBM-932",
172
+ "EUC-JP" => "IBM-eucJP"
173
+ }[name]
174
+ else
175
+ name
176
+ end
177
+ end
178
+ end # CodeConvFilter
179
+
180
+ def initialize(server, root, options={}, default=Config::WebDAVHandler)
181
+ super
182
+ @cconv = CodeConvFilter.new(@options)
183
+ end
184
+
185
+ def service(req, res)
186
+ codeconv_req!(req)
187
+ super
188
+ end
189
+
190
+ # TODO:
191
+ # class 2 protocols; LOCK UNLOCK
192
+ #def do_LOCK(req, res)
193
+ #end
194
+ #def do_UNLOCK(req, res)
195
+ #end
196
+
197
+ def do_OPTIONS(req, res)
198
+ @logger.debug "run do_OPTIONS"
199
+ #res["DAV"] = "1,2"
200
+ res["DAV"] = "1"
201
+ res["MS-Author-Via"] = "DAV"
202
+ super
203
+ end
204
+
205
+ def do_PROPFIND(req, res)
206
+ map_filename(req, res)
207
+ @logger.debug "propfind requeset depth=#{req['Depth']}"
208
+ depth = (req["Depth"].nil? || req["Depth"] == "infinity") ? nil : req["Depth"].to_i
209
+ raise HTTPStatus::Forbidden unless depth # deny inifinite propfind
210
+
211
+ begin
212
+ req_doc = REXML::Document.new req.body
213
+ rescue REXML::ParseException
214
+ raise HTTPStatus::BadRequest
215
+ end
216
+
217
+ ns = {""=>"DAV:"}
218
+ req_props = []
219
+ all_props = %w(creationdate getlastmodified getetag
220
+ resourcetype getcontenttype getcontentlength)
221
+
222
+ if req.body.nil? || !REXML::XPath.match(req_doc, "/propfind/allprop", ns).empty?
223
+ req_props = all_props
224
+ elsif !REXML::XPath.match(req_doc, "/propfind/propname", ns).empty?
225
+ # TODO: support propname
226
+ raise HTTPStatus::NotImplemented
227
+ elsif !REXML::XPath.match(req_doc, "/propfind/prop", ns).empty?
228
+ REXML::XPath.each(req_doc, "/propfind/prop/*", ns){|e|
229
+ req_props << e.name
230
+ }
231
+ else
232
+ raise HTTPStatus::BadRequest
233
+ end
234
+
235
+ ret = get_rec_prop(req, res, res.filename,
236
+ HTTPUtils.escape(codeconv_str_fscode2utf(req.path)),
237
+ req_props, *[depth].compact)
238
+ res.body << build_multistat(ret).to_s(0)
239
+ res["Content-Type"] = 'text/xml; charset="utf-8"'
240
+ raise HTTPStatus::MultiStatus
241
+ end
242
+
243
+ def do_PROPPATCH(req, res)
244
+ map_filename(req, res)
245
+ ret = []
246
+ ns = {""=>"DAV:"}
247
+ begin
248
+ req_doc = REXML::Document.new req.body
249
+ rescue REXML::ParseException
250
+ raise HTTPStatus::BadRequest
251
+ end
252
+ REXML::XPath.each(req_doc, "/propertyupdate/remove/prop/*", ns){|e|
253
+ ps = REXML::Element.new "D:propstat"
254
+ ps.add_element("D:prop").add_element "D:"+e.name
255
+ ps << elem_status(req, res, HTTPStatus::Forbidden)
256
+ ret << ps
257
+ }
258
+ REXML::XPath.each(req_doc, "/propertyupdate/set/prop/*", ns){|e|
259
+ ps = REXML::Element.new "D:propstat"
260
+ ps.add_element("D:prop").add_element "D:"+e.name
261
+ begin
262
+ e.namespace.nil? || e.namespace == "DAV:" or raise Unsupported
263
+ case e.name
264
+ when "getlastmodified"
265
+ File.utime(Time.now, Time.httpdate(e.text), res.filename)
266
+ else
267
+ raise Unsupported
268
+ end
269
+ ps << elem_status(req, res, HTTPStatus::OK)
270
+ rescue Errno::EACCES, ArgumentError
271
+ ps << elem_status(req, res, HTTPStatus::Conflict)
272
+ rescue Unsupported
273
+ ps << elem_status(req, res, HTTPStatus::Forbidden)
274
+ rescue
275
+ ps << elem_status(req, res, HTTPStatus::InternalServerError)
276
+ end
277
+ ret << ps
278
+ }
279
+ res.body << build_multistat([[req.request_uri, *ret]]).to_s(0)
280
+ res["Content-Type"] = 'text/xml; charset="utf-8"'
281
+ raise HTTPStatus::MultiStatus
282
+ end
283
+
284
+ def do_MKCOL(req, res)
285
+ req.body.nil? or raise HTTPStatus::MethodNotAllowed
286
+ begin
287
+ @logger.debug "mkdir #{@root+req.path_info}"
288
+ Dir.mkdir(@root + req.path_info)
289
+ rescue Errno::ENOENT, Errno::EACCES
290
+ raise HTTPStatus::Forbidden
291
+ rescue Errno::ENOSPC
292
+ raise HTTPStatus::InsufficientStorage
293
+ rescue Errno::EEXIST
294
+ raise HTTPStatus::Conflict
295
+ end
296
+ raise HTTPStatus::Created
297
+ end
298
+
299
+ def do_DELETE(req, res)
300
+ map_filename(req, res)
301
+ begin
302
+ @logger.debug "rm_rf #{res.filename}"
303
+ FileUtils.rm_rf(res.filename)
304
+ rescue Errno::EPERM
305
+ raise HTTPStatus::Forbidden
306
+ #rescue
307
+ # FIXME: to return correct error.
308
+ # we needs to stop useing rm_rf and check each deleted entries.
309
+ end
310
+ raise HTTPStatus::NoContent
311
+ end
312
+
313
+ def do_PUT(req, res)
314
+ file = @root + req.path_info
315
+ if req['range']
316
+ ranges = HTTPUtils::parse_range_header(req['range']) or
317
+ raise HTTPStatus::BadRequest,
318
+ "Unrecognized range-spec: \"#{req['range']}\""
319
+ end
320
+
321
+ if !ranges.nil? && ranges.length != 1
322
+ raise HTTPStatus::NotImplemented
323
+ end
324
+
325
+ begin
326
+ File.open(file, "w+") {|f|
327
+ if ranges
328
+ # TODO: supports multiple range
329
+ ranges.each{|range|
330
+ first, last = prepare_range(range, filesize)
331
+ first + req.content_length != last and
332
+ raise HTTPStatus::BadRequest
333
+ f.pos = first
334
+ req.body {|buf| f << buf }
335
+ }
336
+ else
337
+ req.body {|buf| f << buf }
338
+ end
339
+ }
340
+ rescue Errno::ENOENT
341
+ raise HTTPStatus::Conflict
342
+ rescue Errno::ENOSPC
343
+ raise HTTPStatus::InsufficientStorage
344
+ end
345
+ end
346
+
347
+ def do_COPY(req, res)
348
+ src, dest, depth, exists_p = cp_mv_precheck(req, res)
349
+ @logger.debug "copy #{src} -> #{dest}"
350
+ begin
351
+ if depth.nil? # infinity
352
+ FileUtils.cp_r(src, dest, {:preserve => true})
353
+ elsif depth == 0
354
+ if File.directory?(src)
355
+ st = File.stat(src)
356
+ Dir.mkdir(dest)
357
+ begin
358
+ File.utime(st.atime, st.mtime, dest)
359
+ rescue
360
+ # simply ignore
361
+ end
362
+ else
363
+ FileUtils.cp(src, dest, {:preserve => true})
364
+ end
365
+ end
366
+ rescue Errno::ENOENT
367
+ raise HTTPStatus::Conflict
368
+ # FIXME: use multi status(?) and check error URL.
369
+ rescue Errno::ENOSPC
370
+ raise HTTPStatus::InsufficientStorage
371
+ end
372
+
373
+ raise exists_p ? HTTPStatus::NoContent : HTTPStatus::Created
374
+ end
375
+
376
+ def do_MOVE(req, res)
377
+ src, dest, depth, exists_p = cp_mv_precheck(req, res)
378
+ @logger.debug "rename #{src} -> #{dest}"
379
+ begin
380
+ File.rename(src, dest)
381
+ rescue Errno::ENOENT
382
+ raise HTTPStatus::Conflict
383
+ # FIXME: use multi status(?) and check error URL.
384
+ rescue Errno::ENOSPC
385
+ raise HTTPStatus::InsufficientStorage
386
+ end
387
+
388
+ if exists_p
389
+ raise HTTPStatus::NoContent
390
+ else
391
+ raise HTTPStatus::Created
392
+ end
393
+ end
394
+
395
+
396
+ ######################
397
+ private
398
+
399
+ def get_handler(req)
400
+ return DefaultFileHandler
401
+ end
402
+
403
+ def search_index_file(req, res)
404
+ return nil
405
+ end
406
+
407
+ def cp_mv_precheck(req, res)
408
+ depth = (req["Depth"].nil? || req["Depth"] == "infinity") ? nil : req["Depth"].to_i
409
+ depth.nil? || depth == 0 or raise HTTPStatus::BadRequest
410
+ @logger.debug "copy/move requested. Deistnation=#{req['Destination']}"
411
+ dest_uri = URI.parse(req["Destination"])
412
+ unless "#{req.host}:#{req.port}" == "#{dest_uri.host}:#{dest_uri.port}"
413
+ raise HTTPStatus::BadGateway
414
+ # TODO: anyone needs to copy other server?
415
+ end
416
+ src = @root + req.path_info
417
+ dest = @root + resolv_destpath(req)
418
+
419
+ src == dest and raise HTTPStatus::Forbidden
420
+
421
+ exists_p = false
422
+ if File.exists?(dest)
423
+ exists_p = true
424
+ if req["Overwrite"] == "T"
425
+ @logger.debug "copy/move precheck: Overwrite flug=T, deleteing #{dest}"
426
+ FileUtils.rm_rf(dest)
427
+ else
428
+ raise HTTPStatus::PreconditionFailed
429
+ end
430
+ end
431
+ return *[src, dest, depth, exists_p]
432
+ end
433
+
434
+ def codeconv_req!(req)
435
+ @logger.debug "codeconv req obj: orig; path_info='#{req.path_info}', dest='#{req["Destination"]}'"
436
+ begin
437
+ @cconv.conv2fscode!(req)
438
+ rescue Iconv::IllegalSequence
439
+ @logger.warn "code conversion fail! for request object. #{@cconv.detect(req)}->(fscode)"
440
+ end
441
+ @logger.debug "codeconv req obj: ret; path_info='#{req.path_info}', dest='#{req["Destination"]}'"
442
+ true
443
+ end
444
+
445
+ def codeconv_str_fscode2utf(str)
446
+ return str if @options[:FileSystemCoding] == "UTF-8"
447
+ @logger.debug "codeconv str fscode2utf: orig='#{str}'"
448
+ begin
449
+ ret = Iconv.iconv("UTF-8", @options[:FileSystemCoding], str).first
450
+ rescue Iconv::IllegalSequence
451
+ @logger.warn "code conversion fail! #{@options[:FileSystemCoding]}->UTF-8 str=#{str.dump}"
452
+ ret = str
453
+ end
454
+ @logger.debug "codeconv str fscode2utf: ret='#{ret}'"
455
+ ret
456
+ end
457
+
458
+ def map_filename(req, res)
459
+ raise HTTPStatus::NotFound, "`#{req.path}' not found" unless @root
460
+ set_filename(req, res)
461
+ end
462
+
463
+ def build_multistat(rs)
464
+ m = elem_multistat
465
+ rs.each {|href, *cont|
466
+ res = m.add_element "D:response"
467
+ res.add_element("D:href").text = href
468
+ cont.flatten.each {|c| res.elements << c}
469
+ }
470
+ REXML::Document.new << m
471
+ end
472
+
473
+ def elem_status(req, res, retcodesym)
474
+ gen_element("D:status",
475
+ "HTTP/#{req.http_version} #{retcodesym.code} #{retcodesym.reason_phrase}")
476
+ end
477
+
478
+ def get_rec_prop(req, res, file, r_uri, props, depth = 5000)
479
+ ret_set = []
480
+ depth -= 1
481
+ ret_set << [r_uri, get_propstat(req, res, file, props)]
482
+ @logger.debug "get prop file='#{file}' depth=#{depth}"
483
+ return ret_set if !(File.directory?(file) && depth >= 0)
484
+ Dir.entries(file).each {|d|
485
+ d == ".." || d == "." and next
486
+ (@options[:NondisclosureName]+@options[:NotInListName]).find {|pat|
487
+ File.fnmatch(pat, d) } and next
488
+ if File.directory?("#{file}/#{d}")
489
+ ret_set += get_rec_prop(req, res, "#{file}/#{d}",
490
+ HTTPUtils.normalize_path(
491
+ r_uri+HTTPUtils.escape(
492
+ codeconv_str_fscode2utf("/#{d}/"))),
493
+ props, depth)
494
+ else
495
+ ret_set << [HTTPUtils.normalize_path(
496
+ r_uri+HTTPUtils.escape(
497
+ codeconv_str_fscode2utf("/#{d}"))),
498
+ get_propstat(req, res, "#{file}/#{d}", props)]
499
+ end
500
+ }
501
+ ret_set
502
+ end
503
+
504
+ def get_propstat(req, res, file, props)
505
+ propstat = REXML::Element.new "D:propstat"
506
+ errstat = {}
507
+ begin
508
+ st = File::lstat(file)
509
+ pe = REXML::Element.new "D:prop"
510
+ props.each {|pname|
511
+ begin
512
+ if respond_to?("get_prop_#{pname}", true)
513
+ pe << __send__("get_prop_#{pname}", file, st)
514
+ else
515
+ raise HTTPStatus::NotFound
516
+ end
517
+ rescue IgnoreProp
518
+ # simple ignore
519
+ rescue HTTPStatus::Status
520
+ # FIXME: add to errstat
521
+ end
522
+ }
523
+ propstat.elements << pe
524
+ propstat.elements << elem_status(req, res, HTTPStatus::OK)
525
+ rescue
526
+ propstat.elements << elem_status(req, res, HTTPStatus::InternalServerError)
527
+ end
528
+ propstat
529
+ end
530
+
531
+ def get_prop_creationdate(file, st)
532
+ gen_element "D:creationdate", st.ctime.xmlschema
533
+ end
534
+
535
+ def get_prop_getlastmodified(file, st)
536
+ gen_element "D:getlastmodified", st.mtime.httpdate
537
+ end
538
+
539
+ def get_prop_getetag(file, st)
540
+ gen_element "D:getetag", sprintf('%x-%x-%x', st.ino, st.size, st.mtime.to_i)
541
+ end
542
+
543
+ def get_prop_resourcetype(file, st)
544
+ t = gen_element "D:resourcetype"
545
+ File.directory?(file) and t.add_element("D:collection")
546
+ t
547
+ end
548
+
549
+ def get_prop_getcontenttype(file, st)
550
+ gen_element("D:getcontenttype",
551
+ File.file?(file) ?
552
+ HTTPUtils::mime_type(file, @config[:MimeTypes]) :
553
+ "httpd/unix-directory")
554
+ end
555
+
556
+ def get_prop_getcontentlength(file, st)
557
+ File.file?(file) or raise HTTPStatus::NotFound
558
+ gen_element "D:getcontentlength", st.size
559
+ end
560
+
561
+ def elem_multistat
562
+ gen_element "D:multistatus", nil, {"xmlns:D" => "DAV:"}
563
+ end
564
+
565
+ def gen_element(elem, text = nil, attrib = {})
566
+ e = REXML::Element.new elem
567
+ text and e.text = text
568
+ attrib.each {|k, v| e.attributes[k] = v }
569
+ e
570
+ end
571
+
572
+ def resolv_destpath(req)
573
+ if /^#{Regexp.escape(req.script_name)}/ =~
574
+ HTTPUtils.unescape(URI.parse(req["Destination"]).path)
575
+ return $'
576
+ else
577
+ @logger.error "[BUG] can't resolv destination path. script='#{req.script_name}', path='#{req.path}', dest='#{req["Destination"]}', root='#{@root}'"
578
+ raise HTTPStatus::InternalServerError
579
+ end
580
+ end
581
+
582
+ end # WebDAVHandler
583
+ end; end # HTTPServlet; WEBrick
metadata ADDED
@@ -0,0 +1,39 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.10
3
+ specification_version: 1
4
+ name: webrick-webdav
5
+ version: !ruby/object:Gem::Version
6
+ version: "1.0"
7
+ date: 2005-07-24
8
+ summary: "WebDAV handler for WEBrick, Ruby's HTTP toolkit."
9
+ require_paths:
10
+ - lib
11
+ email: sugi@nemui.org
12
+ homepage: http://sugi.nemui.org/
13
+ rubyforge_project:
14
+ description: "A class for handling WebDAV requests through WEBrick, Ruby's built-in HTTP
15
+ server toolkit. This class was originally a part of Sarada, a configurable
16
+ WebDAV server available at: http://sugi.nemui.org/pub/ruby/sarada/."
17
+ autorequire:
18
+ default_executable:
19
+ bindir: bin
20
+ has_rdoc: false
21
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
22
+ requirements:
23
+ -
24
+ - ">"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.0
27
+ version:
28
+ platform: ruby
29
+ authors:
30
+ - Tatsuki Sugiura
31
+ files:
32
+ - lib/webrick/httpservlet/webdavhandler.rb
33
+ test_files: []
34
+ rdoc_options: []
35
+ extra_rdoc_files: []
36
+ executables: []
37
+ extensions: []
38
+ requirements: []
39
+ dependencies: []