Skip to content

Commit

Permalink
Merge branch 'master' into unmount-fail
Browse files Browse the repository at this point in the history
  • Loading branch information
m3nu authored Dec 16, 2020
2 parents 8d7a256 + d6f368a commit 0db515a
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 34 deletions.
23 changes: 21 additions & 2 deletions src/vorta/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,32 @@
import peewee
from vorta._version import __version__
from vorta.config import SETTINGS_DIR
from vorta.log import init_logger
from vorta.log import init_logger, logger
from vorta.models import init_db
from vorta.updater import get_updater
from vorta.utils import parse_args


def main():
def exception_handler(type, value, tb):
from traceback import format_exception
from PyQt5.QtWidgets import QMessageBox
logger.critical("Uncaught exception, file a report at https://github.com/borgbase/vorta/issues/new",
exc_info=(type, value, tb))
full_exception = ''.join(format_exception(type, value, tb))
if app:
QMessageBox.critical(None,
app.tr("Fatal Error"),
app.tr(
"Uncaught exception, please file a report with this text at\n"
"https://github.com/borgbase/vorta/issues/new\n") + full_exception)
else:
# Crashed before app startup, cannot translate
sys.exit(1)

sys.excepthook = exception_handler
app = None

args = parse_args()
signal.signal(signal.SIGINT, signal.SIG_DFL) # catch ctrl-c and exit

Expand All @@ -34,7 +53,7 @@ def main():

# Init app after database is available
from vorta.application import VortaApp
app = VortaApp(sys.argv, single_app=True)
app = VortaApp(sys.argv, single_app=args.profile is None)
app.updater = get_updater()

sys.exit(app.exec())
Expand Down
50 changes: 44 additions & 6 deletions src/vorta/application.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import logging
import os
import sys
import time
import ast

from PyQt5 import QtCore
from PyQt5.QtWidgets import QMessageBox

from vorta.borg.borg_thread import BorgThread
from vorta.borg.create import BorgCreateThread
from vorta.borg.version import BorgVersionThread
from vorta.config import TEMP_DIR
Expand All @@ -15,6 +20,8 @@
from vorta.views.main_window import MainWindow
from vorta.notifications import VortaNotifications

logger = logging.getLogger(__name__)

APP_ID = os.path.join(TEMP_DIR, "socket")


Expand All @@ -35,10 +42,18 @@ class VortaApp(QtSingleApplication):
def __init__(self, args_raw, single_app=False):

super().__init__(APP_ID, args_raw)
if self.isRunning() and single_app:
self.sendMessage("open main window")
print('An instance of Vorta is already running. Opening main window.')
sys.exit()
args = parse_args()
if self.isRunning():
if single_app:
self.sendMessage("open main window")
print('An instance of Vorta is already running. Opening main window.')
sys.exit()
elif args.profile:
self.sendMessage(f"create {args.profile}")
print('Creating backups using existing Vorta instance.')
sys.exit()
elif args.profile:
sys.exit('Vorta must already be running for --create to work')

init_translations(self)

Expand All @@ -50,7 +65,6 @@ def __init__(self, args_raw, single_app=False):
self.tray = TrayMenu(self)
self.main_window = MainWindow(self)

args = parse_args()
if getattr(args, 'daemonize', False):
pass
elif SettingsModel.get(key='foreground').value:
Expand All @@ -63,8 +77,25 @@ def __init__(self, args_raw, single_app=False):
self.set_borg_details_action()
self.installEventFilter(self)

def create_backups_cmdline(self, profiles):
self.completedProfiles = []
self.validProfiles = []
for profile_name in profiles:
profile = BackupProfileModel.get_or_none(name=profile_name)
if profile is not None:
if profile.repo is None:
logger.warning(f"Add a repository to {profile_name}")
continue
self.validProfiles.append(profile_name)
# Wait a bit in case something is running
while BorgThread.is_running():
time.sleep(0.1)
self.create_backup_action(profile_id=profile.id)
else:
logger.warning(f"Invalid profile name {profile_name}")

def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.ApplicationPaletteChange and type(source) == MainWindow:
if event.type() == QtCore.QEvent.ApplicationPaletteChange and isinstance(source, MainWindow):
self.main_window.set_icons()
self.main_window.repoTab.set_icons()
self.main_window.archiveTab.set_icons()
Expand Down Expand Up @@ -110,6 +141,13 @@ def backup_cancelled_event_response(self):
def message_received_event_response(self, message):
if message == "open main window":
self.open_main_window_action()
elif message.startswith("create"):
message = message[7:] # Remove create
profiles = ast.literal_eval(message) # Safely parse string array
if BorgThread.is_running():
logger.warning("Cannot run while backups are already running")
else:
self.create_backups_cmdline(profiles)

def set_borg_details_action(self):
params = BorgVersionThread.prepare()
Expand Down
5 changes: 5 additions & 0 deletions src/vorta/borg/borg_thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ def prepare(cls, profile):
logger.debug("Using %s keyring to store passwords.", keyring.__class__.__name__)
ret['password'] = keyring.get_password('vorta-repo', profile.repo.url)

