dav4rack 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/LICENSE +42 -0
- data/README.rdoc +137 -0
- data/bin/dav4rack +65 -0
- data/dav4rack.gemspec +31 -0
- data/lib/dav4rack.rb +13 -0
- data/lib/dav4rack/controller.rb +457 -0
- data/lib/dav4rack/file_resource.rb +176 -0
- data/lib/dav4rack/handler.rb +44 -0
- data/lib/dav4rack/http_status.rb +108 -0
- data/lib/dav4rack/interceptor.rb +20 -0
- data/lib/dav4rack/interceptor_resource.rb +119 -0
- data/lib/dav4rack/remote_file.rb +62 -0
- data/lib/dav4rack/resource.rb +250 -0
- data/spec/handler_spec.rb +270 -0
- metadata +81 -0
data/.gitignore
ADDED
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.
|
data/README.rdoc
ADDED
@@ -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.
|
data/bin/dav4rack
ADDED
@@ -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)
|
data/dav4rack.gemspec
ADDED
@@ -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
|
data/lib/dav4rack.rb
ADDED
@@ -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
|