1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import base64
23 import httplib
24 import re
25 import urllib
26 import urllib2
27 import xml.dom.minidom
28
29 import duplicity.backend
30 from duplicity import globals
31 from duplicity import log
32 from duplicity.errors import *
33 from duplicity import urlparse_2_5 as urlparser
34
36 """
37 This request subclass allows explicit specification of
38 the HTTP request method. Basic urllib2.Request class
39 chooses GET or POST depending on self.has_data()
40 """
41 - def __init__(self, method, *args, **kwargs):
42 self.method = method
43 urllib2.Request.__init__(self, *args, **kwargs)
44
47
48
50 """Backend for accessing a WebDAV repository.
51
52 webdav backend contributed in 2006 by Jesper Zedlitz <jesper@zedlitz.de>
53 """
54 listbody = """\
55 <?xml version="1.0" encoding="utf-8" ?>
56 <D:propfind xmlns:D="DAV:">
57 <D:allprop/>
58 </D:propfind>
59
60 """
61
62 """Connect to remote store using WebDAV Protocol"""
64 duplicity.backend.Backend.__init__(self, parsed_url)
65 self.headers = {'Connection': 'keep-alive'}
66 self.parsed_url = parsed_url
67 self.digest_challenge = None
68 self.digest_auth_handler = None
69
70 if parsed_url.path:
71 foldpath = re.compile('/+')
72 self.directory = foldpath.sub('/', parsed_url.path + '/' )
73 else:
74 self.directory = '/'
75
76 log.Info("Using WebDAV host %s" % (parsed_url.hostname,))
77 log.Info("Using WebDAV port %s" % (parsed_url.port,))
78 log.Info("Using WebDAV directory %s" % (self.directory,))
79 log.Info("Using WebDAV protocol %s" % (globals.webdav_proto,))
80
81 if parsed_url.scheme == 'webdav':
82 self.conn = httplib.HTTPConnection(parsed_url.hostname, parsed_url.port)
83 elif parsed_url.scheme == 'webdavs':
84 self.conn = httplib.HTTPSConnection(parsed_url.hostname, parsed_url.port)
85 else:
86 raise BackendException("Unknown URI scheme: %s" % (parsed_url.scheme))
87
88 - def _getText(self,nodelist):
89 rc = ""
90 for node in nodelist:
91 if node.nodeType == node.TEXT_NODE:
92 rc = rc + node.data
93 return rc
94
97
98 - def request(self, method, path, data=None):
99 """
100 Wraps the connection.request method to retry once if authentication is
101 required
102 """
103 quoted_path = urllib.quote(path)
104
105 if self.digest_challenge is not None:
106 self.headers['Authorization'] = self.get_digest_authorization(path)
107 self.conn.request(method, quoted_path, data, self.headers)
108 response = self.conn.getresponse()
109 if response.status == 401:
110 response.close()
111 self.headers['Authorization'] = self.get_authorization(response, quoted_path)
112 self.conn.request(method, quoted_path, data, self.headers)
113 response = self.conn.getresponse()
114
115 return response
116
118 """
119 Fetches the auth header based on the requested method (basic or digest)
120 """
121 try:
122 auth_hdr = response.getheader('www-authenticate', '')
123 token, challenge = auth_hdr.split(' ', 1)
124 except ValueError:
125 return None
126 if token.lower() == 'basic':
127 return self.get_basic_authorization()
128 else:
129 self.digest_challenge = self.parse_digest_challenge(challenge)
130 return self.get_digest_authorization(path)
131
133 return urllib2.parse_keqv_list(urllib2.parse_http_list(challenge_string))
134
136 """
137 Returns the basic auth header
138 """
139 auth_string = '%s:%s' % (self.parsed_url.username, self.get_password())
140 return 'Basic %s' % base64.encodestring(auth_string).strip()
141
143 """
144 Returns the digest auth header
145 """
146 u = self.parsed_url
147 if self.digest_auth_handler is None:
148 pw_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
149 pw_manager.add_password(None, self.conn.host, u.username, self.get_password())
150 self.digest_auth_handler = urllib2.HTTPDigestAuthHandler(pw_manager)
151
152
153
154 scheme = u.scheme == 'webdavs' and 'https' or 'http'
155 hostname = u.port and "%s:%s" % (u.hostname, u.port) or u.hostname
156 dummy_url = "%s://%s%s" % (scheme, hostname, path)
157 dummy_req = CustomMethodRequest(self.conn._method, dummy_url)
158 auth_string = self.digest_auth_handler.get_authorization(dummy_req, self.digest_challenge)
159 return 'Digest %s' % auth_string
160
162 """List files in directory"""
163 for n in range(1, globals.num_retries+1):
164 log.Info("Listing directory %s on WebDAV server" % (self.directory,))
165 self.headers['Depth'] = "1"
166 response = self.request("PROPFIND", self.directory, self.listbody)
167 del self.headers['Depth']
168
169 if response.status == 404:
170 log.Info("Directory '%s' being created." % self.directory)
171 res = self.request("MKCOL", self.directory)
172 log.Info("WebDAV MKCOL status: %s %s" % (res.status, res.reason))
173 continue
174 if response.status == 207:
175 document = response.read()
176 break
177 log.Info("WebDAV PROPFIND attempt #%d failed: %s %s" % (n, response.status, response.reason))
178 if n == globals.num_retries +1:
179 log.Warn("WebDAV backend giving up after %d attempts to PROPFIND %s" % (globals.num_retries, self.directory))
180 raise BackendException((response.status, response.reason))
181
182 log.Info("%s" % (document,))
183 dom = xml.dom.minidom.parseString(document)
184 result = []
185 for href in dom.getElementsByTagName('d:href') + dom.getElementsByTagName('D:href'):
186 filename = self.__taste_href(href)
187 if filename:
188 result.append(filename)
189 return result
190
192 """
193 Internal helper to taste the given href node and, if
194 it is a duplicity file, collect it as a result file.
195
196 @return: A matching filename, or None if the href did not match.
197 """
198 raw_filename = self._getText(href.childNodes).strip()
199 parsed_url = urlparser.urlparse(urllib.unquote(raw_filename))
200 filename = parsed_url.path
201 log.Debug("webdav path decoding and translation: "
202 "%s -> %s" % (raw_filename, filename))
203
204
205
206
207
208
209
210 if not parsed_url.hostname is None \
211 and not (parsed_url.hostname == self.parsed_url.hostname):
212 m = "Received filename was in the form of a "\
213 "full url, but the hostname (%s) did "\
214 "not match that of the webdav backend "\
215 "url (%s) - aborting as a conservative "\
216 "safety measure. If this happens to you, "\
217 "please report the problem"\
218 "" % (parsed_url.hostname,
219 self.parsed_url.hostname)
220 raise BackendException(m)
221
222 if filename.startswith(self.directory):
223 filename = filename.replace(self.directory,'',1)
224 return filename
225 else:
226 return None
227
228 - def get(self, remote_filename, local_path):
229 """Get remote filename, saving it to local_path"""
230 url = self.directory + remote_filename
231 target_file = local_path.open("wb")
232 for n in range(1, globals.num_retries+1):
233 log.Info("Retrieving %s from WebDAV server" % (url ,))
234 response = self.request("GET", url)
235 if response.status == 200:
236 target_file.write(response.read())
237 assert not target_file.close()
238 local_path.setdata()
239 return
240 log.Info("WebDAV GET attempt #%d failed: %s %s" % (n, response.status, response.reason))
241 log.Warn("WebDAV backend giving up after %d attempts to GET %s" % (globals.num_retries, url))
242 raise BackendException((response.status, response.reason))
243
244 - def put(self, source_path, remote_filename = None):
245 """Transfer source_path to remote_filename"""
246 if not remote_filename:
247 remote_filename = source_path.get_filename()
248 url = self.directory + remote_filename
249 source_file = source_path.open("rb")
250 for n in range(1, globals.num_retries+1):
251 log.Info("Saving %s on WebDAV server" % (url ,))
252 response = self.request("PUT", url, source_file.read())
253 if response.status == 201:
254 response.read()
255 assert not source_file.close()
256 return
257 log.Info("WebDAV PUT attempt #%d failed: %s %s" % (n, response.status, response.reason))
258 log.Warn("WebDAV backend giving up after %d attempts to PUT %s" % (globals.num_retries, url))
259 raise BackendException((response.status, response.reason))
260
261 - def delete(self, filename_list):
262 """Delete files in filename_list"""
263 for filename in filename_list:
264 url = self.directory + filename
265 for n in range(1, globals.num_retries+1):
266 log.Info("Deleting %s from WebDAV server" % (url ,))
267 response = self.request("DELETE", url)
268 if response.status == 204:
269 response.read()
270 break
271 log.Info("WebDAV DELETE attempt #%d failed: %s %s" % (n, response.status, response.reason))
272 if n == globals.num_retries +1:
273 log.Warn("WebDAV backend giving up after %d attempts to DELETE %s" % (globals.num_retries, url))
274 raise BackendException((response.status, response.reason))
275
276 duplicity.backend.register_backend("webdav", WebDAVBackend)
277 duplicity.backend.register_backend("webdavs", WebDAVBackend)
278