# Check if keyring is locked
if profile.repo.encryption != 'none' and not keyring.is_unlocked:
ret['message'] = trans_late('messages', 'Please unlock your password manager.')
return ret

# Try to fall back to DB Keyring, if we use the system keychain.
if ret['password'] is None and keyring.is_primary:
logger.debug('Password not found in primary keyring. Falling back to VortaDBKeyring.')
Expand Down
1 change: 1 addition & 0 deletions src/vorta/borg/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def started_event(self):

def finished_event(self, result):
self.app.backup_finished_event.emit(result)
self.result.emit(result)
self.pre_post_backup_cmd(self.params, cmd='post_backup_cmd', returncode=result['returncode'])

@classmethod
Expand Down
12 changes: 9 additions & 3 deletions src/vorta/borg/info.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from collections import namedtuple
from .borg_thread import BorgThread
from vorta.i18n import trans_late
from vorta.models import RepoModel
from vorta.keyring.abc import get_keyring

FakeRepo = namedtuple('Repo', ['url', 'id', 'extra_borg_arguments'])
FakeRepo = namedtuple('Repo', ['url', 'id', 'extra_borg_arguments', 'encryption'])
FakeProfile = namedtuple('FakeProfile', ['repo', 'name', 'ssh_key'])


Expand All @@ -18,9 +19,9 @@ def prepare(cls, params):
Used to validate existing repository when added.
"""

# Build fake profile because we don't have it in the DB yet.
# Build fake profile because we don't have it in the DB yet. Assume unencrypted.
profile = FakeProfile(
FakeRepo(params['repo_url'], 999, params['extra_borg_arguments']),
FakeRepo(params['repo_url'], 999, params['extra_borg_arguments'], 'none'),
'New Repo',
params['ssh_key']
)
Expand All @@ -43,6 +44,11 @@ def prepare(cls, params):
ret['password'] = '999999' # Dummy password if the user didn't supply one. To avoid prompt.
else:
ret['password'] = params['password']
# Cannot tell if repo has encryption, assuming based off of password
if not get_keyring().is_unlocked:
ret['message'] = trans_late('messages', 'Please unlock your password manager.')
return ret

ret['ok'] = True
ret['cmd'] = cmd

Expand Down
3 changes: 2 additions & 1 deletion src/vorta/borg/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ def prepare(cls, params):

# Build fake profile because we don't have it in the DB yet.
profile = FakeProfile(
FakeRepo(params['repo_url'], 999, params['extra_borg_arguments']), 'Init Repo', params['ssh_key']
FakeRepo(params['repo_url'], 999, params['extra_borg_arguments'],
params['encryption']), 'Init Repo', params['ssh_key']
)

ret = super().prepare(profile)
Expand Down
20 changes: 19 additions & 1 deletion src/vorta/keyring/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
fall back to a simple database keystore if needed.
"""
import sys
from pkg_resources import parse_version

_keyring = None


class VortaKeyring:
def set_password(self, service, repo_url, password):
"""
Writes a password to the underlying store.
"""
raise NotImplementedError

def get_password(self, service, repo_url):
Expand All @@ -26,6 +30,13 @@ def is_primary(self):
"""
return True

@property
def is_unlocked(self):
"""
Returns True if the keyring is open. Return False if it is closed or locked
"""
raise NotImplementedError


def get_keyring():
"""
Expand All @@ -40,9 +51,16 @@ def get_keyring():
else: # Try to use DBus and Gnome-Keyring (available on Linux and *BSD)
import secretstorage
from .secretstorage import VortaSecretStorageKeyring

# secretstorage has two different libraries based on version
if parse_version(secretstorage.__version__) >= parse_version("3.0.0"):
from jeepney.wrappers import DBusErrorResponse as DBusException
else:
from dbus.exceptions import DBusException

try:
_keyring = VortaSecretStorageKeyring()
except secretstorage.SecretServiceNotAvailableException: # Try to use KWallet
except (secretstorage.exceptions.SecretStorageException, DBusException): # Try to use KWallet (KDE)
from .kwallet import VortaKWallet5Keyring, KWalletNotAvailableException
try:
_keyring = VortaKWallet5Keyring()
Expand Down
23 changes: 20 additions & 3 deletions src/vorta/keyring/darwin.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,31 @@ def _set_keychain(self):
from Foundation import NSBundle
Security = NSBundle.bundleWithIdentifier_('com.apple.security')

# https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
S_functions = [
('SecKeychainGetTypeID', b'I'),
('SecKeychainItemGetTypeID', b'I'),
('SecKeychainAddGenericPassword', b'i^{OpaqueSecKeychainRef=}I*I*I*o^^{OpaqueSecKeychainItemRef}'),
('SecKeychainOpen', b'i*o^^{OpaqueSecKeychainRef}'),
('SecKeychainFindGenericPassword', b'i@I*I*o^Io^^{OpaquePassBuff}o^^{OpaqueSecKeychainItemRef}'),
('SecKeychainGetStatus', b'i^{OpaqueSecKeychainRef=}o^I'),
]

