webrick-webdav 1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []