Merge lp:~bit-team/backintime/password into lp:backintime/1.0

Proposed by Germar
Status: Merged
Merged at revision: 836
Proposed branch: lp:~bit-team/backintime/password
Merge into: lp:backintime/1.0
Diff against target: 2372 lines (+1492/-76)
31 files modified
CHANGES (+1/-0)
common/Makefile.template (+5/-0)
common/askpass.py (+49/-0)
common/backintime (+5/-1)
common/backintime-askpass (+27/-0)
common/backintime.desktop (+8/-0)
common/backintime.py (+35/-0)
common/config.py (+88/-1)
common/configfile.py (+6/-4)
common/debian_specific/conffiles (+1/-0)
common/debian_specific/control (+1/-1)
common/debian_specific/rules (+4/-0)
common/dummytools.py (+5/-3)
common/makedeb-src.sh (+7/-0)
common/man/C/backintime.1 (+5/-3)
common/mount.py (+25/-14)
common/password.py (+446/-0)
common/password_ipc.py (+112/-0)
common/sshtools.py (+63/-3)
common/tools.py (+106/-2)
gnome/backintime-gnome (+4/-1)
gnome/debian_specific/control (+1/-1)
gnome/messagebox.py (+20/-2)
gnome/settingsdialog.glade (+183/-25)
gnome/settingsdialog.py (+95/-4)
gnome/textinputdialog.glade (+14/-1)
kde4/app.py (+3/-3)
kde4/debian_specific/control (+1/-1)
kde4/messagebox.py (+49/-0)
kde4/settingsdialog.py (+122/-6)
makedeb.sh (+1/-0)
To merge this branch: bzr merge lp:~bit-team/backintime/password
Reviewer Review Type Date Requested Status
Dan Approve
Review via email: mp+152305@code.launchpad.net

Description of the change

Add ability to store passwords in Backintime. There are three different modes:
-store password in system keyring
-store password in system keyring and cache it as soon as the user log in first time so it can be used during cronjobs if the user is logged out
- ask user for Password