objc.loadBundleFunctions(Security, globals(), S_functions)

SecKeychainRef = objc.registerCFSignature('SecKeychainRef', b'^{OpaqueSecKeychainRef=}', SecKeychainGetTypeID())
SecKeychainItemRef = objc.registerCFSignature('SecKeychainItemRef', b'^{OpaqueSecKeychainItemRef=}', SecKeychainItemGetTypeID())
SecKeychainItemRef = objc.registerCFSignature(
'SecKeychainItemRef', b'^{OpaqueSecKeychainItemRef=}', SecKeychainItemGetTypeID())

PassBuffRef = objc.createOpaquePointerType('PassBuffRef', b'^{OpaquePassBuff=}', None)

# Get the login keychain
result, login_keychain = SecKeychainOpen(b'login.keychain', None)
self.login_keychain = login_keychain

def set_password(self, service, repo_url, password):
if not self.login_keychain: self._set_keychain()
if not self.login_keychain:
self._set_keychain()

SecKeychainAddGenericPassword(
self.login_keychain,
Expand All @@ -52,7 +57,8 @@ def set_password(self, service, repo_url, password):
None)

def get_password(self, service, repo_url):
if not self.login_keychain: self._set_keychain()
if not self.login_keychain:
self._set_keychain()

result, password_length, password_buffer, keychain_item = SecKeychainFindGenericPassword(
self.login_keychain, len(service), service.encode(), len(repo_url), repo_url.encode(), None, None, None)
Expand All @@ -62,6 +68,17 @@ def get_password(self, service, repo_url):
password = _resolve_password(password_length, password_buffer)
return password

@property
def is_unlocked(self):
kSecUnlockStateStatus = 1

if not self.login_keychain:
self._set_keychain()

result, keychain_status = SecKeychainGetStatus(self.login_keychain, None)

return keychain_status & kSecUnlockStateStatus


def _resolve_password(password_length, password_buffer):
from ctypes import c_char
Expand Down
4 changes: 4 additions & 0 deletions src/vorta/keyring/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ def get_password(self, service, repo_url):
@property
def is_primary(self):
return False

@property
def is_unlocked(self):
return True
50 changes: 32 additions & 18 deletions src/vorta/keyring/secretstorage.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import secretstorage
import asyncio
import sys

import secretstorage

from vorta.keyring.abc import VortaKeyring
from vorta.log import logger

Expand All @@ -16,23 +19,34 @@ def __init__(self):
secretstorage.get_default_collection(self.connection)

def set_password(self, service, repo_url, password):
asyncio.set_event_loop(asyncio.new_event_loop())
collection = secretstorage.get_default_collection(self.connection)
attributes = {
'application': 'Vorta',
'service': service,
'repo_url': repo_url,
'xdg:schema': 'org.freedesktop.Secret.Generic'}
collection.create_item(repo_url, attributes, password, replace=True)
try:
asyncio.set_event_loop(asyncio.new_event_loop())
collection = secretstorage.get_default_collection(self.connection)
attributes = {
'application': 'Vorta',
'service': service,
'repo_url': repo_url,
'xdg:schema': 'org.freedesktop.Secret.Generic'}
collection.create_item(repo_url, attributes, password, replace=True)
except secretstorage.exceptions.ItemNotFoundException:
logger.error("SecretStorage writing failed", exc_info=sys.exc_info())

def get_password(self, service, repo_url):
asyncio.set_event_loop(asyncio.new_event_loop())
collection = secretstorage.get_default_collection(self.connection)
if collection.is_locked():
collection.unlock()
attributes = {'application': 'Vorta', 'service': service, 'repo_url': repo_url}
items = list(collection.search_items(attributes))
logger.debug('Found %i passwords matching repo URL.', len(items))
if len(items) > 0:
return items[0].get_secret().decode("utf-8")
if self.is_unlocked:
asyncio.set_event_loop(asyncio.new_event_loop())
collection = secretstorage.get_default_collection(self.connection)
attributes = {'application': 'Vorta', 'service': service, 'repo_url': repo_url}
items = list(collection.search_items(attributes))
logger.debug('Found %i passwords matching repo URL.', len(items))
if len(items) > 0:
return items[0].get_secret().decode("utf-8")
return None

@property
def is_unlocked(self):
try:
collection = secretstorage.get_default_collection(self.connection)
return not collection.is_locked()
except secretstorage.exceptions.SecretServiceNotAvailableException:
logger.debug('SecretStorage is closed.')
return False
6 changes: 6 additions & 0 deletions src/vorta/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,12 @@ def parse_args():
parser.add_argument('--daemonize', '-d',
action='store_true',
help="Fork to background and don't open window on startup.")
parser.add_argument(
'--create',
nargs='+',
dest='profile',
help='Create a backup in the background using the given profile(s). '
'Vorta must already be running for this to work.')

return parser.parse_known_args()[0]

Expand Down

0 comments on commit 0db515a

Please sign in to comment.