Merge lp:~ed.so/duplicity/webdav.lftp.ssl-overhaul into lp:~duplicity-team/duplicity/0.7-series

Proposed by edso
Status: Merged
Merged at revision: 1196
Proposed branch: lp:~ed.so/duplicity/webdav.lftp.ssl-overhaul
Merge into: lp:~duplicity-team/duplicity/0.7-series
Diff against target: 404 lines (+95/-65)
6 files modified
bin/duplicity.1 (+20/-4)
duplicity/backend.py (+2/-0)
duplicity/backends/lftpbackend.py (+22/-21)
duplicity/backends/webdavbackend.py (+47/-37)
duplicity/commandline.py (+2/-2)
duplicity/globals.py (+2/-1)
To merge this branch: bzr merge lp:~ed.so/duplicity/webdav.lftp.ssl-overhaul
Reviewer Review Type Date Requested Status
duplicity-team Pending
Review via email: mp+287654@code.launchpad.net

Description of the change

duplicity.1, commandline.py, globals.py
- added --ssl-cacert-path parameter
backend.py
- make sure url path component is properly url decoded,
  in case it contains special chars (eg. @ or space)
lftpbackend.py
- quote _all_ cmd line params
- added missing lftp+ftpes protocol
- fix empty list result when chdir failed silently
- added ssl_cacert_path support
webdavbackend.py
- add ssl default context support for python 2.7.9+
  (using system certs eg. in /etc/ssl/certs)
- added ssl_cacert_path support for python 2.7.9+
- gettext wrapped all log messages
- minor refinements

To post a comment you must log in.
1197. By ed.so

