dav4rack 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ *~
2
+ doc/*
3
+ pkg/*
4
+ test/repo
data/LICENSE ADDED
@@ -0,0 +1,42 @@
1
+ Current DAV4Rack license:
2
+
3
+ Copyright (c) 2010 Chris Roberts <chrisroberts.code@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to
7
+ deal in the Software without restriction, including without limitation the
8
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9
+ sell copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+
23
+ Original rack_dav source license:
24
+
25
+ Copyright (c) 2009 Matthias Georgi <http://www.matthias-georgi.de>
26
+
27
+ Permission is hereby granted, free of charge, to any person obtaining a copy
28
+ of this software and associated documentation files (the "Software"), to
29
+ deal in the Software without restriction, including without limitation the
30
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
31
+ sell copies of the Software, and to permit persons to whom the Software is
32
+ furnished to do so, subject to the following conditions:
33
+
34
+ The above copyright notice and this permission notice shall be included in
35
+ all copies or substantial portions of the Software.
36
+
37
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
40
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
41
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
42
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,137 @@
1
+ == DAV4Rack - Web Authoring for Rack
2
+
3
+ DAV4Rack is a framework for providing WebDAV via Rack allowing content
4
+ authoring over HTTP. It is based off the {original RackDAV framework}[http://github.com/georgi/rack_dav]
5
+ to provide better Resource support for building customized WebDAV resources
6
+ completely abstracted from the filesystem. Support for authentication and
7
+ locking has been added as well as a move from REXML to Nokogiri. Enjoy!
8
+
9
+ == Install
10
+
11
+ === Via RubyGems
12
+
13
+ gem install dav4rack
14
+
15
+ == Quickstart
16
+
17
+ If you just want to share a folder over WebDAV, you can just start a
18
+ simple server with:
19
+
20
+ dav4rack
21
+
22
+ This will start a WEBrick server on port 3000, which you can connect
23
+ to without authentication. The simple file resource allows very basic authentication
24
+ which is used for an example. To enable it:
25
+
26
+ dav4rack --username=user --password=pass
27
+
28
+ == Rack Handler
29
+
30
+ Using DAV4Rack within a rack application is pretty simple. A very slim
31
+ rackup script would look something like this:
32
+
33
+ require 'rubygems'
34
+ require 'dav4rack'
35
+
36
+ use Rack::CommonLogger
37
+ run DAV4Rack::Handler.new(:root => '/path/to/public/fileshare')
38
+
39
+ This will use the included FileResource and set the share path. However,
40
+ DAV4Rack has some nifty little extras that can be enabled in the rackup script. First,
41
+ an example of how to use a custom resource:
42
+
43
+ run DAV4Rack::Handler.new(:resource_class => CustomResource, :custom => 'options', :passed => 'to resource')
44
+
45
+ Next, lets venture into mapping a path for our WebDAV access. In this example, we
46
+ will use default FileResource like in the first example, but instead of connecting
47
+ directly to the host, we will have to connect to: http://host/webdav/share/
48
+
49
+ require 'rubygems'
50
+ require 'dav4rack'
51
+
52
+ use Rack::CommonLogger
53
+
54
+ app = Rack::Builder.new{
55
+ map '/webdav/share/' do
56
+ run DAV4Rack::Handler.new(:root => '/path/to/public/fileshare', :root_uri_path => '/webdav/share/')
57
+ end
58
+ }.to_app
59
+ run app
60
+
61
+ Aside from the #map block, notice the new option passed to the Handler's initialization, :root_uri_path. When
62
+ DAV4Rack receives a request, it will automatically convert the request to the proper path and pass it to
63
+ the resource.
64
+
65
+ Another tool available when building the rackup script is the Interceptor. The Interceptor's job is to simply
66
+ intecept WebDAV requests received up the path hierarchy where no resources are currently mapped. For example,
67
+ lets continue with the last example but this time include the interceptor:
68
+
69
+ require 'rubygems'
70
+ require 'dav4rack'
71
+
72
+ use Rack::CommonLogger
73
+ app = Rack::Builder.new{
74
+ map '/webdav/share/' do
75
+ run DAV4Rack::Handler.new(:root => '/path/to/public/fileshare', :root_uri_path => '/webdav/share/')
76
+ end
77
+ map '/webdav/share2/' do
78
+ run DAV4Rack::Handler.new(:resource_class => CustomResource, :root_uri_path => '/webdav/share2/')
79
+ end
80
+ map '/' do
81
+ use DAV4Rack::Interceptor, :mappings => {
82
+ '/webdav/share/' => {:resource_class => FileResource, :options => {:custom => 'option'}},
83
+ '/webdav/share2/' => {:resource_class => CustomResource}
84
+ }
85
+ use Rails::Rack::Static
86
+ run ActionController::Dispatcher.new
87
+ end
88
+ }.to_app
89
+ run app
90
+
91
+ In this example we have two WebDAV resources restricted by path. This means those resources will handle requests to /webdav/share/*
92
+ and /webdav/share2/* but nothing above that. To allow webdav to respond, we provide the Interceptor. The Interceptor does not
93
+ provide any authentication support. It simply creates a virtual file system view to the provided mapped paths. Once the actual
94
+ resources have been reached, authentication will be enforced based on the requirements defined by the individual resource. Also
95
+ note in the root map you can see we are running a Rails application. This is how you can easily enable DAV4Rack with your Rails
96
+ application.
97
+
98
+ == Custom Resources
99
+
100
+ Creating your own resource is easy. Simply inherit the DAV4Rack::Resource class, and start redefining all the methods
101
+ you want to customize. The DAV4Rack::Resource class only has implementations for methods that can be provided extremely
102
+ generically. This means that most things will require at least some sort of implementation. However, because the Resource
103
+ is defined so generically, and the Controller simply passes the request on to the Resource, it is easy to create fully
104
+ virtualized resources.
105
+
106
+ == Helpers
107
+
108
+ There are some helpers worth mentioning that make things a little easier. DAV4Rack::Resource#accept_redirect? method is available to Resources.
109
+ If true, the currently connected client will accept and properly use a 302 redirect for a GET request. Most clients do not properly
110
+ support this, which can be a real pain when working with virtualized files that may located some where else, like S3. To deal with
111
+ those clients that don't support redirects, a helper has been provided so resources don't have to deal with proxying themselves. The
112
+ DAV4Rack::RemoteFile allows the resource to simple tell Rack to download and send the file from the provided resource and go away, allowing the
113
+ process to be freed up to deal with other waiters. A very simple example:
114
+
115
+ class MyResource < DAV4Rack::Resource
116
+ def initialize(*args)
117
+ super(*args)
118
+ @item = method_to_fill_this_properly
119
+ end
120
+
121
+ def get
122
+ if(accept_redirect?)
123
+ response.redirect item[:url]
124
+ else
125
+ response.body = DAV4Rack::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type)
126
+ OK
127
+ end
128
+ end
129
+ end
130
+
131
+ == Issues/Bugs/Questions
132
+
133
+ Please use the issues at github: http://github.com/chrisroberts/dav4rack
134
+
135
+ == License
136
+
137
+ Just like RackDAV before it, this software is distributed under the MIT license.
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'dav4rack'
5
+ require 'getoptlong'
6
+
7
+ def print_help_msg
8
+ print_version_info
9
+ puts "Usage: dav4rack [opts]"
10
+ puts " --help Print help message"
11
+ puts " --version Print version information"
12
+ puts " --username name Set username"
13
+ puts " --password pass Set password"
14
+ puts " --root /share/path Set path to share directory"
15
+ end
16
+
17
+ def print_version_info
18
+ puts "DAV 4 Rack - Rack based WebDAV Framework - Version: #{DAV4Rack::VERSION}"
19
+ end
20
+
21
+ opts = GetoptLong.new(
22
+ ['--username', '-u', GetoptLong::REQUIRED_ARGUMENT],
23
+ ['--password', '-p', GetoptLong::REQUIRED_ARGUMENT],
24
+ ['--help', '-h', GetoptLong::NO_ARGUMENT],
25
+ ['--version', '-v', GetoptLong::NO_ARGUMENT],
26
+ ['--root', '-r', GetoptLong::REQUIRED_ARGUMENT]
27
+ )
28
+
29
+ credentials = {}
30
+
31
+ opts.each do |opt,arg|
32
+ case opt
33
+ when '--help'
34
+ print_help_msg
35
+ exit(0)
36
+ when '--username'
37
+ credentials[:username] = arg
38
+ when '--password'
39
+ credentials[:password] = arg
40
+ when '--root'
41
+ credentials[:root] = arg
42
+ unless(File.exists?(arg) && File.directory?(arg))
43
+ puts "ERROR: Path provided is not a valid directory (#{arg})"
44
+ exit(-1)
45
+ end
46
+ when '--version'
47
+ print_version_info
48
+ exit(0)
49
+ else
50
+ puts "ERROR: Unknown option provided"
51
+ exit(-1)
52
+ end
53
+ end
54
+
55
+ app = Rack::Builder.new do
56
+ use Rack::ShowExceptions
57
+ use Rack::CommonLogger
58
+ use Rack::Reloader
59
+ use Rack::Lint
60
+
61
+ run DAV4Rack::Handler.new(credentials)
62
+
63
+ end.to_app
64
+
65
+ Rack::Handler::WEBrick.run(app, :Port => 3000)
@@ -0,0 +1,31 @@
1
+ require 'dav4rack'
2
+ Gem::Specification.new do |s|
3
+ s.name = 'dav4rack'
4
+ s.version = DAV4Rack::VERSION
5
+ s.summary = 'WebDAV handler for Rack'
6
+ s.author = 'Chris Roberts'
7
+ s.email = 'chris@chrisroberts.org'
8
+ s.homepage = 'http://github.com/dav4rack'
9
+ s.description = 'WebDAV handler for Rack'
10
+ s.require_path = 'lib'
11
+ s.executables << 'dav4rack'
12
+ s.has_rdoc = true
13
+ s.extra_rdoc_files = ['README.rdoc']
14
+ s.files = %w{
15
+ .gitignore
16
+ LICENSE
17
+ dav4rack.gemspec
18
+ lib/dav4rack.rb
19
+ lib/dav4rack/file_resource.rb
20
+ lib/dav4rack/handler.rb
21
+ lib/dav4rack/controller.rb
22
+ lib/dav4rack/http_status.rb
23
+ lib/dav4rack/resource.rb
24
+ lib/dav4rack/interceptor.rb
25
+ lib/dav4rack/interceptor_resource.rb
26
+ lib/dav4rack/remote_file.rb
27
+ bin/dav4rack
28
+ spec/handler_spec.rb
29
+ README.rdoc
30
+ }
31
+ end
@@ -0,0 +1,13 @@
1
+ require 'time'
2
+ require 'uri'
3
+ require 'nokogiri'
4
+
5
+ require 'rack'
6
+ require 'dav4rack/http_status'
7
+ require 'dav4rack/resource'
8
+ require 'dav4rack/handler'
9
+ require 'dav4rack/controller'
10
+
11
+ module DAV4Rack
12
+ VERSION = '0.0.1'
13
+ end
@@ -0,0 +1,457 @@
1
+ module DAV4Rack
2
+
3
+ class Controller
4
+ include DAV4Rack::HTTPStatus
5
+
6
+ attr_reader :request, :response, :resource
7
+
8
+ # request:: Rack::Request
9
+ # response:: Rack::Response
10
+ # options:: Options hash
11
+ # Create a new Controller.
12
+ # NOTE: options will be passed to Resource
13
+ def initialize(request, response, options={})
14
+ @request = request
15
+ @response = response
16
+ @options = options
17
+ @resource = resource_class.new(actual_path, implied_path, @request, @options)
18
+ authenticate
19
+ raise Forbidden if request.path_info.include?('../')
20
+ end
21
+
22
+ # s:: string
23
+ # Escape URL string
24
+ def url_escape(s)
25
+ s.gsub(/([^\/a-zA-Z0-9_.-]+)/n) do
26
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
27
+ end.tr(' ', '+')
28
+ end
29
+
30
+ # s:: string
31
+ # Unescape URL string
32
+ def url_unescape(s)
33
+ s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n) do
34
+ [$1.delete('%')].pack('H*')
35
+ end
36
+ end
37
+
38
+ # Return response to OPTIONS
39
+ def options
40
+ response["Allow"] = 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK'
41
+ response["Dav"] = "2"
42
+ response["Ms-Author-Via"] = "DAV"
43
+ NoContent
44
+ end
45
+
46
+ # Return response to HEAD
47
+ def head
48
+ raise NotFound unless resource.exist?
49
+ response['Etag'] = resource.etag
50
+ response['Content-Type'] = resource.content_type
51
+ response['Last-Modified'] = resource.last_modified.httpdate
52
+ NoContent
53
+ end
54
+
55
+ # Return response to GET
56
+ def get
57
+ raise NotFound unless resource.exist?
58
+ res = resource.get(request, response)
59
+ if(response.status == 200 && !resource.collection?)
60
+ response['Etag'] = resource.etag
61
+ response['Content-Type'] = resource.content_type
62
+ response['Content-Length'] = resource.content_length.to_s
63
+ response['Last-Modified'] = resource.last_modified.httpdate
64
+ end
65
+ res
66
+ end
67
+
68
+ # Return response to PUT
69
+ def put
70
+ raise Forbidden if resource.collection?
71
+ resource.put(request, response)
72
+ end
73
+
74
+ # Return response to POST
75
+ def post
76
+ resource.post(request, response)
77
+ end
78
+
79
+ # Return response to DELETE
80
+ def delete
81
+ raise NotFound unless resource.exist?
82
+ resource.delete
83
+ end
84
+
85
+ # Return response to MKCOL
86
+ def mkcol
87
+ resource.make_collection
88
+ end
89
+
90
+ # Return response to COPY
91
+ def copy
92
+ move(:copy)
93
+ end
94
+
95
+ # args:: Only argument used: :copy
96
+ # Move Resource to new location. If :copy is provided,
97
+ # Resource will be copied (implementation ease)
98
+ def move(*args)
99
+ raise NotFound unless resource.exist?
100
+ dest_uri = URI.parse(env['HTTP_DESTINATION'])
101
+ destination = url_unescape(dest_uri.path)
102
+ raise BadGateway if dest_uri.host and dest_uri.host != request.host
103
+ raise Forbidden if destination == resource.public_path
104
+ dest = resource_class.new(destination, clean_path(destination), @request, @options)
105
+ if(args.include?(:copy))
106
+ resource.copy(dest, overwrite)
107
+ else
108
+ raise Conflict unless depth.is_a?(Symbol) || depth > 1
109
+ resource.move(dest)
110
+ end
111
+ end
112
+
113
+ # Return respoonse to PROPFIND
114
+ def propfind
115
+ raise NotFound unless resource.exist?
116
+ unless(request_document.xpath("//#{ns}propfind/#{ns}allprop").empty?)
117
+ names = resource.property_names
118
+ else
119
+ names = request_document.xpath("//#{ns}propfind/#{ns}prop").children.find_all{|n|n.element?}.map{|n|n.name}
120
+ names = resource.property_names if names.empty?
121
+ end
122
+ multistatus do |xml|
123
+ find_resources.each do |resource|
124
+ xml.response do
125
+ xml.href "#{scheme}://#{host}:#{port}#{url_escape(resource.public_path)}"
126
+ propstats(xml, get_properties(resource, names))
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ # Return response to PROPPATCH
133
+ def proppatch
134
+ raise NotFound unless resource.exist?
135
+ prop_rem = request_match('/propertyupdate/remove/prop').children.map{|n| [n.name] }
136
+ prop_set = request_match('/propertyupdate/set/prop').children.map{|n| [n.name, n.text] }
137
+ multistatus do |xml|
138
+ find_resources.each do |resource|
139
+ xml.response do
140
+ xml.href "#{scheme}://#{host}:#{port}#{url_escape(resource.public_path)}"
141
+ propstats(xml, set_properties(resource, prop_set))
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+
148
+ # Lock current resource
149
+ # NOTE: This will pass an argument hash to Resource#lock and
150
+ # wait for a success/failure response.
151
+ def lock
152
+ raise NotFound unless resource.exist?
153
+ lockinfo = request_document.xpath("//#{ns}lockinfo")
154
+ asked = {}
155
+ asked[:timeout] = request.env['Timeout'].split(',').map{|x|x.strip} if request.env['Timeout']
156
+ asked[:depth] = depth
157
+ raise BadRequest unless [0, :infinity].include?(asked[:depth])
158
+ asked[:scope] = lockinfo.xpath("//#{ns}lockscope").children.find_all{|n|n.element?}.map{|n|n.name}.first
159
+ asked[:type] = lockinfo.xpath("#{ns}locktype").children.find_all{|n|n.element?}.map{|n|n.name}.first
160
+ asked[:owner] = lockinfo.xpath("//#{ns}owner/#{ns}href").children.map{|n|n.text}.first
161
+ begin
162
+ lock_time, locktoken = resource.lock(asked)
163
+ render_xml(:prop) do |xml|
164
+ xml.lockdiscovery do
165
+ xml.activelock do
166
+ if(asked[:scope])
167
+ xml.lockscope do
168
+ xml.send(asked[:scope])
169
+ end
170
+ end
171
+ if(asked[:type])
172
+ xml.locktype do
173
+ xml.send(asked[:type])
174
+ end
175
+ end
176
+ xml.depth asked[:depth].to_s
177
+ xml.timeout lock_time ? "Second-#{lock_time}" : 'infinity'
178
+ xml.locktoken do
179
+ xml.href locktoken
180
+ end
181
+ end
182
+ end
183
+ end
184
+ response.status = resource.exist? ? OK : Created
185
+ rescue LockFailure => e
186
+ multistatus do |xml|
187
+ e.path_status.each_pair do |path, status|
188
+ xml.response do
189
+ xml.href path
190
+ xml.status "#{http_version} #{status.status_line}"
191
+ end
192
+ end
193
+ end
194
+ response.status = MultiStatus
195
+ end
196
+ end
197
+
198
+ # Unlock current resource
199
+ def unlock
200
+ resource.unlock(lock_token)
201
+ end
202
+
203
+ # ************************************************************
204
+ # private methods
205
+
206
+ private
207
+
208
+ # Request environment variables
209
+ def env
210
+ @request.env
211
+ end
212
+
213
+ # Current request scheme (http/https)
214
+ def scheme
215
+ request.scheme
216
+ end
217
+
218
+ # Request host
219
+ def host
220
+ request.host
221
+ end
222
+
223
+ # Request port
224
+ def port
225
+ request.port
226
+ end
227
+
228
+ # Class of the resource in use
229
+ def resource_class
230
+ @options[:resource_class]
231
+ end
232
+
233
+ # Root URI path for the resource
234
+ def root_uri_path
235
+ @options[:root_uri_path]
236
+ end
237
+
238
+ # Returns Resource path with root URI removed
239
+ def implied_path
240
+ clean_path(@request.path.dup)
241
+ end
242
+
243
+ # x:: request path
244
+ # Unescapes path and removes root URI if applicable
245
+ def clean_path(x)
246
+ ip = url_unescape(x)
247
+ ip.gsub!(/^#{Regexp.escape(root_uri_path)}/, '') if root_uri_path
248
+ ip
249
+ end
250
+
251
+ # Unescaped request path
252
+ def actual_path
253
+ url_unescape(@request.path.dup)
254
+ end
255
+
256
+ # Lock token if provided by client
257
+ def lock_token
258
+ env['HTTP_LOCK_TOKEN'] || nil
259
+ end
260
+
261
+ # Requested depth
262
+ def depth
263
+ d = env['HTTP_DEPTH']
264
+ if(d =~ /^\d+$/)
265
+ d = d.to_i
266
+ else
267
+ d = :infinity
268
+ end
269
+ d
270
+ end
271
+
272
+ # Current HTTP version being used
273
+ def http_version
274
+ env['HTTP_VERSION'] || env['SERVER_PROTOCOL'] || 'HTTP/1.0'
275
+ end
276
+
277
+ # Overwrite is allowed
278
+ def overwrite
279
+ env['HTTP_OVERWRITE'].to_s.upcase != 'F'
280
+ end
281
+
282
+ # Find resources at depth requested
283
+ def find_resources
284
+ ary = nil
285
+ case depth
286
+ when 0
287
+ ary = [resource]
288
+ when 1
289
+ ary = resource.children
290
+ else
291
+ ary = resource.descendants
292
+ end
293
+ ary ? ary : []
294
+ end
295
+
296
+ # XML parsed request
297
+ def request_document
298
+ @request_document ||= Nokogiri.XML(request.body.read)
299
+ rescue
300
+ raise BadRequest
301
+ end
302
+
303
+ # Namespace being used within XML document
304
+ # TODO: Make this better
305
+ def ns
306
+ _ns = ''
307
+ if(request_document && request_document.root && request_document.root.namespace_definitions.size > 0)
308
+ _ns = request_document.root.namespace_definitions.first.prefix.to_s
309
+ _ns += ':' unless _ns.empty?
310
+ end
311
+ _ns
312
+ end
313
+
314
+ # pattern:: XPath pattern
315
+ # Search XML document for given XPath
316
+ def request_match(pattern)
317
+ nil unless request_document
318
+ request_document.xpath(pattern, request_document.root.namespaces)
319
+ end
320
+
321
+ # root_type:: Root tag name
322
+ # Render XML and set Rack::Response#body= to final XML
323
+ def render_xml(root_type)
324
+ raise ArgumentError.new 'Expecting block' unless block_given?
325
+ doc = Nokogiri::XML::Builder.new do |xml|
326
+ xml.send(root_type.to_s, 'xmlns' => 'DAV:') do
327
+ # xml.parent.namespace = xml.parent.namespace_definitions.first
328
+ yield xml
329
+ end
330
+ end
331
+
332
+ response.body = doc.to_xml
333
+ response["Content-Type"] = 'text/xml; charset="utf-8"'
334
+ response["Content-Length"] = response.body.size.to_s
335
+ end
336
+
337
+ # block:: block
338
+ # Creates a multistatus response using #render_xml and
339
+ # returns the correct status
340
+ def multistatus(&block)
341
+ render_xml(:multistatus, &block)
342
+ MultiStatus
343
+ end
344
+
345
+ # xml:: Nokogiri::XML::Builder
346
+ # errors:: Array of errors
347
+ # Crafts responses for errors
348
+ def response_errors(xml, errors)
349
+ for path, status in errors
350
+ xml.response do
351
+ xml.href "#{scheme}://#{host}:#{port}#{path}"
352
+ xml.status "#{http_version} #{status.status_line}"
353
+ end
354
+ end
355
+ end
356
+
357
+ # resource:: Resource
358
+ # names:: Property names
359
+ # Returns array of property values for given names
360
+ def get_properties(resource, names)
361
+ stats = Hash.new { |h, k| h[k] = [] }
362
+ for name in names
363
+ begin
364
+ val = resource.get_property(name)
365
+ stats[OK].push [name, val] if val
366
+ rescue Status
367
+ stats[$!] << name
368
+ end
369
+ end
370
+ stats
371
+ end
372
+
373
+ # resource:: Resource
374
+ # pairs:: name value pairs
375
+ # Sets the given properties
376
+ def set_properties(resource, pairs)
377
+ stats = Hash.new { |h, k| h[k] = [] }
378
+ for name, value in pairs
379
+ begin
380
+ stats[OK] << [name, resource.set_property(name, value)]
381
+ rescue Status
382
+ stats[$!] << name
383
+ end
384
+ end
385
+ stats
386
+ end
387
+
388
+ # xml:: Nokogiri::XML::Builder
389
+ # stats:: Array of stats
390
+ # Build propstats response
391
+ def propstats(xml, stats)
392
+ return if stats.empty?
393
+ for status, props in stats
394
+ xml.propstat do
395
+ xml.prop do
396
+ for name, value in props
397
+ if(value.is_a?(Nokogiri::XML::Node))
398
+ xml.send(name) do
399
+ xml_convert(xml, value)
400
+ end
401
+ elsif(value.is_a?(Symbol))
402
+ xml.send(name) do
403
+ xml.send(value)
404
+ end
405
+ else
406
+ xml.send(name, value)
407
+ end
408
+ end
409
+ end
410
+ xml.status "#{http_version} #{status.status_line}"
411
+ end
412
+ end
413
+ end
414
+
415
+ # xml:: Nokogiri::XML::Builder
416
+ # element:: Nokogiri::XML::Element
417
+ # Converts element into proper text
418
+ def xml_convert(xml, element)
419
+ if element.children.empty?
420
+ if element.text?
421
+ xml.send(element.name, element.text, element.attributes)
422
+ else
423
+ xml.send(element.name, element.attributes)
424
+ end
425
+ else
426
+ xml.send(element.name, element.attributes) do
427
+ element.elements.each do |child|
428
+ xml_convert(xml, child)
429
+ end
430
+ end
431
+ end
432
+ end
433
+
434
+ # Perform authentication
435
+ # NOTE: Authentication will only be performed if the Resource
436
+ # has defined an #authenticate method
437
+ def authenticate
438
+ authed = true
439
+ if(resource.respond_to?(:authenticate))
440
+ authed = false
441
+ if(request.env['HTTP_AUTHORIZATION'])
442
+ auth = Rack::Auth::Basic::Request.new(request.env)
443
+ if(auth.basic? && auth.credentials)
444
+ authed = resource.authenticate(auth.credentials[0], auth.credentials[1])
445
+ end
446
+ end
447
+ end
448
+ unless(authed)
449
+ response.body = resource.respond_to?(:authentication_error_msg) ? resource.authentication_error_msg : 'Not Authorized'
450
+ response['WWW-Authenticate'] = "Basic realm=\"#{resource.respond_to?(:authentication_realm) ? resource.authentication_realm : 'Locked content'}\""
451
+ raise Unauthorized.new unless authed
452
+ end
453
+ end
454
+
455
+ end
456
+
457
+ end