dav4rack 0.2.11 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,109 @@
1
+ === v0.3.0
2
+ * New minor release towards full litmus pass
3
+ * Litmus passing: basic, copymove, props
4
+ * Litmus in progress: locks
5
+ * Internal reorganization of file structures
6
+ * Propfind updates to provide proper returns (thanks {schmurfy}[https://github.com/schmurfy])
7
+ * Properly detect malformed propfind requests
8
+ * Updated custom attribute storage
9
+ * Allow customized DAV headers (thanks {schmurfy}[https://github.com/schmurfy])
10
+ * Extract parent collection detection to resource (thanks {schmurfy}[https://github.com/schmurfy])
11
+ * Start of new locking implementation
12
+ * Fix rack #serving usage in DAV4Rack::File (thanks {pifleo}[https://github.com/pifleo])
13
+ * Force file namespacing in file resource to prevent conflicts with DAV4Rack::File (thanks {pifleo}[https://github.com/pifleo])
14
+ * Add ability to build xml from within a resource (thanks {mlmorg}[https://github.com/mlmorg])
15
+
16
+ === v0.2.11
17
+ * URL escaping updates (thanks {exabugs}[https://github.com/exabugs])
18
+ * Return status updates to match RFC (thanks {exabugs}[https://github.com/exabugs])
19
+ * Add option to provide httpdate formatted creation date to MS clients (thanks {doxavore}[https://github.com/doxavore])
20
+ * New MongoDB resource (thanks {exabugs}[https://github.com/exabugs])
21
+ * Controller subclass support (thanks {inferiorhumanorgans}[https://github.com/inferiorhumanorgans])
22
+ * Root XML attributes (thanks {inferiorhumanorgans}[https://github.com/inferiorhumanorgans])
23
+ * Allow propstat to return relative paths (apple carddav hack) (thanks {inferiorhumanorgans}[https://github.com/inferiorhumanorgans])
24
+
25
+ === v0.2.10
26
+ * Fix unicorn starting from exec script (thanks {spicyj}[https://github.com/spicyj])
27
+ * Return correct size using #bytesize instead of #size (thanks {TurchenkoAlex}[https://github.com/TurchenkoAlex])
28
+
29
+ === v0.2.9
30
+ * Be less restrictive of Nokogiri dependency
31
+
32
+ === v0.2.8
33
+ * Allow custom logger types to be used
34
+ * Allow resource to handle existence on locking (fixes issue {#21}[https://github.com/chrisroberts/dav4rack/issues/21] thanks {doxavore}[https://github.com/doxavore])
35
+ * Removed exception based control flow in favor of logic based control flow
36
+
37
+ === v0.2.7
38
+ * Include location content within PUT response body (fixes issue described in {#20}[https://github.com/chrisroberts/dav4rack/issues/20])
39
+
40
+ === v0.2.6
41
+ * Update response header from PUT to use Location
42
+
43
+ === v0.2.5
44
+ * Return Created response in favor of current multi status response on PUT (thanks {buffym}[https://github.com/buffym])
45
+ * Show class 1 compliance to be in accordance with WebDAV spec (thanks {buffym}[https://github.com/buffym])
46
+ * Adds setup method to skip alias list for resources (thanks {jbangert}[https://github.com/jbangert])
47
+ * Allow existing logger instance to be provided
48
+
49
+ === v0.2.4
50
+ * Return absolute URI from #mkcol and #put (thanks {teefax}[http://github.com/teefax])
51
+ * Nodes with text children properly serialized (pointed out by {jeffhos}[http://github.com/jeffhos])
52
+ * Fixed bug in file locking (pointed out by {clyfe}[http://github.com/clyfe] with fix provided by {teefax}[http://github.com/teefax])
53
+
54
+ === v0.2.3
55
+ * Completing missed step in last packaging
56
+
57
+ === v0.2.2
58
+ * Fix for port numbers in host (thanks {krug}[http://github.com/krug])
59
+
60
+ === v0.2.1
61
+ * Fix for better handling of MOVEs with badly encoded URLS
62
+
63
+ === v0.2.0
64
+ * Update to remote URL is passed to NGINX for proxying. Use headers instead of request
65
+
66
+ === v0.1.8
67
+ * Better exception handling for error logging
68
+ * Send overwrite flag to Resource#move
69
+
70
+ === v0.1.7
71
+ * Fix in interceptor to use correct File
72
+
73
+ === v0.1.6
74
+ * Add DAV4Rack::File that overloads just enough of Rack::File to allow explicit path setting
75
+
76
+ === v0.1.5
77
+ * Remove support for options[:delete_dotfiles]
78
+ * Allow HTTP methods to be ignored within interceptor
79
+ * Add owner information to lock response
80
+ * Initial update of spec to work with DAV4Rack
81
+ * Copy and delete recursively
82
+ * Add expected overwrite for copy/move on resource (thanks {clyfe}[http://github.com/clyfe])
83
+ * Add overwrite logic for copy/move on FileResource
84
+ * Removed callback authentication from FileResource (uses simple controller based auth)
85
+
86
+ === v0.1.4
87
+ * Fix for Rack::File issue (thanks {clyfe}[http://github.com/clyfe])
88
+ * Logging now optional on executable
89
+ * Include propstats even if empty (this resolves an issue in cyberduck not displaying files)
90
+
91
+ === v0.1.3
92
+ * Fix for Hash modification issues in Ruby 1.9.2 (thanks {antiloopgmbh}[http://github.com/antiloopgmbh])
93
+ * Fix executable to properly fallback
94
+ * Use callback authentication in FileResource to allow for no auth
95
+
96
+ === v0.1.2
97
+ * Add sendfile support (currently only tested on nginx)
98
+
99
+ === v0.1.1
100
+ * Add logging capability
101
+ * Simplify Interceptor mappings (provide options in hash instead of explicit :options)
102
+
103
+ === v0.1.0
104
+ * Callbacks available for resources
105
+ * RemoteFile more aligned with Rack::File
106
+ * Return multistatus responses PUT MKCOL and COPY/MOVE
107
+ * Executable now uses Unicorn, Mongrel and WEBrick in that order
108
+ * Simple resource locking enabled by default
109
+ * Updated FileResource to work properly with new architecture
@@ -286,6 +286,11 @@ Something special to notice in the last example is the DAV_ prefix on authentica
286
286
  any callbacks being applied to the given method. This allows us to provide a public method that the callback can access on the resource
287
287
  without getting stuck in a loop.
288
288
 
289
+ == Software using DAV4Rack!
290
+
291
+ * {meishi}[https://github.com/inferiorhumanorgans/meishi] - Lightweight CardDAV implementation in Rails
292
+ * {dav4rack_ext}[https://github.com/schmurfy/dav4rack_ext] - CardDAV extension. (CalDAV planned)
293
+
289
294
  == Issues/Bugs/Questions
290
295
 
291
296
  === Known Issues
@@ -313,6 +318,9 @@ A big thanks to everyone contributing to help make this project better.
313
318
  * {TurchenkoAlex}[https://github.com/TurchenkoAlex]
314
319
  * {exabugs}[https://github.com/exabugs]
315
320
  * {inferiorhumanorgans}[https://github.com/inferiorhumanorgans]
321
+ * {schmurfy}[https://github.com/schmurfy]
322
+ * {pifleo}[https://github.com/pifleo]
323
+ * {mlmorg}[https://github.com/mlmorg]
316
324
 
317
325
  == License
318
326
 
@@ -15,26 +15,5 @@ Gem::Specification.new do |s|
15
15
  s.add_dependency 'nokogiri', '>= 1.4.2'
16
16
  s.add_dependency 'uuidtools', '~> 2.1.1'
17
17
  s.add_dependency 'rack', '>= 1.1.0'
18
- s.files = %w{
19
- .gitignore
20
- LICENSE
21
- dav4rack.gemspec
22
- lib/dav4rack.rb
23
- lib/dav4rack/file_resource.rb
24
- lib/dav4rack/handler.rb
25
- lib/dav4rack/controller.rb
26
- lib/dav4rack/http_status.rb
27
- lib/dav4rack/resource.rb
28
- lib/dav4rack/interceptor.rb
29
- lib/dav4rack/interceptor_resource.rb
30
- lib/dav4rack/remote_file.rb
31
- lib/dav4rack/file.rb
32
- lib/dav4rack/lock.rb
33
- lib/dav4rack/lock_store.rb
34
- lib/dav4rack/logger.rb
35
- lib/dav4rack/version.rb
36
- bin/dav4rack
37
- spec/handler_spec.rb
38
- README.rdoc
39
- }
18
+ s.files = %w(dav4rack.gemspec README.rdoc CHANGELOG.rdoc LICENSE) + Dir.glob("{bin,lib}/**/*")
40
19
  end
@@ -3,7 +3,12 @@ require 'uri'
3
3
  require 'nokogiri'
4
4
 
5
5
  require 'rack'
6
+ require 'dav4rack/utils'
6
7
  require 'dav4rack/http_status'
7
8
  require 'dav4rack/resource'
8
9
  require 'dav4rack/handler'
9
10
  require 'dav4rack/controller'
11
+
12
+ module DAV4Rack
13
+ IS_18 = RUBY_VERSION[0,3] == '1.8'
14
+ end
@@ -4,6 +4,7 @@ module DAV4Rack
4
4
 
5
5
  class Controller
6
6
  include DAV4Rack::HTTPStatus
7
+ include DAV4Rack::Utils
7
8
 
8
9
  attr_reader :request, :response, :resource
9
10
 
@@ -17,7 +18,15 @@ module DAV4Rack
17
18
  @request = request
18
19
  @response = response
19
20
  @options = options
21
+
22
+ @dav_extensions = options.delete(:dav_extensions) || []
23
+ @always_include_dav_header = options.delete(:always_include_dav_header)
24
+
20
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
21
30
  end
22
31
 
23
32
  # s:: string
@@ -34,13 +43,20 @@ module DAV4Rack
34
43
  # Unescape URL string
35
44
  def url_unescape(s)
36
45
  URI.unescape(s)
37
- end
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
38
54
 
39
55
  # Return response to OPTIONS
40
56
  def options
41
- response["Allow"] = 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK'
42
- response["Dav"] = "1, 2"
43
- response["Ms-Author-Via"] = "DAV"
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'
44
60
  OK
45
61
  end
46
62
 
@@ -76,10 +92,10 @@ module DAV4Rack
76
92
  def put
77
93
  if(resource.collection?)
78
94
  Forbidden
79
- elsif(!resource.parent_exists? || !resource.parent.collection?)
95
+ elsif(!resource.parent_exists? || !resource.parent_collection?)
80
96
  Conflict
81
97
  else
82
- resource.lock_check
98
+ resource.lock_check if resource.supports_locking?
83
99
  status = resource.put(request, response)
84
100
  response['Location'] = "#{scheme}://#{host}:#{port}#{url_format(resource)}" if status == Created
85
101
  response.body = response['Location']
@@ -95,7 +111,7 @@ module DAV4Rack
95
111
  # Return response to DELETE
96
112
  def delete
97
113
  if(resource.exist?)
98
- resource.lock_check
114
+ resource.lock_check if resource.supports_locking?
99
115
  resource.delete
100
116
  else
101
117
  NotFound
@@ -104,7 +120,7 @@ module DAV4Rack
104
120
 
105
121
  # Return response to MKCOL
106
122
  def mkcol
107
- resource.lock_check
123
+ resource.lock_check if resource.supports_locking?
108
124
  status = resource.make_collection
109
125
  gen_url = "#{scheme}://#{host}:#{port}#{url_format(resource)}" if status == Created
110
126
  if(resource.use_compat_mkcol_response?)
@@ -132,7 +148,7 @@ module DAV4Rack
132
148
  unless(resource.exist?)
133
149
  NotFound
134
150
  else
135
- resource.lock_check unless args.include?(:copy)
151
+ resource.lock_check if resource.supports_locking? && !args.include(:copy)
136
152
  destination = url_unescape(env['HTTP_DESTINATION'].sub(%r{https?://([^/]+)}, ''))
137
153
  dest_host = $1
138
154
  if(dest_host && dest_host.gsub(/:\d{2,5}$/, '') != request.host)
@@ -165,25 +181,33 @@ module DAV4Rack
165
181
  end
166
182
  end
167
183
 
168
- # Return respoonse to PROPFIND
184
+ # Return response to PROPFIND
169
185
  def propfind
170
186
  unless(resource.exist?)
171
187
  NotFound
172
188
  else
173
189
  unless(request_document.xpath("//#{ns}propfind/#{ns}allprop").empty?)
174
- names = resource.property_names
190
+ properties = resource.properties
175
191
  else
176
- names = (
177
- ns.empty? ? request_document.remove_namespaces! : request_document
178
- ).xpath(
179
- "//#{ns}propfind/#{ns}prop"
180
- ).children.find_all{ |item|
181
- item.element? && item.name.start_with?(ns)
182
- }.map{ |item|
183
- item.name.sub("#{ns}::", '')
184
- }
185
- raise BadRequest if names.empty?
186
- names = resource.property_names if names.empty?
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
187
211
  end
188
212
  multistatus do |xml|
189
213
  find_resources.each do |resource|
@@ -193,7 +217,7 @@ module DAV4Rack
193
217
  else
194
218
  xml.href url_format(resource)
195
219
  end
196
- propstats(xml, get_properties(resource, names))
220
+ propstats(xml, get_properties(resource, properties.empty? ? resource.properties : properties))
197
221
  end
198
222
  end
199
223
  end
@@ -205,14 +229,32 @@ module DAV4Rack
205
229
  unless(resource.exist?)
206
230
  NotFound
207
231
  else
208
- resource.lock_check
209
- prop_rem = request_match('/propertyupdate/remove/prop').children.map{|n| [n.name] }
210
- prop_set = request_match('/propertyupdate/set/prop').children.map{|n| [n.name, n.text] }
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
211
246
  multistatus do |xml|
212
247
  find_resources.each do |resource|
213
248
  xml.response do
214
249
  xml.href "#{scheme}://#{host}:#{port}#{url_format(resource)}"
215
- propstats(xml, set_properties(resource, prop_set))
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
216
258
  end
217
259
  end
218
260
  end
@@ -260,6 +302,7 @@ module DAV4Rack
260
302
  end
261
303
  end
262
304
  end
305
+ response.headers['Lock-Token'] = locktoken
263
306
  response.status = resource.exist? ? OK : Created
264
307
  rescue LockFailure => e
265
308
  multistatus do |xml|
@@ -300,9 +343,6 @@ module DAV4Rack
300
343
  raise Unauthorized unless authed
301
344
  end
302
345
 
303
- # ************************************************************
304
- # private methods
305
-
306
346
  private
307
347
 
308
348
  # Request environment variables
@@ -402,22 +442,21 @@ module DAV4Rack
402
442
 
403
443
  # Namespace being used within XML document
404
444
  # TODO: Make this better
405
- def ns
445
+ def ns(wanted_uri="DAV:")
406
446
  _ns = ''
407
447
  if(request_document && request_document.root && request_document.root.namespace_definitions.size > 0)
408
- _ns = request_document.root.namespace_definitions.first.prefix.to_s
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
409
455
  _ns += ':' unless _ns.empty?
410
456
  end
411
457
  _ns
412
458
  end
413
459
 
414
- # pattern:: XPath pattern
415
- # Search XML document for given XPath
416
- # TODO: Stripping namespaces not so great
417
- def request_match(pattern)
418
- request_document.remove_namespaces!.xpath(pattern, request_document.root.namespaces)
419
- end
420
-
421
460
  # root_type:: Root tag name
422
461
  # Render XML and set Rack::Response#body= to final XML
423
462
  def render_xml(root_type)
@@ -462,35 +501,44 @@ module DAV4Rack
462
501
  end
463
502
 
464
503
  # resource:: Resource
465
- # names:: Property names
504
+ # elements:: Property hashes (name, ns_href, children)
466
505
  # Returns array of property values for given names
467
- def get_properties(resource, names)
506
+ def get_properties(resource, elements)
468
507
  stats = Hash.new { |h, k| h[k] = [] }
469
- for name in names
508
+ for element in elements
470
509
  begin
471
- val = resource.get_property(name)
472
- stats[OK].push [name, val]
510
+ val = resource.get_property(element)
511
+ stats[OK] << [element, val]
473
512
  rescue Unauthorized => u
474
513
  raise u
475
514
  rescue Status
476
- stats[$!.class] << name
515
+ stats[$!.class] << element
477
516
  end
478
517
  end
479
518
  stats
480
519
  end
481
520
 
482
521
  # resource:: Resource
483
- # pairs:: name value pairs
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)
484
532
  # Sets the given properties
485
- def set_properties(resource, pairs)
533
+ def set_properties(resource, elements)
486
534
  stats = Hash.new { |h, k| h[k] = [] }
487
- for name, value in pairs
535
+ for element, value in elements
488
536
  begin
489
- stats[OK] << [name, resource.set_property(name, value)]
537
+ stats[OK] << [element, resource.set_property(element, value)]
490
538
  rescue Unauthorized => u
491
539
  raise u
492
540
  rescue Status
493
- stats[$!.class] << name
541
+ stats[$!.class] << element
494
542
  end
495
543
  end
496
544
  stats
@@ -504,20 +552,36 @@ module DAV4Rack
504
552
  for status, props in stats
505
553
  xml.propstat do
506
554
  xml.prop do
507
- for name, value in props
508
- if(value.is_a?(Nokogiri::XML::DocumentFragment))
509
- xml.__send__ :insert, value
510
- elsif(value.is_a?(Nokogiri::XML::Node))
511
- xml.send(name) do
512
- xml_convert(xml, value)
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
513
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
514
572
  elsif(value.is_a?(Symbol))
515
- xml.send(name) do
516
- xml.send(value)
573
+ ns_xml.send(element[:name]) do
574
+ ns_xml.send(value)
517
575
  end
518
576
  else
519
- xml.send(name, value)
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
520
581
  end
582
+
583
+ # This is gross, but make sure we set the current namespace back to DAV:
584
+ xml['D']
521
585
  end
522
586
  end
523
587
  xml.status "#{http_version} #{status.status_line}"