Dav4Rack gem replaced with a local copy
This commit is contained in:
parent
8ece0ac007
commit
a8ce29e2c0
@ -6,6 +6,10 @@ Changelog for Redmine DMSF
|
||||
|
||||
Javascript on pages is loaded asynchronously
|
||||
Obsolete Dav4Rack gem replaced with an up to date fork by Planio (Consequently WebDAV caching has been removed, sorry...)
|
||||
Instead of usage their gem in the Gemfile:
|
||||
gem 'dav4rack', git: 'https://github.com/planio-gmbh/dav4rack.git', branch: 'master'
|
||||
The library is a part of the project (lib/dav4rack).
|
||||
Cloned from gem https://github.com/planio-gmbh/dav4rack.git
|
||||
Project members can be chosen as recipients when sending documents by email
|
||||
Responsive view
|
||||
Direct editing of document in MS Office
|
||||
|
||||
11
Gemfile
11
Gemfile
@ -26,8 +26,17 @@ gem 'rubyzip', '>= 1.0.0'
|
||||
gem 'zip-zip'
|
||||
gem 'simple_enum'
|
||||
gem 'uuidtools'
|
||||
gem 'dav4rack', git: 'https://github.com/planio-gmbh/dav4rack.git', branch: 'master'
|
||||
gem 'dalli'
|
||||
|
||||
# Redmine extensions
|
||||
unless %w(easyproject easy_gantt).any? { |plugin| Dir.exist?(File.expand_path("../../#{plugin}", __FILE__)) }
|
||||
gem 'redmine_extensions', '~> 0.2.5'
|
||||
end
|
||||
|
||||
# Dav4Rack
|
||||
gem 'rake'
|
||||
gem 'mongo'
|
||||
gem 'unicorn'
|
||||
gem 'byebug', platforms: :mri
|
||||
gem 'ruby-prof'
|
||||
gem 'ox'
|
||||
|
||||
15
lib/dav4rack.rb
Normal file
15
lib/dav4rack.rb
Normal file
@ -0,0 +1,15 @@
|
||||
require 'time'
|
||||
require 'uri'
|
||||
require 'nokogiri'
|
||||
require 'ox'
|
||||
|
||||
require 'rack'
|
||||
require 'dav4rack/utils'
|
||||
require 'dav4rack/http_status'
|
||||
require 'dav4rack/resource'
|
||||
require 'dav4rack/handler'
|
||||
require 'dav4rack/controller'
|
||||
|
||||
module DAV4Rack
|
||||
IS_18 = RUBY_VERSION[0,3] == '1.8'
|
||||
end
|
||||
426
lib/dav4rack/controller.rb
Normal file
426
lib/dav4rack/controller.rb
Normal file
@ -0,0 +1,426 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'uri'
|
||||
require 'dav4rack/destination_header'
|
||||
require 'dav4rack/request'
|
||||
require 'dav4rack/xml_elements'
|
||||
require 'dav4rack/xml_response'
|
||||
|
||||
module DAV4Rack
|
||||
|
||||
class Controller
|
||||
include DAV4Rack::HTTPStatus
|
||||
include DAV4Rack::Utils
|
||||
|
||||
attr_reader :request, :response, :resource
|
||||
|
||||
|
||||
# request:: DAV4Rack::Request
|
||||
# response:: Rack::Response
|
||||
# options:: Options hash
|
||||
# Create a new Controller.
|
||||
#
|
||||
# options will be passed on to the resource
|
||||
def initialize(request, response, options={})
|
||||
@options = options
|
||||
|
||||
@request = request
|
||||
@response = response
|
||||
|
||||
@dav_extensions = options[:dav_extensions]
|
||||
|
||||
@always_include_dav_header = !!options[:always_include_dav_header]
|
||||
@allow_unauthenticated_options_on_root = !!options[:allow_unauthenticated_options_on_root]
|
||||
|
||||
setup_resource
|
||||
|
||||
if @always_include_dav_header
|
||||
add_dav_header
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# main entry point, called by the Handler
|
||||
def process
|
||||
if skip_authorization? || authenticate
|
||||
status = process_action || OK
|
||||
else
|
||||
status = HTTPStatus::Unauthorized
|
||||
end
|
||||
rescue HTTPStatus::Status => e
|
||||
status = e
|
||||
ensure
|
||||
if status
|
||||
response.status = status.code
|
||||
if status.code == 401
|
||||
response.body = authentication_error_message
|
||||
response['WWW-Authenticate'] = "Basic realm=\"#{authentication_realm}\""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
# delegates to the handler method matching this requests http method.
|
||||
# must return an HTTPStatus. If nil / false, the resulting status will be
|
||||
# 200/OK
|
||||
def process_action
|
||||
send request.request_method.downcase
|
||||
end
|
||||
|
||||
|
||||
# if true, resource#authenticate will never be called
|
||||
def skip_authorization?
|
||||
|
||||
# Microsoft Web Client workaround (from RedmineDmsf plugin):
|
||||
#
|
||||
# MS web client will not attempt any authentication (it'd seem)
|
||||
# until it's acknowledged a completed OPTIONS request.
|
||||
#
|
||||
# If the request method is OPTIONS return true, controller will simply
|
||||
# call the options method within, which accesses nothing, just returns
|
||||
# headers about the dav env.
|
||||
return @allow_unauthenticated_options_on_root &&
|
||||
request.request_method.downcase == 'options' &&
|
||||
(request.path_info == '/' || request.path_info.empty?)
|
||||
|
||||
end
|
||||
|
||||
|
||||
# Return response to OPTIONS
|
||||
def options
|
||||
status = resource.options request, response
|
||||
if(status == OK)
|
||||
add_dav_header
|
||||
response['Allow'] ||= 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK'
|
||||
response['Ms-Author-Via'] ||= 'DAV'
|
||||
end
|
||||
status
|
||||
end
|
||||
|
||||
# Return response to HEAD
|
||||
def head
|
||||
if(resource.exist?)
|
||||
response['Etag'] = resource.etag
|
||||
response['Content-Type'] = resource.content_type
|
||||
response['Content-Length'] = resource.content_length.to_s
|
||||
response['Last-Modified'] = resource.last_modified.httpdate
|
||||
resource.head(request, response)
|
||||
OK
|
||||
else
|
||||
NotFound
|
||||
end
|
||||
end
|
||||
|
||||
# Return response to GET
|
||||
def get
|
||||
if(resource.exist?)
|
||||
res = resource.get(request, response)
|
||||
if(res == OK && !resource.collection?)
|
||||
response['Etag'] ||= resource.etag
|
||||
response['Content-Type'] ||= resource.content_type
|
||||
response['Content-Length'] ||= resource.content_length.to_s
|
||||
response['Last-Modified'] ||= resource.last_modified.httpdate
|
||||
end
|
||||
res
|
||||
else
|
||||
NotFound
|
||||
end
|
||||
end
|
||||
|
||||
# Return response to PUT
|
||||
def put
|
||||
if request.get_header('HTTP_CONTENT_RANGE')
|
||||
# An origin server that allows PUT on a given target resource MUST send
|
||||
# a 400 (Bad Request) response to a PUT request that contains a
|
||||
# Content-Range header field.
|
||||
# Reference: http://tools.ietf.org/html/rfc7231#section-4.3.4
|
||||
Logger.error 'Content-Range on PUT requests is forbidden.'
|
||||
BadRequest
|
||||
elsif(resource.collection?)
|
||||
Forbidden
|
||||
elsif(!resource.parent_exists? || !resource.parent_collection?)
|
||||
Conflict
|
||||
else
|
||||
resource.lock_check if resource.supports_locking?
|
||||
status = resource.put(request, response)
|
||||
if status == Created
|
||||
response['Location'] ||= request.url_for resource.path
|
||||
end
|
||||
response.body = response['Location'] || ''
|
||||
status
|
||||
end
|
||||
end
|
||||
|
||||
# Return response to POST
|
||||
def post
|
||||
resource.post(request, response)
|
||||
end
|
||||
|
||||
# Return response to DELETE
|
||||
def delete
|
||||
if(resource.exist?)
|
||||
resource.lock_check if resource.supports_locking?
|
||||
resource.delete
|
||||
else
|
||||
NotFound
|
||||
end
|
||||
end
|
||||
|
||||
# Return response to MKCOL
|
||||
def mkcol
|
||||
if request.content_length.to_i > 0
|
||||
return UnsupportedMediaType
|
||||
end
|
||||
return MethodNotAllowed if resource.exist?
|
||||
|
||||
resource.lock_check if resource.supports_locking?
|
||||
status = resource.make_collection
|
||||
|
||||
if(resource.use_compat_mkcol_response?)
|
||||
url = request.url_for resource.path, collection: true
|
||||
r = XmlResponse.new(response, resource.namespaces)
|
||||
r.multistatus do |xml|
|
||||
xml << r.response(url, status)
|
||||
end
|
||||
MultiStatus
|
||||
else
|
||||
status
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Move Resource to new location.
|
||||
def move
|
||||
return NotFound unless(resource.exist?)
|
||||
return BadRequest unless request.depth == :infinity
|
||||
|
||||
return BadRequest unless dest = request.destination
|
||||
if status = dest.validate
|
||||
return status
|
||||
end
|
||||
|
||||
resource.lock_check if resource.supports_locking?
|
||||
|
||||
return resource.move dest.path_info, request.overwrite?
|
||||
end
|
||||
|
||||
|
||||
# Return response to COPY
|
||||
def copy
|
||||
return NotFound unless(resource.exist?)
|
||||
return BadRequest unless request.depth == :infinity or request.depth == 0
|
||||
|
||||
return BadRequest unless dest = request.destination
|
||||
if status = dest.validate(host: request.host,
|
||||
resource_path: resource.path)
|
||||
return status
|
||||
end
|
||||
|
||||
resource.copy dest.path_info, request.overwrite?, request.depth
|
||||
end
|
||||
|
||||
# Return response to PROPFIND
|
||||
def propfind
|
||||
return NotFound unless resource.exist?
|
||||
|
||||
ns = request.ns
|
||||
document = request.document if request.content_length.to_i > 0
|
||||
propfind = document.xpath("//#{ns}propfind") if document
|
||||
|
||||
# propname request
|
||||
if propfind and propfind.xpath("//#{ns}propname").first
|
||||
|
||||
r = XmlResponse.new(response, resource.namespaces)
|
||||
r.multistatus do |xml|
|
||||
xml << Ox::Raw.new(resource.propnames_xml_with_depth)
|
||||
end
|
||||
return MultiStatus
|
||||
end
|
||||
|
||||
|
||||
properties = if propfind.nil? or
|
||||
propfind.empty? or
|
||||
propfind.xpath("//#{ns}allprop").first
|
||||
|
||||
resource.properties
|
||||
|
||||
elsif prop = propfind.xpath("//#{ns}prop").first
|
||||
|
||||
prop.children
|
||||
.find_all{ |item| item.element? }
|
||||
.map{ |item|
|
||||
# We should do this, but Nokogiri transforms prefix w/ null href
|
||||
# into something valid. Oops.
|
||||
# TODO: Hacky grep fix that's horrible
|
||||
hsh = to_element_hash(item)
|
||||
if(hsh.namespace.nil? && !ns.empty?)
|
||||
return BadRequest if prop.to_s.scan(%r{<#{item.name}[^>]+xmlns=""}).empty?
|
||||
end
|
||||
hsh
|
||||
}.compact
|
||||
else
|
||||
Logger.error "no properties found"
|
||||
raise BadRequest
|
||||
end
|
||||
|
||||
properties = properties.map{|property| {element: property}}
|
||||
properties = resource.propfind_add_additional_properties(properties)
|
||||
|
||||
prop_xml = resource.properties_xml_with_depth(get: properties)
|
||||
|
||||
r = XmlResponse.new(response, resource.namespaces)
|
||||
r.multistatus do |xml|
|
||||
xml << r.raw(prop_xml)
|
||||
end
|
||||
MultiStatus
|
||||
end
|
||||
|
||||
|
||||
# Return response to PROPPATCH
|
||||
def proppatch
|
||||
return NotFound unless resource.exist?
|
||||
return BadRequest unless doc = request.document
|
||||
|
||||
resource.lock_check if resource.supports_locking?
|
||||
properties = {}
|
||||
doc.xpath("/#{request.ns}propertyupdate").children.each do |element|
|
||||
action = element.name
|
||||
if action == 'set' || action == 'remove'
|
||||
properties[action] ||= []
|
||||
if prp = element.children.detect{|e|e.name == 'prop'}
|
||||
prp.children.each do |elm|
|
||||
next if elm.name == 'text'
|
||||
properties[action] << { element: to_element_hash(elm), value: elm.text }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
r = XmlResponse.new(response, resource.namespaces)
|
||||
r.multistatus do |xml|
|
||||
xml << Ox::Raw.new(resource.properties_xml_with_depth(properties))
|
||||
end
|
||||
MultiStatus
|
||||
end
|
||||
|
||||
|
||||
# Lock current resource
|
||||
# NOTE: This will pass an argument hash to Resource#lock and
|
||||
# wait for a success/failure response.
|
||||
def lock
|
||||
depth = request.depth
|
||||
return BadRequest unless depth == 0 || depth == :infinity
|
||||
|
||||
asked = { depth: depth }
|
||||
|
||||
if timeout = request.env['Timeout']
|
||||
asked[:timeout] = timeout.split(',').map{|x|x.strip}
|
||||
end
|
||||
|
||||
ns = request.ns
|
||||
if doc = request.document and lockinfo = doc.xpath("//#{ns}lockinfo")
|
||||
|
||||
asked[:scope] = lockinfo.xpath("//#{ns}lockscope").children.find_all{|n|n.element?}.map{|n|n.name}.first
|
||||
asked[:type] = lockinfo.xpath("#{ns}locktype").children.find_all{|n|n.element?}.map{|n|n.name}.first
|
||||
asked[:owner] = lockinfo.xpath("//#{ns}owner/#{ns}href").children.map{|n|n.text}.first
|
||||
end
|
||||
|
||||
r = XmlResponse.new(response, resource.namespaces)
|
||||
begin
|
||||
|
||||
lock_time, locktoken = resource.lock(asked)
|
||||
|
||||
r.render_lockdiscovery(
|
||||
time: lock_time,
|
||||
token: locktoken,
|
||||
depth: asked[:depth].to_s,
|
||||
scope: asked[:scope],
|
||||
type: asked[:type],
|
||||
owner: asked[:owner]
|
||||
)
|
||||
|
||||
response.headers['Lock-Token'] = "<#{locktoken}>"
|
||||
return resource.exist? ? OK : Created
|
||||
|
||||
rescue LockFailure => e
|
||||
|
||||
r.multistatus do |xml|
|
||||
e.path_status.each_pair do |path, status|
|
||||
xml << r.response(path, status)
|
||||
end
|
||||
end
|
||||
|
||||
return MultiStatus
|
||||
end
|
||||
end
|
||||
|
||||
# Unlock current resource
|
||||
def unlock
|
||||
resource.unlock(request.lock_token)
|
||||
end
|
||||
|
||||
|
||||
|
||||
# Perform authentication
|
||||
#
|
||||
# implement your authentication by overriding Resource#authenticate
|
||||
def authenticate
|
||||
uname = nil
|
||||
password = nil
|
||||
if request.authorization?
|
||||
auth = Rack::Auth::Basic::Request.new(request.env)
|
||||
if(auth.basic? && auth.credentials)
|
||||
uname = auth.credentials[0]
|
||||
password = auth.credentials[1]
|
||||
end
|
||||
end
|
||||
resource.authenticate uname, password
|
||||
end
|
||||
|
||||
def authentication_error_message
|
||||
resource.authentication_error_message
|
||||
end
|
||||
|
||||
def authentication_realm
|
||||
resource.authentication_realm
|
||||
end
|
||||
|
||||
|
||||
# override this for custom resource initialization
|
||||
def setup_resource
|
||||
@resource = resource_class.new(
|
||||
request.unescaped_path_info, request, response, @options
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
# Class of the resource in use
|
||||
def resource_class
|
||||
unless @options[:resource_class]
|
||||
require 'dav4rack/resources/file_resource'
|
||||
@options[:resource_class] = FileResource
|
||||
@options[:root] ||= Dir.pwd
|
||||
end
|
||||
|
||||
@options[:resource_class]
|
||||
end
|
||||
|
||||
|
||||
def add_dav_header
|
||||
unless response['Dav']
|
||||
dav_support = ['1']
|
||||
if resource.supports_locking?
|
||||
# compliance is resource specific, only advertise 2 (locking) if
|
||||
# supported on the resource.
|
||||
dav_support << '2'
|
||||
end
|
||||
dav_support += @dav_extensions if @dav_extensions
|
||||
response['Dav'] = dav_support * ', '
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
39
lib/dav4rack/destination_header.rb
Normal file
39
lib/dav4rack/destination_header.rb
Normal file
@ -0,0 +1,39 @@
|
||||
require 'addressable/uri'
|
||||
|
||||
module DAV4Rack
|
||||
class DestinationHeader
|
||||
|
||||
attr_reader :host, :path, :path_info
|
||||
|
||||
def initialize(value, script_name: nil)
|
||||
@script_name = script_name.to_s
|
||||
@value = value.to_s.strip
|
||||
parse
|
||||
end
|
||||
|
||||
def parse
|
||||
uri = Addressable::URI.parse @value
|
||||
|
||||
@host = uri.host
|
||||
@path = Addressable::URI.unencode uri.path
|
||||
|
||||
if @script_name
|
||||
if @path =~ /\A(?<path>#{Regexp.escape @script_name}(?<path_info>\/.*))\z/
|
||||
@path_info = $~[:path_info]
|
||||
else
|
||||
raise ArgumentError, 'invalid destination header value'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# host must be the same, but path must differ
|
||||
def validate(host: nil, resource_path: nil)
|
||||
if host and self.host and self.host != host
|
||||
DAV4Rack::HTTPStatus::BadGateway
|
||||
elsif self.path == resource_path
|
||||
DAV4Rack::HTTPStatus::Forbidden
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
39
lib/dav4rack/file.rb
Normal file
39
lib/dav4rack/file.rb
Normal file
@ -0,0 +1,39 @@
|
||||
require 'time'
|
||||
require 'rack/utils'
|
||||
require 'rack/mime'
|
||||
|
||||
module DAV4Rack
|
||||
# DAV4Rack::File simply allows us to use Rack::File but with the
|
||||
# specific location we deem appropriate
|
||||
#
|
||||
# FIXME is that used anywhere?
|
||||
class File < Rack::File
|
||||
attr_accessor :path
|
||||
|
||||
alias :to_path :path
|
||||
|
||||
def initialize(path)
|
||||
@path = path
|
||||
end
|
||||
|
||||
def _call(env)
|
||||
begin
|
||||
if F.file?(@path) && F.readable?(@path)
|
||||
serving(env)
|
||||
else
|
||||
raise Errno::EPERM
|
||||
end
|
||||
rescue SystemCallError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
def not_found
|
||||
body = "File not found: #{Rack::Utils.unescape(env["PATH_INFO"])}\n"
|
||||
[404, {"Content-Type" => "text/plain",
|
||||
"Content-Length" => body.size.to_s,
|
||||
"X-Cascade" => "pass"},
|
||||
[body]]
|
||||
end
|
||||
end
|
||||
end
|
||||
162
lib/dav4rack/file_resource_lock.rb
Normal file
162
lib/dav4rack/file_resource_lock.rb
Normal file
@ -0,0 +1,162 @@
|
||||
require 'pstore'
|
||||
|
||||
module DAV4Rack
|
||||
class FileResourceLock
|
||||
attr_accessor :path
|
||||
attr_accessor :token
|
||||
attr_accessor :timeout
|
||||
attr_accessor :depth
|
||||
attr_accessor :user
|
||||
attr_accessor :scope
|
||||
attr_accessor :kind
|
||||
attr_accessor :owner
|
||||
attr_reader :created_at
|
||||
attr_reader :root
|
||||
|
||||
class << self
|
||||
def explicitly_locked?(path, croot=nil)
|
||||
store = init_pstore(croot)
|
||||
!!store.transaction(true){
|
||||
store[:paths][path]
|
||||
}
|
||||
end
|
||||
|
||||
def implicitly_locked?(path, croot=nil)
|
||||
store = init_pstore(croot)
|
||||
!!store.transaction(true){
|
||||
store[:paths].keys.detect do |check|
|
||||
check.start_with?(path)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
def explicit_locks(path, croot, args={})
|
||||
end
|
||||
|
||||
def implicit_locks(path, croot)
|
||||
end
|
||||
|
||||
def find_by_path(path, croot=nil)
|
||||
lock = self.class.new(:path => path, :root => croot)
|
||||
lock.token.nil? ? nil : lock
|
||||
end
|
||||
|
||||
def find_by_token(token, croot=nil)
|
||||
store = init_pstore(croot)
|
||||
struct = store.transaction(true){
|
||||
store[:tokens][token]
|
||||
}
|
||||
if(struct)
|
||||
new(path: struct[:path], root: croot)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def generate(path, user, token, croot)
|
||||
lock = self.new(:root => croot)
|
||||
lock.user = user
|
||||
lock.path = path
|
||||
lock.token = token
|
||||
lock.save
|
||||
lock
|
||||
end
|
||||
|
||||
def root=(path)
|
||||
@root = path
|
||||
end
|
||||
|
||||
def root
|
||||
@root || '/tmp/dav4rack'
|
||||
end
|
||||
|
||||
def init_pstore(croot)
|
||||
path = File.join(croot, '.attribs', 'locks.pstore')
|
||||
FileUtils.mkdir_p(File.dirname(path)) unless File.directory?(File.dirname(path))
|
||||
store = IS_18 ? PStore.new(path) : PStore.new(path, true)
|
||||
store.transaction do
|
||||
unless(store[:paths])
|
||||
store[:paths] = {}
|
||||
store[:tokens] = {}
|
||||
store.commit
|
||||
end
|
||||
end
|
||||
store
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(args={})
|
||||
@path = args[:path]
|
||||
@root = args[:root]
|
||||
@owner = args[:owner]
|
||||
@store = init_pstore(@root)
|
||||
@max_timeout = args[:max_timeout] || 86400
|
||||
@default_timeout = args[:max_timeout] || 60
|
||||
load_if_exists!
|
||||
@new_record = true if token.nil?
|
||||
end
|
||||
|
||||
def owner?(user)
|
||||
user == owner
|
||||
end
|
||||
|
||||
def reload
|
||||
load_if_exists
|
||||
self
|
||||
end
|
||||
|
||||
def remaining_timeout
|
||||
t = timeout.to_i - (Time.now.to_i - created_at.to_i)
|
||||
t < 0 ? 0 : t
|
||||
end
|
||||
|
||||
def save
|
||||
struct = {
|
||||
:path => path,
|
||||
:token => token,
|
||||
:timeout => timeout,
|
||||
:depth => depth,
|
||||
:created_at => Time.now,
|
||||
:owner => owner
|
||||
}
|
||||
@store.transaction do
|
||||
@store[:paths][path] = struct
|
||||
@store[:tokens][token] = struct
|
||||
@store.commit
|
||||
end
|
||||
@new_record = false
|
||||
self
|
||||
end
|
||||
|
||||
def destroy
|
||||
@store.transaction do
|
||||
@store[:paths].delete(path)
|
||||
@store[:tokens].delete(token)
|
||||
@store.commit
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_if_exists!
|
||||
struct = @store.transaction do
|
||||
@store[:paths][path]
|
||||
end
|
||||
if(struct)
|
||||
@path = struct[:path]
|
||||
@token = struct[:token]
|
||||
@timeout = struct[:timeout]
|
||||
@depth = struct[:depth]
|
||||
@created_at = struct[:created_at]
|
||||
@owner = struct[:owner]
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
def init_pstore(croot=nil)
|
||||
self.class.init_pstore(croot || @root)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
80
lib/dav4rack/handler.rb
Normal file
80
lib/dav4rack/handler.rb
Normal file
@ -0,0 +1,80 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'dav4rack/logger'
|
||||
|
||||
module DAV4Rack
|
||||
class Handler
|
||||
include DAV4Rack::HTTPStatus
|
||||
|
||||
# Options:
|
||||
#
|
||||
# - resource_class: your Resource implementation
|
||||
# - controller_class: custom Controller implementation (optional).
|
||||
# - root_uri_path: Path the handler is mapped to. Any resources
|
||||
# instantiated will only see the part of the path below this root.
|
||||
# - recursive_propfind_allowed (true) : set to false to disable
|
||||
# potentially expensive Depth: Infinity propfinds
|
||||
# - all options are passed on to your resource implementation and are
|
||||
# accessible there as @options.
|
||||
#
|
||||
def initialize(options={})
|
||||
@options = options.dup
|
||||
|
||||
Logger.set(*@options[:log_to])
|
||||
end
|
||||
|
||||
def call(env)
|
||||
start = Time.now
|
||||
request = setup_request env
|
||||
response = Rack::Response.new
|
||||
|
||||
Logger.info "Processing WebDAV request: #{request.path} (for #{request.ip} at #{Time.now}) [#{request.request_method}]"
|
||||
|
||||
controller = setup_controller request, response
|
||||
controller.process
|
||||
postprocess_response response
|
||||
|
||||
# Apache wants the body dealt with, so just read it and junk it
|
||||
buf = true
|
||||
buf = request.body.read(8192) while buf
|
||||
|
||||
if Logger.debug? and response.body.is_a?(String)
|
||||
Logger.debug "Response String:\n#{response.body}"
|
||||
end
|
||||
Logger.info "Completed in: #{((Time.now.to_f - start.to_f) * 1000).to_i} ms | #{response.status} [#{request.url}]"
|
||||
|
||||
response.finish
|
||||
|
||||
rescue Exception => e
|
||||
Logger.error "WebDAV Error: #{e}"
|
||||
raise e
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
|
||||
def postprocess_response(response)
|
||||
if response.body.is_a?(String)
|
||||
response['Content-Length'] ||= response.body.length.to_s
|
||||
end
|
||||
response.body = [response.body] unless response.body.respond_to?(:each)
|
||||
end
|
||||
|
||||
|
||||
def setup_request(env)
|
||||
::DAV4Rack::Request.new env, @options
|
||||
end
|
||||
|
||||
|
||||
def setup_controller(request, response)
|
||||
controller_class.new(request, response, @options)
|
||||
end
|
||||
|
||||
def controller_class
|
||||
@options[:controller_class] || ::DAV4Rack::Controller
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
100
lib/dav4rack/http_status.rb
Normal file
100
lib/dav4rack/http_status.rb
Normal file
@ -0,0 +1,100 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DAV4Rack
|
||||
|
||||
module HTTPStatus
|
||||
|
||||
class Status < Exception
|
||||
|
||||
class << self
|
||||
attr_accessor :code, :reason_phrase
|
||||
alias_method :to_i, :code
|
||||
|
||||
def status_line
|
||||
"#{code} #{reason_phrase}"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def code
|
||||
self.class.code
|
||||
end
|
||||
|
||||
def reason_phrase
|
||||
self.class.reason_phrase
|
||||
end
|
||||
|
||||
def status_line
|
||||
self.class.status_line
|
||||
end
|
||||
|
||||
def to_i
|
||||
self.class.to_i
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
StatusMessage = {
|
||||
100 => 'Continue',
|
||||
101 => 'Switching Protocols',
|
||||
102 => 'Processing',
|
||||
200 => 'OK',
|
||||
201 => 'Created',
|
||||
202 => 'Accepted',
|
||||
203 => 'Non-Authoritative Information',
|
||||
204 => 'No Content',
|
||||
205 => 'Reset Content',
|
||||
206 => 'Partial Content',
|
||||
207 => 'Multi-Status',
|
||||
300 => 'Multiple Choices',
|
||||
301 => 'Moved Permanently',
|
||||
302 => 'Found',
|
||||
303 => 'See Other',
|
||||
304 => 'Not Modified',
|
||||
305 => 'Use Proxy',
|
||||
307 => 'Temporary Redirect',
|
||||
400 => 'Bad Request',
|
||||
401 => 'Unauthorized',
|
||||
402 => 'Payment Required',
|
||||
403 => 'Forbidden',
|
||||
404 => 'Not Found',
|
||||
405 => 'Method Not Allowed',
|
||||
406 => 'Not Acceptable',
|
||||
407 => 'Proxy Authentication Required',
|
||||
408 => 'Request Timeout',
|
||||
409 => 'Conflict',
|
||||
410 => 'Gone',
|
||||
411 => 'Length Required',
|
||||
412 => 'Precondition Failed',
|
||||
413 => 'Request Entity Too Large',
|
||||
414 => 'Request-URI Too Large',
|
||||
415 => 'Unsupported Media Type',
|
||||
416 => 'Request Range Not Satisfiable',
|
||||
417 => 'Expectation Failed',
|
||||
422 => 'Unprocessable Entity',
|
||||
423 => 'Locked',
|
||||
424 => 'Failed Dependency',
|
||||
500 => 'Internal Server Error',
|
||||
501 => 'Not Implemented',
|
||||
502 => 'Bad Gateway',
|
||||
503 => 'Service Unavailable',
|
||||
504 => 'Gateway Timeout',
|
||||
505 => 'HTTP Version Not Supported',
|
||||
507 => 'Insufficient Storage'
|
||||
}
|
||||
|
||||
StatusClasses = { }
|
||||
|
||||
StatusMessage.each do |code, reason_phrase|
|
||||
klass = Class.new(Status)
|
||||
klass.code = code
|
||||
klass.reason_phrase = reason_phrase
|
||||
klass_name = reason_phrase.gsub(/[ \-]/,'')
|
||||
const_set(klass_name, klass)
|
||||
StatusClasses[code] = klass
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
22
lib/dav4rack/interceptor.rb
Normal file
22
lib/dav4rack/interceptor.rb
Normal file
@ -0,0 +1,22 @@
|
||||
require 'dav4rack/interceptor_resource'
|
||||
module DAV4Rack
|
||||
class Interceptor
|
||||
def initialize(app, args={})
|
||||
@roots = args[:mappings].keys
|
||||
@args = args
|
||||
@app = app
|
||||
@intercept_methods = %w(OPTIONS PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
|
||||
@intercept_methods -= args[:ignore_methods] if args[:ignore_methods]
|
||||
end
|
||||
|
||||
def call(env)
|
||||
path = env['PATH_INFO'].downcase
|
||||
method = env['REQUEST_METHOD'].upcase
|
||||
app = nil
|
||||
if(@roots.detect{|x| path =~ /^#{Regexp.escape(x.downcase)}\/?/}.nil? && @intercept_methods.include?(method))
|
||||
app = DAV4Rack::Handler.new(:resource_class => InterceptorResource, :mappings => @args[:mappings], :log_to => @args[:log_to])
|
||||
end
|
||||
app ? app.call(env) : @app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
119
lib/dav4rack/interceptor_resource.rb
Normal file
119
lib/dav4rack/interceptor_resource.rb
Normal file
@ -0,0 +1,119 @@
|
||||
require 'digest/sha1'
|
||||
|
||||
module DAV4Rack
|
||||
|
||||
class InterceptorResource < Resource
|
||||
attr_reader :path, :options
|
||||
|
||||
def initialize(*args)
|
||||
super
|
||||
@root_paths = @options[:mappings].keys
|
||||
@mappings = @options[:mappings]
|
||||
end
|
||||
|
||||
def children
|
||||
childs = @root_paths.find_all{|x|x =~ /^#{Regexp.escape(@path)}/}
|
||||
childs = childs.map{|a| child a.gsub(/^#{Regexp.escape(@path)}/, '').split('/').delete_if{|x|x.empty?}.first }.flatten
|
||||
end
|
||||
|
||||
def collection?
|
||||
true if exist?
|
||||
end
|
||||
|
||||
def exist?
|
||||
!@root_paths.find_all{|x| x =~ /^#{Regexp.escape(@path)}/}.empty?
|
||||
end
|
||||
|
||||
def creation_date
|
||||
Time.now
|
||||
end
|
||||
|
||||
def last_modified
|
||||
Time.now
|
||||
end
|
||||
|
||||
def last_modified=(time)
|
||||
Time.now
|
||||
end
|
||||
|
||||
def etag
|
||||
Digest::SHA1.hexdigest(@path)
|
||||
end
|
||||
|
||||
def content_type
|
||||
'text/html'
|
||||
end
|
||||
|
||||
def content_length
|
||||
0
|
||||
end
|
||||
|
||||
def get(request, response)
|
||||
raise Forbidden
|
||||
end
|
||||
|
||||
def put(request, response)
|
||||
raise Forbidden
|
||||
end
|
||||
|
||||
def post(request, response)
|
||||
raise Forbidden
|
||||
end
|
||||
|
||||
def delete
|
||||
raise Forbidden
|
||||
end
|
||||
|
||||
def copy(dest)
|
||||
raise Forbidden
|
||||
end
|
||||
|
||||
def move(dest)
|
||||
raise Forbidden
|
||||
end
|
||||
|
||||
def make_collection
|
||||
raise Forbidden
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
path == other.path
|
||||
end
|
||||
|
||||
def name
|
||||
::File.basename(path)
|
||||
end
|
||||
|
||||
def display_name
|
||||
::File.basename(path.to_s)
|
||||
end
|
||||
|
||||
def child(name, option={})
|
||||
new_path = path.dup
|
||||
new_path = '/' + new_path unless new_path[0,1] == '/'
|
||||
new_path.slice!(-1) if new_path[-1,1] == '/'
|
||||
name = '/' + name unless name[-1,1] == '/'
|
||||
new_path = "#{new_path}#{name}"
|
||||
new_public = public_path.dup
|
||||
new_public = '/' + new_public unless new_public[0,1] == '/'
|
||||
new_public.slice!(-1) if new_public[-1,1] == '/'
|
||||
new_public = "#{new_public}#{name}"
|
||||
if(key = @root_paths.find{|x| new_path =~ /^#{Regexp.escape(x.downcase)}\/?/})
|
||||
@mappings[key][:resource_class].new(new_public, new_path.gsub(key, ''), request, response, {:root_uri_path => key, :user => @user}.merge(options).merge(@mappings[key]))
|
||||
else
|
||||
self.class.new(new_public, new_path, request, response, {:user => @user}.merge(options))
|
||||
end
|
||||
end
|
||||
|
||||
def descendants
|
||||
list = []
|
||||
children.each do |child|
|
||||
list << child
|
||||
list.concat(child.descendants)
|
||||
end
|
||||
list
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
40
lib/dav4rack/lock.rb
Normal file
40
lib/dav4rack/lock.rb
Normal file
@ -0,0 +1,40 @@
|
||||
module DAV4Rack
|
||||
class Lock
|
||||
|
||||
def initialize(args={})
|
||||
@args = args
|
||||
@store = nil
|
||||
@args[:created_at] = Time.now
|
||||
@args[:updated_at] = Time.now
|
||||
end
|
||||
|
||||
def store
|
||||
@store
|
||||
end
|
||||
|
||||
def store=(s)
|
||||
raise TypeError.new 'Expecting LockStore' unless s.respond_to? :remove
|
||||
@store = s
|
||||
end
|
||||
|
||||
def destroy
|
||||
if(@store)
|
||||
@store.remove(self)
|
||||
end
|
||||
end
|
||||
|
||||
def remaining_timeout
|
||||
@args[:timeout].to_i - (Time.now.to_i - @args[:created_at].to_i)
|
||||
end
|
||||
|
||||
def method_missing(*args)
|
||||
if(@args.has_key?(args.first.to_sym))
|
||||
@args[args.first.to_sym]
|
||||
elsif(args.first.to_s[-1,1] == '=')
|
||||
@args[args.first.to_s[0, args.first.to_s.length - 1].to_sym] = args[1]
|
||||
else
|
||||
raise NoMethodError.new "Undefined method #{args.first} for #{self}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
61
lib/dav4rack/lock_store.rb
Normal file
61
lib/dav4rack/lock_store.rb
Normal file
@ -0,0 +1,61 @@
|
||||
require 'dav4rack/lock'
|
||||
module DAV4Rack
|
||||
class LockStore
|
||||
class << self
|
||||
def create
|
||||
@locks_by_path = {}
|
||||
@locks_by_token = {}
|
||||
end
|
||||
def add(lock)
|
||||
@locks_by_path[lock.path] = lock
|
||||
@locks_by_token[lock.token] = lock
|
||||
end
|
||||
|
||||
def remove(lock)
|
||||
@locks_by_path.delete(lock.path)
|
||||
@locks_by_token.delete(lock.token)
|
||||
end
|
||||
|
||||
def find_by_path(path)
|
||||
@locks_by_path.map do |lpath, lock|
|
||||
lpath == path && lock.remaining_timeout > 0 ? lock : nil
|
||||
end.compact.first
|
||||
end
|
||||
|
||||
def find_by_token(token)
|
||||
@locks_by_token.map do |ltoken, lock|
|
||||
ltoken == token && lock.remaining_timeout > 0 ? lock : nil
|
||||
end.compact.first
|
||||
end
|
||||
|
||||
def explicit_locks(path)
|
||||
@locks_by_path.map do |lpath, lock|
|
||||
lpath == path && lock.remaining_timeout > 0 ? lock : nil
|
||||
end.compact
|
||||
end
|
||||
|
||||
def implicit_locks(path)
|
||||
@locks_by_path.map do |lpath, lock|
|
||||
lpath =~ /^#{Regexp.escape(path)}/ && lock.remaining_timeout > 0 && lock.depth > 0 ? lock : nil
|
||||
end.compact
|
||||
end
|
||||
|
||||
def explicitly_locked?(path)
|
||||
self.explicit_locks(path).size > 0
|
||||
end
|
||||
|
||||
def implicitly_locked?(path)
|
||||
self.implicit_locks(path).size > 0
|
||||
end
|
||||
|
||||
def generate(path, user, token)
|
||||
l = Lock.new(:path => path, :user => user, :token => token)
|
||||
l.store = self
|
||||
add(l)
|
||||
l
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
DAV4Rack::LockStore.create
|
||||
30
lib/dav4rack/logger.rb
Normal file
30
lib/dav4rack/logger.rb
Normal file
@ -0,0 +1,30 @@
|
||||
require 'logger'
|
||||
|
||||
module DAV4Rack
|
||||
# This is a simple wrapper for the Logger class. It allows easy access
|
||||
# to log messages from the library.
|
||||
class Logger
|
||||
class << self
|
||||
# args:: Arguments for Logger -> [path, level] (level is optional) or a Logger instance
|
||||
# Set the path to the log file.
|
||||
def set(*args)
|
||||
if(%w(info debug warn fatal).all?{|meth| args.first.respond_to?(meth)})
|
||||
@@logger = args.first
|
||||
elsif(args.first.respond_to?(:to_s) && !args.first.to_s.empty?)
|
||||
@@logger = ::Logger.new(args.first.to_s, 'weekly')
|
||||
elsif(args.first)
|
||||
raise 'Invalid type specified for logger'
|
||||
end
|
||||
if(args.size > 1)
|
||||
@@logger.level = args[1]
|
||||
end
|
||||
end
|
||||
|
||||
def method_missing(*args)
|
||||
if(defined? @@logger)
|
||||
@@logger.send *args
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
149
lib/dav4rack/remote_file.rb
Normal file
149
lib/dav4rack/remote_file.rb
Normal file
@ -0,0 +1,149 @@
|
||||
require 'net/http'
|
||||
require 'uri'
|
||||
require 'digest/sha1'
|
||||
require 'rack/file'
|
||||
|
||||
module DAV4Rack
|
||||
|
||||
#FIXME unused?
|
||||
class RemoteFile < Rack::File
|
||||
|
||||
attr_accessor :path
|
||||
|
||||
alias :to_path :path
|
||||
|
||||
# path:: path to file (Actual path, preferably a URL since this is a *REMOTE* file)
|
||||
# args:: Hash of arguments:
|
||||
# :size -> Integer - number of bytes
|
||||
# :mime_type -> String - mime type
|
||||
# :last_modified -> String/Time - Time of last modification
|
||||
# :sendfile -> True or String to define sendfile header variation
|
||||
# :cache_directory -> Where to store cached files
|
||||
# :cache_ref -> Reference to be used for cache file name (useful for changing URLs like S3)
|
||||
# :sendfile_prefix -> String directory prefix. Eg: 'webdav' will result in: /wedav/#{path.sub('http://', '')}
|
||||
# :sendfile_fail_gracefully -> Boolean if true will simply proxy if unable to determine proper sendfile
|
||||
def initialize(path, args={})
|
||||
@path = path
|
||||
@args = args
|
||||
@heads = {}
|
||||
@cache_file = args[:cache_directory] ? cache_file_path : nil
|
||||
@redefine_prefix = nil
|
||||
if(@cache_file && File.exists?(@cache_file))
|
||||
@root = ''
|
||||
@path_info = @cache_file
|
||||
@path = @path_info
|
||||
elsif(args[:sendfile])
|
||||
@redefine_prefix = 'sendfile'
|
||||
@sendfile_header = args[:sendfile].is_a?(String) ? args[:sendfile] : nil
|
||||
else
|
||||
setup_remote
|
||||
end
|
||||
do_redefines(@redefine_prefix) if @redefine_prefix
|
||||
end
|
||||
|
||||
# env:: Environment variable hash
|
||||
# Process the call
|
||||
def call(env)
|
||||
serving(env)
|
||||
end
|
||||
|
||||
# env:: Environment variable hash
|
||||
# Return an empty result with the proper header information
|
||||
def sendfile_serving(env)
|
||||
header = @sendfile_header || env['sendfile.type'] || env['HTTP_X_SENDFILE_TYPE']
|
||||
unless(header)
|
||||
raise 'Failed to determine proper sendfile header value' unless @args[:sendfile_fail_gracefully]
|
||||
setup_remote
|
||||
do_redefines('remote')
|
||||
call(env)
|
||||
end
|
||||
prefix = (@args[:sendfile_prefix] || env['HTTP_X_ACCEL_REMOTE_MAPPING']).to_s.sub(/^\//, '').sub(/\/$/, '')
|
||||
[200, {
|
||||
"Last-Modified" => last_modified,
|
||||
"Content-Type" => content_type,
|
||||
"Content-Length" => size,
|
||||
"Redirect-URL" => @path,
|
||||
"Redirect-Host" => @path.scan(%r{^https?://([^/\?]+)}).first.first,
|
||||
header => "/#{prefix}"
|
||||
},
|
||||
['']]
|
||||
end
|
||||
|
||||
# env:: Environment variable hash
|
||||
# Return self to be processed
|
||||
def remote_serving(e)
|
||||
[200, {
|
||||
"Last-Modified" => last_modified,
|
||||
"Content-Type" => content_type,
|
||||
"Content-Length" => size
|
||||
}, self]
|
||||
end
|
||||
|
||||
# Get the remote file
|
||||
def remote_each
|
||||
if(@store)
|
||||
yield @store
|
||||
else
|
||||
@con.request_get(@call_path) do |res|
|
||||
res.read_body(@store) do |part|
|
||||
@cf.write part if @cf
|
||||
yield part
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Size based on remote headers or given size
|
||||
def size
|
||||
@heads['content-length'] || @size.to_s
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Content type based on provided or remote headers
|
||||
def content_type
|
||||
@mime_type || @heads['content-type']
|
||||
end
|
||||
|
||||
# Last modified type based on provided, remote headers or current time
|
||||
def last_modified
|
||||
@heads['last-modified'] || @modified || Time.now.httpdate
|
||||
end
|
||||
|
||||
# Builds the path for the cached file
|
||||
def cache_file_path
|
||||
raise IOError.new 'Write permission is required for cache directory' unless File.writable?(@args[:cache_directory])
|
||||
"#{@args[:cache_directory]}/#{Digest::SHA1.hexdigest((@args[:cache_ref] || @path).to_s + size.to_s + last_modified.to_s)}.cache"
|
||||
end
|
||||
|
||||
# prefix:: prefix of methods to be redefined
|
||||
# Redefine methods to do what we want in the proper situation
|
||||
def do_redefines(prefix)
|
||||
self.public_methods.each do |method|
|
||||
m = method.to_s.dup
|
||||
next unless m.slice!(0, prefix.to_s.length + 1) == "#{prefix}_"
|
||||
self.class.class_eval "undef :'#{m}'"
|
||||
self.class.class_eval "alias :'#{m}' :'#{method}'"
|
||||
end
|
||||
end
|
||||
|
||||
# Sets up all the requirements for proxying a remote file
|
||||
def setup_remote
|
||||
if(@cache_file)
|
||||
begin
|
||||
@cf = File.open(@cache_file, 'w+')
|
||||
rescue
|
||||
@cf = nil
|
||||
end
|
||||
end
|
||||
@uri = URI.parse(@path)
|
||||
@con = Net::HTTP.new(@uri.host, @uri.port)
|
||||
@call_path = @uri.path + (@uri.query ? "?#{@uri.query}" : '')
|
||||
res = @con.request_get(@call_path)
|
||||
@heads = res.to_hash
|
||||
res.value
|
||||
@store = nil
|
||||
@redefine_prefix = 'remote'
|
||||
end
|
||||
end
|
||||
end
|
||||
191
lib/dav4rack/request.rb
Normal file
191
lib/dav4rack/request.rb
Normal file
@ -0,0 +1,191 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'uri'
|
||||
require 'addressable/uri'
|
||||
require 'dav4rack/logger'
|
||||
|
||||
module DAV4Rack
|
||||
class Request < Rack::Request
|
||||
|
||||
# Root URI path for the resource
|
||||
attr_reader :root_uri_path
|
||||
|
||||
# options:
|
||||
#
|
||||
# recursive_propfind_allowed (true) : set to false to disable
|
||||
# potentially expensive recursive propfinds
|
||||
#
|
||||
def initialize(env, options = {})
|
||||
super env
|
||||
@options = { recursive_propfind_allowed: true }.merge options
|
||||
sanitize_path_info
|
||||
end
|
||||
|
||||
|
||||
def authorization?
|
||||
!!env['HTTP_AUTHORIZATION']
|
||||
end
|
||||
|
||||
# path relative to root uri
|
||||
def unescaped_path_info
|
||||
@unescaped_path_info ||= self.class.unescape_path path_info
|
||||
end
|
||||
|
||||
# the full path (script_name aka rack mount point + path_info)
|
||||
def unescaped_path
|
||||
@unescaped_path ||= self.class.unescape_path path
|
||||
end
|
||||
|
||||
def self.unescape_path(p)
|
||||
p = p.dup
|
||||
p.force_encoding 'UTF-8'
|
||||
Addressable::URI.unencode p
|
||||
end
|
||||
|
||||
|
||||
# Namespace being used within XML document
|
||||
def ns(wanted_uri = XmlElements::DAV_NAMESPACE)
|
||||
if document and
|
||||
root = document.root and
|
||||
ns_defs = root.namespace_definitions and
|
||||
ns_defs.size > 0
|
||||
|
||||
result = ns_defs.detect{ |nd| nd.href == wanted_uri } || ns_defs.first
|
||||
result = result.prefix.nil? ? 'xmlns' : result.prefix.to_s
|
||||
result += ':' unless result.empty?
|
||||
result
|
||||
else
|
||||
''
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
# Lock token if provided by client
|
||||
def lock_token
|
||||
get_header 'HTTP_LOCK_TOKEN'
|
||||
end
|
||||
|
||||
|
||||
# Requested depth
|
||||
def depth
|
||||
@http_depth ||= begin
|
||||
if d = get_header('HTTP_DEPTH') and (d == '0' or d == '1')
|
||||
d.to_i
|
||||
elsif infinity_depth_allowed?
|
||||
:infinity
|
||||
else
|
||||
1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Destination header
|
||||
def destination
|
||||
@destination ||= if h = get_header('HTTP_DESTINATION')
|
||||
DestinationHeader.new h, script_name: script_name
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Overwrite is allowed
|
||||
def overwrite?
|
||||
get_header('HTTP_OVERWRITE').to_s.upcase != 'F'
|
||||
end
|
||||
|
||||
# content_length as a Fixnum (nil if the header is unset / empty)
|
||||
def content_length
|
||||
if length = (super || get_header('HTTP_X_EXPECTED_ENTITY_LENGTH'))
|
||||
length.to_i
|
||||
end
|
||||
end
|
||||
|
||||
# parsed XML request body if any (Nokogiri XML doc)
|
||||
def document
|
||||
@request_document ||= parse_request_body if content_length && content_length > 0
|
||||
end
|
||||
|
||||
# builds a URL for path using this requests scheme, host, port and
|
||||
# script_name
|
||||
# path must be valid UTF-8 and will be url encoded by this method
|
||||
def url_for(rel_path, collection: false)
|
||||
path = path_for rel_path, collection: collection
|
||||
"#{scheme}://#{host}:#{port}#{path}"
|
||||
end
|
||||
|
||||
# returns an url encoded, absolute path for the given relative path
|
||||
def path_for(rel_path, collection: false)
|
||||
path = Addressable::URI.encode_component rel_path, Addressable::URI::CharacterClasses::PATH
|
||||
if collection and path[-1] != ?/
|
||||
path << '/'
|
||||
end
|
||||
"#{script_name}#{expand_path path}"
|
||||
end
|
||||
|
||||
# expands '/foo/../bar' to '/bar'
|
||||
def expand_path(path)
|
||||
path.squeeze! '/'
|
||||
path = Addressable::URI.normalize_component path, Addressable::URI::CharacterClasses::PATH
|
||||
URI("http://example.com/").merge(path).path
|
||||
end
|
||||
|
||||
|
||||
REDIRECTABLE_CLIENTS = [
|
||||
/cyberduck/i,
|
||||
/konqueror/i
|
||||
]
|
||||
|
||||
# Does client allow GET redirection
|
||||
# TODO: Get a comprehensive list in here.
|
||||
# TODO: Allow this to be dynamic so users can add regexes to match if they know of a client
|
||||
# that can be supported that is not listed.
|
||||
def client_allows_redirect?
|
||||
ua = self.user_agent
|
||||
REDIRECTABLE_CLIENTS.any? { |re| ua =~ re }
|
||||
end
|
||||
|
||||
|
||||
MS_CLIENTS = [
|
||||
/microsoft-webdav/i,
|
||||
/microsoft office/i
|
||||
]
|
||||
|
||||
# Basic user agent testing for MS authored client
|
||||
def is_ms_client?
|
||||
ua = self.user_agent
|
||||
MS_CLIENTS.any? { |re| ua =~ re }
|
||||
end
|
||||
|
||||
def get_header(name)
|
||||
@env[name]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# true if Depth: Infinity is allowed for this request.
|
||||
#
|
||||
# http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
|
||||
# Servers ... should support "infinity" requests. In practice,
|
||||
# support for infinite-depth requests may be disabled, due to the
|
||||
# performance and security concerns associated with this behavior
|
||||
def infinity_depth_allowed?
|
||||
request_method != 'PROPFIND' or @options[:recursive_propfind_allowed]
|
||||
end
|
||||
|
||||
def sanitize_path_info
|
||||
self.path_info.force_encoding 'UTF-8'
|
||||
self.path_info = expand_path path_info
|
||||
end
|
||||
|
||||
def parse_request_body
|
||||
return Nokogiri.XML(body.read){ |config|
|
||||
config.strict
|
||||
} if body
|
||||
rescue
|
||||
DAV4Rack::Logger.error $!.message
|
||||
raise ::DAV4Rack::HTTPStatus::BadRequest
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
657
lib/dav4rack/resource.rb
Normal file
657
lib/dav4rack/resource.rb
Normal file
@ -0,0 +1,657 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'uuidtools'
|
||||
require 'dav4rack/lock_store'
|
||||
require 'dav4rack/xml_elements'
|
||||
|
||||
module DAV4Rack
|
||||
|
||||
class LockFailure < RuntimeError
|
||||
attr_reader :path_status
|
||||
def initialize(*args)
|
||||
super(*args)
|
||||
@path_status = {}
|
||||
end
|
||||
|
||||
def add_failure(path, status)
|
||||
@path_status[path] = status
|
||||
end
|
||||
end
|
||||
|
||||
class Resource
|
||||
include DAV4Rack::Utils
|
||||
include DAV4Rack::XmlElements
|
||||
|
||||
attr_reader :path, :request,
|
||||
:response, :propstat_relative_path, :root_xml_attributes, :namespaces
|
||||
attr_accessor :user
|
||||
@@blocks = {}
|
||||
|
||||
class << self
|
||||
|
||||
# This lets us define a bunch of before and after blocks that are
|
||||
# either called before all methods on the resource, or only specific
|
||||
# methods on the resource
|
||||
def method_missing(*args, &block)
|
||||
class_sym = self.name.to_sym
|
||||
@@blocks[class_sym] ||= {:before => {}, :after => {}}
|
||||
m = args.shift
|
||||
parts = m.to_s.split('_')
|
||||
type = parts.shift.to_s.to_sym
|
||||
method = parts.empty? ? nil : parts.join('_').to_sym
|
||||
if(@@blocks[class_sym][type] && block_given?)
|
||||
if(method)
|
||||
@@blocks[class_sym][type][method] ||= []
|
||||
@@blocks[class_sym][type][method] << block
|
||||
else
|
||||
@@blocks[class_sym][type][:'__all__'] ||= []
|
||||
@@blocks[class_sym][type][:'__all__'] << block
|
||||
end
|
||||
else
|
||||
raise NoMethodError.new("Undefined method #{m} for class #{self}")
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
include DAV4Rack::HTTPStatus
|
||||
|
||||
# path:: Internal resource path (unescaped PATH_INFO)
|
||||
# request:: Rack::Request
|
||||
# options:: Any options provided for this resource
|
||||
# Creates a new instance of the resource.
|
||||
# NOTE: path and public_path will only differ if the root_uri has been set for the resource. The
|
||||
# controller will strip out the starting path so the resource can easily determine what
|
||||
# it is working on. For example:
|
||||
# request -> /my/webdav/directory/actual/path
|
||||
# public_path -> /my/webdav/directory/actual/path
|
||||
# path -> /actual/path
|
||||
# NOTE: Customized Resources should not use initialize for setup. Instead
|
||||
# use the #setup method
|
||||
def initialize(path, request, response, options)
|
||||
if path.nil? || path.empty? || path[0] != ?/
|
||||
raise ArgumentError, 'path must be present and start with a /'
|
||||
end
|
||||
@path = path
|
||||
|
||||
@propstat_relative_path = !!options[:propstat_relative_path]
|
||||
@root_xml_attributes = options.delete(:root_xml_attributes) || {}
|
||||
@namespaces = (options[:namespaces] || {}).merge({DAV_NAMESPACE => DAV_NAMESPACE_NAME})
|
||||
@request = request
|
||||
@response = response
|
||||
unless(options.has_key?(:lock_class))
|
||||
@lock_class = LockStore
|
||||
else
|
||||
@lock_class = options[:lock_class]
|
||||
raise NameError.new("Unknown lock type constant provided: #{@lock_class}") unless @lock_class.nil? || defined?(@lock_class)
|
||||
end
|
||||
@options = options
|
||||
@max_timeout = options[:max_timeout] || 86400
|
||||
@default_timeout = options[:default_timeout] || 60
|
||||
@user = @options[:user] || request.ip
|
||||
|
||||
setup
|
||||
end
|
||||
|
||||
# returns a new instance for the given path
|
||||
def new_for_path(path)
|
||||
self.class.new path, request, response,
|
||||
@options.merge(user: @user, namespaces: @namespaces)
|
||||
end
|
||||
|
||||
# override to implement custom authentication
|
||||
# should return true for successful authentication, false otherwise
|
||||
def authenticate(username, password)
|
||||
true
|
||||
end
|
||||
|
||||
def authentication_error_message
|
||||
'Not Authorized'
|
||||
end
|
||||
|
||||
def authentication_realm
|
||||
'Locked content'
|
||||
end
|
||||
|
||||
|
||||
# override in child classes for custom setup
|
||||
def setup
|
||||
end
|
||||
private :setup
|
||||
|
||||
|
||||
# Returns if resource supports locking
|
||||
def supports_locking?
|
||||
false #true
|
||||
end
|
||||
|
||||
# Returns supported lock types (an array of [lockscope, locktype] pairs)
|
||||
# i.e. [%w(D:exclusive D:write)]
|
||||
def supported_locks
|
||||
[]
|
||||
end
|
||||
|
||||
# If this is a collection, return the child resources.
|
||||
def children
|
||||
NotImplemented
|
||||
end
|
||||
|
||||
# Is this resource a collection?
|
||||
def collection?
|
||||
NotImplemented
|
||||
end
|
||||
|
||||
# Does this resource exist?
|
||||
def exist?
|
||||
NotImplemented
|
||||
end
|
||||
|
||||
# Does the parent resource exist?
|
||||
def parent_exists?
|
||||
parent.exist?
|
||||
end
|
||||
|
||||
# Is the parent resource a collection?
|
||||
def parent_collection?
|
||||
parent.collection?
|
||||
end
|
||||
|
||||
# Return the creation time.
|
||||
def creation_date
|
||||
raise NotImplemented
|
||||
end
|
||||
|
||||
# Return the time of last modification.
|
||||
def last_modified
|
||||
raise NotImplemented
|
||||
end
|
||||
|
||||
# Set the time of last modification.
|
||||
def last_modified=(time)
|
||||
# Is this correct?
|
||||
raise NotImplemented
|
||||
end
|
||||
|
||||
# Return an Etag, an unique hash value for this resource.
|
||||
def etag
|
||||
raise NotImplemented
|
||||
end
|
||||
|
||||
# Return the resource type. Generally only used to specify
|
||||
# resource is a collection.
|
||||
def resource_type
|
||||
:collection if collection?
|
||||
end
|
||||
|
||||
# Return the mime type of this resource.
|
||||
def content_type
|
||||
raise NotImplemented
|
||||
end
|
||||
|
||||
# Return the size in bytes for this resource.
|
||||
def content_length
|
||||
raise NotImplemented
|
||||
end
|
||||
|
||||
# HTTP OPTIONS request.
|
||||
# resources should override this to set the Allow header to indicate the
|
||||
# allowed methods. By default, all WebDAV methods are advertised on all
|
||||
# resources.
|
||||
def options(request, response)
|
||||
OK
|
||||
end
|
||||
|
||||
# HTTP GET request.
|
||||
#
|
||||
# Write the content of the resource to the response.body.
|
||||
def get(request, response)
|
||||
NotImplemented
|
||||
end
|
||||
|
||||
def head(request, response)
|
||||
#no-op, but called by the controller
|
||||
end
|
||||
|
||||
# HTTP PUT request.
|
||||
#
|
||||
# Save the content of the request.body.
|
||||
def put(request, response)
|
||||
NotImplemented
|
||||
end
|
||||
|
||||
# HTTP POST request.
|
||||
#
|
||||
# Usually forbidden.
|
||||
def post(request, response)
|
||||
NotImplemented
|
||||
end
|
||||
|
||||
# HTTP DELETE request.
|
||||
#
|
||||
# Delete this resource.
|
||||
def delete
|
||||
NotImplemented
|
||||
end
|
||||
|
||||
# HTTP COPY request.
|
||||
#
|
||||
# Copy this resource to given destination path.
|
||||
def copy(dest_path, overwrite = false, depth = nil)
|
||||
NotImplemented
|
||||
end
|
||||
|
||||
# HTTP MOVE request.
|
||||
#
|
||||
# Move this resource to given destination path.
|
||||
def move(dest_path, overwrite=false)
|
||||
NotImplemented
|
||||
end
|
||||
|
||||
# args:: Hash of lock arguments
|
||||
# Request for a lock on the given resource. A valid lock should lock
|
||||
# all descendents. Failures should be noted and returned as an exception
|
||||
# using LockFailure.
|
||||
# Valid args keys: :timeout -> requested timeout
|
||||
# :depth -> lock depth
|
||||
# :scope -> lock scope
|
||||
# :type -> lock type
|
||||
# :owner -> lock owner
|
||||
# Should return a tuple: [lock_time, locktoken] where lock_time is the
|
||||
# given timeout
|
||||
# NOTE: See section 9.10 of RFC 4918 for guidance about
|
||||
# how locks should be generated and the expected responses
|
||||
# (http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10)
|
||||
|
||||
def lock(args)
|
||||
raise NotImplemented unless @lock_class
|
||||
raise Conflict unless parent_exists?
|
||||
|
||||
lock_check(args[:scope])
|
||||
lock = @lock_class.explicit_locks(@path).find{|l| l.scope == args[:scope] && l.kind == args[:type] && l.user == @user}
|
||||
unless(lock)
|
||||
token = UUIDTools::UUID.random_create.to_s
|
||||
lock = @lock_class.generate(@path, @user, token)
|
||||
lock.scope = args[:scope]
|
||||
lock.kind = args[:type]
|
||||
lock.owner = args[:owner]
|
||||
lock.depth = args[:depth].is_a?(Symbol) ? args[:depth] : args[:depth].to_i
|
||||
if(args[:timeout])
|
||||
lock.timeout = args[:timeout] <= @max_timeout && args[:timeout] > 0 ? args[:timeout] : @max_timeout
|
||||
else
|
||||
lock.timeout = @default_timeout
|
||||
end
|
||||
lock.save if lock.respond_to? :save
|
||||
end
|
||||
begin
|
||||
lock_check(args[:type])
|
||||
rescue DAV4Rack::LockFailure => lock_failure
|
||||
lock.destroy
|
||||
raise lock_failure
|
||||
rescue HTTPStatus::Status => status
|
||||
status
|
||||
end
|
||||
[lock.remaining_timeout, lock.token]
|
||||
end
|
||||
|
||||
# lock_scope:: scope of lock
|
||||
# Check if resource is locked. Raise DAV4Rack::LockFailure if locks are in place.
|
||||
def lock_check(lock_scope=nil)
|
||||
return unless @lock_class
|
||||
if(@lock_class.explicitly_locked?(@path))
|
||||
raise Locked if @lock_class.explicit_locks(@path).find_all{|l|l.scope == 'exclusive' && l.user != @user}.size > 0
|
||||
elsif(@lock_class.implicitly_locked?(@path))
|
||||
if(lock_scope.to_s == 'exclusive')
|
||||
locks = @lock_class.implicit_locks(@path)
|
||||
failure = DAV4Rack::LockFailure.new("Failed to lock: #{@path}")
|
||||
locks.each do |lock|
|
||||
failure.add_failure(@path, Locked)
|
||||
end
|
||||
raise failure
|
||||
else
|
||||
locks = @lock_class.implict_locks(@path).find_all{|l| l.scope == 'exclusive' && l.user != @user}
|
||||
if(locks.size > 0)
|
||||
failure = LockFailure.new("Failed to lock: #{@path}")
|
||||
locks.each do |lock|
|
||||
failure.add_failure(@path, Locked)
|
||||
end
|
||||
raise failure
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# token:: Lock token
|
||||
# Remove the given lock
|
||||
def unlock(token)
|
||||
return NotImplemented unless @lock_class
|
||||
|
||||
token = token.slice(1, token.length - 2)
|
||||
if(token.nil? || token.empty?)
|
||||
BadRequest
|
||||
else
|
||||
lock = @lock_class.find_by_token(token)
|
||||
if(lock.nil? || lock.user != @user)
|
||||
Forbidden
|
||||
elsif(lock.path !~ /^#{Regexp.escape(@path)}.*$/)
|
||||
Conflict
|
||||
else
|
||||
lock.destroy
|
||||
NoContent
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Create this resource as collection.
|
||||
def make_collection
|
||||
NotImplemented
|
||||
end
|
||||
|
||||
# other:: Resource
|
||||
# Returns if current resource is equal to other resource
|
||||
def ==(other)
|
||||
path == other.path
|
||||
end
|
||||
|
||||
# Name of the resource
|
||||
def name
|
||||
File.basename(path)
|
||||
end
|
||||
|
||||
# Name of the resource to be displayed to the client
|
||||
def display_name
|
||||
name
|
||||
end
|
||||
|
||||
|
||||
# Available properties
|
||||
#
|
||||
# These are returned by PROPFIND without body, or with an allprop body.
|
||||
DAV_PROPERTIES = %w(
|
||||
getetag
|
||||
resourcetype
|
||||
getcontenttype
|
||||
getcontentlength
|
||||
getlastmodified
|
||||
creationdate
|
||||
displayname
|
||||
).map{|prop| { name: prop, ns_href: DAV_NAMESPACE } }.freeze
|
||||
|
||||
def properties
|
||||
props = DAV_PROPERTIES
|
||||
if supports_locking?
|
||||
props = props.dup # do not attempt to modify the (frozen) constant
|
||||
props << { name: 'supportedlock', ns_href: DAV_NAMESPACE }
|
||||
end
|
||||
props
|
||||
end
|
||||
|
||||
# Properties to be returned for <propname/> PROPFIND
|
||||
#
|
||||
# this should include the names of all properties defined on the resource
|
||||
def propname_properties
|
||||
props = self.properties
|
||||
if supports_locking?
|
||||
props = props.dup if props.frozen?
|
||||
props << { name: 'lockdiscovery', ns_href: DAV_NAMESPACE }
|
||||
end
|
||||
props
|
||||
end
|
||||
|
||||
# name:: String - Property name
|
||||
# Returns the value of the given property
|
||||
def get_property(element)
|
||||
return NotFound if (element[:ns_href] != DAV_NAMESPACE)
|
||||
case element[:name]
|
||||
when 'resourcetype' then resource_type
|
||||
when 'displayname' then display_name
|
||||
when 'creationdate' then use_ms_compat_creationdate? ? creation_date.httpdate : creation_date.xmlschema
|
||||
when 'getcontentlength' then content_length.to_s
|
||||
when 'getcontenttype' then content_type
|
||||
when 'getetag' then etag
|
||||
when 'getlastmodified' then last_modified.httpdate
|
||||
when 'supportedlock' then supported_locks_xml
|
||||
when 'lockdiscovery' then lockdiscovery_xml
|
||||
else NotImplemented
|
||||
end
|
||||
end
|
||||
|
||||
# name:: String - Property name
|
||||
# value:: New value
|
||||
# Set the property to the given value
|
||||
#
|
||||
# This default implementation does not allow any properties to be changed.
|
||||
def set_property(element, value)
|
||||
return Forbidden
|
||||
end
|
||||
|
||||
# name:: Property name
|
||||
# Remove the property from the resource
|
||||
def remove_property(element)
|
||||
Forbidden
|
||||
end
|
||||
|
||||
# name:: Name of child
|
||||
# Create a new child with the given name
|
||||
# NOTE:: Include trailing '/' if child is collection
|
||||
def child(name)
|
||||
new_path = @path.dup
|
||||
new_path << ?/ unless new_path[-1] == ?/
|
||||
new_path << name
|
||||
new_for_path new_path
|
||||
end
|
||||
|
||||
# Return parent of this resource
|
||||
def parent
|
||||
return nil if @path == '/'
|
||||
unless @path.to_s.empty?
|
||||
new_for_path File.split(@path).first
|
||||
end
|
||||
end
|
||||
|
||||
# Return list of descendants
|
||||
def descendants
|
||||
list = []
|
||||
children.each do |child|
|
||||
list << child
|
||||
list.concat(child.descendants)
|
||||
end
|
||||
list
|
||||
end
|
||||
|
||||
# Index page template for GETs on collection
|
||||
def index_page
|
||||
'<html><head> <title>%s</title>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" /></head>
|
||||
<body> <h1>%s</h1> <hr /> <table> <tr> <th class="name">Name</th>
|
||||
<th class="size">Size</th> <th class="type">Type</th>
|
||||
<th class="mtime">Last Modified</th> </tr> %s </table> <hr /> </body></html>'
|
||||
end
|
||||
|
||||
def properties_xml_with_depth(process_properties, depth = request.depth)
|
||||
xml_with_depth self, depth do |element, ox_doc|
|
||||
ox_doc << element.properties_xml(process_properties)
|
||||
end
|
||||
end
|
||||
|
||||
def propnames_xml_with_depth(depth = request.depth)
|
||||
xml_with_depth self, depth do |element, ox_doc|
|
||||
ox_doc << element.propnames_xml
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a complete URL for this resource.
|
||||
# If the propstat_relative_path option is set, just an absolute path will
|
||||
# be returned.
|
||||
# If this is a collection, the result will end with a '/'
|
||||
def href
|
||||
@href ||= build_href(path, collection: self.collection?)
|
||||
end
|
||||
|
||||
# Returns a complete URL for the given path.
|
||||
#
|
||||
# If the propstat_relative_path option is set, just an absolute path will
|
||||
# be returned. If the :collection argument is true, the returned path will
|
||||
# end with a '/'
|
||||
def build_href(path, collection: false)
|
||||
if propstat_relative_path
|
||||
request.path_for path, collection: collection
|
||||
else
|
||||
request.url_for path, collection: collection
|
||||
end
|
||||
end
|
||||
|
||||
def propnames_xml
|
||||
response = Ox::Element.new(D_RESPONSE)
|
||||
response << ox_element(D_HREF, href)
|
||||
propstats response, { OK => Hash[propname_properties.map{|p| [p,nil]}] }
|
||||
response
|
||||
end
|
||||
|
||||
def properties_xml(process_properties)
|
||||
response = Ox::Element.new(D_RESPONSE)
|
||||
response << ox_element(D_HREF, href)
|
||||
|
||||
process_properties.each do |type, properties|
|
||||
propstats(response, self.send("#{type}_properties_with_status",properties))
|
||||
end
|
||||
response
|
||||
end
|
||||
|
||||
def supported_locks_xml
|
||||
supported_locks.map do |scope, type|
|
||||
ox_lockentry scope, type
|
||||
end
|
||||
end
|
||||
|
||||
# array of lock info hashes
|
||||
# required keys are :time, :token, :depth
|
||||
# other valid keys are :scope, :type, :root and :owner
|
||||
def lockdiscovery
|
||||
[]
|
||||
end
|
||||
|
||||
# returns an array of activelock ox elements
|
||||
def lockdiscovery_xml
|
||||
if supports_locking?
|
||||
lockdiscovery.map do |lock|
|
||||
ox_activelock(**lock)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_properties_with_status(properties)
|
||||
stats = Hash.new { |h, k| h[k] = [] }
|
||||
properties.each do |property|
|
||||
val = self.get_property(property[:element])
|
||||
if val.is_a?(Class)
|
||||
stats[val] << property[:element]
|
||||
else
|
||||
stats[OK] << [property[:element], val]
|
||||
end
|
||||
end
|
||||
stats
|
||||
end
|
||||
|
||||
def set_properties_with_status(properties)
|
||||
stats = Hash.new { |h, k| h[k] = [] }
|
||||
properties.each do |property|
|
||||
val = self.set_property(property[:element], property[:value])
|
||||
if val.is_a?(Class)
|
||||
stats[val] << property[:element]
|
||||
else
|
||||
stats[OK] << [property[:element], val]
|
||||
end
|
||||
end
|
||||
stats
|
||||
end
|
||||
|
||||
|
||||
# resource:: Resource
|
||||
# elements:: Property hashes (name, namespace, children)
|
||||
# Removes the given properties from a resource
|
||||
def remove_properties_with_status(properties)
|
||||
stats = Hash.new { |h, k| h[k] = [] }
|
||||
properties.each do |property|
|
||||
val = self.remove_property(property[:element])
|
||||
if val.is_a?(Class)
|
||||
stats[val] << property[:element]
|
||||
else
|
||||
stats[OK] << [property[:element], val]
|
||||
end
|
||||
end
|
||||
stats
|
||||
end
|
||||
|
||||
|
||||
# adds the given xml namespace to namespaces and returns the prefix
|
||||
def add_namespace(ns, prefix = "unknown#{rand 65536}")
|
||||
return nil if ns.nil? || ns.empty?
|
||||
unless namespaces.key? ns
|
||||
namespaces[ns] = prefix
|
||||
return prefix
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# returns the prefix for the given namespace, adding it if necessary
|
||||
def prefix_for(ns_href)
|
||||
namespaces[ns_href] || add_namespace(ns_href)
|
||||
end
|
||||
|
||||
|
||||
# response:: parent Ox::Element
|
||||
# stats:: Array of stats
|
||||
# Build propstats response
|
||||
def propstats(response, stats)
|
||||
return if stats.empty?
|
||||
stats.each do |status, props|
|
||||
propstat = Ox::Element.new(D_PROPSTAT)
|
||||
prop = Ox::Element.new(D_PROP)
|
||||
|
||||
props.each do |element, value|
|
||||
|
||||
name = element[:name]
|
||||
if prefix = prefix_for(element[:ns_href])
|
||||
name = "#{prefix}:#{name}"
|
||||
end
|
||||
|
||||
prop_element = Ox::Element.new(name)
|
||||
ox_append prop_element, value, prefix: prefix
|
||||
prop << prop_element
|
||||
|
||||
end
|
||||
|
||||
propstat << prop
|
||||
propstat << ox_element(D_STATUS, "#{http_version} #{status.status_line}")
|
||||
|
||||
response << propstat
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def use_compat_mkcol_response?
|
||||
@options[:compat_mkcol] || @options[:compat_all]
|
||||
end
|
||||
|
||||
# Returns true if using an MS client
|
||||
def use_ms_compat_creationdate?
|
||||
if(@options[:compat_ms_mangled_creationdate] || @options[:compat_all])
|
||||
request.is_ms_client?
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Callback function that adds additional properties to the propfind REQUEST
|
||||
# These properties will then be parsed and processed as though they were sent
|
||||
# by the client. This makes sure we can add whatever property we want
|
||||
# to the response and make it look like the client asked for them.
|
||||
def propfind_add_additional_properties(properties)
|
||||
# Default implementation doesn't need to add anything
|
||||
properties
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
379
lib/dav4rack/resources/file_resource.rb
Normal file
379
lib/dav4rack/resources/file_resource.rb
Normal file
@ -0,0 +1,379 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'pstore'
|
||||
require 'webrick/httputils'
|
||||
require 'dav4rack/file_resource_lock'
|
||||
require 'dav4rack/security_utils'
|
||||
|
||||
module DAV4Rack
|
||||
|
||||
class FileResource < Resource
|
||||
|
||||
include WEBrick::HTTPUtils
|
||||
include DAV4Rack::Utils
|
||||
|
||||
# If this is a collection, return the child resources.
|
||||
def children
|
||||
Dir[file_path + '/*'].map do |path|
|
||||
child ::File.basename(path)
|
||||
end
|
||||
end
|
||||
|
||||
# Is this resource a collection?
|
||||
def collection?
|
||||
::File.directory?(file_path)
|
||||
end
|
||||
|
||||
# Does this recource exist?
|
||||
def exist?
|
||||
::File.exist?(file_path)
|
||||
end
|
||||
|
||||
# Return the creation time.
|
||||
def creation_date
|
||||
stat.ctime
|
||||
end
|
||||
|
||||
# Return the time of last modification.
|
||||
def last_modified
|
||||
stat.mtime
|
||||
end
|
||||
|
||||
# Set the time of last modification.
|
||||
def last_modified=(time)
|
||||
::File.utime(Time.now, time, file_path)
|
||||
end
|
||||
|
||||
# Return an Etag, an unique hash value for this resource.
|
||||
def etag
|
||||
sprintf('%x-%x-%x', stat.ino, stat.size, stat.mtime.to_i)
|
||||
end
|
||||
|
||||
# Return the mime type of this resource.
|
||||
def content_type
|
||||
if stat.directory?
|
||||
"text/html"
|
||||
else
|
||||
mime_type(file_path, DefaultMimeTypes)
|
||||
end
|
||||
end
|
||||
|
||||
# Return the size in bytes for this resource.
|
||||
def content_length
|
||||
stat.size
|
||||
end
|
||||
|
||||
# HTTP GET request.
|
||||
#
|
||||
# Write the content of the resource to the response.body.
|
||||
def get(request, response)
|
||||
return NotFound unless exist?
|
||||
if stat.directory?
|
||||
response.body = "".dup
|
||||
Rack::Directory.new(root).call(request.env)[2].each do |line|
|
||||
response.body << line
|
||||
end
|
||||
response['Content-Length'] = response.body.bytesize.to_s
|
||||
OK
|
||||
else
|
||||
status, headers, body = Rack::File.new(root).call(request.env)
|
||||
headers.each do |k, v|
|
||||
response[k] = v
|
||||
end
|
||||
response.body = body
|
||||
StatusClasses[status]
|
||||
end
|
||||
end
|
||||
|
||||
# HTTP PUT request.
|
||||
#
|
||||
# Save the content of the request.body.
|
||||
def put(request, response)
|
||||
write(request.body)
|
||||
Created
|
||||
end
|
||||
|
||||
# HTTP POST request.
|
||||
#
|
||||
# Usually forbidden.
|
||||
def post(request, response)
|
||||
raise HTTPStatus::Forbidden
|
||||
end
|
||||
|
||||
# HTTP DELETE request.
|
||||
#
|
||||
# Delete this resource.
|
||||
def delete
|
||||
if stat.directory?
|
||||
FileUtils.rm_rf(file_path)
|
||||
else
|
||||
::File.unlink(file_path)
|
||||
end
|
||||
::File.unlink(prop_path) if ::File.exist?(prop_path)
|
||||
@_prop_hash = nil
|
||||
NoContent
|
||||
end
|
||||
|
||||
# HTTP COPY request.
|
||||
#
|
||||
# Copy this resource to given destination path.
|
||||
def copy(dest_path, overwrite, depth = nil)
|
||||
|
||||
is_new = true
|
||||
|
||||
dest = new_for_path dest_path
|
||||
unless dest.parent.exist? and dest.parent.collection?
|
||||
return Conflict
|
||||
end
|
||||
|
||||
if dest.exist?
|
||||
if overwrite
|
||||
FileUtils.rm_r dest.file_path, secure: true
|
||||
else
|
||||
return PreconditionFailed
|
||||
end
|
||||
is_new = false
|
||||
end
|
||||
|
||||
if collection?
|
||||
|
||||
if request.depth == 0
|
||||
Dir.mkdir dest.file_path
|
||||
else
|
||||
FileUtils.cp_r(file_path, dest.file_path)
|
||||
end
|
||||
|
||||
else
|
||||
|
||||
FileUtils.cp(file_path, dest.file_path.sub(/\/$/, ''))
|
||||
FileUtils.cp(prop_path, dest.prop_path) if ::File.exist? prop_path
|
||||
|
||||
end
|
||||
|
||||
is_new ? Created : NoContent
|
||||
end
|
||||
|
||||
# HTTP MOVE request.
|
||||
#
|
||||
# Move this resource to given destination resource.
|
||||
def move(*args)
|
||||
result = copy(*args)
|
||||
delete if [Created, NoContent].include?(result)
|
||||
result
|
||||
end
|
||||
|
||||
# HTTP MKCOL request.
|
||||
#
|
||||
# Create this resource as collection.
|
||||
def make_collection
|
||||
if(request.body.read.to_s == '')
|
||||
if(::File.directory?(file_path))
|
||||
MethodNotAllowed
|
||||
else
|
||||
if(::File.directory?(::File.dirname(file_path)) && !::File.exist?(file_path))
|
||||
Dir.mkdir(file_path)
|
||||
Created
|
||||
else
|
||||
Conflict
|
||||
end
|
||||
end
|
||||
else
|
||||
UnsupportedMediaType
|
||||
end
|
||||
end
|
||||
|
||||
# Write to this resource from given IO.
|
||||
def write(io)
|
||||
tempfile = "#{file_path}.#{Process.pid}.#{object_id}"
|
||||
open(tempfile, "wb") do |file|
|
||||
while part = io.read(8192)
|
||||
file << part
|
||||
end
|
||||
end
|
||||
::File.rename(tempfile, file_path)
|
||||
ensure
|
||||
::File.unlink(tempfile) rescue nil
|
||||
end
|
||||
|
||||
# name:: String - Property name
|
||||
# Returns the value of the given property
|
||||
def get_property(name)
|
||||
if name[:ns_href] == DAV_NAMESPACE
|
||||
super
|
||||
else
|
||||
custom_props(name)
|
||||
end
|
||||
end
|
||||
|
||||
# name:: String - Property name
|
||||
# value:: New value
|
||||
# Set the property to the given value
|
||||
def set_property(name, value)
|
||||
# let Resource handle DAV properties
|
||||
if name[:ns_href] == DAV_NAMESPACE
|
||||
super
|
||||
else
|
||||
set_custom_props name, value
|
||||
end
|
||||
end
|
||||
|
||||
def remove_property(element)
|
||||
prop_hash.transaction do
|
||||
prop_hash.delete(to_element_key(element))
|
||||
prop_hash.commit
|
||||
end
|
||||
val = prop_hash.transaction{ prop_hash[to_element_key(element)] }
|
||||
end
|
||||
|
||||
def lock(args)
|
||||
unless(parent_exists?)
|
||||
Conflict
|
||||
else
|
||||
lock_check(args[:type])
|
||||
lock = FileResourceLock.explicit_locks(@path, root, :scope => args[:scope], :kind => args[:type], :user => @user)
|
||||
unless(lock)
|
||||
token = UUIDTools::UUID.random_create.to_s
|
||||
lock = FileResourceLock.generate(@path, @user, token, root)
|
||||
lock.scope = args[:scope]
|
||||
lock.kind = args[:type]
|
||||
lock.owner = args[:owner]
|
||||
lock.depth = args[:depth]
|
||||
if(args[:timeout])
|
||||
lock.timeout = args[:timeout] <= @max_timeout && args[:timeout] > 0 ? args[:timeout] : @max_timeout
|
||||
else
|
||||
lock.timeout = @default_timeout
|
||||
end
|
||||
lock.save
|
||||
end
|
||||
begin
|
||||
lock_check(args[:type])
|
||||
rescue DAV4Rack::LockFailure => lock_failure
|
||||
lock.destroy
|
||||
raise lock_failure
|
||||
rescue HTTPStatus::Status => status
|
||||
status
|
||||
end
|
||||
[lock.remaining_timeout, lock.token]
|
||||
end
|
||||
end
|
||||
|
||||
def unlock(token)
|
||||
token = token.slice(1, token.length - 2)
|
||||
if(token.nil? || token.empty?)
|
||||
BadRequest
|
||||
else
|
||||
lock = FileResourceLock.find_by_token(token, root)
|
||||
if(lock.nil? || lock.user != @user)
|
||||
Forbidden
|
||||
elsif(lock.path !~ /^#{Regexp.escape(@path)}.*$/)
|
||||
Conflict
|
||||
else
|
||||
lock.destroy
|
||||
NoContent
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def authenticate(user, pass)
|
||||
if(@options[:username])
|
||||
# This comparison uses & so that it doesn't short circuit and
|
||||
# uses `variable_size_secure_compare` so that length information
|
||||
# isn't leaked.
|
||||
SecurityUtils.variable_size_secure_compare(
|
||||
user, @options[:username]
|
||||
) &
|
||||
SecurityUtils.variable_size_secure_compare(
|
||||
pass, @options[:password]
|
||||
)
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def file_path
|
||||
::File.expand_path ::File.join(root, path)
|
||||
end
|
||||
|
||||
def prop_path
|
||||
path = ::File.join(store_directory, "#{::File.join(::File.dirname(file_path), ::File.basename(file_path)).gsub('/', '_')}.pstore")
|
||||
unless(::File.directory?(::File.dirname(path)))
|
||||
FileUtils.mkdir_p(::File.dirname(path))
|
||||
end
|
||||
path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def lock_check(lock_type=nil)
|
||||
if(FileResourceLock.explicitly_locked?(@path, root))
|
||||
raise Locked if lock_type && lock_type == 'exclusive'
|
||||
#raise Locked if FileResourceLock.explicit_locks(@path, root).find(:all, :conditions => ["scope = 'exclusive' AND user_id != ?", @user.id]).size > 0
|
||||
elsif(FileResourceLock.implicitly_locked?(@path, root))
|
||||
if(lock_type.to_s == 'exclusive')
|
||||
locks = FileResourceLock.implicit_locks(@path)
|
||||
failure = DAV4Rack::LockFailure.new("Failed to lock: #{@path}")
|
||||
locks.each do |lock|
|
||||
failure.add_failure(@path, Locked)
|
||||
end
|
||||
raise failure
|
||||
else
|
||||
locks = FileResourceLock.implict_locks(@path).find(:all, :conditions => ["scope = 'exclusive' AND user_id != ?", @user.id])
|
||||
if(locks.size > 0)
|
||||
failure = LockFailure.new("Failed to lock: #{@path}")
|
||||
locks.each do |lock|
|
||||
failure.add_failure(@path, Locked)
|
||||
end
|
||||
raise failure
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def set_custom_props(element, val)
|
||||
prop_hash.transaction do
|
||||
prop_hash[to_element_key(element)] = val
|
||||
prop_hash.commit
|
||||
end
|
||||
end
|
||||
|
||||
def custom_props(element)
|
||||
val = prop_hash.transaction(true) do
|
||||
prop_hash[to_element_key(element)]
|
||||
end
|
||||
val || NotFound
|
||||
end
|
||||
|
||||
def store_directory
|
||||
path = ::File.join(root, '.attrib_store')
|
||||
unless(::File.directory?(::File.dirname(path)))
|
||||
FileUtils.mkdir_p(::File.dirname(path))
|
||||
end
|
||||
path
|
||||
end
|
||||
|
||||
def lock_path
|
||||
path = ::File.join(store_directory, 'locks.pstore')
|
||||
unless(::File.directory?(::File.dirname(path)))
|
||||
FileUtils.mkdir_p(::File.dirname(path))
|
||||
end
|
||||
path
|
||||
end
|
||||
|
||||
def prop_hash
|
||||
@_prop_hash ||= IS_18 ? PStore.new(prop_path) : PStore.new(prop_path, true)
|
||||
end
|
||||
|
||||
def root
|
||||
@options[:root]
|
||||
end
|
||||
|
||||
def stat
|
||||
@stat ||= ::File.stat(file_path)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
268
lib/dav4rack/resources/mongo_resource.rb
Normal file
268
lib/dav4rack/resources/mongo_resource.rb
Normal file
@ -0,0 +1,268 @@
|
||||
# coding: utf-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'mime/types'
|
||||
require 'mongo'
|
||||
require 'erb'
|
||||
require 'dav4rack/security_utils'
|
||||
|
||||
module DAV4Rack
|
||||
|
||||
class MongoResource < DAV4Rack::Resource
|
||||
|
||||
def self.database=(db)
|
||||
@database = db
|
||||
end
|
||||
def self.database; @database end
|
||||
|
||||
|
||||
def setup
|
||||
@filesystem = Mongo::Grid::FSBucket.new(self.class.database)
|
||||
if @options[:bson]
|
||||
@bson = @options[:bson]
|
||||
elsif path.empty? || path == '/'
|
||||
@bson = {'filename' => '/'}
|
||||
else
|
||||
@bson = @filesystem.find(filename: /^#{Regexp.escape(path)}\/?$/).first rescue nil
|
||||
end
|
||||
end
|
||||
|
||||
def child(bson)
|
||||
our_options = @options.dup
|
||||
@options[:bson] = bson
|
||||
child = new_for_path bson['filename']
|
||||
@options = our_options
|
||||
child
|
||||
end
|
||||
|
||||
# If this is a collection, return the child resources.
|
||||
def children
|
||||
@filesystem.find(filename: /^#{Regexp.escape(@bson['filename'])}[^\/]+\/?$/).map do |bson|
|
||||
child bson
|
||||
end
|
||||
end
|
||||
|
||||
# Is this resource a collection?
|
||||
def collection?
|
||||
@bson && _collection?(@bson['filename'])
|
||||
end
|
||||
|
||||
# Does this recource exist?
|
||||
def exist?
|
||||
!!@bson
|
||||
end
|
||||
|
||||
# Return the creation time.
|
||||
def creation_date
|
||||
@bson['uploadDate'] || Date.new
|
||||
end
|
||||
|
||||
# Return the time of last modification.
|
||||
def last_modified
|
||||
@bson['uploadDate'] || Date.new
|
||||
end
|
||||
|
||||
# Set the time of last modification.
|
||||
def last_modified=(time)
|
||||
end
|
||||
|
||||
# Return an Etag, an unique hash value for this resource.
|
||||
def etag
|
||||
@bson['_id'].to_s
|
||||
end
|
||||
|
||||
# Return the mime type of this resource.
|
||||
def content_type
|
||||
@bson['contentType'] || "text/html"
|
||||
end
|
||||
|
||||
# Return the size in bytes for this resource.
|
||||
def content_length
|
||||
@bson['length'] || 0
|
||||
end
|
||||
|
||||
# HTTP GET request.
|
||||
#
|
||||
# Write the content of the resource to the response.body.
|
||||
def get(request, response)
|
||||
return NotFound unless exist?
|
||||
|
||||
if collection?
|
||||
response.body = "<html>".dup
|
||||
response.body << "<h2>" + ERB::Util.html_escape(path) + "</h2>"
|
||||
children.each do |child|
|
||||
name = ERB::Util.html_escape child.name
|
||||
|
||||
path = ERB::Util.html_escape request.path_for(child.path,
|
||||
collection: child.collection?)
|
||||
response.body << "<a href='" + path + "'>" + name + "</a>"
|
||||
response.body << "</br>"
|
||||
end
|
||||
response.body << "</html>"
|
||||
response['Content-Type'] = 'text/html'
|
||||
else
|
||||
@filesystem.open_download_stream_by_name(path, revision: -1) do |s|
|
||||
# not sure if the copying is necessary, but keeping the reference to
|
||||
# s outside the block seems unsafe as we dont know if/when that
|
||||
# stream will be closed
|
||||
#response.body = s
|
||||
response.body = s.read
|
||||
response['Content-Type'] = @bson['contentType']
|
||||
end
|
||||
end
|
||||
OK
|
||||
rescue Mongo::Error::InvalidFileRevision
|
||||
NotFound
|
||||
end
|
||||
|
||||
# HTTP PUT request.
|
||||
#
|
||||
# Save the content of the request.body.
|
||||
def put(request, response)
|
||||
exists = exist?
|
||||
|
||||
@filesystem.open_upload_stream(
|
||||
path, content_type: content_type_for(path)
|
||||
) { |f| f.write request.body }
|
||||
|
||||
exists ? NoContent : Created
|
||||
end
|
||||
|
||||
# HTTP POST request.
|
||||
#
|
||||
# Usually forbidden.
|
||||
def post(request, response)
|
||||
raise HTTPStatus::Forbidden
|
||||
end
|
||||
|
||||
# HTTP DELETE request.
|
||||
#
|
||||
# Delete this resource.
|
||||
def delete
|
||||
if collection?
|
||||
@filesystem.find(filename: /^#{Regexp.escape(@bson['filename'])}/).each do |bson|
|
||||
@filesystem.delete(bson['_id'])
|
||||
end
|
||||
else
|
||||
@filesystem.delete(@bson['_id'])
|
||||
end
|
||||
NoContent
|
||||
end
|
||||
|
||||
# HTTP COPY request.
|
||||
#
|
||||
# Copy this resource to given destination path.
|
||||
def copy(dest_path, overwrite = false, depth = :infinity)
|
||||
dest = new_for_path dest_path
|
||||
dest.collection! if collection?
|
||||
|
||||
src = @bson['filename']
|
||||
dst = dest.path
|
||||
exists = @filesystem.find(filename: /^#{Regexp.escape(dst)}/).any?
|
||||
|
||||
if overwrite
|
||||
@filesystem.find(filename: /^#{Regexp.escape(dst)}/).each do |bson|
|
||||
@filesystem.delete bson['_id']
|
||||
end
|
||||
elsif @filesystem.find(filename: /^#{Regexp.escape(dst)}/).any?
|
||||
return PreconditionFailed
|
||||
end
|
||||
|
||||
@filesystem.find(filename: /^#{Regexp.escape(src)}/).each do |bson|
|
||||
src_name = bson['filename']
|
||||
dst_name = dst + src_name.slice(src.length, src_name.length)
|
||||
|
||||
@filesystem.open_download_stream_by_name(src_name) do |i|
|
||||
@filesystem.open_upload_stream(dst_name,
|
||||
content_type: bson['contentType']) do |o|
|
||||
i.each{|chunk| o.write chunk }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
exists ? NoContent : Created
|
||||
end
|
||||
|
||||
# HTTP MOVE request.
|
||||
#
|
||||
# Move this resource to given destination path.
|
||||
def move(dest_path, overwrite = false)
|
||||
dest = new_for_path dest_path
|
||||
dest.collection! if collection?
|
||||
|
||||
src = @bson['filename']
|
||||
dst = dest.path
|
||||
exists = @filesystem.find(filename: /^#{Regexp.escape(dst)}/).any?
|
||||
|
||||
if overwrite
|
||||
@filesystem.find(filename: /^#{Regexp.escape(dst)}/).each do |bson|
|
||||
@filesystem.delete bson['_id']
|
||||
end
|
||||
elsif @filesystem.find(filename: /^#{Regexp.escape(dst)}/).any?
|
||||
return PreconditionFailed
|
||||
end
|
||||
|
||||
@filesystem.find(filename: /^#{Regexp.escape(src)}/).each do |bson|
|
||||
src_name = bson['filename']
|
||||
dst_name = dst + src_name.slice(src.length, src_name.length)
|
||||
|
||||
# http://mongoid.org/docs/persistence/atomic.html
|
||||
# http://rubydoc.info/github/mongoid/mongoid/master/Mongoid/Collection#update-instance_method
|
||||
mongo_collection.find_one_and_update({'_id' => bson['_id']}, {'$set' => {'filename' => dst_name}}, safe: true)
|
||||
|
||||
end
|
||||
|
||||
exists ? NoContent : Created
|
||||
end
|
||||
|
||||
# HTTP MKCOL request.
|
||||
#
|
||||
# Create this resource as collection.
|
||||
def make_collection
|
||||
|
||||
if @filesystem.find(filename: path).any?
|
||||
raise 'resource exists'
|
||||
end
|
||||
collection!
|
||||
@filesystem.open_upload_stream(path) { |f| }
|
||||
|
||||
Created
|
||||
end
|
||||
|
||||
def collection!
|
||||
path << '/' unless _collection?(path)
|
||||
end
|
||||
|
||||
def authenticate(user, pass)
|
||||
if(@options[:username])
|
||||
# This comparison uses & so that it doesn't short circuit and
|
||||
# uses `variable_size_secure_compare` so that length information
|
||||
# isn't leaked.
|
||||
SecurityUtils.variable_size_secure_compare(
|
||||
user, @options[:username]
|
||||
) &
|
||||
SecurityUtils.variable_size_secure_compare(
|
||||
pass, @options[:password]
|
||||
)
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mongo_collection
|
||||
@mongo_collection ||= self.class.database['fs.files']
|
||||
end
|
||||
|
||||
def content_type_for(filename)
|
||||
MIME::Types.type_for(filename).first.to_s || 'text/html'
|
||||
end
|
||||
|
||||
def _collection?(path)
|
||||
path && path.end_with?('/')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
24
lib/dav4rack/security_utils.rb
Normal file
24
lib/dav4rack/security_utils.rb
Normal file
@ -0,0 +1,24 @@
|
||||
require 'digest'
|
||||
|
||||
module DAV4Rack
|
||||
|
||||
# Implements secure string comparison methods.
|
||||
# Taken straight from ActiveSupport
|
||||
module SecurityUtils
|
||||
def secure_compare(a, b)
|
||||
return false unless a.bytesize == b.bytesize
|
||||
|
||||
l = a.unpack "C#{a.bytesize}"
|
||||
|
||||
res = 0
|
||||
b.each_byte { |byte| res |= byte ^ l.shift }
|
||||
res == 0
|
||||
end
|
||||
module_function :secure_compare
|
||||
|
||||
def variable_size_secure_compare(a, b)
|
||||
secure_compare(::Digest::SHA256.hexdigest(a), ::Digest::SHA256.hexdigest(b))
|
||||
end
|
||||
module_function :variable_size_secure_compare
|
||||
end
|
||||
end
|
||||
48
lib/dav4rack/utils.rb
Normal file
48
lib/dav4rack/utils.rb
Normal file
@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'ostruct'
|
||||
|
||||
module DAV4Rack
|
||||
|
||||
# Simple wrapper for formatted elements
|
||||
class DAVElement < OpenStruct
|
||||
def [](key)
|
||||
self.send(key)
|
||||
end
|
||||
end
|
||||
|
||||
module Utils
|
||||
DEFAULT_HTTP_VERSION = 'HTTP/1.1'
|
||||
|
||||
def to_element_hash(element)
|
||||
ns = element.namespace
|
||||
DAVElement.new(
|
||||
:namespace => ns,
|
||||
:name => element.name,
|
||||
:ns_href => (ns.href if ns),
|
||||
:children => element.children.collect{|e|
|
||||
to_element_hash(e) if e.element?
|
||||
}.compact,
|
||||
:attributes => attributes_hash(element)
|
||||
)
|
||||
end
|
||||
|
||||
def to_element_key(element)
|
||||
ns = element.namespace
|
||||
"#{ns.href if ns}!!#{element.name}"
|
||||
end
|
||||
|
||||
def http_version
|
||||
DEFAULT_HTTP_VERSION
|
||||
end
|
||||
|
||||
private
|
||||
def attributes_hash(node)
|
||||
node.attributes.inject({}) do |ret, (key,attr)|
|
||||
ret[attr.name] = attr.value
|
||||
ret
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
17
lib/dav4rack/version.rb
Normal file
17
lib/dav4rack/version.rb
Normal file
@ -0,0 +1,17 @@
|
||||
module DAV4Rack
|
||||
class Version
|
||||
|
||||
attr_reader :major, :minor, :tiny
|
||||
|
||||
def initialize(version)
|
||||
version = version.split('.')
|
||||
@major, @minor, @tiny = [version[0].to_i, version[1].to_i, version[2].to_i]
|
||||
end
|
||||
|
||||
def to_s
|
||||
"#{@major}.#{@minor}.#{@tiny}"
|
||||
end
|
||||
end
|
||||
|
||||
VERSION = Version.new('1.0.0')
|
||||
end
|
||||
129
lib/dav4rack/xml_elements.rb
Normal file
129
lib/dav4rack/xml_elements.rb
Normal file
@ -0,0 +1,129 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DAV4Rack
|
||||
module XmlElements
|
||||
|
||||
DAV_NAMESPACE = 'DAV:'
|
||||
DAV_NAMESPACE_NAME = 'd'
|
||||
DAV_XML_NS = 'xmlns:d'
|
||||
|
||||
XML_VERSION = '1.0'
|
||||
XML_CONTENT_TYPE = 'application/xml; charset=utf-8'
|
||||
|
||||
%w(
|
||||
activelock
|
||||
depth
|
||||
error
|
||||
href
|
||||
lockdiscovery
|
||||
lockentry
|
||||
lockroot
|
||||
lockscope
|
||||
locktoken
|
||||
lock-token-submitted
|
||||
locktype
|
||||
multistatus
|
||||
owner
|
||||
prop
|
||||
propstat
|
||||
response
|
||||
status
|
||||
timeout
|
||||
).each do |name|
|
||||
const_set "D_#{name.upcase.gsub('-', '_')}", "#{DAV_NAMESPACE_NAME}:#{name}"
|
||||
end
|
||||
|
||||
INFINITY = 'infinity'
|
||||
ZERO = '0'
|
||||
|
||||
def ox_element(name, content = nil)
|
||||
e = Ox::Element.new(name)
|
||||
if content
|
||||
e << content
|
||||
end
|
||||
e
|
||||
end
|
||||
|
||||
def ox_append(element, value, prefix: DAV_NAMESPACE_NAME)
|
||||
case value
|
||||
when Ox::Element
|
||||
element << value
|
||||
when Symbol
|
||||
element << Ox::Element.new("#{prefix}:#{value}")
|
||||
when Enumerable
|
||||
value.each{|v| ox_append element, v, prefix: prefix }
|
||||
else
|
||||
element << value.to_s if value
|
||||
end
|
||||
end
|
||||
|
||||
def ox_lockentry(scope, type)
|
||||
Ox::Element.new(D_LOCKENTRY).tap do |e|
|
||||
e << ox_element(D_LOCKSCOPE, Ox::Element.new(scope))
|
||||
e << ox_element(D_LOCKTYPE, Ox::Element.new(type))
|
||||
end
|
||||
end
|
||||
|
||||
def ox_response(path, status)
|
||||
Ox::Element.new(D_RESPONSE).tap do |e|
|
||||
# path = "#{scheme}://#{host}:#{port}#{URI.escape(path)}"
|
||||
e << ox_element(D_HREF, path)
|
||||
e << ox_element(D_STATUS, "#{http_version} #{status.status_line}")
|
||||
end
|
||||
end
|
||||
|
||||
# returns an activelock Ox::Element for the given lock data
|
||||
def ox_activelock(time: nil, token:, depth:,
|
||||
scope: nil, type: nil, owner: nil, root: nil)
|
||||
|
||||
Ox::Element.new(D_ACTIVELOCK).tap do |activelock|
|
||||
if scope
|
||||
activelock << ox_element(D_LOCKSCOPE, scope)
|
||||
end
|
||||
if type
|
||||
activelock << ox_element(D_LOCKTYPE, type)
|
||||
end
|
||||
activelock << ox_element(D_DEPTH, depth)
|
||||
activelock << ox_element(D_TIMEOUT,
|
||||
(time ? "Second-#{time}" : INFINITY))
|
||||
|
||||
token = ox_element(D_HREF, token)
|
||||
activelock << ox_element(D_LOCKTOKEN, token)
|
||||
|
||||
if owner
|
||||
activelock << ox_element(D_OWNER, owner)
|
||||
end
|
||||
|
||||
if root
|
||||
root = ox_element(D_HREF, root)
|
||||
activelock << ox_element(D_LOCKROOT, root)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# block is called for each element (at least self, depending on depth also
|
||||
# with children / further descendants)
|
||||
def xml_with_depth(resource, depth, &block)
|
||||
partial_document = Ox::Document.new()
|
||||
|
||||
yield resource, partial_document
|
||||
|
||||
case depth
|
||||
when 0
|
||||
when 1
|
||||
resource.children.each do |child|
|
||||
yield child, partial_document
|
||||
end
|
||||
else
|
||||
resource.descendants.each do |desc|
|
||||
yield desc, partial_document
|
||||
end
|
||||
end
|
||||
|
||||
Ox.dump(partial_document, {indent: -1})
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
end
|
||||
120
lib/dav4rack/xml_response.rb
Normal file
120
lib/dav4rack/xml_response.rb
Normal file
@ -0,0 +1,120 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DAV4Rack
|
||||
class XmlResponse
|
||||
include XmlElements
|
||||
|
||||
def initialize(response, namespaces, http_version: 'HTTP/1.1')
|
||||
@response = response
|
||||
@namespaces = namespaces
|
||||
@http_version = http_version
|
||||
end
|
||||
|
||||
def render_xml(xml_body)
|
||||
@namespaces.each do |href, prefix|
|
||||
xml_body["xmlns:#{prefix}"] = href
|
||||
end
|
||||
|
||||
xml_doc = Ox::Document.new(:version => '1.0')
|
||||
xml_doc << xml_body
|
||||
|
||||
@response.body = Ox.dump(xml_doc, {indent: -1, with_xml: true})
|
||||
|
||||
@response["Content-Type"] = 'application/xml; charset=utf-8'
|
||||
@response["Content-Length"] = @response.body.bytesize.to_s
|
||||
end
|
||||
|
||||
|
||||
def multistatus
|
||||
multistatus = Ox::Element.new(D_MULTISTATUS)
|
||||
yield multistatus
|
||||
render_xml multistatus
|
||||
end
|
||||
|
||||
|
||||
def render_failed_precondition(status, href)
|
||||
error = Ox::Element.new(D_ERROR)
|
||||
case status.code
|
||||
when 423
|
||||
l = Ox::Element.new(D_LOCK_TOKEN_SUBMITTED)
|
||||
l << ox_element(D_HREF, href)
|
||||
error << l
|
||||
end
|
||||
render_xml error
|
||||
end
|
||||
|
||||
|
||||
def render_lock_errors(errors)
|
||||
multistatus do |xml|
|
||||
errors.each do |href, status|
|
||||
r = response href, status
|
||||
if status.code == 423
|
||||
r << ox_element(D_ERROR, Ox::Element.new(D_LOCK_TOKEN_SUBMITTED))
|
||||
end
|
||||
xml << r
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def render_lockdiscovery(*args)
|
||||
render_xml ox_element(D_PROP,
|
||||
ox_element(D_LOCKDISCOVERY,
|
||||
activelock(*args))
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# helpers for creating single elements
|
||||
#
|
||||
|
||||
|
||||
# returns an activelock Ox::Element for the given lock data
|
||||
def activelock(time: nil, token:, depth:,
|
||||
scope: nil, type: nil, owner: nil, root: nil)
|
||||
|
||||
Ox::Element.new(D_ACTIVELOCK).tap do |activelock|
|
||||
if scope
|
||||
activelock << ox_element(D_LOCKSCOPE, scope)
|
||||
end
|
||||
if type
|
||||
activelock << ox_element(D_LOCKTYPE, type)
|
||||
end
|
||||
activelock << ox_element(D_DEPTH, depth)
|
||||
activelock << ox_element(D_TIMEOUT,
|
||||
(time ? "Second-#{time}" : INFINITY))
|
||||
|
||||
token = ox_element(D_HREF, token)
|
||||
activelock << ox_element(D_LOCKTOKEN, token)
|
||||
|
||||
if owner
|
||||
activelock << ox_element(D_OWNER, owner)
|
||||
end
|
||||
|
||||
if root
|
||||
root = ox_element(D_HREF, root)
|
||||
activelock << ox_element(D_LOCKROOT, root)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def response(href, status)
|
||||
r = Ox::Element.new(D_RESPONSE)
|
||||
r << ox_element(D_HREF, href)
|
||||
r << self.status(status)
|
||||
r
|
||||
end
|
||||
|
||||
def raw(xml)
|
||||
Ox::Raw.new xml
|
||||
end
|
||||
|
||||
def status(status)
|
||||
ox_element D_STATUS, "#{@http_version} #{status.status_line}"
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
end
|
||||
@ -41,6 +41,7 @@ if defined?(EasyExtensions)
|
||||
end
|
||||
|
||||
# Load up classes that make up our WebDAV solution ontop of DAV4Rack
|
||||
require 'dav4rack'
|
||||
require 'redmine_dmsf/webdav/custom_middleware'
|
||||
require 'redmine_dmsf/webdav/base_resource'
|
||||
require 'redmine_dmsf/webdav/dmsf_resource'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user