To post a comment you must log in.
Revision history for this message
Dan (danleweb) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'CHANGES'
2--- CHANGES 2013-03-04 21:16:20 +0000
3+++ CHANGES 2013-03-07 23:23:23 +0000
4@@ -1,6 +1,7 @@
5 Back In Time
6
7 Version 1.0.22
8+* Add Password storage mode ssh
9 * Add "Full rsync mode" (can be faster but ...)
10 * Fix bug: "Restore to..." failed due to spaces in directory name (https://bugs.launchpad.net/backintime/+bug/1096319)
11 * Fix bug: host not found in known_hosts if port != 22 (https://bugs.launchpad.net/backintime/+bug/1130356)
12
13=== modified file 'common/Makefile.template'
14--- common/Makefile.template 2012-11-02 17:34:44 +0000
15+++ common/Makefile.template 2013-03-07 23:23:23 +0000
16@@ -38,4 +38,9 @@
17 #install application
18 install -d $(DEST)/bin
19 install backintime $(DEST)/bin
20+ install backintime-askpass $(DEST)/bin
21+
22+ #install autostart
23+ install -d $(DEST)/../etc/xdg/autostart
24+ install --mode=644 backintime.desktop $(DEST)/../etc/xdg/autostart
25
26
27=== added file 'common/askpass.py'
28--- common/askpass.py 1970-01-01 00:00:00 +0000
29+++ common/askpass.py 2013-03-07 23:23:23 +0000
30@@ -0,0 +1,49 @@
31+#!/usr/bin/env python
32+# Copyright (c) 2012-2013 Germar Reitze
33+#
34+# This program is free software; you can redistribute it and/or modify
35+# it under the terms of the GNU General Public License as published by
36+# the Free Software Foundation; either version 2 of the License, or
37+# (at your option) any later version.
38+#
39+# This program is distributed in the hope that it will be useful,
40+# but WITHOUT ANY WARRANTY; without even the implied warranty of
41+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
42+# GNU General Public License for more details.
43+#
44+# You should have received a copy of the GNU General Public License along
45+# with this program; if not, write to the Free Software Foundation, Inc.,
46+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
47+
48+import os
49+import sys
50+import base64
51+
52+import password
53+import password_ipc
54+import tools
55+import config
56+
57+if __name__ == '__main__':
58+ """
59+ return password.
60+ """
61+ cfg = config.Config()
62+ tools.load_env(cfg)
63+ try:
64+ profile_id = os.environ['ASKPASS_PROFILE_ID']
65+ mode = os.environ['ASKPASS_MODE']
66+ except KeyError:
67+ sys.exit(1)
68+ try:
69+ temp_file = os.environ['ASKPASS_TEMP']
70+ except KeyError:
71+ #normal mode, get password from module password
72+ pw = password.Password(cfg)
73+ print(pw.get_password(None, profile_id, mode))
74+ else:
75+ #temp mode
76+ fifo = password_ipc.FIFO(temp_file)
77+ pw_base64 = fifo.read(5)
78+ if pw_base64:
79+ print(base64.decodestring(pw_base64))
80\ No newline at end of file
81
82=== modified file 'common/backintime'
83--- common/backintime 2009-11-02 13:52:00 +0000
84+++ common/backintime 2013-03-07 23:23:23 +0000
85@@ -23,5 +23,9 @@
86 APP_PATH="/usr/share/backintime/common"
87 fi
88
89-python $APP_PATH/backintime.py "$@"
90+#starting a new ssh-agent all the time is just a workaround for
91+#https://bugs.launchpad.net/ubuntu/+source/openssh/+bug/841672
92+#normally this should only be necessary if run as cronjob
93+#and the user is not logged in
94+ssh-agent python $APP_PATH/backintime.py "$@"
95
96
97=== added file 'common/backintime-askpass'
98--- common/backintime-askpass 1970-01-01 00:00:00 +0000
99+++ common/backintime-askpass 2013-03-07 23:23:23 +0000
100@@ -0,0 +1,27 @@
101+#!/bin/sh
102+
103+# Back In Time
104+# Copyright (C) 2008-2009 Oprea Dan, Bart de Koning, Richard Bailey
105+#
106+# This program is free software; you can redistribute it and/or modify
107+# it under the terms of the GNU General Public License as published by
108+# the Free Software Foundation; either version 2 of the License, or
109+# (at your option) any later version.
110+#
111+# This program is distributed in the hope that it will be useful,
112+# but WITHOUT ANY WARRANTY; without even the implied warranty of
113+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
114+# GNU General Public License for more details.
115+#
116+# You should have received a copy of the GNU General Public License along
117+# with this program; if not, write to the Free Software Foundation, Inc.,
118+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
119+
120+if [ -f askpass.py ]; then
121+ APP_PATH="."
122+else
123+ APP_PATH="/usr/share/backintime/common"
124+fi
125+
126+python $APP_PATH/askpass.py "$@"
127+
128
129=== added file 'common/backintime.desktop'
130--- common/backintime.desktop 1970-01-01 00:00:00 +0000
131+++ common/backintime.desktop 2013-03-07 23:23:23 +0000
132@@ -0,0 +1,8 @@
133+[Desktop Entry]
134+Version=1.0
135+Name=Backintime Password Cache
136+Exec=backintime --pw-cache start
137+Comment=Cache passwords for non-interactive Backintime cronjobs
138+Icon=gtk-save
139+Terminal=false
140+Type=Application
141
142=== modified file 'common/backintime.py'
143--- common/backintime.py 2012-10-21 13:35:30 +0000
144+++ common/backintime.py 2013-03-07 23:23:23 +0000
145@@ -28,6 +28,7 @@
146 import tools
147 import sshtools
148 import mount
149+import password
150
151 _=gettext.gettext
152
153@@ -46,6 +47,7 @@
154
155 def take_snapshot( cfg, force = True ):
156 logger.openlog()
157+ tools.load_env(cfg)
158 _mount(cfg)
159 snapshots.Snapshots( cfg ).take_snapshot( force )
160 _umount(cfg)
161@@ -105,6 +107,8 @@
162 print '\tUnmount the profile.'
163 print '--benchmark-cipher [file-size]'
164 print '\tShow a benchmark of all ciphers for ssh transfer (and exit)'
165+ print '--pw-cache [start|stop|restart|reload|status]'
166+ print '\tControl Password Cache for non-interactive cronjobs'
167 print '-v | --version'
168 print '\tShow version (and exit)'
169 print '--license'
170@@ -251,6 +255,37 @@
171 print('ssh is not configured !')
172 sys.exit(0)
173
174+ if arg == '--pw-cache':
175+ if not cfg.is_configured():
176+ print "The application is not configured !"
177+ sys.exit(0)
178+ else:
179+ daemon = password.Password_Cache(cfg)
180+ try:
181+ if sys.argv[index + 1].startswith('-'):
182+ daemon.run()
183+ elif 'start' == sys.argv[index + 1]:
184+ daemon.start()
185+ elif 'stop' == sys.argv[index + 1]:
186+ daemon.stop()
187+ elif 'restart' == sys.argv[index + 1]:
188+ daemon.restart()
189+ elif 'reload' == sys.argv[index + 1]:
190+ daemon.reload()
191+ elif 'status' == sys.argv[index + 1]:
192+ print 'Backintime Password Cache:',
193+ if daemon.status():
194+ print 'running'
195+ else:
196+ print 'not running'
197+ else:
198+ print "Unknown command"
199+ print "usage: %s %s start|stop|restart|reload|status" % (sys.argv[0], sys.argv[index])
200+ sys.exit(2)
201+ except IndexError:
202+ daemon.run()
203+ sys.exit(0)
204+
205 if arg == '--snapshots' or arg == '-s':
206 continue
207
208
209=== modified file 'common/config.py'
210--- common/config.py 2013-03-04 21:16:20 +0000
211+++ common/config.py 2013-03-07 23:23:23 +0000
212@@ -29,6 +29,7 @@
213 import mount
214 import sshtools
215 ##import dummytools
216+import password
217
218 _=gettext.gettext
219
220@@ -91,9 +92,11 @@
221 SNAPSHOT_MODES = {
222 #mode : (<mounttools>, _('ComboBox Text') ),
223 'local' : (None, _('Local') ),
224- 'ssh' : (sshtools.SSH, _('SSH (without password)') )
225+ 'ssh' : (sshtools.SSH, _('SSH') )
226 ## 'dummy' : (dummytools.Dummy, _('Dummy') )
227 }
228+
229+ SNAPSHOT_MODES_NEED_PASSWORD = ['ssh', 'dummy']
230
231 MOUNT_ROOT = '/tmp/backintime'
232
233@@ -219,6 +222,7 @@
234 self.save()
235
236 self.current_hash_id = 'local'
237+ self.pw = password.Password(self)
238
239 def save( self ):
240 configfile.ConfigFile.save( self, self._LOCAL_CONFIG_PATH )
241@@ -449,6 +453,26 @@
242 if len(path) == 0:
243 path = './'
244 return (host, port, user, path)
245+
246+ def get_ssh_private_key_file(self, profile_id = None):
247+ ssh = self.get_ssh_private_key_folder()
248+ default = ''
249+ for file in ['id_dsa', 'id_rsa', 'identity']:
250+ private_key = os.path.join(ssh, file)
251+ if os.path.isfile(private_key):
252+ default = private_key
253+ break
254+ file = self.get_profile_str_value( 'snapshots.ssh.private_key_file', default, profile_id )
255+ if len(file) == 0:
256+ return default
257+ else:
258+ return file
259+
260+ def get_ssh_private_key_folder(self):
261+ return os.path.join(os.path.expanduser('~'), '.ssh')
262+
263+ def set_ssh_private_key_file( self, value, profile_id = None ):
264+ self.set_profile_str_value( 'snapshots.ssh.private_key_file', value, profile_id )
265
266 ## def get_dummy_host( self, profile_id = None ):
267 ## return self.get_profile_str_value( 'snapshots.dummy.host', '', profile_id )
268@@ -468,6 +492,57 @@
269 ## def set_dummy_user( self, value, profile_id = None ):
270 ## self.set_profile_str_value( 'snapshots.dummy.user', value, profile_id )
271
272+ def get_password_save( self, profile_id = None, mode = None ):
273+ if mode is None:
274+ mode = self.get_snapshots_mode(profile_id)
275+ return self.get_profile_bool_value( 'snapshots.%s.password.save' % mode, True, profile_id )
276+
277+ def set_password_save( self, value, profile_id = None, mode = None ):
278+ if mode is None:
279+ mode = self.get_snapshots_mode(profile_id)
280+ self.set_profile_bool_value( 'snapshots.%s.password.save' % mode, value, profile_id )
281+
282+ def get_password_use_cache( self, profile_id = None, mode = None ):
283+ if mode is None:
284+ mode = self.get_snapshots_mode(profile_id)
285+ default = not tools.check_home_encrypt()
286+ return self.get_profile_bool_value( 'snapshots.%s.password.use_cache' % mode, default, profile_id )
287+
288+ def set_password_use_cache( self, value, profile_id = None, mode = None ):
289+ if mode is None:
290+ mode = self.get_snapshots_mode(profile_id)
291+ self.set_profile_bool_value( 'snapshots.%s.password.use_cache' % mode, value, profile_id )
292+
293+ def get_password( self, parent = None, profile_id = None, mode = None, only_from_keyring = False ):
294+ if profile_id is None:
295+ profile_id = self.get_current_profile()
296+ if mode is None:
297+ mode = self.get_snapshots_mode(profile_id)
298+ return self.pw.get_password(parent, profile_id, mode, only_from_keyring)
299+
300+ def set_password( self, password, profile_id = None, mode = None ):
301+ if profile_id is None:
302+ profile_id = self.get_current_profile()
303+ if mode is None:
304+ mode = self.get_snapshots_mode(profile_id)
305+ self.pw.set_password(password, profile_id, mode)
306+
307+ def get_keyring_backend(self):
308+ return self.get_str_value('keyring.backend', '')
309+
310+ def set_keyring_backend(self, value):
311+ self.set_str_value('keyring.backend', value)
312+
313+ def get_keyring_service_name( self, profile_id = None, mode = None ):
314+ if mode is None:
315+ mode = self.get_snapshots_mode(profile_id)
316+ return 'backintime/%s' % mode
317+
318+ def get_keyring_user_name( self, profile_id = None ):
319+ if profile_id is None:
320+ profile_id = self.get_current_profile()
321+ return 'profile_id_%s' % profile_id
322+
323 def get_auto_host_user_profile( self, profile_id = None ):
324 return self.get_profile_bool_value( 'snapshots.path.auto', True, profile_id )
325
326@@ -891,6 +966,18 @@
327 def get_take_snapshot_user_callback( self, profile_id = None ):
328 return os.path.join( self._LOCAL_CONFIG_FOLDER, "user-callback" )
329
330+ def get_password_cache_folder( self ):
331+ return os.path.join( self._LOCAL_DATA_FOLDER, "password_cache" )
332+
333+ def get_password_cache_pid( self ):
334+ return os.path.join( self.get_password_cache_folder(), "PID" )
335+
336+ def get_password_cache_fifo( self ):
337+ return os.path.join( self.get_password_cache_folder(), "FIFO" )
338+
339+ def get_cron_env_file( self ):
340+ return os.path.join( self._LOCAL_DATA_FOLDER, "cron_env" )
341+
342 def get_license( self ):
343 return tools.read_file( os.path.join( self.get_doc_path(), 'LICENSE' ), '' )
344
345
346=== modified file 'common/configfile.py'
347--- common/configfile.py 2012-10-12 07:09:14 +0000
348+++ common/configfile.py 2013-03-07 23:23:23 +0000
349@@ -61,11 +61,11 @@
350 except:
351 pass
352
353- def load( self, filename ):
354+ def load( self, filename, **kwargs ):
355 self.dict = {}
356- self.append( filename )
357+ self.append( filename, **kwargs )
358
359- def append( self, filename ):
360+ def append( self, filename, maxsplit = -1 ):
361 lines = []
362
363 try:
364@@ -76,7 +76,7 @@
365 pass
366
367 for line in lines:
368- items = line.split( '=' )
369+ items = line.split( '=', maxsplit )
370 if len( items ) == 2:
371 self.dict[ items[ 0 ] ] = items[ 1 ][ : -1 ]
372
373@@ -137,6 +137,8 @@
374 for key in remove_keys:
375 del self.dict[ key ]
376
377+ def get_keys(self):
378+ return self.dict.keys()
379
380 class ConfigFileWithProfiles( ConfigFile ):
381 def __init__( self, default_profile_name = '' ):
382
383=== added file 'common/debian_specific/conffiles'
384--- common/debian_specific/conffiles 1970-01-01 00:00:00 +0000
385+++ common/debian_specific/conffiles 2013-03-07 23:23:23 +0000
386@@ -0,0 +1,1 @@
387+/etc/xdg/autostart/backintime.desktop
388
389=== modified file 'common/debian_specific/control'
390--- common/debian_specific/control 2013-01-01 20:26:37 +0000
391+++ common/debian_specific/control 2013-03-07 23:23:23 +0000
392@@ -6,7 +6,7 @@
393 Maintainer: BIT Team <bit-team@lists.launchpad.net>
394 Homepage: http://backintime.le-web.org
395 Architecture: all
396-Depends: python, rsync, cron, openssh-client, sshfs
397+Depends: python, rsync, cron, openssh-client, sshfs, python-keyring
398 Recommends: powermgmt-base
399 Conflicts: backintime
400 Replaces: backintime
401
402=== modified file 'common/debian_specific/rules'
403--- common/debian_specific/rules 2010-03-01 13:22:45 +0000
404+++ common/debian_specific/rules 2013-03-07 23:23:23 +0000
405@@ -38,6 +38,10 @@
406 #install application
407 #dh_installdirs /usr/bin
408 dh_install backintime /usr/bin
409+ dh_install backintime-askpass /usr/bin
410+
411+ #install autostart
412+ dh_install backintime.desktop /etc/xdg/autostart
413
414 #install langs
415 [INSTALL_LANGS]
416
417=== modified file 'common/dummytools.py'
418--- common/dummytools.py 2012-10-23 19:26:20 +0000
419+++ common/dummytools.py 2013-03-07 23:23:23 +0000
420@@ -1,4 +1,4 @@
421-# Copyright (c) 2012 Germar Reitze
422+# Copyright (c) 2012-2013 Germar Reitze
423 #
424 # This program is free software; you can redistribute it and/or modify
425 # it under the terms of the GNU General Public License as published by
426@@ -35,17 +35,18 @@
427 be used exactly like they were in this class.
428 Methodes from MountControl also can be overriden in here if you need
429 something different."""
430- def __init__(self, cfg = None, profile_id = None, hash_id = None, tmp_mount = False, **kwargs):
431+ def __init__(self, cfg = None, profile_id = None, hash_id = None, tmp_mount = False, parent = None **kwargs):
432 self.config = cfg
433 if self.config is None:
434 self.config = config.Config()
435
436 self.profile_id = profile_id
437- if not self.profile_id:
438+ if self.profile_id is None:
439 self.profile_id = self.config.get_current_profile()
440
441 self.tmp_mount = tmp_mount
442 self.hash_id = hash_id
443+ self.parent = parent
444
445 #init MountControl
446 mount.MountControl.__init__(self)
447@@ -62,6 +63,7 @@
448 self.setattr_kwargs('user', self.config.get_dummy_user(self.profile_id), **kwargs)
449 self.setattr_kwargs('host', self.config.get_dummy_host(self.profile_id), **kwargs)
450 self.setattr_kwargs('port', self.config.get_dummy_port(self.profile_id), **kwargs)
451+ self.setattr_kwargs('password', self.config.get_password(parent, self.profile_id), store = False, **kwargs)
452
453 self.set_default_args()
454
455
456=== modified file 'common/makedeb-src.sh'
457--- common/makedeb-src.sh 2012-11-17 13:45:00 +0000
458+++ common/makedeb-src.sh 2013-03-07 23:23:23 +0000
459@@ -17,6 +17,10 @@
460
461 #app
462 cp backintime $DEST/
463+cp backintime-askpass $DEST/
464+
465+#autostart
466+cp backintime.desktop $DEST/
467
468 #python files
469 cp *.py $DEST/
470@@ -44,6 +48,9 @@
471 #debian: rules
472 cp debian_specific/rules $DEST/debian
473
474+#debian: conffiles
475+cp debian_specific/conffiles $DEST/debian
476+
477 #add languages to rules
478 for langfile in `ls po/*.po`; do
479 lang=`echo $langfile | cut -d/ -f2 | cut -d. -f1`
480
481=== modified file 'common/man/C/backintime.1'
482--- common/man/C/backintime.1 2013-01-01 20:26:37 +0000
483+++ common/man/C/backintime.1 2013-03-07 23:23:23 +0000
484@@ -83,9 +83,11 @@
485 To prepare your user account for ssh-mode you have to add the user to group 'fuse' by typing 'sudo adduser <USER> fuse' in terminal. To apply changes you have to logout and login again.
486 .PP
487 Next you have to create a password-less login to the remote host (for further informations look at http://www.debian-administration.org/articles/152).
488-Type in terminal 'ssh-keygen -t rsa' hit enter for default path and keep the passphrase empty.
489-.PP
490-Finally type 'ssh-copy-id -i ~/.ssh/id_rsa.pub <REMOTE_USER>@<HOST>' and enter your password on remote host.
491+Type in terminal 'ssh-keygen -t dsa' hit enter for default path and enter a passphrase for the private key.
492+.PP
493+Finally type 'ssh-copy-id -i ~/.ssh/id_dsa.pub <REMOTE_USER>@<HOST>' and enter your password on remote host.
494+.PP
495+In Settingsdialog you need to set the host and remote user. If you enter a relative path (no leading / ) it will start from remote users homedir. The password has to be the passphrase for your private key.
496 .PP
497 .B Cipher
498 (the algorithm used to encrypt the data during transfer)
499
500=== modified file 'common/mount.py'
501--- common/mount.py 2012-10-23 19:26:20 +0000
502+++ common/mount.py 2013-03-07 23:23:23 +0000
503@@ -1,4 +1,4 @@
504-# Copyright (c) 2012 Germar Reitze
505+# Copyright (c) 2012-2013 Germar Reitze
506 #
507 # This program is free software; you can redistribute it and/or modify
508 # it under the terms of the GNU General Public License as published by
509@@ -24,6 +24,7 @@
510
511 import config
512 import logger
513+import tools
514
515 _=gettext.gettext
516
517@@ -34,7 +35,7 @@
518 pass
519
520 class Mount(object):
521- def __init__(self, cfg = None, profile_id = None, tmp_mount = False):
522+ def __init__(self, cfg = None, profile_id = None, tmp_mount = False, parent = None):
523 self.config = cfg
524 if self.config is None:
525 self.config = config.Config()
526@@ -44,6 +45,7 @@
527 self.profile_id = self.config.get_current_profile()
528
529 self.tmp_mount = tmp_mount
530+ self.parent = parent
531
532 def mount(self, mode = None, check = True, **kwargs):
533 if mode is None:
534@@ -56,7 +58,9 @@
535 while True:
536 try:
537 mounttools = self.config.SNAPSHOT_MODES[mode][0]
538- tools = mounttools(cfg = self.config, profile_id = self.profile_id, tmp_mount = self.tmp_mount, mode = mode, **kwargs)
539+ tools = mounttools(cfg = self.config, profile_id = self.profile_id,
540+ tmp_mount = self.tmp_mount, mode = mode,
541+ parent = self.parent, **kwargs)
542 return tools.mount(check = check)
543 except HashCollision as ex:
544 logger.warning(str(ex))
545@@ -79,7 +83,9 @@
546 kwargs = json.loads(data_string)
547 mode = kwargs.pop('mode')
548 mounttools = self.config.SNAPSHOT_MODES[mode][0]
549- tools = mounttools(cfg = self.config, profile_id = self.profile_id, tmp_mount = self.tmp_mount, mode = mode, hash_id = hash_id, **kwargs)
550+ tools = mounttools(cfg = self.config, profile_id = self.profile_id,
551+ tmp_mount = self.tmp_mount, mode = mode,
552+ hash_id = hash_id, parent = self.parent, **kwargs)
553 tools.umount()
554
555 def pre_mount_check(self, mode = None, first_run = False, **kwargs):
556@@ -93,7 +99,9 @@
557 return True
558 else:
559 mounttools = self.config.SNAPSHOT_MODES[mode][0]
560- tools = mounttools(cfg = self.config, profile_id = self.profile_id, tmp_mount = self.tmp_mount, mode = mode, **kwargs)
561+ tools = mounttools(cfg = self.config, profile_id = self.profile_id,
562+ tmp_mount = self.tmp_mount, mode = mode,
563+ parent = self.parent, **kwargs)
564 return tools.pre_mount_check(first_run)
565
566 def remount(self, new_profile_id, mode = None, hash_id = None, **kwargs):
567@@ -117,7 +125,9 @@
568 return self.mount(mode = mode, **kwargs)
569
570 mounttools = self.config.SNAPSHOT_MODES[mode][0]
571- tools = mounttools(cfg = self.config, profile_id = new_profile_id, tmp_mount = self.tmp_mount, mode = mode, **kwargs)
572+ tools = mounttools(cfg = self.config, profile_id = new_profile_id,
573+ tmp_mount = self.tmp_mount, mode = mode,
574+ parent = self.parent, **kwargs)
575 if tools.compare_remount(hash_id):
576 #profiles uses the same settings. just swap the symlinks
577 tools.remove_symlink(profile_id = self.profile_id)
578@@ -214,14 +224,12 @@
579
580 def is_mounted(self):
581 """return True if path is is already mounted"""
582- try:
583- subprocess.check_call(['mountpoint', self.mountpoint], stdout=open(os.devnull, 'w'))
584- except subprocess.CalledProcessError:
585+ if tools.check_mountpoint(self.mountpoint):
586+ return True
587+ else:
588 if len(os.listdir(self.mountpoint)) > 0:
589 raise MountException( _('mountpoint %s not empty.') % self.mountpoint)
590 return False
591- else:
592- return True
593
594 def create_mountstructure(self):
595 """ folder structure in /tmp/backintime/<user>/:
596@@ -331,7 +339,7 @@
597 os.remove(os.path.join(self.mount_user_path, symlink))
598 return False
599
600- def setattr_kwargs(self, arg, default, **kwargs):
601+ def setattr_kwargs(self, arg, default, store = True, **kwargs):
602 """if kwargs[arg] exist set self.<arg> to kwargs[arg]
603 else set self.<arg> to default which should be the value from config"""
604 if arg in kwargs:
605@@ -339,8 +347,9 @@
606 else:
607 value = default
608 setattr(self, arg, value)
609- #make dictionary with all used args for umount
610- self.all_kwargs[arg] = value
611+ if store:
612+ #make dictionary with all used args for umount
613+ self.all_kwargs[arg] = value
614
615 def write_umount_info(self):
616 """dump dictionary self.all_kwargs to umount_info file"""
617@@ -393,6 +402,8 @@
618 src = mountpoint
619 else:
620 src = os.path.join(mountpoint, self.symlink_subfolder)
621+ if os.path.exists(dst):
622+ os.remove(dst)
623 os.symlink(src, dst)
624
625 def remove_symlink(self, profile_id = None, tmp_mount = None):
626
627=== added file 'common/password.py'
628--- common/password.py 1970-01-01 00:00:00 +0000
629+++ common/password.py 2013-03-07 23:23:23 +0000
630@@ -0,0 +1,446 @@
631+# Copyright (c) 2012-2013 Germar Reitze
632+#
633+# This program is free software; you can redistribute it and/or modify
634+# it under the terms of the GNU General Public License as published by
635+# the Free Software Foundation; either version 2 of the License, or
636+# (at your option) any later version.
637+#
638+# This program is distributed in the hope that it will be useful,
639+# but WITHOUT ANY WARRANTY; without even the implied warranty of
640+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
641+# GNU General Public License for more details.
642+#
643+# You should have received a copy of the GNU General Public License along
644+# with this program; if not, write to the Free Software Foundation, Inc.,
645+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
646+
647+import sys
648+import os
649+import time
650+import atexit
651+import signal
652+import base64
653+import subprocess
654+import gettext
655+import keyring
656+
657+import config
658+import configfile
659+import tools
660+import password_ipc
661+
662+_=gettext.gettext
663+
664+class Daemon:
665+ """
666+ A generic daemon class.
667+
668+ Usage: subclass the Daemon class and override the run() method
669+
670+ Daemon Copyright by Sander Marechal
671+ License CC BY-SA 3.0
672+ http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/
673+ """
674+ def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/stdout', stderr='/dev/null'):
675+ self.stdin = stdin
676+ self.stdout = stdout
677+ self.stderr = stderr
678+ self.pidfile = pidfile
679+
680+ def daemonize(self):
681+ """
682+ do the UNIX double-fork magic, see Stevens' "Advanced
683+ Programming in the UNIX Environment" for details (ISBN 0201563177)
684+ http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
685+ """
686+ try:
687+ pid = os.fork()
688+ if pid > 0:
689+ # exit first parent
690+ sys.exit(0)
691+ except OSError, e:
692+ sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
693+ sys.exit(1)
694+
695+ # decouple from parent environment
696+ os.chdir("/")
697+ os.setsid()
698+ os.umask(0)
699+
700+ # do second fork
701+ try:
702+ pid = os.fork()
703+ if pid > 0:
704+ # exit from second parent
705+ sys.exit(0)
706+ except OSError, e:
707+ sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
708+ sys.exit(1)
709+
710+ # redirect standard file descriptors
711+ sys.stdout.flush()
712+ sys.stderr.flush()
713+ si = file(self.stdin, 'r')
714+ so = file(self.stdout, 'a+')
715+ se = file(self.stderr, 'a+', 0)
716+ os.dup2(si.fileno(), sys.stdin.fileno())
717+ os.dup2(so.fileno(), sys.stdout.fileno())
718+ os.dup2(se.fileno(), sys.stderr.fileno())
719+
720+ # write pidfile
721+ sys.stdout.write('write pidfile\n')
722+ atexit.register(self.delpid)
723+ signal.signal(signal.SIGTERM, self._cleanup_handler)
724+ pid = str(os.getpid())
725+ with open(self.pidfile, 'w+') as pidfile:
726+ pidfile.write("%s\n" % pid)
727+ os.chmod(self.pidfile, 0600)
728+
729+ def _cleanup_handler(self, signum, frame):
730+ self.fifo.delfifo()
731+ self.delpid()
732+ sys.exit(0)
733+
734+ def delpid(self):
735+ try:
736+ os.remove(self.pidfile)
737+ except:
738+ pass
739+
740+ def start(self):
741+ """
742+ Start the daemon
743+ """
744+ # Check for a pidfile to see if the daemon already runs
745+ try:
746+ pf = file(self.pidfile,'r')
747+ pid = int(pf.read().strip())
748+ pf.close()
749+ except IOError:
750+ pid = None
751+
752+ if pid:
753+ if tools.is_process_alive(pid):
754+ message = "pidfile %s already exist. Daemon already running?\n"
755+ sys.stderr.write(message % self.pidfile)
756+ sys.exit(1)
757+ else:
758+ self.delpid()
759+
760+ # Start the daemon
761+ self.daemonize()
762+ self.run()
763+
764+ def stop(self):
765+ """
766+ Stop the daemon
767+ """
768+ # Get the pid from the pidfile
769+ try:
770+ pf = file(self.pidfile,'r')
771+ pid = int(pf.read().strip())
772+ pf.close()
773+ except IOError:
774+ pid = None
775+
776+ if not pid:
777+ message = "pidfile %s does not exist. Daemon not running?\n"
778+ sys.stderr.write(message % self.pidfile)
779+ return # not an error in a restart
780+
781+ # Try killing the daemon process
782+ try:
783+ while 1:
784+ os.kill(pid, signal.SIGTERM)
785+ time.sleep(0.1)
786+ except OSError, err:
787+ if err.errno == 3:
788+ if os.path.exists(self.pidfile):
789+ os.remove(self.pidfile)
790+ else:
791+ print err.strerror
792+ sys.exit(1)
793+
794+ def restart(self):
795+ """
796+ Restart the daemon
797+ """
798+ self.stop()
799+ self.start()
800+
801+ def reload(self):
802+ """
803+ send SIGHUP signal to process
804+ """
805+ # Get the pid from the pidfile
806+ try:
807+ pf = file(self.pidfile,'r')
808+ pid = int(pf.read().strip())
809+ pf.close()
810+ except IOError:
811+ pid = None
812+
813+ if not pid:
814+ message = "pidfile %s does not exist. Daemon not running?\n"
815+ sys.stderr.write(message % self.pidfile)
816+ return
817+
818+ # Try killing the daemon process
819+ try:
820+ os.kill(pid, signal.SIGHUP)
821+ except OSError, err:
822+ if err.errno == 3:
823+ if os.path.exists(self.pidfile):
824+ os.remove(self.pidfile)
825+ else:
826+ sys.stderr.write(err.strerror)
827+ sys.exit(1)
828+
829+ def status(self):
830+ """
831+ return status
832+ """
833+ # Get the pid from the pidfile
834+ try:
835+ pf = file(self.pidfile,'r')
836+ pid = int(pf.read().strip())
837+ pf.close()
838+ except IOError:
839+ pid = None
840+
841+ if not pid:
842+ return False
843+
844+ # Try killing the daemon process
845+ try:
846+ os.kill(pid, 0)
847+ except OSError, err:
848+ if err.errno == 3:
849+ if os.path.exists(self.pidfile):
850+ os.remove(self.pidfile)
851+ return False
852+ else:
853+ sys.stderr.write(err.strerror)
854+ return False
855+ return True
856+
857+ def run(self):
858+ """
859+ You should override this method when you subclass Daemon. It will be called after the process has been
860+ daemonized by start() or restart().
861+ """
862+ pass
863+
864+class Password_Cache(Daemon):
865+ """
866+ Password_Cache get started on User login. It provides passwords for
867+ BIT cronjobs because keyring is not available when the User is not
868+ logged in. Does not start if there is no password to cache
869+ (e.g. no profile allows to cache).
870+ """
871+ def __init__(self, cfg = None, *args, **kwargs):
872+ self.config = cfg
873+ if self.config is None:
874+ self.config = config.Config()
875+ pw_cache_path = self.config.get_password_cache_folder()
876+ if not os.path.isdir(pw_cache_path):
877+ os.mkdir(pw_cache_path, 0700)
878+ else:
879+ os.chmod(pw_cache_path, 0700)
880+ Daemon.__init__(self, self.config.get_password_cache_pid(), *args, **kwargs)
881+ self.db = {}
882+ self.fifo = password_ipc.FIFO(self.config.get_password_cache_fifo())
883+
884+ if self.config.get_keyring_backend() == 'kde':
885+ keyring.set_keyring(keyring.backend.KDEKWallet())
886+ else:
887+ keyring.set_keyring(keyring.backend.GnomeKeyring())
888+
889+ def run(self):
890+ """
891+ wait for password request on FIFO and answer with password
892+ from self.db through FIFO.
893+ """
894+ tools.save_env(self.config)
895+ if tools.check_home_encrypt():
896+ sys.stdout.write('Home is encrypt. Doesn\'t make sense to cache passwords. Quit.')
897+ sys.exit(0)
898+ self._collect_passwords()
899+ if len(self.db) == 0:
900+ sys.stdout.write('Nothing to cache. Quit.')
901+ sys.exit(0)
902+ self.fifo.create()
903+ atexit.register(self.fifo.delfifo)
904+ signal.signal(signal.SIGHUP, self._reload_handler)
905+ while True:
906+ try:
907+ request = self.fifo.read()
908+ request = request.split('\n')[0]
909+ if request in self.db.keys():
910+ answer = self.db[request]
911+ else:
912+ answer = ''
913+ self.fifo.write(answer, 5)
914+ except IOError as e:
915+ sys.stderr.write('Error in writing answer to FIFO: %s\n' % e.strerror)
916+ except KeyboardInterrupt:
917+ print('Quit.')
918+ break
919+ except tools.Timeout:
920+ sys.stderr.write('FIFO timeout\n')
921+ except StandardError as e:
922+ sys.stderr.write('ERROR: %s\n' % str(e))
923+
924+ def _reload_handler(self, signum, frame):
925+ """
926+ reload passwords during runtime.
927+ """
928+ sys.stdout.write('Reloading\n')
929+ del(self.db)
930+ self.db = {}
931+ self._collect_passwords()
932+
933+ def _collect_passwords(self):
934+ """
935+ search all profiles in config and collect passwords from keyring.
936+ """
937+ profiles = self.config.get_profiles()
938+ for profile_id in profiles:
939+ mode = self.config.get_snapshots_mode(profile_id)
940+ if mode in self.config.SNAPSHOT_MODES_NEED_PASSWORD:
941+ if self.config.get_password_save(profile_id):
942+ if self.config.get_password_use_cache(profile_id):
943+ service_name = self.config.get_keyring_service_name(profile_id, mode)
944+ user_name = self.config.get_keyring_user_name(profile_id)
945+
946+ password = keyring.get_password(service_name, user_name)
947+ if password is None:
948+ continue
949+ #add some snakeoil
950+ pw_base64 = base64.encodestring(password)
951+ self.db[profile_id] = pw_base64
952+
953+class Password(object):
954+ """
955+ provide passwords for BIT either from keyring, Password_Cache or
956+ by asking User.
957+ """
958+ def __init__(self, cfg = None):
959+ self.config = cfg
960+ if self.config is None:
961+ self.config = config.Config()
962+ self.pw_cache = Password_Cache(self.config)
963+ self.fifo = password_ipc.FIFO(self.config.get_password_cache_fifo())
964+ self.db = {}
965+
966+ if self.config.get_keyring_backend() == 'kwallet':
967+ keyring.set_keyring(keyring.backend.KDEKWallet())
968+ else:
969+ keyring.set_keyring(keyring.backend.GnomeKeyring())
970+
971+ def get_password(self, parent, profile_id, mode, only_from_keyring = False):
972+ """
973+ based on profile settings return password from keyring,
974+ Password_Cache or by asking User.
975+ """
976+ if not mode in self.config.SNAPSHOT_MODES_NEED_PASSWORD:
977+ return ''
978+ try:
979+ return self.db[profile_id][mode]
980+ except KeyError:
981+ pass
982+ if self.config.get_password_save(profile_id):
983+ if self.config.get_password_use_cache(profile_id) and not only_from_keyring:
984+ password = self._get_password_from_pw_cache(profile_id)
985+ else:
986+ password = self._get_password_from_keyring(profile_id, mode)
987+ else:
988+ if not only_from_keyring:
989+ password = self._get_password_from_user(parent, profile_id, mode)
990+ else:
991+ password = ''
992+ self._set_password_db(profile_id, mode, password)
993+ return password
994+
995+ def _get_password_from_keyring(self, profile_id, mode):
996+ """
997+ get password from system keyring (seahorse). The keyring is only
998+ available if User is logged in.
999+ """
1000+ service_name = self.config.get_keyring_service_name(profile_id, mode)
1001+ user_name = self.config.get_keyring_user_name(profile_id)
1002+ return keyring.get_password(service_name, user_name)
1003+
1004+ def _get_password_from_pw_cache(self, profile_id):
1005+ """
1006+ get password from Password_Cache
1007+ """
1008+ if self.pw_cache:
1009+ self.fifo.write(profile_id, timeout = 5)
1010+ pw_base64 = self.fifo.read(timeout = 5)
1011+ return base64.decodestring(pw_base64)
1012+ else:
1013+ return ''
1014+
1015+ def _get_password_from_user(self, parent, profile_id, mode):
1016+ """
1017+ ask user for password. This does even work when run as cronjob
1018+ and user is logged in.
1019+ """
1020+ prompt = _('Enter Password for profile \'%(profile)s\' mode %(mode)s: ') % {'profile': self.config.get_profile_name(profile_id), 'mode': self.config.SNAPSHOT_MODES[mode][1]}
1021+
1022+ gnome = os.path.join(self.config.get_app_path(), 'gnome')
1023+ kde = os.path.join(self.config.get_app_path(), 'kde4')
1024+ for path in (gnome, kde):
1025+ if os.path.isdir(path):
1026+ sys.path = [path] + sys.path
1027+ break
1028+
1029+ x_server = tools.check_x_server()
1030+ import_successful = False
1031+ if x_server:
1032+ try:
1033+ import messagebox
1034+ import_successful = True
1035+ except ImportError:
1036+ pass
1037+
1038+ if not import_successful or not x_server:
1039+ import getpass
1040+ alarm = tools.Alarm()
1041+ alarm.start(300)
1042+ try:
1043+ password = getpass.getpass(prompt)
1044+ alarm.stop()
1045+ except tools.Timeout:
1046+ password = ''
1047+ return password
1048+
1049+ password = messagebox.ask_password_dialog(parent, self.config, 'Back in Time',
1050+ prompt = prompt,
1051+ timeout = 300)
1052+ return password
1053+
1054+ def _set_password_db(self, profile_id, mode, password):
1055+ """
1056+ internal Password cache. Prevent to ask password several times
1057+ during runtime.
1058+ """
1059+ if not profile_id in self.db.keys():
1060+ self.db[profile_id] = {}
1061+ self.db[profile_id][mode] = password
1062+
1063+ def set_password(self, password, profile_id, mode):
1064+ """
1065+ store password to keyring (seahorse). If caching is allowed
1066+ reload Password_Cache
1067+ """
1068+ if mode in self.config.SNAPSHOT_MODES_NEED_PASSWORD:
1069+ service_name = self.config.get_keyring_service_name(profile_id, mode)
1070+ user_name = self.config.get_keyring_user_name(profile_id)
1071+ if self.config.get_password_save(profile_id):
1072+ keyring.set_password(service_name, user_name, password)
1073+ if self.config.get_password_use_cache(profile_id):
1074+ if self.pw_cache.status():
1075+ self.pw_cache.reload()
1076+ self._set_password_db(profile_id, mode, password)
1077
1078=== added file 'common/password_ipc.py'
1079--- common/password_ipc.py 1970-01-01 00:00:00 +0000
1080+++ common/password_ipc.py 2013-03-07 23:23:23 +0000
1081@@ -0,0 +1,112 @@
1082+# Copyright (c) 2012-2013 Germar Reitze
1083+#
1084+# This program is free software; you can redistribute it and/or modify
1085+# it under the terms of the GNU General Public License as published by
1086+# the Free Software Foundation; either version 2 of the License, or
1087+# (at your option) any later version.
1088+#
1089+# This program is distributed in the hope that it will be useful,
1090+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1091+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1092+# GNU General Public License for more details.
1093+#
1094+# You should have received a copy of the GNU General Public License along
1095+# with this program; if not, write to the Free Software Foundation, Inc.,
1096+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
1097+
1098+import os
1099+import sys
1100+import stat
1101+import signal
1102+import tools
1103+import threading
1104+import base64
1105+
1106+class FIFO(object):
1107+ """
1108+ interprocess-communication with named pipes
1109+ """
1110+ def __init__(self, fname):
1111+ self.fifo = fname
1112+ self.alarm = tools.Alarm()
1113+
1114+ def delfifo(self):
1115+ """
1116+ remove FIFO
1117+ """
1118+ try:
1119+ os.remove(self.fifo)
1120+ except:
1121+ pass
1122+
1123+ def create(self):
1124+ """
1125+ create the FIFO in a way that only the current user can access it.
1126+ """
1127+ if os.path.exists(self.fifo):
1128+ self.delfifo()
1129+ try:
1130+ os.mkfifo(self.fifo, 0600)
1131+ except OSError, e:
1132+ sys.stderr.write('Failed to create FIFO: %s\n' % e.strerror)
1133+ sys.exit(1)
1134+
1135+ def read(self, timeout = 0):
1136+ """
1137+ read from fifo untill timeout. If timeout is 0 it will wait forever
1138+ for input.
1139+ """
1140+ #sys.stdout.write('read fifo\n')
1141+ if not self.is_fifo():
1142+ sys.stderr.write('%s is not a FIFO\n' % self.fifo)
1143+ sys.exit(1)
1144+ self.alarm.start(timeout)
1145+ with open(self.fifo, 'r') as fifo:
1146+ ret = fifo.read()
1147+ self.alarm.stop()
1148+ return ret
1149+
1150+ def write(self, string, timeout = 0):
1151+ """
1152+ write to fifo untill timeout. If timeout is 0 it will wait forever
1153+ for an other process that will read this.
1154+ """
1155+ #sys.stdout.write('write fifo\n')
1156+ if not self.is_fifo():
1157+ sys.stderr.write('%s is not a FIFO\n' % self.fifo)
1158+ sys.exit(1)
1159+ self.alarm.start(timeout)
1160+ with open(self.fifo, 'a') as fifo:
1161+ fifo.write(string)
1162+ self.alarm.stop()
1163+
1164+ def is_fifo(self):
1165+ """
1166+ make sure file is still a FIFO
1167+ """
1168+ try:
1169+ return stat.S_ISFIFO(os.stat(self.fifo).st_mode)
1170+ except OSError:
1171+ return False
1172+
1173+class TempPasswordThread(threading.Thread):
1174+ """
1175+ in case BIT is not configured yet provide password through temp FIFO
1176+ to backintime-askpass.
1177+ """
1178+ def __init__(self, string, temp_file):
1179+ threading.Thread.__init__(self)
1180+ self.pw_base64 = base64.encodestring(string)
1181+ self.fifo = FIFO(temp_file)
1182+
1183+ def run(self):
1184+ self.fifo.create()
1185+ self.fifo.write(self.pw_base64)
1186+ self.fifo.delfifo()
1187+
1188+ def read(self):
1189+ """
1190+ read fifo to end the blocking fifo.write
1191+ use only if thread timeout.
1192+ """
1193+ self.fifo.read()
1194\ No newline at end of file
1195
1196=== modified file 'common/sshtools.py'
1197--- common/sshtools.py 2013-02-20 16:37:48 +0000
1198+++ common/sshtools.py 2013-03-07 23:23:23 +0000
1199@@ -1,4 +1,4 @@
1200-# Copyright (c) 2012 Germar Reitze
1201+# Copyright (c) 2012-2013 Germar Reitze
1202 #
1203 # This program is free software; you can redistribute it and/or modify
1204 # it under the terms of the GNU General Public License as published by
1205@@ -22,11 +22,13 @@
1206 import random
1207 import tempfile
1208 from time import sleep
1209+import threading
1210
1211 import config
1212 import mount
1213 import logger
1214 import tools
1215+import password_ipc
1216
1217 _=gettext.gettext
1218
1219@@ -35,17 +37,18 @@
1220 Mount remote path with sshfs. The real take_snapshot process will use
1221 rsync over ssh. Other commands run remote over ssh.
1222 """
1223- def __init__(self, cfg = None, profile_id = None, hash_id = None, tmp_mount = False, **kwargs):
1224+ def __init__(self, cfg = None, profile_id = None, hash_id = None, tmp_mount = False, parent = None, **kwargs):
1225 self.config = cfg
1226 if self.config is None:
1227 self.config = config.Config()
1228
1229 self.profile_id = profile_id
1230- if not self.profile_id:
1231+ if self.profile_id is None:
1232 self.profile_id = self.config.get_current_profile()
1233
1234 self.tmp_mount = tmp_mount
1235 self.hash_id = hash_id
1236+ self.parent = parent
1237
1238 #init MountControl
1239 mount.MountControl.__init__(self)
1240@@ -61,6 +64,8 @@
1241 self.setattr_kwargs('port', self.config.get_ssh_port(self.profile_id), **kwargs)
1242 self.setattr_kwargs('path', self.config.get_snapshots_path_ssh(self.profile_id), **kwargs)
1243 self.setattr_kwargs('cipher', self.config.get_ssh_cipher(self.profile_id), **kwargs)
1244+ self.setattr_kwargs('private_key_file', self.config.get_ssh_private_key_file(self.profile_id), **kwargs)
1245+ self.setattr_kwargs('password', None, store = False, **kwargs)
1246
1247 if len(self.path) == 0:
1248 self.path = './'
1249@@ -70,6 +75,8 @@
1250 self.user_host_path = '%s@%s:%s' % (self.user, self.host, self.path)
1251 self.log_command = '%s: %s' % (self.mode, self.user_host_path)
1252
1253+ self.unlock_ssh_agent()
1254+
1255 def _mount(self):
1256 """mount the service"""
1257 sshfs = ['sshfs', '-p', str(self.port)]
1258@@ -125,6 +132,59 @@
1259 raise MountException( _('Error discription') ) if not"""
1260 return True
1261
1262+ def unlock_ssh_agent(self):
1263+ """using askpass.py to unlock private key in ssh-agent"""
1264+ env = os.environ.copy()
1265+ env['SSH_ASKPASS'] = 'backintime-askpass'
1266+ env['ASKPASS_PROFILE_ID'] = self.profile_id
1267+ env['ASKPASS_MODE'] = self.mode
1268+
1269+ output = subprocess.Popen(['ssh-add', '-l'], stdout = subprocess.PIPE).communicate()[0]
1270+ if not output.find(self.private_key_file) >= 0:
1271+ if not self.config.get_password_save(self.profile_id) and not tools.check_x_server():
1272+ #we need to unlink stdin from ssh-add in order to make it
1273+ #use our own backintime-askpass.
1274+ #But because of this we can NOT use getpass inside backintime-askpass
1275+ #if password is not saved and there is no x-server.
1276+ #So, let's just keep ssh-add asking for the password in that case.
1277+ alarm = tools.Alarm()
1278+ alarm.start(10)
1279+ try:
1280+ proc = subprocess.call(['ssh-add', self.private_key_file])
1281+ alarm.stop()
1282+ except tools.Timeout:
1283+ pass
1284+ else:
1285+ if not self.password is None:
1286+ #write password directly to temp FIFO
1287+ temp_file = os.path.join(tempfile.mkdtemp(), 'FIFO')
1288+ env['ASKPASS_TEMP'] = temp_file
1289+ thread = password_ipc.TempPasswordThread(self.password, temp_file)
1290+ thread.start()
1291+
1292+ proc = subprocess.Popen(['ssh-add', self.private_key_file],
1293+ stdin=subprocess.PIPE,
1294+ stdout=subprocess.PIPE,
1295+ stderr=subprocess.PIPE,
1296+ env = env,
1297+ preexec_fn = os.setsid)
1298+ output, error = proc.communicate()
1299+ if proc.returncode:
1300+ print( _('Failed to unlock SSH private key:\nError: %s') % error)
1301+ output = subprocess.Popen(['ssh-add', '-l'], stdout = subprocess.PIPE).communicate()[0]
1302+ if not output.find(self.private_key_file) >= 0:
1303+ raise mount.MountException( _('Could not unlock ssh private key. Wrong password or password not available for cron.'))
1304+
1305+ if not self.password is None:
1306+ thread.join(5)
1307+ if thread.isAlive():
1308+ #threading does not support signal.alarm
1309+ thread.read()
1310+ try:
1311+ os.rmdir(os.path.dirname(temp_file))
1312+ except OSError:
1313+ pass
1314+
1315 def check_fuse(self):
1316 """check if sshfs is installed and user is part of group fuse"""
1317 if not self.pathexists('sshfs'):
1318
1319=== modified file 'common/tools.py'
1320--- common/tools.py 2012-10-19 15:47:27 +0000
1321+++ common/tools.py 2013-03-07 23:23:23 +0000
1322@@ -22,7 +22,9 @@
1323 import subprocess
1324 import hashlib
1325 import commands
1326+import signal
1327
1328+import configfile
1329
1330 ON_AC = 0
1331 ON_BATTERY = 1
1332@@ -420,8 +422,73 @@
1333 except ValueError:
1334 return False
1335
1336-#
1337-#
1338+def check_mountpoint(path):
1339+ '''return True if path is a mountpoint'''
1340+ try:
1341+ subprocess.check_call(['mountpoint', path], stdout=open(os.devnull, 'w'))
1342+ except subprocess.CalledProcessError:
1343+ return False
1344+ return True
1345+
1346+def check_home_encrypt():
1347+ '''return True if users home is encrypted'''
1348+ home = os.path.expanduser('~')
1349+ if not check_mountpoint(home):
1350+ return False
1351+ if check_command('ecryptfs-verify'):
1352+ try:
1353+ subprocess.check_call(['ecryptfs-verify', '--home'],
1354+ stdout=open(os.devnull, 'w'),
1355+ stderr=open(os.devnull, 'w'))
1356+ except subprocess.CalledProcessError:
1357+ pass
1358+ else:
1359+ return True
1360+ if check_command('encfs'):
1361+ mount = subprocess.Popen(['mount'], stdout=subprocess.PIPE).communicate()[0]
1362+ for line in mount.split('\n'):
1363+ line_split = line.split(' ')
1364+ if len(line_split) < 5:
1365+ continue
1366+ if not line_split[2] == home:
1367+ continue
1368+ if line_split[4] == 'fuse.encfs':
1369+ return True
1370+ return False
1371+
1372+def load_env(cfg):
1373+ env = os.environ.copy()
1374+ env_file = configfile.ConfigFile()
1375+ env_file.load(cfg.get_cron_env_file(), maxsplit = 1)
1376+ for key in env_file.get_keys():
1377+ value = env_file.get_str_value(key)
1378+ if not value:
1379+ continue
1380+ if not key in env.keys():
1381+ os.environ[key] = value
1382+ del(env_file)
1383+
1384+def save_env(cfg):
1385+ """
1386+ save environ variables to file that are needed by cron
1387+ to connect to keyring. This will only work if the user is logged in.
1388+ """
1389+ env = os.environ.copy()
1390+ env_file = configfile.ConfigFile()
1391+ #ubuntu
1392+ set_env_key(env, env_file, 'GNOME_KEYRING_CONTROL')
1393+ set_env_key(env, env_file, 'DBUS_SESSION_BUS_ADDRESS')
1394+ set_env_key(env, env_file, 'DISPLAY')
1395+ #debian
1396+ set_env_key(env, env_file, 'XAUTHORITY')
1397+
1398+ env_file.save(cfg.get_cron_env_file())
1399+ del(env_file)
1400+
1401+def set_env_key(env, env_file, key):
1402+ if key in env.keys():
1403+ env_file.set_str_value(key, env[key])
1404+
1405 class UniquenessSet:
1406 '''a class to check for uniqueness of snapshots of the same [item]'''
1407
1408@@ -474,3 +541,40 @@
1409 if verb: print " >> skip (it's a duplicate)"
1410 return False
1411
1412+class Timeout(Exception):
1413+ pass
1414+
1415+class Alarm(object):
1416+ """
1417+ Timeout for FIFO. This does not work with threading.
1418+ """
1419+ def __init__(self, callback = None):
1420+ self.callback = callback
1421+
1422+ def start(self, timeout):
1423+ """
1424+ start timer
1425+ """
1426+ try:
1427+ signal.signal(signal.SIGALRM, self.handler)
1428+ signal.alarm(timeout)
1429+ except ValueError:
1430+ pass
1431+
1432+ def stop(self):
1433+ """
1434+ stop timer before it come to an end
1435+ """
1436+ try:
1437+ signal.alarm(0)
1438+ except:
1439+ pass
1440+
1441+ def handler(self, signum, frame):
1442+ """
1443+ timeout occur.
1444+ """
1445+ if self.callback is None:
1446+ raise Timeout()
1447+ else:
1448+ self.callback()
1449\ No newline at end of file
1450
1451=== modified file 'gnome/backintime-gnome'
1452--- gnome/backintime-gnome 2009-11-02 13:52:00 +0000
1453+++ gnome/backintime-gnome 2013-03-07 23:23:23 +0000
1454@@ -23,5 +23,8 @@
1455 APP_PATH="/usr/share/backintime/gnome"
1456 fi
1457
1458-python ${APP_PATH}/app.py "$@"
1459+#starting a new ssh-agent all the time is just a workaround for
1460+#https://bugs.launchpad.net/ubuntu/+source/openssh/+bug/841672
1461+#normally we don't need to start ssh-agent for backintime-gnome
1462+ssh-agent python ${APP_PATH}/app.py "$@"
1463
1464
1465=== modified file 'gnome/debian_specific/control'
1466--- gnome/debian_specific/control 2013-01-09 21:02:50 +0000
1467+++ gnome/debian_specific/control 2013-03-07 23:23:23 +0000
1468@@ -5,6 +5,6 @@
1469 Maintainer: BIT Team <bit-team@lists.launchpad.net>
1470 Architecture: all
1471 Homepage: http://backintime.le-web.org
1472-Depends: x11-utils, python-glade2, python-gnome2, python-notify, meld, gksu, libnotify-bin, backintime-common (>= 1.0.21~), backintime-notify (>= 1.0.21~)
1473+Depends: x11-utils, python-glade2, python-gnome2, python-notify, python-keyring-gnome | python-gnomekeyring, meld, gksu, libnotify-bin, backintime-common (>= 1.0.21~), backintime-notify (>= 1.0.21~)
1474 Description: Simple backup system for GNOME Desktop
1475 This is a GNOME GUI frontend for backintime-common.
1476
1477=== modified file 'gnome/messagebox.py'
1478--- gnome/messagebox.py 2012-10-28 23:11:24 +0000
1479+++ gnome/messagebox.py 2013-03-07 23:23:23 +0000
1480@@ -23,6 +23,7 @@
1481 pygtk.require("2.0")
1482 import gtk
1483 import gettext
1484+import gobject
1485
1486 import config
1487
1488@@ -46,7 +47,7 @@
1489 dialog.destroy()
1490 return retVal
1491
1492-def text_input_dialog( parent, config, title, default_value = '' ):
1493+def text_input_dialog( parent, config, title, default_value = '', prompt = None, hide_input = False , timeout = None):
1494
1495 builder = gtk.Builder()
1496 builder.set_translation_domain('backintime')
1497@@ -58,15 +59,27 @@
1498
1499 dialog = builder.get_object( 'TextInputDialog' )
1500 dialog.set_title( title )
1501- dialog.set_transient_for( parent )
1502+ if not parent is None:
1503+ dialog.set_transient_for( parent )
1504+
1505+ prompt_text = builder.get_object('prompt_text')
1506+ if not prompt is None:
1507+ prompt_text.set_text(prompt)
1508+ prompt_text.set_visible(True)
1509
1510 edit = builder.get_object( 'edit_text' )
1511+
1512+ if hide_input:
1513+ edit.set_visibility(False)
1514
1515 if not default_value is None:
1516 edit.set_text( default_value )
1517
1518 edit.grab_focus()
1519
1520+ if not timeout is None:
1521+ gobject.timeout_add(1000 * timeout, dialog.destroy)
1522+
1523 text = None
1524 if gtk.RESPONSE_OK == dialog.run():
1525 text = edit.get_text()
1526@@ -76,3 +89,8 @@
1527 dialog.destroy()
1528 return text
1529
1530+def ask_password_dialog(parent, config, title, prompt, timeout = None):
1531+ password = text_input_dialog(parent, config, title,
1532+ prompt = prompt,
1533+ hide_input = True, timeout = timeout)
1534+ return(password)
1535
1536=== modified file 'gnome/settingsdialog.glade'
1537--- gnome/settingsdialog.glade 2013-03-04 21:16:20 +0000
1538+++ gnome/settingsdialog.glade 2013-03-07 23:23:23 +0000
1539@@ -475,31 +475,6 @@
1540 <property name="position">1</property>
1541 </packing>
1542 </child>
1543- <child>
1544- <object class="GtkLabel" id="lbl_ssh_cipher">
1545- <property name="visible">True</property>
1546- <property name="can_focus">False</property>
1547- <property name="xalign">1</property>
1548- <property name="label" translatable="yes">Cipher:</property>
1549- <property name="width_chars">8</property>
1550- </object>
1551- <packing>
1552- <property name="expand">False</property>
1553- <property name="fill">True</property>
1554- <property name="position">2</property>
1555- </packing>
1556- </child>
1557- <child>
1558- <object class="GtkComboBox" id="combo_ssh_cipher">
1559- <property name="visible">True</property>
1560- <property name="can_focus">False</property>
1561- </object>
1562- <packing>
1563- <property name="expand">True</property>
1564- <property name="fill">True</property>
1565- <property name="position">3</property>
1566- </packing>
1567- </child>
1568 </object>
1569 <packing>
1570 <property name="expand">True</property>
1571@@ -507,6 +482,108 @@
1572 <property name="position">1</property>
1573 </packing>
1574 </child>
1575+ <child>
1576+ <object class="GtkExpander" id="expander_ssh">
1577+ <property name="visible">True</property>
1578+ <property name="can_focus">True</property>
1579+ <child>
1580+ <object class="GtkHBox" id="hbox_ssh3">
1581+ <property name="visible">True</property>
1582+ <property name="can_focus">False</property>
1583+ <property name="spacing">2</property>
1584+ <child>
1585+ <object class="GtkLabel" id="lbl_ssh_cipher">
1586+ <property name="visible">True</property>
1587+ <property name="can_focus">False</property>
1588+ <property name="xalign">1</property>
1589+ <property name="label" translatable="yes">Cipher:</property>
1590+ <property name="width_chars">8</property>
1591+ </object>
1592+ <packing>
1593+ <property name="expand">False</property>
1594+ <property name="fill">True</property>
1595+ <property name="position">0</property>
1596+ </packing>
1597+ </child>
1598+ <child>
1599+ <object class="GtkComboBox" id="combo_ssh_cipher">
1600+ <property name="visible">True</property>
1601+ <property name="can_focus">False</property>
1602+ </object>
1603+ <packing>
1604+ <property name="expand">True</property>
1605+ <property name="fill">True</property>
1606+ <property name="position">1</property>
1607+ </packing>
1608+ </child>
1609+ <child>
1610+ <object class="GtkLabel" id="lbl_ssh_private_key_file">
1611+ <property name="visible">True</property>
1612+ <property name="can_focus">False</property>
1613+ <property name="xalign">1</property>
1614+ <property name="label" translatable="yes">Private key:</property>
1615+ <property name="width_chars">12</property>
1616+ </object>
1617+ <packing>
1618+ <property name="expand">False</property>
1619+ <property name="fill">True</property>
1620+ <property name="position">2</property>
1621+ </packing>
1622+ </child>
1623+ <child>
1624+ <object class="GtkEntry" id="txt_ssh_private_key_file">
1625+ <property name="visible">True</property>
1626+ <property name="can_focus">True</property>
1627+ <property name="editable">False</property>
1628+ <property name="invisible_char">•</property>
1629+ <property name="primary_icon_activatable">False</property>
1630+ <property name="secondary_icon_activatable">False</property>
1631+ <property name="primary_icon_sensitive">True</property>
1632+ <property name="secondary_icon_sensitive">True</property>
1633+ </object>
1634+ <packing>
1635+ <property name="expand">True</property>
1636+ <property name="fill">True</property>
1637+ <property name="position">3</property>
1638+ </packing>
1639+ </child>
1640+ <child>
1641+ <object class="GtkButton" id="btn_ssh_private_key_file">
1642+ <property name="visible">True</property>
1643+ <property name="can_focus">True</property>
1644+ <property name="receives_default">True</property>
1645+ <property name="use_action_appearance">False</property>
1646+ <signal name="clicked" handler="on_btn_ssh_private_key_file_clicked" swapped="no"/>
1647+ <child>
1648+ <object class="GtkImage" id="image_ssh_private_key_file">
1649+ <property name="visible">True</property>
1650+ <property name="can_focus">False</property>
1651+ <property name="stock">gtk-directory</property>
1652+ </object>
1653+ </child>
1654+ </object>
1655+ <packing>
1656+ <property name="expand">False</property>
1657+ <property name="fill">True</property>
1658+ <property name="position">4</property>
1659+ </packing>
1660+ </child>
1661+ </object>
1662+ </child>
1663+ <child type="label">
1664+ <object class="GtkLabel" id="label_ssh_advanced">
1665+ <property name="visible">True</property>
1666+ <property name="can_focus">False</property>
1667+ <property name="label" translatable="yes">Advanced SSH Settings</property>
1668+ </object>
1669+ </child>
1670+ </object>
1671+ <packing>
1672+ <property name="expand">True</property>
1673+ <property name="fill">True</property>
1674+ <property name="position">2</property>
1675+ </packing>
1676+ </child>
1677 </object>
1678 </child>
1679 </object>
1680@@ -645,6 +722,87 @@
1681 </packing>
1682 </child>
1683 <child>
1684+ <object class="GtkFrame" id="password">
1685+ <property name="can_focus">False</property>
1686+ <property name="label_xalign">0</property>
1687+ <property name="shadow_type">none</property>
1688+ <child>
1689+ <object class="GtkAlignment" id="alignment_password">
1690+ <property name="visible">True</property>
1691+ <property name="can_focus">False</property>
1692+ <property name="left_padding">12</property>
1693+ <child>
1694+ <object class="GtkVBox" id="vbox_password">
1695+ <property name="visible">True</property>
1696+ <property name="can_focus">False</property>
1697+ <property name="spacing">2</property>
1698+ <child>
1699+ <object class="GtkEntry" id="txt_password">
1700+ <property name="visible">True</property>
1701+ <property name="can_focus">True</property>
1702+ <property name="invisible_char">•</property>
1703+ <property name="primary_icon_activatable">False</property>
1704+ <property name="secondary_icon_activatable">False</property>
1705+ <property name="primary_icon_sensitive">True</property>
1706+ <property name="secondary_icon_sensitive">True</property>
1707+ </object>
1708+ <packing>
1709+ <property name="expand">True</property>
1710+ <property name="fill">True</property>
1711+ <property name="position">0</property>
1712+ </packing>
1713+ </child>
1714+ <child>
1715+ <object class="GtkCheckButton" id="cb_password_save">
1716+ <property name="label" translatable="yes">Save Password to Keyring</property>
1717+ <property name="visible">True</property>
1718+ <property name="can_focus">True</property>
1719+ <property name="receives_default">False</property>
1720+ <property name="use_action_appearance">False</property>
1721+ <property name="draw_indicator">True</property>
1722+ <signal name="toggled" handler="on_cb_password_save_toggled" swapped="no"/>
1723+ </object>
1724+ <packing>
1725+ <property name="expand">False</property>
1726+ <property name="fill">True</property>
1727+ <property name="position">1</property>
1728+ </packing>
1729+ </child>
1730+ <child>
1731+ <object class="GtkCheckButton" id="cb_password_use_cache">
1732+ <property name="label" translatable="yes">Cache Password for Cron (Security issue: root can read password)</property>
1733+ <property name="visible">True</property>
1734+ <property name="can_focus">True</property>
1735+ <property name="receives_default">False</property>
1736+ <property name="use_action_appearance">False</property>
1737+ <property name="draw_indicator">True</property>
1738+ </object>
1739+ <packing>
1740+ <property name="expand">False</property>
1741+ <property name="fill">True</property>
1742+ <property name="position">2</property>
1743+ </packing>
1744+ </child>
1745+ </object>
1746+ </child>
1747+ </object>
1748+ </child>
1749+ <child type="label">
1750+ <object class="GtkLabel" id="lbl_password">
1751+ <property name="visible">True</property>
1752+ <property name="can_focus">False</property>
1753+ <property name="label" translatable="yes">&lt;b&gt;Password&lt;/b&gt;</property>
1754+ <property name="use_markup">True</property>
1755+ </object>
1756+ </child>
1757+ </object>
1758+ <packing>
1759+ <property name="expand">False</property>
1760+ <property name="fill">True</property>
1761+ <property name="position">850</property>
1762+ </packing>
1763+ </child>
1764+ <child>
1765 <object class="GtkFrame" id="advanced">
1766 <property name="visible">True</property>
1767 <property name="can_focus">False</property>
1768
1769=== modified file 'gnome/settingsdialog.py'
1770--- gnome/settingsdialog.py 2013-03-04 21:16:20 +0000
1771+++ gnome/settingsdialog.py 2013-03-07 23:23:23 +0000
1772@@ -26,12 +26,14 @@
1773 import gobject
1774 import datetime
1775 import gettext
1776+import subprocess
1777+import keyring
1778
1779 import config
1780 import messagebox
1781 import tools
1782 import mount
1783-
1784+import password
1785
1786 _=gettext.gettext
1787
1788@@ -77,7 +79,9 @@
1789 'on_btn_where_clicked': self.on_btn_where_clicked,
1790 'on_cb_backup_mode_changed': self.on_cb_backup_mode_changed,
1791 'on_cb_auto_host_user_profile_toggled': self.update_host_user_profile,
1792- 'on_combo_modes_changed': self.on_combo_modes_changed
1793+ 'on_combo_modes_changed': self.on_combo_modes_changed,
1794+ 'on_cb_password_save_toggled': self.update_password_save,
1795+ 'on_btn_ssh_private_key_file_clicked': self.on_btn_ssh_private_key_file_clicked
1796 }
1797
1798 builder.connect_signals(signals)
1799@@ -139,6 +143,7 @@
1800 self.txt_ssh_port = get('txt_ssh_port')
1801 self.txt_ssh_user = get('txt_ssh_user')
1802 self.txt_ssh_path = get('txt_ssh_path')
1803+ self.txt_private_key_file = get('txt_ssh_private_key_file')
1804
1805 self.store_ssh_cipher = gtk.ListStore(str, str)
1806 keys = self.config.SSH_CIPHERS.keys()
1807@@ -159,6 +164,13 @@
1808 ## self.txt_dummy_port = get('txt_dummy_port')
1809 ## self.txt_dummy_user = get('txt_dummy_user')
1810
1811+ #password
1812+ self.frame_password = get('password')
1813+ self.txt_password = get('txt_password')
1814+ self.txt_password.set_visibility(False)
1815+ self.cb_password_save = get('cb_password_save')
1816+ self.cb_password_use_cache = get('cb_password_use_cache')
1817+
1818 #automatic backup mode store
1819 self.store_backup_mode = gtk.ListStore( str, int )
1820 map = self.config.AUTOMATIC_BACKUP_MODES
1821@@ -413,6 +425,22 @@
1822 else:
1823 fcd.destroy()
1824
1825+ def on_btn_ssh_private_key_file_clicked( self, button ):
1826+ file = self.txt_private_key_file.get_text()
1827+
1828+ fcd = gtk.FileChooserDialog( _('SSH private key'), self.dialog, gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK) )
1829+ if len( file ) > 0:
1830+ fcd.set_filename( file )
1831+ else:
1832+ fcd.set_filename(self.config.get_ssh_private_key_folder())
1833+
1834+ if fcd.run() == gtk.RESPONSE_OK:
1835+ new_file = tools.prepare_path( fcd.get_filename() )
1836+ fcd.destroy()
1837+ self.txt_private_key_file.set_text( new_file )
1838+ else:
1839+ fcd.destroy()
1840+
1841 def on_cb_backup_mode_changed( self, *params ):
1842 iter = self.cb_backup_mode.get_active_iter()
1843 if iter is None:
1844@@ -459,6 +487,12 @@
1845 self.lbl_profile.set_sensitive( value )
1846 self.txt_profile.set_sensitive( value )
1847
1848+ def update_password_save(self, *params):
1849+ value = self.cb_password_save.get_active()
1850+ if value and tools.check_home_encrypt():
1851+ value = False
1852+ self.cb_password_use_cache.set_sensitive(value)
1853+
1854 def on_combo_modes_changed(self, *params):
1855 iter = self.combo_modes.get_active_iter()
1856 if iter is None:
1857@@ -472,6 +506,10 @@
1858 else:
1859 getattr(self, 'mode_%s' % mode).hide()
1860 self.mode = active_mode
1861+ if active_mode in self.config.SNAPSHOT_MODES_NEED_PASSWORD:
1862+ self.frame_password.show()
1863+ else:
1864+ self.frame_password.hide()
1865
1866 def on_combo_profiles_changed( self, *params ):
1867 if self.disable_combo_changed:
1868@@ -541,6 +579,7 @@
1869 self.txt_ssh_port.set_text( str(self.config.get_ssh_port( self.profile_id )) )
1870 self.txt_ssh_user.set_text( self.config.get_ssh_user( self.profile_id ) )
1871 self.txt_ssh_path.set_text( self.config.get_snapshots_path_ssh( self.profile_id ) )
1872+ self.txt_private_key_file.set_text( self.config.get_ssh_private_key_file( self.profile_id ) )
1873 #set chipher
1874 i = 0
1875 iter = self.store_ssh_cipher.get_iter_first()
1876@@ -557,6 +596,15 @@
1877 ## self.txt_dummy_port.set_text( str(self.config.get_dummy_port( self.profile_id )) )
1878 ## self.txt_dummy_user.set_text( self.config.get_dummy_user( self.profile_id ) )
1879
1880+ #password
1881+ password = self.config.get_password( profile_id = self.profile_id, mode = self.mode, only_from_keyring = True )
1882+ if password is None:
1883+ password = ''
1884+ self.txt_password.set_text(password)
1885+ self.cb_password_save.set_active( self.config.get_password_save( self.profile_id, self.mode ) )
1886+ self.cb_password_use_cache.set_active( self.config.get_password_use_cache( self.profile_id, self.mode ) )
1887+ self.update_password_save()
1888+
1889 #per directory schedule
1890 #self.cb_per_directory_schedule.set_active( self.config.get_per_directory_schedule() )
1891
1892@@ -749,6 +797,9 @@
1893 self.error_handler( _('Custom Hours can only be a comma seperate list of hours (e.g. 8,12,18,23) or */3 for periodic backups every 3 hours') )
1894 return False
1895
1896+ #password
1897+ password = self.txt_password.get_text()
1898+
1899 mount_kwargs = {}
1900
1901 #ssh settings
1902@@ -756,10 +807,18 @@
1903 ssh_port = self.txt_ssh_port.get_text()
1904 ssh_user = self.txt_ssh_user.get_text()
1905 ssh_path = self.txt_ssh_path.get_text()
1906+ ssh_private_key_file = self.txt_private_key_file.get_text()
1907 iter = self.combo_ssh_cipher.get_active_iter()
1908 ssh_cipher = self.store_ssh_cipher.get_value( iter, 1 )
1909 if mode == 'ssh':
1910- mount_kwargs = { 'host': ssh_host, 'port': int(ssh_port), 'user': ssh_user, 'path': ssh_path, 'cipher': ssh_cipher }
1911+ mount_kwargs = {'host': ssh_host,
1912+ 'port': int(ssh_port),
1913+ 'user': ssh_user,
1914+ 'path': ssh_path,
1915+ 'cipher': ssh_cipher,
1916+ 'private_key_file': ssh_private_key_file,
1917+ 'password': password
1918+ }
1919
1920 ## #dummy settings
1921 ## dummy_host = self.txt_dummy_host.get_text()
1922@@ -769,7 +828,11 @@
1923 ## #values must have exactly the same Type (str, int or bool)
1924 ## #as they are set in config or you will run into false-positive
1925 ## #HashCollision warnings
1926-## mount_kwargs = { 'host': dummy_host, 'port': int(dummy_port), 'user': dummy_user }
1927+## mount_kwargs = {'host': dummy_host,
1928+## 'port': int(dummy_port),
1929+## 'user': dummy_user,
1930+## 'password': password
1931+## }
1932
1933 if not self.config.SNAPSHOT_MODES[mode][0] is None:
1934 #pre_mount_check
1935@@ -805,12 +868,32 @@
1936 self.config.set_ssh_user(ssh_user, self.profile_id)
1937 self.config.set_snapshots_path_ssh(ssh_path, self.profile_id)
1938 self.config.set_ssh_cipher(ssh_cipher, self.profile_id)
1939+ self.config.set_ssh_private_key_file(ssh_private_key_file, self.profile_id)
1940
1941 ## #save dummy
1942 ## self.config.set_dummy_host(dummy_host, self.profile_id)
1943 ## self.config.set_dummy_port(dummy_port, self.profile_id)
1944 ## self.config.set_dummy_user(dummy_user, self.profile_id)
1945
1946+ #save password
1947+ if self.cb_password_save.get_active():
1948+ if self.config.get_keyring_backend() == '':
1949+ self.config.set_keyring_backend('gnomekeyring')
1950+ if self.config.get_keyring_backend() == 'kwallet':
1951+ if keyring.backend.KDEKWallet().supported() == 1:
1952+ keyring.set_keyring(keyring.backend.KDEKWallet())
1953+ else:
1954+ self.config.set_keyring_backend('gnomekeyring')
1955+ if self.config.get_keyring_backend() == 'gnomekeyring':
1956+ if keyring.backend.GnomeKeyring().supported() == 1:
1957+ keyring.set_keyring(keyring.backend.GnomeKeyring())
1958+ else:
1959+ self.error_handler( _('Can\'t connect to gnomekeyring to save password'))
1960+ return False
1961+ self.config.set_password_save(self.cb_password_save.get_active(), self.profile_id, mode)
1962+ self.config.set_password_use_cache(self.cb_password_use_cache.get_active(), self.profile_id, mode)
1963+ self.config.set_password(password, self.profile_id, mode)
1964+
1965 #if not msg is None:
1966 # messagebox.show_error( self.dialog, self.config, msg )
1967 # return False
1968@@ -1062,5 +1145,13 @@
1969 return False
1970
1971 self.config.save()
1972+
1973+ #start Password_Cache if not running
1974+ daemon = password.Password_Cache(self.config)
1975+ if not daemon.status():
1976+ try:
1977+ subprocess.check_call(['backintime', '--pw-cache', 'start'], stdout=open(os.devnull, 'w'))
1978+ except subprocess.CalledProcessError as e:
1979+ messagebox.show_error(self.dialog, self.config, _('start Password Cache failed: %s') % e.strerror)
1980 return True
1981
1982
1983=== modified file 'gnome/textinputdialog.glade'
1984--- gnome/textinputdialog.glade 2011-11-21 09:27:27 +0000
1985+++ gnome/textinputdialog.glade 2013-03-07 23:23:23 +0000
1986@@ -63,6 +63,19 @@
1987 </packing>
1988 </child>
1989 <child>
1990+ <object class="GtkLabel" id="prompt_text">
1991+ <property name="visible">False</property>
1992+ <property name="can_focus">False</property>
1993+ <property name="xalign">0</property>
1994+ <property name="label" translatable="yes">Text</property>
1995+ </object>
1996+ <packing>
1997+ <property name="expand">True</property>
1998+ <property name="fill">True</property>
1999+ <property name="position">1</property>
2000+ </packing>
2001+ </child>
2002+ <child>
2003 <object class="GtkEntry" id="edit_text">
2004 <property name="visible">True</property>
2005 <property name="can_focus">True</property>
2006@@ -72,7 +85,7 @@
2007 <packing>
2008 <property name="expand">False</property>
2009 <property name="fill">True</property>
2010- <property name="position">1</property>
2011+ <property name="position">2</property>
2012 </packing>
2013 </child>
2014 </object>
2015
2016=== modified file 'kde4/app.py'
2017--- kde4/app.py 2012-10-31 21:45:38 +0000
2018+++ kde4/app.py 2013-03-07 23:23:23 +0000
2019@@ -343,7 +343,7 @@
2020
2021 #mount
2022 try:
2023- mnt = mount.Mount(cfg = self.config, profile_id = profile_id)
2024+ mnt = mount.Mount(cfg = self.config, profile_id = profile_id, parent = self)
2025 hash_id = mnt.mount()
2026 except mount.MountException as ex:
2027 KMessageBox.error( self, QString.fromUtf8( str(ex) ) )
2028@@ -413,7 +413,7 @@
2029
2030 #umount
2031 try:
2032- mnt = mount.Mount(cfg = self.config)
2033+ mnt = mount.Mount(cfg = self.config, parent = self)
2034 mnt.umount(self.config.current_hash_id)
2035 except mount.MountException as ex:
2036 KMessageBox.error( self, QString.fromUtf8( str(ex) ) )
2037@@ -465,7 +465,7 @@
2038
2039 def remount( self, new_profile_id, old_profile_id):
2040 try:
2041- mnt = mount.Mount(cfg = self.config, profile_id = old_profile_id)
2042+ mnt = mount.Mount(cfg = self.config, profile_id = old_profile_id, parent = self)
2043 hash_id = mnt.remount(new_profile_id)
2044 except mount.MountException as ex:
2045 KMessageBox.error( self, QString.fromUtf8( str(ex) ) )
2046
2047=== modified file 'kde4/debian_specific/control'
2048--- kde4/debian_specific/control 2013-01-09 21:02:50 +0000
2049+++ kde4/debian_specific/control 2013-03-07 23:23:23 +0000
2050@@ -7,7 +7,7 @@
2051 Architecture: all
2052 Homepage: http://backintime.le-web.org
2053 Depends: x11-utils, kdesudo, libnotify-bin, python-kde4 (>= 4.1), backintime-common (>= 1.0.21~), backintime-notify (>= 1.0.21~)
2054-Suggests: kompare
2055+Suggests: kompare, python-keyring-kwallet
2056 Conflicts: backintime-kde
2057 Replaces: backintime-kde
2058 Description: Simple backup system for KDE4 Desktop
2059
2060=== added file 'kde4/messagebox.py'
2061--- kde4/messagebox.py 1970-01-01 00:00:00 +0000
2062+++ kde4/messagebox.py 2013-03-07 23:23:23 +0000
2063@@ -0,0 +1,49 @@
2064+# Copyright (c) 2012-2013 Germar Reitze
2065+#
2066+# This program is free software; you can redistribute it and/or modify
2067+# it under the terms of the GNU General Public License as published by
2068+# the Free Software Foundation; either version 2 of the License, or
2069+# (at your option) any later version.
2070+#
2071+# This program is distributed in the hope that it will be useful,
2072+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2073+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2074+# GNU General Public License for more details.
2075+#
2076+# You should have received a copy of the GNU General Public License along
2077+# with this program; if not, write to the Free Software Foundation, Inc.,
2078+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
2079+
2080+import sys
2081+from PyKDE4.kdecore import ki18n, KAboutData, KCmdLineArgs
2082+from PyKDE4.kdeui import KApplication, KPasswordDialog
2083+from PyQt4.QtCore import QString, QTimer, SIGNAL
2084+
2085+import app
2086+
2087+def ask_password_dialog(parent, config, title, prompt, timeout = None):
2088+ if parent is None:
2089+ kapp, kaboutData = app.create_kapplication( config )
2090+
2091+ dialog = KPasswordDialog()
2092+
2093+ timer = QTimer()
2094+ if not timeout is None:
2095+ dialog.connect(timer, SIGNAL("timeout()"), dialog.close)
2096+ timer.setInterval(timeout * 1000)
2097+ timer.start()
2098+
2099+ dialog.setPrompt( QString.fromUtf8(prompt))
2100+ dialog.show()
2101+ KApplication.processEvents()
2102+
2103+ if parent is None:
2104+ kapp.exec_()
2105+ else:
2106+ dialog.exec_()
2107+
2108+ timer.stop()
2109+ password = dialog.password()
2110+ del(dialog)
2111+
2112+ return(password)
2113
2114=== modified file 'kde4/settingsdialog.py'
2115--- kde4/settingsdialog.py 2013-03-04 21:16:20 +0000
2116+++ kde4/settingsdialog.py 2013-03-07 23:23:23 +0000
2117@@ -22,6 +22,8 @@
2118 import datetime
2119 import gettext
2120 import copy
2121+import subprocess
2122+import keyring
2123
2124 from PyQt4.QtGui import *
2125 from PyQt4.QtCore import *
2126@@ -33,6 +35,7 @@
2127 import tools
2128 import kde4tools
2129 import mount
2130+import password
2131
2132
2133 _=gettext.gettext
2134@@ -150,6 +153,8 @@
2135 vlayout.addLayout( hlayout1 )
2136 hlayout2 = QHBoxLayout()
2137 vlayout.addLayout( hlayout2 )
2138+ hlayout3 = QHBoxLayout()
2139+ vlayout.addLayout( hlayout3 )
2140
2141 self.lbl_ssh_host = QLabel( QString.fromUtf8( _( 'Host:' ) ), self )
2142 hlayout1.addWidget( self.lbl_ssh_host )
2143@@ -172,11 +177,21 @@
2144 hlayout2.addWidget( self.txt_ssh_path )
2145
2146 self.lbl_ssh_cipher = QLabel( QString.fromUtf8( _( 'Cipher:' ) ), self )
2147- hlayout2.addWidget( self.lbl_ssh_cipher )
2148+ hlayout3.addWidget( self.lbl_ssh_cipher )
2149 self.combo_ssh_cipher = KComboBox( self )
2150- hlayout2.addWidget( self.combo_ssh_cipher )
2151+ hlayout3.addWidget( self.combo_ssh_cipher )
2152 self.fill_combo( self.combo_ssh_cipher, self.config.SSH_CIPHERS )
2153
2154+ self.lbl_ssh_private_key_file = QLabel( QString.fromUtf8( _( 'Private Key:' ) ), self )
2155+ hlayout3.addWidget( self.lbl_ssh_private_key_file )
2156+ self.txt_ssh_private_key_file = KLineEdit( self )
2157+ self.txt_ssh_private_key_file.setReadOnly( True )
2158+ hlayout3.addWidget( self.txt_ssh_private_key_file )
2159+
2160+ self.btn_ssh_private_key_file = KPushButton( KIcon( 'folder' ), '', self )
2161+ hlayout3.addWidget( self.btn_ssh_private_key_file )
2162+ QObject.connect( self.btn_ssh_private_key_file, SIGNAL('clicked()'), self.on_btn_ssh_private_key_file_clicked )
2163+
2164 ## #Dummy
2165 ## group_box = QGroupBox( self )
2166 ## self.mode_dummy = group_box
2167@@ -203,12 +218,36 @@
2168 ## self.txt_dummy_user = KLineEdit( self )
2169 ## hlayout.addWidget( self.txt_dummy_user )
2170
2171+ #password
2172+ group_box = QGroupBox( self )
2173+ self.frame_password = group_box
2174+ group_box.setTitle( QString.fromUtf8( _( 'Password' ) ) )
2175+ layout.addWidget( group_box )
2176+
2177+ vlayout = QVBoxLayout( group_box )
2178+
2179+ self.txt_password = KLineEdit( self )
2180+ self.txt_password.setPasswordMode(True)
2181+ vlayout.addWidget( self.txt_password )
2182+
2183+ self.cb_password_save = QCheckBox( QString.fromUtf8( _( 'Save Password to Keyring' ) ), self )
2184+ QObject.connect( self.cb_password_save, SIGNAL('stateChanged(int)'), self.update_password_save )
2185+ vlayout.addWidget( self.cb_password_save )
2186+
2187+ self.cb_password_use_cache = QCheckBox( QString.fromUtf8( _( 'Cache Password for Cron (Security issue: root can read password)' ) ), self )
2188+ vlayout.addWidget( self.cb_password_use_cache )
2189+
2190+ #mode change
2191 QObject.connect( self.combo_modes, SIGNAL('currentIndexChanged(int)'), self.on_combo_modes_changed )
2192 self.on_combo_modes_changed()
2193
2194 #host, user, profile id
2195- hlayout = QHBoxLayout()
2196- layout.addLayout( hlayout )
2197+ group_box = QGroupBox( self )
2198+ self.frame_advanced = group_box
2199+ group_box.setTitle( QString.fromUtf8( _( 'Advanced' ) ) )
2200+ layout.addWidget( group_box )
2201+
2202+ hlayout = QHBoxLayout( group_box )
2203 hlayout.addSpacing( 12 )
2204
2205 vlayout2 = QVBoxLayout()
2206@@ -620,6 +659,12 @@
2207 self.lbl_profile.setEnabled( enabled )
2208 self.txt_profile.setEnabled( enabled )
2209
2210+ def update_password_save( self ):
2211+ enabled = self.cb_password_save.isChecked()
2212+ if enabled and tools.check_home_encrypt():
2213+ enabled = False
2214+ self.cb_password_use_cache.setEnabled( enabled )
2215+
2216 def update_profiles( self ):
2217 self.update_profile()
2218 current_profile_id = self.config.get_current_profile()
2219@@ -657,12 +702,22 @@
2220 self.txt_ssh_user.setText( QString.fromUtf8( self.config.get_ssh_user() ) )
2221 self.txt_ssh_path.setText( QString.fromUtf8( self.config.get_snapshots_path_ssh() ) )
2222 self.set_combo_value( self.combo_ssh_cipher, self.config.get_ssh_cipher(), type = 'str' )
2223+ self.txt_ssh_private_key_file.setText( QString.fromUtf8( self.config.get_ssh_private_key_file() ) )
2224
2225 ## #dummy
2226 ## self.txt_dummy_host.setText( QString.fromUtf8( self.config.get_dummy_host() ) )
2227 ## self.txt_dummy_port.setText( QString.fromUtf8( self.config.get_dummy_port() ) )
2228 ## self.txt_dummy_user.setText( QString.fromUtf8( self.config.get_dummy_user() ) )
2229
2230+ #password
2231+ password = self.config.get_password( mode = self.mode, only_from_keyring = True )
2232+ if password is None:
2233+ password = ''
2234+ self.txt_password.setText( QString.fromUtf8( password ) )
2235+ self.cb_password_save.setChecked( self.config.get_password_save( mode = self.mode ) )
2236+ self.cb_password_use_cache.setChecked( self.config.get_password_use_cache( mode = self.mode ) )
2237+ self.update_password_save()
2238+
2239 self.cb_auto_host_user_profile.setChecked( self.config.get_auto_host_user_profile() )
2240 host, user, profile = self.config.get_host_user_profile()
2241 self.txt_host.setText( QString.fromUtf8( host ) )
2242@@ -750,14 +805,25 @@
2243 self.config.set_snapshots_mode( mode )
2244 mount_kwargs = {}
2245
2246+ #password
2247+ password = str( self.txt_password.text().toUtf8() )
2248+
2249 #ssh
2250 ssh_host = str( self.txt_ssh_host.text().toUtf8() )
2251 ssh_port = str( self.txt_ssh_port.text().toUtf8() )
2252 ssh_user = str( self.txt_ssh_user.text().toUtf8() )
2253 ssh_path = str( self.txt_ssh_path.text().toUtf8() )
2254 ssh_cipher = str( self.combo_ssh_cipher.itemData( self.combo_ssh_cipher.currentIndex() ).toString().toUtf8() )
2255+ ssh_private_key_file = str( self.txt_ssh_private_key_file.text().toUtf8() )
2256 if mode == 'ssh':
2257- mount_kwargs = { 'host': ssh_host, 'port': int(ssh_port), 'user': ssh_user, 'path': ssh_path, 'cipher': ssh_cipher }
2258+ mount_kwargs = {'host': ssh_host,
2259+ 'port': int(ssh_port),
2260+ 'user': ssh_user,
2261+ 'path': ssh_path,
2262+ 'cipher': ssh_cipher,
2263+ 'private_key_file': ssh_private_key_file,
2264+ 'password': password
2265+ }
2266
2267 ## #dummy
2268 ## dummy_host = str( self.txt_dummy_host.text().toUtf8() )
2269@@ -767,7 +833,11 @@
2270 ## #values must have exactly the same Type (str, int or bool)
2271 ## #as they are set in config or you will run into false-positive
2272 ## #HashCollision warnings
2273-## mount_kwargs = { 'host': dummy_host, 'port': int(dummy_port), 'user': dummy_user }
2274+## mount_kwargs = {'host': dummy_host,
2275+## 'port': int(dummy_port),
2276+## 'user': dummy_user,
2277+## 'password': password
2278+## }
2279
2280 if not self.config.SNAPSHOT_MODES[mode][0] is None:
2281 #pre_mount_check
2282@@ -805,12 +875,34 @@
2283 self.config.set_ssh_user(ssh_user)
2284 self.config.set_snapshots_path_ssh(ssh_path)
2285 self.config.set_ssh_cipher(ssh_cipher)
2286+ self.config.set_ssh_private_key_file(ssh_private_key_file)
2287
2288 ## #save dummy
2289 ## self.config.set_dummy_host(dummy_host)
2290 ## self.config.set_dummy_port(dummy_port)
2291 ## self.config.set_dummy_user(dummy_user)
2292
2293+ #save password
2294+ if self.cb_password_save.isChecked():
2295+ if self.config.get_keyring_backend() == '':
2296+ self.config.set_keyring_backend('kwallet')
2297+ if self.config.get_keyring_backend() == 'gnomekeyring':
2298+ if keyring.backend.GnomeKeyring().supported() == 1:
2299+ keyring.set_keyring(keyring.backend.GnomeKeyring())
2300+ else:
2301+ self.config.set_keyring_backend('kwallet')
2302+ if self.config.get_keyring_backend() == 'kwallet':
2303+ if keyring.backend.KDEKWallet().supported() == 1:
2304+ keyring.set_keyring(keyring.backend.KDEKWallet())
2305+ else:
2306+ #in Kubuntu keyring.backend.KDEKWallet is included in python-keyring
2307+ #but Debian uses an extra package for this: python-keyring-kwallet
2308+ self.error_handler( _('Can\'t connect to KWallet to save password. On Debian you need to install:\n\'apt-get install python-keyring-kwallet\''))
2309+ return False
2310+ self.config.set_password_save(self.cb_password_save.isChecked(), mode = mode)
2311+ self.config.set_password_use_cache(self.cb_password_use_cache.isChecked(), mode = mode)
2312+ self.config.set_password(password, mode = mode)
2313+
2314 #include list
2315 include_list = []
2316 for index in xrange( self.list_include.topLevelItemCount() ):
2317@@ -992,6 +1084,15 @@
2318 return False
2319
2320 self.config.save()
2321+
2322+ #start Password_Cache if not running
2323+ daemon = password.Password_Cache(self.config)
2324+ if not daemon.status():
2325+ try:
2326+ subprocess.check_call(['backintime', '--pw-cache', 'start'], stdout=open(os.devnull, 'w'))
2327+ except subprocess.CalledProcessError as e:
2328+ self.error_handler( _('start Password Cache failed: %s') % e.strerror)
2329+
2330 return True
2331
2332 def on_btn_exclude_remove_clicked ( self ):
2333@@ -1085,6 +1186,17 @@
2334 if not self.question_handler( _('Are you sure you want to change snapshots folder ?') ):
2335 return
2336 self.edit_snapshots_path.setText( QString.fromUtf8( self.config.prepare_path( path ) ) )
2337+
2338+ def on_btn_ssh_private_key_file_clicked( self ):
2339+ old_file = str( self.txt_ssh_private_key_file.text().toUtf8() )
2340+
2341+ if len(old_file) > 0:
2342+ start_dir = KUrl( self.txt_ssh_private_key_file.text() )
2343+ else:
2344+ start_dir = KUrl( self.config.get_ssh_private_key_folder() )
2345+ file = str( KFileDialog.getOpenFileName( start_dir, QString.fromUtf8(''), self, QString.fromUtf8( _( 'SSH private key' ) ) ).toUtf8() )
2346+ if len( file ) > 0 :
2347+ self.txt_ssh_private_key_file.setText( QString.fromUtf8( self.config.prepare_path( file ) ) )
2348
2349 def on_combo_modes_changed(self, *params):
2350 if len(params) == 0:
2351@@ -1099,6 +1211,10 @@
2352 else:
2353 getattr(self, 'mode_%s' % mode).hide()
2354 self.mode = active_mode
2355+ if active_mode in self.config.SNAPSHOT_MODES_NEED_PASSWORD:
2356+ self.frame_password.show()
2357+ else:
2358+ self.frame_password.hide()
2359
2360 def accept( self ):
2361 if self.validate():
2362
2363=== modified file 'makedeb.sh'
2364--- makedeb.sh 2012-10-27 14:42:44 +0000
2365+++ makedeb.sh 2013-03-07 23:23:23 +0000
2366@@ -21,6 +21,7 @@
2367 echo "Installed-Size: `du -sk tmp | cut -f1`" >> tmp/DEBIAN/control
2368
2369 cp $i/debian_specific/postrm tmp/DEBIAN/postrm
2370+ [ -e $i/debian_specific/conffiles ] && cp $i/debian_specific/conffiles tmp/DEBIAN/conffiles
2371
2372 dpkg --build tmp/ $PKGNAME-${PKGVER}_$PKGARCH.deb
2373

Subscribers

People subscribed via source and target branches