path may be unset

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bin/duplicity.1'
2--- bin/duplicity.1 2016-02-18 18:22:35 +0000
3+++ bin/duplicity.1 2016-03-01 16:25:36 +0000
4@@ -854,15 +854,23 @@
5
6 .TP
7 .BI "--ssl-cacert-file " file
8-.B (only webdav backend)
9+.B (only webdav & lftp backend)
10 Provide a cacert file for ssl certificate verification.
11 .br
12 See also
13 .BR "A NOTE ON SSL CERTIFICATE VERIFICATION" .
14
15 .TP
16+.BI "--ssl-cacert-path " path/to/certs/
17+.B (only webdav backend and python 2.7.9+ OR lftp+webdavs and a recent lftp)
18+Provide a path to a folder containing cacert files for ssl certificate verification.
19+.br
20+See also
21+.BR "A NOTE ON SSL CERTIFICATE VERIFICATION" .
22+
23+.TP
24 .BI --ssl-no-check-certificate
25-.B (only webdav backend)
26+.B (only webdav & lftp backend)
27 Disable ssl certificate verification.
28 .br
29 See also
30@@ -1873,8 +1881,16 @@
31 an sftp service running on the backend server, which is sometimes not an option.
32
33 .SH A NOTE ON SSL CERTIFICATE VERIFICATION
34-Certificate verification as implemented right now [01.2013] only in the webdav backend needs a file
35-based database of certification authority certificates (cacert file). It has to be a
36+Certificate verification as implemented right now [02.2016] only in the webdav
37+and lftp backends. older pythons 2.7.8- and older lftp binaries need a file
38+based database of certification authority certificates (cacert file).
39+.br
40+Newer python 2.7.9+ and recent lftp versions however support the system default
41+certificates (usually in /etc/ssl/certs) and also giving an alternative ca cert
42+folder via
43+.BR --ssl-cacert-path .
44+.PP
45+The cacert file has to be a
46 .B PEM
47 formatted text file as currently provided by the
48 .B CURL
49
50=== modified file 'duplicity/backend.py'
51--- duplicity/backend.py 2016-01-24 17:56:38 +0000
52+++ duplicity/backend.py 2016-03-01 16:25:36 +0000
53@@ -266,6 +266,8 @@
54
55 try:
56 self.path = pu.path
57+ if self.path:
58+ self.path = urllib.unquote(self.path)
59 except Exception:
60 raise InvalidBackendURL("Syntax error (path) in: %s" % url_string)
61
62
63=== modified file 'duplicity/backends/lftpbackend.py'
64--- duplicity/backends/lftpbackend.py 2016-01-24 17:30:02 +0000
65+++ duplicity/backends/lftpbackend.py 2016-03-01 16:25:36 +0000
66@@ -95,24 +95,21 @@
67 cacert_candidates = ["~/.duplicity/cacert.pem",
68 "~/duplicity_cacert.pem",
69 "/etc/duplicity/cacert.pem"]
70- #
71+ # look for a default cacert file
72 if not self.cacert_file:
73 for path in cacert_candidates:
74 path = os.path.expanduser(path)
75 if (os.path.isfile(path)):
76 self.cacert_file = path
77 break
78- # still no cacert file, inform user
79- if not self.cacert_file:
80- raise duplicity.errors.FatalBackendException("""For certificate verification a cacert database file is needed in one of these locations: %s
81-Hints:
82- Consult the man page, chapter 'SSL Certificate Verification'.
83- Consider using the options --ssl-cacert-file, --ssl-no-check-certificate .""" % ", ".join(cacert_candidates))
84
85+ # save config into a reusable temp file
86 self.tempfile, self.tempname = tempdir.default().mkstemp()
87 os.write(self.tempfile, "set ssl:verify-certificate " + ("false" if globals.ssl_no_check_certificate else "true") + "\n")
88- if globals.ssl_cacert_file:
89- os.write(self.tempfile, "set ssl:ca-file '" + globals.ssl_cacert_file + "'\n")
90+ if self.cacert_file:
91+ os.write(self.tempfile, "set ssl:ca-file " + cmd_quote(self.cacert_file) + "\n")
92+ if globals.ssl_cacert_path:
93+ os.write(self.tempfile, "set ssl:ca-path " + cmd_quote(globals.ssl_cacert_path) + "\n")
94 if self.parsed_url.scheme == 'ftps':
95 os.write(self.tempfile, "set ftp:ssl-allow true\n")
96 os.write(self.tempfile, "set ftp:ssl-protect-data true\n")
97@@ -134,13 +131,14 @@
98 else:
99 os.write(self.tempfile, "open %s %s\n" % (self.authflag, self.url_string))
100 os.close(self.tempfile)
101+ # print settings in debug mode
102 if log.getverbosity() >= log.DEBUG:
103 f = open(self.tempname, 'r')
104 log.Debug("SETTINGS: \n"
105- "%s" % f.readlines())
106+ "%s" % f.read())
107
108 def _put(self, source_path, remote_filename):
109- commandline = "lftp -c 'source %s; mkdir -p %s; put %s -o %s'" % (
110+ commandline = "lftp -c \"source %s; mkdir -p %s; put %s -o %s\"" % (
111 self.tempname,
112 cmd_quote(self.remote_path),
113 cmd_quote(source_path.name),
114@@ -155,8 +153,8 @@
115 "%s" % (l))
116
117 def _get(self, remote_filename, local_path):
118- commandline = "lftp -c 'source %s; get %s -o %s'" % (
119- self.tempname,
120+ commandline = "lftp -c \"source %s; get %s -o %s\"" % (
121+ cmd_quote(self.tempname),
122 cmd_quote(self.remote_path) + remote_filename,
123 cmd_quote(local_path.name)
124 )
125@@ -172,9 +170,11 @@
126 # remote_dir = urllib.unquote(self.parsed_url.path.lstrip('/')).rstrip()
127 remote_dir = urllib.unquote(self.parsed_url.path)
128 # print remote_dir
129- commandline = "lftp -c 'source %s; cd %s || exit 0; ls'" % (
130- self.tempname,
131- cmd_quote(self.remote_path)
132+ quoted_path = cmd_quote(self.remote_path);
133+ # failing to cd into the folder might be because it was not created already
134+ commandline = "lftp -c \"source %s; ( cd %s && ls ) || ( mkdir -p %s && cd %s && ls )\"" % (
135+ cmd_quote(self.tempname),
136+ quoted_path, quoted_path, quoted_path
137 )
138 log.Debug("CMD: %s" % commandline)
139 _, l, e = self.subprocess_popen(commandline)
140@@ -187,10 +187,10 @@
141 return [x.split()[-1] for x in l.split('\n') if x]
142
143 def _delete(self, filename):
144- commandline = "lftp -c 'source %s; cd %s; rm %s'" % (
145- self.tempname,
146+ commandline = "lftp -c \"source %s; cd %s; rm %s\"" % (
147+ cmd_quote(self.tempname),
148 cmd_quote(self.remote_path),
149- filename
150+ cmd_quote(filename)
151 )
152 log.Debug("CMD: %s" % commandline)
153 _, l, e = self.subprocess_popen(commandline)
154@@ -204,10 +204,10 @@
155 duplicity.backend.register_backend("fish", LFTPBackend)
156 duplicity.backend.register_backend("ftpes", LFTPBackend)
157
158-
159 duplicity.backend.register_backend("lftp+ftp", LFTPBackend)
160 duplicity.backend.register_backend("lftp+ftps", LFTPBackend)
161 duplicity.backend.register_backend("lftp+fish", LFTPBackend)
162+duplicity.backend.register_backend("lftp+ftpes", LFTPBackend)
163 duplicity.backend.register_backend("lftp+sftp", LFTPBackend)
164 duplicity.backend.register_backend("lftp+webdav", LFTPBackend)
165 duplicity.backend.register_backend("lftp+webdavs", LFTPBackend)
166@@ -216,7 +216,8 @@
167
168 duplicity.backend.uses_netloc.extend(['ftp', 'ftps', 'fish', 'ftpes',
169 'lftp+ftp', 'lftp+ftps',
170- 'lftp+fish', 'lftp+sftp',
171+ 'lftp+fish', 'lftp+ftpes',
172+ 'lftp+sftp',
173 'lftp+webdav', 'lftp+webdavs',
174 'lftp+http', 'lftp+https']
175 )
176
177=== modified file 'duplicity/backends/webdavbackend.py'
178--- duplicity/backends/webdavbackend.py 2016-02-05 09:58:57 +0000
179+++ duplicity/backends/webdavbackend.py 2016-03-01 16:25:36 +0000
180@@ -58,31 +58,25 @@
181 import socket
182 import ssl
183 except ImportError:
184- raise FatalBackendException("Missing socket or ssl libraries.")
185+ raise FatalBackendException(_("Missing socket or ssl python modules."))
186
187 httplib.HTTPSConnection.__init__(self, *args, **kwargs)
188
189 self.cacert_file = globals.ssl_cacert_file
190- cacert_candidates = ["~/.duplicity/cacert.pem",
191+ self.cacert_candidates = ["~/.duplicity/cacert.pem",
192 "~/duplicity_cacert.pem",
193 "/etc/duplicity/cacert.pem"]
194- #
195+ # if no cacert file was given search default locations
196 if not self.cacert_file:
197- for path in cacert_candidates:
198+ for path in self.cacert_candidates:
199 path = os.path.expanduser(path)
200 if (os.path.isfile(path)):
201 self.cacert_file = path
202 break
203- # still no cacert file, inform user
204- if not self.cacert_file:
205- raise FatalBackendException("""\
206-For certificate verification a cacert database file is needed in one of these locations: %s
207-Hints:
208- Consult the man page, chapter 'SSL Certificate Verification'.
209- Consider using the options --ssl-cacert-file, --ssl-no-check-certificate .""" % ", ".join(cacert_candidates))
210+
211 # check if file is accessible (libssl errors are not very detailed)
212- if not os.access(self.cacert_file, os.R_OK):
213- raise FatalBackendException("Cacert database file '%s' is not readable." % self.cacert_file)
214+ if self.cacert_file and not os.access(self.cacert_file, os.R_OK):
215+ raise FatalBackendException(_("Cacert database file '%s' is not readable.") % self.cacert_file)
216
217 def connect(self):
218 # create new socket
219@@ -92,8 +86,24 @@
220 self.sock = sock
221 self.tunnel()
222
223- # wrap the socket in ssl using verification
224- self.sock = ssl.wrap_socket(sock,
225+ # python 2.7.9+ supports default system certs now
226+ if "create_default_context" in dir(ssl):
227+ context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=self.cacert_file, capath=globals.ssl_cacert_path)
228+ self.sock = context.wrap_socket(sock, server_hostname=self.host)
229+ # the legacy way needing a cert file
230+ else:
231+ if globals.ssl_cacert_path:
232+ raise FatalBackendException(_("Option '--ssl-cacert-path' is not supported with python 2.7.8 and below."))
233+
234+ if not self.cacert_file:
235+ raise FatalBackendException(_("""\
236+For certificate verification with python 2.7.8 or earlier a cacert database file is needed in one of these locations: %s
237+Hints:
238+ Consult the man page, chapter 'SSL Certificate Verification'.
239+ Consider using the options --ssl-cacert-file, --ssl-no-check-certificate .""") % ", ".join(self.cacert_candidates))
240+
241+ # wrap the socket in ssl using verification
242+ self.sock = ssl.wrap_socket(sock,
243 cert_reqs=ssl.CERT_REQUIRED,
244 ca_certs=self.cacert_file,
245 )
246@@ -129,9 +139,9 @@
247 self.password = self.get_password()
248 self.directory = self.sanitize_path(parsed_url.path)
249
250- log.Info("Using WebDAV protocol %s" % (globals.webdav_proto,))
251- log.Info("Using WebDAV host %s port %s" % (parsed_url.hostname, parsed_url.port))
252- log.Info("Using WebDAV directory %s" % (self.directory,))
253+ log.Info(_("Using WebDAV protocol %s") % (globals.webdav_proto,))
254+ log.Info(_("Using WebDAV host %s port %s") % (parsed_url.hostname, parsed_url.port))
255+ log.Info(_("Using WebDAV directory %s") % (self.directory,))
256
257 self.conn = None
258
259@@ -162,7 +172,7 @@
260 and self.conn.host == self.parsed_url.hostname:
261 return
262
263- log.Info("WebDAV create connection on '%s'" % (self.parsed_url.hostname))
264+ log.Info(_("WebDAV create connection on '%s'") % (self.parsed_url.hostname))
265 self._close()
266 # http schemes needed for redirect urls from servers
267 if self.parsed_url.scheme in ['webdav', 'http']:
268@@ -173,7 +183,7 @@
269 else:
270 self.conn = VerifiedHTTPSConnection(self.parsed_url.hostname, self.parsed_url.port)
271 else:
272- raise FatalBackendException("WebDAV Unknown URI scheme: %s" % (self.parsed_url.scheme))
273+ raise FatalBackendException(_("WebDAV Unknown URI scheme: %s") % (self.parsed_url.scheme))
274
275 def _close(self):
276 if self.conn:
277@@ -192,34 +202,34 @@
278 if self.digest_challenge is not None:
279 self.headers['Authorization'] = self.get_digest_authorization(path)
280
281- log.Info("WebDAV %s %s request with headers: %s " % (method, quoted_path, self.headers))
282- log.Info("WebDAV data length: %s " % len(str(data)))
283+ log.Info(_("WebDAV %s %s request with headers: %s ") % (method, quoted_path, self.headers))
284+ log.Info(_("WebDAV data length: %s ") % len(str(data)))
285 self.conn.request(method, quoted_path, data, self.headers)
286 response = self.conn.getresponse()
287- log.Info("WebDAV response status %s with reason '%s'." % (response.status, response.reason))
288+ log.Info(_("WebDAV response status %s with reason '%s'.") % (response.status, response.reason))
289 # resolve redirects and reset url on listing requests (they usually come before everything else)
290 if response.status in [301, 302] and method == 'PROPFIND':
291 redirect_url = response.getheader('location', None)
292 response.close()
293 if redirect_url:
294- log.Notice("WebDAV redirect to: %s " % urllib.unquote(redirect_url))
295+ log.Notice(_("WebDAV redirect to: %s ") % urllib.unquote(redirect_url))
296 if redirected > 10:
297- raise FatalBackendException("WebDAV redirected 10 times. Giving up.")
298+ raise FatalBackendException(_("WebDAV redirected 10 times. Giving up."))
299 self.parsed_url = duplicity.backend.ParsedUrl(redirect_url)
300 self.directory = self.sanitize_path(self.parsed_url.path)
301 return self.request(method, self.directory, data, redirected + 1)
302 else:
303- raise FatalBackendException("WebDAV missing location header in redirect response.")
304+ raise FatalBackendException(_("WebDAV missing location header in redirect response."))
305 elif response.status == 401:
306 response.read()
307 response.close()
308 self.headers['Authorization'] = self.get_authorization(response, quoted_path)
309- log.Info("WebDAV retry request with authentification headers.")
310- log.Info("WebDAV %s %s request2 with headers: %s " % (method, quoted_path, self.headers))
311- log.Info("WebDAV data length: %s " % len(str(data)))
312+ log.Info(_("WebDAV retry request with authentification headers."))
313+ log.Info(_("WebDAV %s %s request2 with headers: %s ") % (method, quoted_path, self.headers))
314+ log.Info(_("WebDAV data length: %s ") % len(str(data)))
315 self.conn.request(method, quoted_path, data, self.headers)
316 response = self.conn.getresponse()
317- log.Info("WebDAV response2 status %s with reason '%s'." % (response.status, response.reason))
318+ log.Info(_("WebDAV response2 status %s with reason '%s'.") % (response.status, response.reason))
319
320 return response
321
322@@ -337,11 +347,11 @@
323 log.Info("Checking existence dir %s: %d" % (d, response.status))
324
325 if response.status == 404:
326- log.Info("Creating missing directory %s" % d)
327+ log.Info(_("Creating missing directory %s") % d)
328
329 res = self.request("MKCOL", d)
330 if res.status != 201:
331- raise BackendException("WebDAV MKCOL %s failed: %s %s" % (d, res.status, res.reason))
332+ raise BackendException(_("WebDAV MKCOL %s failed: %s %s") % (d, res.status, res.reason))
333
334 def taste_href(self, href):
335 """
336@@ -353,8 +363,8 @@
337 raw_filename = self.getText(href.childNodes).strip()
338 parsed_url = urlparse.urlparse(urllib.unquote(raw_filename))
339 filename = parsed_url.path
340- log.Debug("webdav path decoding and translation: "
341- "%s -> %s" % (raw_filename, filename))
342+ log.Debug(_("WebDAV path decoding and translation: "
343+ "%s -> %s") % (raw_filename, filename))
344
345 # at least one WebDAV server returns files in the form
346 # of full URL:s. this may or may not be
347@@ -397,7 +407,7 @@
348 status = response.status
349 reason = response.reason
350 response.close()
351- raise BackendException("Bad status code %s reason %s." % (status, reason))
352+ raise BackendException(_("WebDAV GET Bad status code %s reason %s.") % (status, reason))
353 except Exception as e:
354 raise e
355 finally:
356@@ -418,7 +428,7 @@
357 status = response.status
358 reason = response.reason
359 response.close()
360- raise BackendException("Bad status code %s reason %s." % (status, reason))
361+ raise BackendException(_("WebDAV PUT Bad status code %s reason %s.") % (status, reason))
362 except Exception as e:
363 raise e
364 finally:
365@@ -437,7 +447,7 @@
366 status = response.status
367 reason = response.reason
368 response.close()
369- raise BackendException("Bad status code %s reason %s." % (status, reason))
370+ raise BackendException(_("WebDAV DEL Bad status code %s reason %s.") % (status, reason))
371 except Exception as e:
372 raise e
373 finally:
374
375=== modified file 'duplicity/commandline.py'
376--- duplicity/commandline.py 2016-02-10 17:15:49 +0000
377+++ duplicity/commandline.py 2016-03-01 16:25:36 +0000
378@@ -582,9 +582,9 @@
379 # user added ssh options
380 parser.add_option("--ssh-options", action="extend", metavar=_("options"))
381
382- # user added ssl options (webdav backend)
383+ # user added ssl options (used by webdav, lftp backend)
384 parser.add_option("--ssl-cacert-file", metavar=_("pem formatted bundle of certificate authorities"))
385-
386+ parser.add_option("--ssl-cacert-path", metavar=_("path to a folder with certificate authority files"))
387 parser.add_option("--ssl-no-check-certificate", action="store_true")
388
389 # Working directory for the tempfile module. Defaults to /tmp on most systems.
390
391=== modified file 'duplicity/globals.py'
392--- duplicity/globals.py 2015-10-10 00:02:35 +0000
393+++ duplicity/globals.py 2016-03-01 16:25:36 +0000
394@@ -242,8 +242,9 @@
395 # default cf backend is pyrax
396 cf_backend = "pyrax"
397
398-# HTTPS ssl optons (currently only webdav)
399+# HTTPS ssl options (currently only webdav, lftp)
400 ssl_cacert_file = None
401+ssl_cacert_path = None
402 ssl_no_check_certificate = False
403
404 # user added rsync options

Subscribers

People subscribed via source and target branches