lib/net/dav.rb in net_dav-0.5.0 vs lib/net/dav.rb in net_dav-0.5.1
- old
+ new
@@ -1,25 +1,34 @@
require 'net/https'
require 'uri'
require 'nokogiri'
-require 'net/dav/item'
+require File.dirname(__FILE__) + "/dav/item"
require 'base64'
require 'digest/md5'
+require File.dirname(__FILE__) + "/dav/version"
+
begin
require 'curb'
rescue LoadError
end
module Net #:nodoc:
# Implement a WebDAV client
class DAV
MAX_REDIRECTS = 10
+
+ def last_status
+ @handler.last_status
+ end
+
class NetHttpHandler
attr_writer :user, :pass
attr_accessor :disable_basic_auth
+ attr_reader :last_status
+
def verify_callback=(callback)
@http.verify_callback = callback
end
def verify_server=(value)
@@ -60,41 +69,47 @@
def open_timeout=(sec)
@http.read_timeout = sec
end
def request_sending_stream(verb, path, stream, length, headers)
+ headers ||= {}
+ headers = {"User-Agent" => "Ruby"}.merge(headers)
req =
case verb
when :put
Net::HTTP::Put.new(path)
else
raise "unkown sending_stream verb #{verb}"
end
req.body_stream = stream
req.content_length = length
headers.each_pair { |key, value| req[key] = value } if headers
- req.content_type = 'text/xml; charset="utf-8"'
+ req.content_type = 'application/octet-stream'
res = handle_request(req, headers)
res
end
def request_sending_body(verb, path, body, headers)
+ headers ||= {}
+ headers = {"User-Agent" => "Ruby"}.merge(headers)
req =
case verb
when :put
Net::HTTP::Put.new(path)
else
raise "unkown sending_body verb #{verb}"
end
req.body = body
headers.each_pair { |key, value| req[key] = value } if headers
- req.content_type = 'text/xml; charset="utf-8"'
+ req.content_type = 'application/octet-stream'
res = handle_request(req, headers)
res
end
def request_returning_body(verb, path, headers, &block)
+ headers ||= {}
+ headers = {"User-Agent" => "Ruby"}.merge(headers)
req =
case verb
when :get
Net::HTTP::Get.new(path)
else
@@ -104,10 +119,12 @@
res = handle_request(req, headers, MAX_REDIRECTS, &block)
res.body
end
def request(verb, path, body, headers)
+ headers ||= {}
+ headers = {"User-Agent" => "Ruby"}.merge(headers)
req =
case verb
when :propfind
Net::HTTP::Propfind.new(path)
when :mkcol
@@ -118,10 +135,14 @@
Net::HTTP::Move.new(path)
when :copy
Net::HTTP::Copy.new(path)
when :proppatch
Net::HTTP::Proppatch.new(path)
+ when :lock
+ Net::HTTP::Lock.new(path)
+ when :unlock
+ Net::HTTP::Unlock.new(path)
else
raise "unkown verb #{verb}"
end
req.body = body
headers.each_pair { |key, value| req[key] = value } if headers
@@ -132,36 +153,45 @@
def handle_request(req, headers, limit = MAX_REDIRECTS, &block)
# You should choose better exception.
raise ArgumentError, 'HTTP redirect too deep' if limit == 0
+ case @authorization
+ when :basic
+ req.basic_auth @user, @pass
+ when :digest
+ digest_auth(req, @user, @pass, response)
+ end
+
response = nil
if block
@http.request(req) {|res|
# Only start returning a body if we will not retry
res.read_body nil, &block if !res.is_a?(Net::HTTPUnauthorized) && !res.is_a?(Net::HTTPRedirection)
response = res
}
else
response = @http.request(req)
end
+
+ @last_status = response.code.to_i
case response
when Net::HTTPSuccess then
return response
when Net::HTTPUnauthorized then
response.error! unless @user
response.error! if req['authorization']
new_req = clone_req(req.path, req, headers)
- if response['www-authenticate'] =~ /^Basic/
+ if response['www-authenticate'] =~ /^basic/i
if disable_basic_auth
raise "server requested basic auth, but that is disabled"
end
- new_req.basic_auth @user, @pass
+ @authorization = :basic
else
- digest_auth(new_req, @user, @pass, response)
+ @authorization = :digest
end
- return handle_request(new_req, headers, limit - 1, &block)
+ return handle_request(req, headers, limit - 1, &block)
when Net::HTTPRedirection then
location = URI.parse(response['location'])
if (@uri.scheme != location.scheme ||
@uri.host != location.host ||
@uri.port != location.port)
@@ -175,12 +205,17 @@
end
def clone_req(path, req, headers)
new_req = req.class.new(path)
new_req.body = req.body if req.body
- new_req.body_stream = req.body_stream if req.body_stream
+ if (req.body_stream)
+ req.body_stream.rewind
+ new_req.body_stream = req.body_stream
+ end
+ new_req.content_length = req.content_length if req.content_length
headers.each_pair { |key, value| new_req[key] = value } if headers
+ new_req.content_type = req.content_type if req.content_type
return new_req
end
CNONCE = Digest::MD5.hexdigest("%x" % (Time.now.to_i + rand(65535))).slice(0, 8)
@@ -216,10 +251,31 @@
header << "algorithm=\"MD5\""
header = header.join(', ')
request['Authorization'] = header
end
+
+ def cert_file(cert_file)
+ # expects a OpenSSL::X509::Certificate object as client certificate
+ @http.cert = OpenSSL::X509::Certificate.new(File.read(cert_file))
+ #puts @http.cert.not_after
+ #puts @http.cert.subject
+ end
+
+ def cert_key(cert_file, cert_file_password)
+ # expects a OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object
+ if cert_file_password then
+ @http.key = OpenSSL::PKey::RSA.new(File.read(cert_file),cert_file_password)
+ else
+ @http.key = OpenSSL::PKey::RSA.new(File.read(cert_file))
+ end
+ end
+
+ # path of a CA certification file in PEM format. The file can contain several CA certificates.
+ def ca_file(ca_file)
+ @http.ca_file = ca_file
+ end
end
class CurlHandler < NetHttpHandler
def verify_callback=(callback)
@@ -265,18 +321,38 @@
yield frag
frag.length
end
end
curl.perform
+
+ @last_status = curl.response_code
+
unless curl.response_code >= 200 && curl.response_code < 300
header_block = curl.header_str.split(/\r?\n\r?\n/)[-1]
msg = header_block.split(/\r?\n/)[0]
msg.gsub!(/^HTTP\/\d+.\d+ /, '')
raise Net::HTTPError.new(msg, nil)
end
curl.body_str
end
+
+ def cert_file(cert_file)
+ # expects a cert file
+ @curl.cert = cert_file
+ end
+
+ def cert_key(cert_file, cert_file_password)
+ if cert_file_password then
+ @curl.certpassword = cert_file_password
+ end
+ @curl.key = cert_key
+ end
+
+ def ca_file(ca_file)
+ # path of a cacert bundle for this instance. This file will be used to validate SSL certificates.
+ @curl.cacert = ca_file
+ end
end
# Disable basic auth - to protect passwords from going in the clear
# through a man-in-the-middle attack.
@@ -330,17 +406,20 @@
# The path part of the URI is used to handle relative URLs
# in subsequent requests.
# You can pass :curl => false if you want to disable use
# of the curb (libcurl) gem if present for acceleration
def initialize(uri, options = nil)
+ @last_status = 0
+
@have_curl = Curl rescue nil
if options && options.has_key?(:curl) && !options[:curl]
@have_curl = false
end
@uri = uri
@uri = URI.parse(@uri) if @uri.is_a? String
@handler = @have_curl ? CurlHandler.new(@uri) : NetHttpHandler.new(@uri)
+ @headers = options && options[:headers] ? options[:headers] : {}
end
# Opens the connection to the host. Yields self to the block.
#
# Example:
@@ -358,12 +437,37 @@
# Set credentials for basic authentication
def credentials(user, pass)
@handler.user = user
@handler.pass = pass
+
+ # Return something explicitly since this command might be run in a
+ # console where the last statement would be printed.
+ nil
end
+
+ # Set credentials for ssl certificate authentication
+ def ssl_certificate(cert_file, *cert_file_password)
+ @handler.cert_file(cert_file)
+ @handler.cert_key(cert_file, cert_file_password)
+
+ # Return something explicitly since this command might be run in a
+ # console where the last statement would be printed.
+ nil
+ end
+
+ # Set additional ssl authorities for ssl certificate authentication
+ def ssl_authority(ca_file)
+ @handler.ca_file(ca_file)
+ nil
+ end
+ # Set extra headers for the dav request
+ def headers(headers)
+ @headers = headers
+ end
+
# Perform a PROPFIND request
#
# Example:
#
# Basic propfind:
@@ -389,11 +493,11 @@
body = options[0]
end
if(!body)
body = '<?xml version="1.0" encoding="utf-8"?><DAV:propfind xmlns:DAV="DAV:"><DAV:allprop/></DAV:propfind>'
end
- res = @handler.request(:propfind, path, body, headers)
+ res = @handler.request(:propfind, path, body, headers.merge(@headers))
Nokogiri::XML.parse(res.body)
end
# Find files and directories, yields Net::DAV::Item
#
@@ -438,11 +542,11 @@
path.sub!(/\/$/, '')
doc./('.//x:response', namespaces).each do |item|
uri = @uri.merge(item.xpath("x:href", namespaces).inner_text)
size = item.%(".//x:getcontentlength", namespaces).inner_text rescue nil
type = item.%(".//x:collection", namespaces) ? :directory : :file
- res = Item.new(self, uri, type, size)
+ res = Item.new(self, uri, type, size, item)
if type == :file then
if(options[:filename])then
search_term = options[:filename]
filename = File.basename(uri.path)
@@ -491,11 +595,11 @@
# the socket. Note that in this case, the returned response
# object will *not* contain a (meaningful) body.
def get(path, &block)
path = @uri.merge(path).path
- body = @handler.request_returning_body(:get, path, nil, &block)
+ body = @handler.request_returning_body(:get, path, @headers, &block)
body
end
# Stores the content of a stream to a URL
#
@@ -503,32 +607,32 @@
# File.open(file, "r") do |stream|
# dav.put(url.path, stream, File.size(file))
# end
def put(path, stream, length)
path = @uri.merge(path).path
- res = @handler.request_sending_stream(:put, path, stream, length, nil)
+ res = @handler.request_sending_stream(:put, path, stream, length, @headers)
res.body
end
# Stores the content of a string to a URL
#
# Example:
# dav.put(url.path, "hello world")
#
def put_string(path, str)
path = @uri.merge(path).path
- res = @handler.request_sending_body(:put, path, str, nil)
+ res = @handler.request_sending_body(:put, path, str, @headers)
res.body
end
# Delete request
#
# Example:
# dav.delete(uri.path)
def delete(path)
path = @uri.merge(path).path
- res = @handler.request(:delete, path, nil, nil)
+ res = @handler.request(:delete, path, nil, @headers)
res.body
end
# Send a move request to the server.
#
@@ -536,11 +640,11 @@
# dav.move(original_path, new_path)
def move(path,destination)
path = @uri.merge(path).path
destination = @uri.merge(destination).to_s
headers = {'Destination' => destination}
- res = @handler.request(:move, path, nil, headers)
+ res = @handler.request(:move, path, nil, headers.merge(@headers))
res.body
end
# Send a copy request to the server.
#
@@ -548,54 +652,81 @@
# dav.copy(original_path, destination)
def copy(path,destination)
path = @uri.merge(path).path
destination = @uri.merge(destination).to_s
headers = {'Destination' => destination}
- res = @handler.request(:copy, path, nil, headers)
+ res = @handler.request(:copy, path, nil, headers.merge(@headers))
res.body
end
# Do a proppatch request to the server to
# update properties on resources or collections.
#
# Example:
- # dav.proppatch(uri.path,"<d:creationdate>#{new_date}</d:creationdate>")
+ # dav.proppatch(uri.path,
+ # "<d:set><d:prop>" +
+ # "<d:creationdate>#{new_date}</d:creationdate>" +
+ # "</d:set></d:prop>" +
+ # )
def proppatch(path, xml_snippet)
path = @uri.merge(path).path
headers = {'Depth' => '1'}
body = '<?xml version="1.0"?>' +
'<d:propertyupdate xmlns:d="DAV:">' +
- '<d:set>' +
- '<d:prop>' +
- xml_snippet +
- '</d:prop>' +
- '</d:set>' +
+ xml_snippet +
'</d:propertyupdate>'
- res = @handler.request(:proppatch, path, body, headers)
+ res = @handler.request(:proppatch, path, body, headers.merge(@headers))
Nokogiri::XML.parse(res.body)
end
+ # Send a lock request to the server
+ #
+ # On success returns an XML response body with a Lock-Token
+ #
+ # Example:
+ # dav.lock(uri.path, "<d:lockscope><d:exclusive/></d:lockscope><d:locktype><d:write/></d:locktype><d:owner>Owner</d:owner>")
+ def lock(path, xml_snippet)
+ path = @uri.merge(path).path
+ headers = {'Depth' => '1'}
+ body = '<?xml version="1.0"?>' +
+ '<d:lockinfo xmlns:d="DAV:">' +
+ xml_snippet +
+ '</d:lockinfo>'
+ res = @handler.request(:lock, path, body, headers.merge(@headers))
+ Nokogiri::XML.parse(res.body)
+ end
+
+ # Send an unlock request to the server
+ #
+ # Example:
+ # dav.unlock(uri.path, "opaquelocktoken:eee47ade-09ac-626b-02f7-e354175d984e")
+ def unlock(path, locktoken)
+ headers = {'Lock-Token' => '<'+locktoken+'>'}
+ path = @uri.merge(path).path
+ res = @handler.request(:unlock, path, nil, headers.merge(@headers))
+ end
+
# Returns true if resource exists on server.
#
# Example:
# dav.exists?('https://www.example.com/collection/') => true
# dav.exists?('/collection/') => true
def exists?(path)
path = @uri.merge(path).path
headers = {'Depth' => '1'}
body = '<?xml version="1.0" encoding="utf-8"?><DAV:propfind xmlns:DAV="DAV:"><DAV:allprop/></DAV:propfind>'
begin
- res = @handler.request(:propfind, path, body, headers)
+ res = @handler.request(:propfind, path, body, headers.merge(@headers))
rescue
return false
end
return (res.is_a? Net::HTTPSuccess)
end
# Makes a new directory (collection)
def mkdir(path)
path = @uri.merge(path).path
- res = @handler.request(:mkcol, path, nil, nil)
+ res = @handler.request(:mkcol, path, nil, @headers)
res.body
end
def verify_callback=(callback)
@handler.verify_callback = callback