rack-webdav 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ require 'time'
2
+ require 'uri'
3
+ require 'nokogiri'
4
+
5
+ require 'rack'
6
+ require 'rack-webdav/utils'
7
+ require 'rack-webdav/http_status'
8
+ require 'rack-webdav/resource'
9
+ require 'rack-webdav/handler'
10
+ require 'rack-webdav/controller'
11
+
12
+ module RackWebDAV
13
+ IS_18 = RUBY_VERSION[0,3] == '1.8'
14
+ end
@@ -0,0 +1,601 @@
1
+ require 'uri'
2
+
3
+ module RackWebDAV
4
+
5
+ class Controller
6
+ include RackWebDAV::HTTPStatus
7
+ include RackWebDAV::Utils
8
+
9
+ attr_reader :request, :response, :resource
10
+
11
+ # request:: Rack::Request
12
+ # response:: Rack::Response
13
+ # options:: Options hash
14
+ # Create a new Controller.
15
+ # NOTE: options will be passed to Resource
16
+ def initialize(request, response, options={})
17
+ raise Forbidden if request.path_info.include?('..')
18
+ @request = request
19
+ @response = response
20
+ @options = options
21
+
22
+ @dav_extensions = options.delete(:dav_extensions) || []
23
+ @always_include_dav_header = options.delete(:always_include_dav_header)
24
+
25
+ @resource = resource_class.new(actual_path, implied_path, @request, @response, @options)
26
+
27
+ if(@always_include_dav_header)
28
+ add_dav_header
29
+ end
30
+ end
31
+
32
+ # s:: string
33
+ # Escape URL string
34
+ def url_format(resource)
35
+ ret = URI.escape(resource.public_path)
36
+ if resource.collection? and ret[-1,1] != '/'
37
+ ret += '/'
38
+ end
39
+ ret
40
+ end
41
+
42
+ # s:: string
43
+ # Unescape URL string
44
+ def url_unescape(s)
45
+ URI.unescape(s)
46
+ end
47
+
48
+ def add_dav_header
49
+ unless(response['Dav'])
50
+ dav_support = %w(1 2) + @dav_extensions
51
+ response['Dav'] = dav_support.join(', ')
52
+ end
53
+ end
54
+
55
+ # Return response to OPTIONS
56
+ def options
57
+ add_dav_header
58
+ response['Allow'] = 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK'
59
+ response['Ms-Author-Via'] = 'DAV'
60
+ OK
61
+ end
62
+
63
+ # Return response to HEAD
64
+ def head
65
+ if(resource.exist?)
66
+ response['Etag'] = resource.etag
67
+ response['Content-Type'] = resource.content_type
68
+ response['Last-Modified'] = resource.last_modified.httpdate
69
+ OK
70
+ else
71
+ NotFound
72
+ end
73
+ end
74
+
75
+ # Return response to GET
76
+ def get
77
+ if(resource.exist?)
78
+ res = resource.get(request, response)
79
+ if(res == OK && !resource.collection?)
80
+ response['Etag'] = resource.etag
81
+ response['Content-Type'] = resource.content_type
82
+ response['Content-Length'] = resource.content_length.to_s
83
+ response['Last-Modified'] = resource.last_modified.httpdate
84
+ end
85
+ res
86
+ else
87
+ NotFound
88
+ end
89
+ end
90
+
91
+ # Return response to PUT
92
+ def put
93
+ if(resource.collection?)
94
+ Forbidden
95
+ elsif(!resource.parent_exists? || !resource.parent_collection?)
96
+ Conflict
97
+ else
98
+ resource.lock_check if resource.supports_locking?
99
+ status = resource.put(request, response)
100
+ response['Location'] = "#{scheme}://#{host}:#{port}#{url_format(resource)}" if status == Created
101
+ response.body = response['Location']
102
+ status
103
+ end
104
+ end
105
+
106
+ # Return response to POST
107
+ def post
108
+ resource.post(request, response)
109
+ end
110
+
111
+ # Return response to DELETE
112
+ def delete
113
+ if(resource.exist?)
114
+ resource.lock_check if resource.supports_locking?
115
+ resource.delete
116
+ else
117
+ NotFound
118
+ end
119
+ end
120
+
121
+ # Return response to MKCOL
122
+ def mkcol
123
+ resource.lock_check if resource.supports_locking?
124
+ status = resource.make_collection
125
+ gen_url = "#{scheme}://#{host}:#{port}#{url_format(resource)}" if status == Created
126
+ if(resource.use_compat_mkcol_response?)
127
+ multistatus do |xml|
128
+ xml.response do
129
+ xml.href gen_url
130
+ xml.status "#{http_version} #{status.status_line}"
131
+ end
132
+ end
133
+ else
134
+ response['Location'] = gen_url
135
+ status
136
+ end
137
+ end
138
+
139
+ # Return response to COPY
140
+ def copy
141
+ move(:copy)
142
+ end
143
+
144
+ # args:: Only argument used: :copy
145
+ # Move Resource to new location. If :copy is provided,
146
+ # Resource will be copied (implementation ease)
147
+ def move(*args)
148
+ unless(resource.exist?)
149
+ NotFound
150
+ else
151
+ resource.lock_check if resource.supports_locking? && !args.include(:copy)
152
+ destination = url_unescape(env['HTTP_DESTINATION'].sub(%r{https?://([^/]+)}, ''))
153
+ dest_host = $1
154
+ if(dest_host && dest_host.gsub(/:\d{2,5}$/, '') != request.host)
155
+ BadGateway
156
+ elsif(destination == resource.public_path)
157
+ Forbidden
158
+ else
159
+ collection = resource.collection?
160
+ dest = resource_class.new(destination, clean_path(destination), @request, @response, @options.merge(:user => resource.user))
161
+ status = nil
162
+ if(args.include?(:copy))
163
+ status = resource.copy(dest, overwrite)
164
+ else
165
+ return Conflict unless depth.is_a?(Symbol) || depth > 1
166
+ status = resource.move(dest, overwrite)
167
+ end
168
+ response['Location'] = "#{scheme}://#{host}:#{port}#{url_format(dest)}" if status == Created
169
+ # RFC 2518
170
+ if collection
171
+ multistatus do |xml|
172
+ xml.response do
173
+ xml.href "#{scheme}://#{host}:#{port}#{url_format(status == Created ? dest : resource)}"
174
+ xml.status "#{http_version} #{status.status_line}"
175
+ end
176
+ end
177
+ else
178
+ status
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ # Return response to PROPFIND
185
+ def propfind
186
+ unless(resource.exist?)
187
+ NotFound
188
+ else
189
+ unless(request_document.xpath("//#{ns}propfind/#{ns}allprop").empty?)
190
+ properties = resource.properties
191
+ else
192
+ check = request_document.xpath("//#{ns}propfind")
193
+ if(check && !check.empty?)
194
+ properties = request_document.xpath(
195
+ "//#{ns}propfind/#{ns}prop"
196
+ ).children.find_all{ |item|
197
+ item.element?
198
+ }.map{ |item|
199
+ # We should do this, but Nokogiri transforms prefix w/ null href into
200
+ # something valid. Oops.
201
+ # TODO: Hacky grep fix that's horrible
202
+ hsh = to_element_hash(item)
203
+ if(hsh.namespace.nil? && !ns.empty?)
204
+ raise BadRequest if request_document.to_s.scan(%r{<#{item.name}[^>]+xmlns=""}).empty?
205
+ end
206
+ hsh
207
+ }.compact
208
+ else
209
+ raise BadRequest
210
+ end
211
+ end
212
+ multistatus do |xml|
213
+ find_resources.each do |resource|
214
+ xml.response do
215
+ unless(resource.propstat_relative_path)
216
+ xml.href "#{scheme}://#{host}:#{port}#{url_format(resource)}"
217
+ else
218
+ xml.href url_format(resource)
219
+ end
220
+ propstats(xml, get_properties(resource, properties.empty? ? resource.properties : properties))
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ # Return response to PROPPATCH
228
+ def proppatch
229
+ unless(resource.exist?)
230
+ NotFound
231
+ else
232
+ resource.lock_check if resource.supports_locking?
233
+ prop_actions = []
234
+ request_document.xpath("/#{ns}propertyupdate").children.each do |element|
235
+ case element.name
236
+ when 'set', 'remove'
237
+ prp = element.children.detect{|e|e.name == 'prop'}
238
+ if(prp)
239
+ prp.children.each do |elm|
240
+ next if elm.name == 'text'
241
+ prop_actions << {:type => element.name, :name => to_element_hash(elm), :value => elm.text}
242
+ end
243
+ end
244
+ end
245
+ end
246
+ multistatus do |xml|
247
+ find_resources.each do |resource|
248
+ xml.response do
249
+ xml.href "#{scheme}://#{host}:#{port}#{url_format(resource)}"
250
+ prop_actions.each do |action|
251
+ case action[:type]
252
+ when 'set'
253
+ propstats(xml, set_properties(resource, action[:name] => action[:value]))
254
+ when 'remove'
255
+ rm_properties(resource, action[:name] => action[:value])
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
263
+
264
+
265
+ # Lock current resource
266
+ # NOTE: This will pass an argument hash to Resource#lock and
267
+ # wait for a success/failure response.
268
+ def lock
269
+ lockinfo = request_document.xpath("//#{ns}lockinfo")
270
+ asked = {}
271
+ asked[:timeout] = request.env['Timeout'].split(',').map{|x|x.strip} if request.env['Timeout']
272
+ asked[:depth] = depth
273
+ unless([0, :infinity].include?(asked[:depth]))
274
+ BadRequest
275
+ else
276
+ asked[:scope] = lockinfo.xpath("//#{ns}lockscope").children.find_all{|n|n.element?}.map{|n|n.name}.first
277
+ asked[:type] = lockinfo.xpath("#{ns}locktype").children.find_all{|n|n.element?}.map{|n|n.name}.first
278
+ asked[:owner] = lockinfo.xpath("//#{ns}owner/#{ns}href").children.map{|n|n.text}.first
279
+ begin
280
+ lock_time, locktoken = resource.lock(asked)
281
+ render_xml(:prop) do |xml|
282
+ xml.lockdiscovery do
283
+ xml.activelock do
284
+ if(asked[:scope])
285
+ xml.lockscope do
286
+ xml.send(asked[:scope])
287
+ end
288
+ end
289
+ if(asked[:type])
290
+ xml.locktype do
291
+ xml.send(asked[:type])
292
+ end
293
+ end
294
+ xml.depth asked[:depth].to_s
295
+ xml.timeout lock_time ? "Second-#{lock_time}" : 'infinity'
296
+ xml.locktoken do
297
+ xml.href locktoken
298
+ end
299
+ if(asked[:owner])
300
+ xml.owner asked[:owner]
301
+ end
302
+ end
303
+ end
304
+ end
305
+ response.headers['Lock-Token'] = locktoken
306
+ response.status = resource.exist? ? OK : Created
307
+ rescue LockFailure => e
308
+ multistatus do |xml|
309
+ e.path_status.each_pair do |path, status|
310
+ xml.response do
311
+ xml.href path
312
+ xml.status "#{http_version} #{status.status_line}"
313
+ end
314
+ end
315
+ end
316
+ end
317
+ end
318
+ end
319
+
320
+ # Unlock current resource
321
+ def unlock
322
+ resource.unlock(lock_token)
323
+ end
324
+
325
+ # Perform authentication
326
+ # NOTE: Authentication will only be performed if the Resource
327
+ # has defined an #authenticate method
328
+ def authenticate
329
+ authed = true
330
+ if(resource.respond_to?(:authenticate, true))
331
+ authed = false
332
+ uname = nil
333
+ password = nil
334
+ if(request.env['HTTP_AUTHORIZATION'])
335
+ auth = Rack::Auth::Basic::Request.new(request.env)
336
+ if(auth.basic? && auth.credentials)
337
+ uname = auth.credentials[0]
338
+ password = auth.credentials[1]
339
+ end
340
+ end
341
+ authed = resource.send(:authenticate, uname, password)
342
+ end
343
+ raise Unauthorized unless authed
344
+ end
345
+
346
+ private
347
+
348
+ # Request environment variables
349
+ def env
350
+ @request.env
351
+ end
352
+
353
+ # Current request scheme (http/https)
354
+ def scheme
355
+ request.scheme
356
+ end
357
+
358
+ # Request host
359
+ def host
360
+ request.host
361
+ end
362
+
363
+ # Request port
364
+ def port
365
+ request.port
366
+ end
367
+
368
+ # Class of the resource in use
369
+ def resource_class
370
+ @options[:resource_class]
371
+ end
372
+
373
+ # Root URI path for the resource
374
+ def root_uri_path
375
+ @options[:root_uri_path]
376
+ end
377
+
378
+ # Returns Resource path with root URI removed
379
+ def implied_path
380
+ clean_path(@request.path.dup)
381
+ end
382
+
383
+ # x:: request path
384
+ # Unescapes path and removes root URI if applicable
385
+ def clean_path(x)
386
+ ip = url_unescape(x)
387
+ ip.gsub!(/^#{Regexp.escape(root_uri_path)}/, '') if root_uri_path
388
+ ip
389
+ end
390
+
391
+ # Unescaped request path
392
+ def actual_path
393
+ url_unescape(@request.path.dup)
394
+ end
395
+
396
+ # Lock token if provided by client
397
+ def lock_token
398
+ env['HTTP_LOCK_TOKEN'] || nil
399
+ end
400
+
401
+ # Requested depth
402
+ def depth
403
+ d = env['HTTP_DEPTH']
404
+ if(d =~ /^\d+$/)
405
+ d = d.to_i
406
+ else
407
+ d = :infinity
408
+ end
409
+ d
410
+ end
411
+
412
+ # Current HTTP version being used
413
+ def http_version
414
+ env['HTTP_VERSION'] || env['SERVER_PROTOCOL'] || 'HTTP/1.0'
415
+ end
416
+
417
+ # Overwrite is allowed
418
+ def overwrite
419
+ env['HTTP_OVERWRITE'].to_s.upcase != 'F'
420
+ end
421
+
422
+ # Find resources at depth requested
423
+ def find_resources(with_current_resource=true)
424
+ ary = nil
425
+ case depth
426
+ when 0
427
+ ary = []
428
+ when 1
429
+ ary = resource.children
430
+ else
431
+ ary = resource.descendants
432
+ end
433
+ with_current_resource ? [resource] + ary : ary
434
+ end
435
+
436
+ # XML parsed request
437
+ def request_document
438
+ @request_document ||= Nokogiri.XML(request.body.read)
439
+ rescue
440
+ raise BadRequest
441
+ end
442
+
443
+ # Namespace being used within XML document
444
+ # TODO: Make this better
445
+ def ns(wanted_uri="DAV:")
446
+ _ns = ''
447
+ if(request_document && request_document.root && request_document.root.namespace_definitions.size > 0)
448
+ _ns = request_document.root.namespace_definitions.collect{|__ns| __ns if __ns.href == wanted_uri}.compact
449
+ if _ns.empty?
450
+ _ns = request_document.root.namespace_definitions.first.prefix.to_s if _ns.empty?
451
+ else
452
+ _ns = _ns.first
453
+ _ns = _ns.prefix.nil? ? 'xmlns' : _ns.prefix.to_s
454
+ end
455
+ _ns += ':' unless _ns.empty?
456
+ end
457
+ _ns
458
+ end
459
+
460
+ # root_type:: Root tag name
461
+ # Render XML and set Rack::Response#body= to final XML
462
+ def render_xml(root_type)
463
+ raise ArgumentError.new 'Expecting block' unless block_given?
464
+ doc = Nokogiri::XML::Builder.new do |xml_base|
465
+ xml_base.send(root_type.to_s, {'xmlns:D' => 'DAV:'}.merge(resource.root_xml_attributes)) do
466
+ xml_base.parent.namespace = xml_base.parent.namespace_definitions.first
467
+ xml = xml_base['D']
468
+ yield xml
469
+ end
470
+ end
471
+
472
+ if(@options[:pretty_xml])
473
+ response.body = doc.to_xml
474
+ else
475
+ response.body = doc.to_xml(
476
+ :save_with => Nokogiri::XML::Node::SaveOptions::AS_XML
477
+ )
478
+ end
479
+ response["Content-Type"] = 'text/xml; charset="utf-8"'
480
+ response["Content-Length"] = response.body.size.to_s
481
+ end
482
+
483
+ # block:: block
484
+ # Creates a multistatus response using #render_xml and
485
+ # returns the correct status
486
+ def multistatus(&block)
487
+ render_xml(:multistatus, &block)
488
+ MultiStatus
489
+ end
490
+
491
+ # xml:: Nokogiri::XML::Builder
492
+ # errors:: Array of errors
493
+ # Crafts responses for errors
494
+ def response_errors(xml, errors)
495
+ for path, status in errors
496
+ xml.response do
497
+ xml.href "#{scheme}://#{host}:#{port}#{URI.escape(path)}"
498
+ xml.status "#{http_version} #{status.status_line}"
499
+ end
500
+ end
501
+ end
502
+
503
+ # resource:: Resource
504
+ # elements:: Property hashes (name, ns_href, children)
505
+ # Returns array of property values for given names
506
+ def get_properties(resource, elements)
507
+ stats = Hash.new { |h, k| h[k] = [] }
508
+ for element in elements
509
+ begin
510
+ val = resource.get_property(element)
511
+ stats[OK] << [element, val]
512
+ rescue Unauthorized => u
513
+ raise u
514
+ rescue Status
515
+ stats[$!.class] << element
516
+ end
517
+ end
518
+ stats
519
+ end
520
+
521
+ # resource:: Resource
522
+ # elements:: Property hashes (name, namespace, children)
523
+ # Removes the given properties from a resource
524
+ def rm_properties(resource, elements)
525
+ for element, value in elements
526
+ resource.remove_property(element)
527
+ end
528
+ end
529
+
530
+ # resource:: Resource
531
+ # elements:: Property hashes (name, namespace, children)
532
+ # Sets the given properties
533
+ def set_properties(resource, elements)
534
+ stats = Hash.new { |h, k| h[k] = [] }
535
+ for element, value in elements
536
+ begin
537
+ stats[OK] << [element, resource.set_property(element, value)]
538
+ rescue Unauthorized => u
539
+ raise u
540
+ rescue Status
541
+ stats[$!.class] << element
542
+ end
543
+ end
544
+ stats
545
+ end
546
+
547
+ # xml:: Nokogiri::XML::Builder
548
+ # stats:: Array of stats
549
+ # Build propstats response
550
+ def propstats(xml, stats)
551
+ return if stats.empty?
552
+ for status, props in stats
553
+ xml.propstat do
554
+ xml.prop do
555
+ for element, value in props
556
+ defn = xml.doc.root.namespace_definitions.find{|ns_def| ns_def.href == element[:ns_href]}
557
+ if defn.nil?
558
+ if element[:ns_href] and not element[:ns_href].empty?
559
+ _ns = "unknown#{rand(65536)}"
560
+ xml.doc.root.add_namespace_definition(_ns, element[:ns_href])
561
+ else
562
+ _ns = nil
563
+ end
564
+ else
565
+ # Unfortunately Nokogiri won't let the null href, non-null prefix happen
566
+ # So we can't properly handle that error.
567
+ _ns = element[:ns_href].nil? ? nil : defn.prefix
568
+ end
569
+ ns_xml = _ns.nil? ? xml : xml[_ns]
570
+ if (value.is_a?(Nokogiri::XML::Node)) or (value.is_a?(Nokogiri::XML::DocumentFragment))
571
+ xml.__send__ :insert, value
572
+ elsif(value.is_a?(Symbol))
573
+ ns_xml.send(element[:name]) do
574
+ ns_xml.send(value)
575
+ end
576
+ else
577
+ ns_xml.send(element[:name], value) do |x|
578
+ # Make sure we return valid XML
579
+ x.parent.namespace = nil if _ns.nil?
580
+ end
581
+ end
582
+
583
+ # This is gross, but make sure we set the current namespace back to DAV:
584
+ xml['D']
585
+ end
586
+ end
587
+ xml.status "#{http_version} #{status.status_line}"
588
+ end
589
+ end
590
+ end
591
+
592
+ # xml:: Nokogiri::XML::Builder
593
+ # element:: Nokogiri::XML::Element
594
+ # Converts element into proper text
595
+ def xml_convert(xml, element)
596
+ xml.doc.root.add_child(element)
597
+ end
598
+
599
+ end
600
+
601
+ end