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