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,37 @@
1
+ require 'time'
2
+ require 'rack/utils'
3
+ require 'rack/mime'
4
+
5
+ module RackWebDAV
6
+ # RackWebDAV::File simply allows us to use Rack::File but with the
7
+ # specific location we deem appropriate
8
+ class File < Rack::File
9
+ attr_accessor :path
10
+
11
+ alias :to_path :path
12
+
13
+ def initialize(path)
14
+ @path = path
15
+ end
16
+
17
+ def _call(env)
18
+ begin
19
+ if F.file?(@path) && F.readable?(@path)
20
+ serving(env)
21
+ else
22
+ raise Errno::EPERM
23
+ end
24
+ rescue SystemCallError
25
+ not_found
26
+ end
27
+ end
28
+
29
+ def not_found
30
+ body = "File not found: #{Rack::Utils.unescape(env["PATH_INFO"])}\n"
31
+ [404, {"Content-Type" => "text/plain",
32
+ "Content-Length" => body.size.to_s,
33
+ "X-Cascade" => "pass"},
34
+ [body]]
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,169 @@
1
+ require 'pstore'
2
+
3
+ module RackWebDAV
4
+ class FileResourceLock
5
+ attr_accessor :path
6
+ attr_accessor :token
7
+ attr_accessor :timeout
8
+ attr_accessor :depth
9
+ attr_accessor :user
10
+ attr_accessor :scope
11
+ attr_accessor :kind
12
+ attr_accessor :owner
13
+ attr_reader :created_at
14
+ attr_reader :root
15
+
16
+ class << self
17
+ def explicitly_locked?(path, croot=nil)
18
+ store = init_pstore(croot)
19
+ !!store.transaction(true){
20
+ store[:paths][path]
21
+ }
22
+ end
23
+
24
+ def implicitly_locked?(path, croot=nil)
25
+ store = init_pstore(croot)
26
+ !!store.transaction(true){
27
+ store[:paths].keys.detect do |check|
28
+ check.start_with?(path)
29
+ end
30
+ }
31
+ end
32
+
33
+ def explicit_locks(path, croot, args={})
34
+ end
35
+
36
+ def implict_locks(path)
37
+ end
38
+
39
+ def find_by_path(path, croot=nil)
40
+ lock = self.class.new(:path => path, :root => croot)
41
+ lock.token.nil? ? nil : lock
42
+ end
43
+
44
+ def find_by_token(token, croot=nil)
45
+ store = init_pstore(croot)
46
+ struct = store.transaction(true){
47
+ store[:tokens][token]
48
+ }
49
+ if !struct
50
+ struct = store.transaction(true) {
51
+ store[:tokens].keys.each { |k| token = k if k.include?(token) }
52
+ store[:tokens][token]
53
+ }
54
+ end
55
+
56
+ if struct
57
+ self.new(:path => struct[:path], :root => croot)
58
+ else
59
+ nil
60
+ end
61
+ end
62
+
63
+ def generate(path, user, token, croot)
64
+ lock = self.new(:root => croot)
65
+ lock.user = user
66
+ lock.path = path
67
+ lock.token = token
68
+ lock.save
69
+ lock
70
+ end
71
+
72
+ def root=(path)
73
+ @root = path
74
+ end
75
+
76
+ def root
77
+ @root || '/tmp/rack-webdav'
78
+ end
79
+
80
+ def init_pstore(croot)
81
+ path = File.join(croot, '.attribs', 'locks.pstore')
82
+ FileUtils.mkdir_p(File.dirname(path)) unless File.directory?(File.dirname(path))
83
+ store = IS_18 ? PStore.new(path) : PStore.new(path, true)
84
+ store.transaction do
85
+ unless(store[:paths])
86
+ store[:paths] = {}
87
+ store[:tokens] = {}
88
+ store.commit
89
+ end
90
+ end
91
+ store
92
+ end
93
+ end
94
+
95
+ def initialize(args={})
96
+ @path = args[:path]
97
+ @root = args[:root]
98
+ @owner = args[:owner]
99
+ @store = init_pstore(@root)
100
+ @max_timeout = args[:max_timeout] || 86400
101
+ @default_timeout = args[:max_timeout] || 60
102
+ load_if_exists!
103
+ @new_record = true if token.nil?
104
+ end
105
+
106
+ def owner?(user)
107
+ user == owner
108
+ end
109
+
110
+ def reload
111
+ load_if_exists
112
+ self
113
+ end
114
+
115
+ def remaining_timeout
116
+ t = timeout.to_i - (Time.now.to_i - created_at.to_i)
117
+ t < 0 ? 0 : t
118
+ end
119
+
120
+ def save
121
+ struct = {
122
+ :path => path,
123
+ :token => token,
124
+ :timeout => timeout,
125
+ :depth => depth,
126
+ :created_at => Time.now,
127
+ :owner => owner
128
+ }
129
+ @store.transaction do
130
+ @store[:paths][path] = struct
131
+ @store[:tokens][token] = struct
132
+ @store.commit
133
+ end
134
+ @new_record = false
135
+ self
136
+ end
137
+
138
+ def destroy
139
+ @store.transaction do
140
+ @store[:paths].delete(path)
141
+ @store[:tokens].delete(token)
142
+ @store.commit
143
+ end
144
+ nil
145
+ end
146
+
147
+ private
148
+
149
+ def load_if_exists!
150
+ struct = @store.transaction do
151
+ @store[:paths][path]
152
+ end
153
+ if(struct)
154
+ @path = struct[:path]
155
+ @token = struct[:token]
156
+ @timeout = struct[:timeout]
157
+ @depth = struct[:depth]
158
+ @created_at = struct[:created_at]
159
+ @owner = struct[:owner]
160
+ end
161
+ self
162
+ end
163
+
164
+ def init_pstore(croot=nil)
165
+ self.class.init_pstore(croot || @root)
166
+ end
167
+
168
+ end
169
+ end
@@ -0,0 +1,63 @@
1
+ require 'rack-webdav/logger'
2
+
3
+ module RackWebDAV
4
+
5
+ class Handler
6
+ include RackWebDAV::HTTPStatus
7
+ def initialize(options={})
8
+ @options = options.dup
9
+ unless(@options[:resource_class])
10
+ require 'rack-webdav/resources/file_resource'
11
+ @options[:resource_class] = FileResource
12
+ @options[:root] ||= Dir.pwd
13
+ end
14
+ Logger.set(*@options[:log_to])
15
+ end
16
+
17
+ def call(env)
18
+ begin
19
+ start = Time.now
20
+ request = Rack::Request.new(env)
21
+ response = Rack::Response.new
22
+
23
+ Logger.info "Processing WebDAV request: #{request.path} (for #{request.ip} at #{Time.now}) [#{request.request_method}]"
24
+
25
+ controller = nil
26
+ begin
27
+ controller_class = @options[:controller_class] || Controller
28
+ controller = controller_class.new(request, response, @options.dup)
29
+ controller.authenticate
30
+ res = controller.send(request.request_method.downcase)
31
+ response.status = res.code if res.respond_to?(:code)
32
+ rescue HTTPStatus::Unauthorized => status
33
+ response.body = controller.resource.respond_to?(:authentication_error_msg) ? controller.resource.authentication_error_msg : 'Not Authorized'
34
+ response['WWW-Authenticate'] = "Basic realm=\"#{controller.resource.respond_to?(:authentication_realm) ? controller.resource.authentication_realm : 'Locked content'}\""
35
+ response.status = status.code
36
+ rescue HTTPStatus::Status => status
37
+ response.status = status.code
38
+ end
39
+
40
+ # Strings in Ruby 1.9 are no longer enumerable. Rack still expects the response.body to be
41
+ # enumerable, however.
42
+
43
+ response['Content-Length'] = response.body.to_s.length unless response['Content-Length'] || !response.body.is_a?(String)
44
+ response.body = [response.body] unless response.body.respond_to? :each
45
+ response.status = response.status ? response.status.to_i : 200
46
+ response.headers.keys.each{|k| response.headers[k] = response[k].to_s}
47
+
48
+ # Apache wants the body dealt with, so just read it and junk it
49
+ buf = true
50
+ buf = request.body.read(8192) while buf
51
+
52
+ Logger.debug "Response in string form. Outputting contents: \n#{response.body}" if response.body.is_a?(String)
53
+ Logger.info "Completed in: #{((Time.now.to_f - start.to_f) * 1000).to_i} ms | #{response.status} [#{request.url}]"
54
+ response
55
+ rescue Exception => e
56
+ Logger.error "WebDAV Error: #{e}\n#{e.backtrace.join("\n")}"
57
+ raise e
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ end
@@ -0,0 +1,108 @@
1
+ module RackWebDAV
2
+
3
+ module HTTPStatus
4
+
5
+ class Status < Exception
6
+
7
+ class << self
8
+ attr_accessor :code, :reason_phrase
9
+ alias_method :to_i, :code
10
+
11
+ def status_line
12
+ "#{code} #{reason_phrase}"
13
+ end
14
+
15
+ end
16
+
17
+ def code
18
+ self.class.code
19
+ end
20
+
21
+ def reason_phrase
22
+ self.class.reason_phrase
23
+ end
24
+
25
+ def status_line
26
+ self.class.status_line
27
+ end
28
+
29
+ def to_i
30
+ self.class.to_i
31
+ end
32
+
33
+ end
34
+
35
+ StatusMessage = {
36
+ 100 => 'Continue',
37
+ 101 => 'Switching Protocols',
38
+ 102 => 'Processing',
39
+ 200 => 'OK',
40
+ 201 => 'Created',
41
+ 202 => 'Accepted',
42
+ 203 => 'Non-Authoritative Information',
43
+ 204 => 'No Content',
44
+ 205 => 'Reset Content',
45
+ 206 => 'Partial Content',
46
+ 207 => 'Multi-Status',
47
+ 300 => 'Multiple Choices',
48
+ 301 => 'Moved Permanently',
49
+ 302 => 'Found',
50
+ 303 => 'See Other',
51
+ 304 => 'Not Modified',
52
+ 305 => 'Use Proxy',
53
+ 307 => 'Temporary Redirect',
54
+ 400 => 'Bad Request',
55
+ 401 => 'Unauthorized',
56
+ 402 => 'Payment Required',
57
+ 403 => 'Forbidden',
58
+ 404 => 'Not Found',
59
+ 405 => 'Method Not Allowed',
60
+ 406 => 'Not Acceptable',
61
+ 407 => 'Proxy Authentication Required',
62
+ 408 => 'Request Timeout',
63
+ 409 => 'Conflict',
64
+ 410 => 'Gone',
65
+ 411 => 'Length Required',
66
+ 412 => 'Precondition Failed',
67
+ 413 => 'Request Entity Too Large',
68
+ 414 => 'Request-URI Too Large',
69
+ 415 => 'Unsupported Media Type',
70
+ 416 => 'Request Range Not Satisfiable',
71
+ 417 => 'Expectation Failed',
72
+ 422 => 'Unprocessable Entity',
73
+ 423 => 'Locked',
74
+ 424 => 'Failed Dependency',
75
+ 500 => 'Internal Server Error',
76
+ 501 => 'Not Implemented',
77
+ 502 => 'Bad Gateway',
78
+ 503 => 'Service Unavailable',
79
+ 504 => 'Gateway Timeout',
80
+ 505 => 'HTTP Version Not Supported',
81
+ 507 => 'Insufficient Storage'
82
+ }
83
+
84
+ StatusMessage.each do |code, reason_phrase|
85
+ klass = Class.new(Status)
86
+ klass.code = code
87
+ klass.reason_phrase = reason_phrase
88
+ klass_name = reason_phrase.gsub(/[ \-]/,'')
89
+ const_set(klass_name, klass)
90
+ end
91
+
92
+ end
93
+
94
+ end
95
+
96
+
97
+ module Rack
98
+ class Response
99
+ module Helpers
100
+ RackWebDAV::HTTPStatus::StatusMessage.each do |code, reason_phrase|
101
+ name = reason_phrase.gsub(/[ \-]/,'_').downcase
102
+ define_method(name + '?') do
103
+ @status == code
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,22 @@
1
+ require 'rack-webdav/interceptor_resource'
2
+ module RackWebDAV
3
+ class Interceptor
4
+ def initialize(app, args={})
5
+ @roots = args[:mappings].keys
6
+ @args = args
7
+ @app = app
8
+ @intercept_methods = %w(OPTIONS PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
9
+ @intercept_methods -= args[:ignore_methods] if args[:ignore_methods]
10
+ end
11
+
12
+ def call(env)
13
+ path = env['PATH_INFO'].downcase
14
+ method = env['REQUEST_METHOD'].upcase
15
+ app = nil
16
+ if(@roots.detect{|x| path =~ /^#{Regexp.escape(x.downcase)}\/?/}.nil? && @intercept_methods.include?(method))
17
+ app = RackWebDAV::Handler.new(:resource_class => InterceptorResource, :mappings => @args[:mappings], :log_to => @args[:log_to])
18
+ end
19
+ app ? app.call(env) : @app.call(env)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,119 @@
1
+ require 'digest/sha1'
2
+
3
+ module RackWebDAV
4
+
5
+ class InterceptorResource < Resource
6
+ attr_reader :path, :options
7
+
8
+ def initialize(*args)
9
+ super
10
+ @root_paths = @options[:mappings].keys
11
+ @mappings = @options[:mappings]
12
+ end
13
+
14
+ def children
15
+ childs = @root_paths.find_all{|x|x =~ /^#{Regexp.escape(@path)}/}
16
+ childs = childs.map{|a| child a.gsub(/^#{Regexp.escape(@path)}/, '').split('/').delete_if{|x|x.empty?}.first }.flatten
17
+ end
18
+
19
+ def collection?
20
+ true if exist?
21
+ end
22
+
23
+ def exist?
24
+ !@root_paths.find_all{|x| x =~ /^#{Regexp.escape(@path)}/}.empty?
25
+ end
26
+
27
+ def creation_date
28
+ Time.now
29
+ end
30
+
31
+ def last_modified
32
+ Time.now
33
+ end
34
+
35
+ def last_modified=(time)
36
+ Time.now
37
+ end
38
+
39
+ def etag
40
+ Digest::SHA1.hexdigest(@path)
41
+ end
42
+
43
+ def content_type
44
+ 'text/html'
45
+ end
46
+
47
+ def content_length
48
+ 0
49
+ end
50
+
51
+ def get(request, response)
52
+ raise Forbidden
53
+ end
54
+
55
+ def put(request, response)
56
+ raise Forbidden
57
+ end
58
+
59
+ def post(request, response)
60
+ raise Forbidden
61
+ end
62
+
63
+ def delete
64
+ raise Forbidden
65
+ end
66
+
67
+ def copy(dest)
68
+ raise Forbidden
69
+ end
70
+
71
+ def move(dest)
72
+ raise Forbidden
73
+ end
74
+
75
+ def make_collection
76
+ raise Forbidden
77
+ end
78
+
79
+ def ==(other)
80
+ path == other.path
81
+ end
82
+
83
+ def name
84
+ ::File.basename(path)
85
+ end
86
+
87
+ def display_name
88
+ ::File.basename(path.to_s)
89
+ end
90
+
91
+ def child(name, option={})
92
+ new_path = path.dup
93
+ new_path = '/' + new_path unless new_path[0,1] == '/'
94
+ new_path.slice!(-1) if new_path[-1,1] == '/'
95
+ name = '/' + name unless name[-1,1] == '/'
96
+ new_path = "#{new_path}#{name}"
97
+ new_public = public_path.dup
98
+ new_public = '/' + new_public unless new_public[0,1] == '/'
99
+ new_public.slice!(-1) if new_public[-1,1] == '/'
100
+ new_public = "#{new_public}#{name}"
101
+ if(key = @root_paths.find{|x| new_path =~ /^#{Regexp.escape(x.downcase)}\/?/})
102
+ @mappings[key][:resource_class].new(new_public, new_path.gsub(key, ''), request, response, {:root_uri_path => key, :user => @user}.merge(options).merge(@mappings[key]))
103
+ else
104
+ self.class.new(new_public, new_path, request, response, {:user => @user}.merge(options))
105
+ end
106
+ end
107
+
108
+ def descendants
109
+ list = []
110
+ children.each do |child|
111
+ list << child
112
+ list.concat(child.descendants)
113
+ end
114
+ list
115
+ end
116
+
117
+ end
118
+
119
+ end