summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTomás Touceda <chiiph@leap.se>2014-06-06 14:39:30 -0300
committerTomás Touceda <chiiph@leap.se>2014-06-06 14:39:30 -0300
commitc621fa7322b4f8151eb37b27f8aeae563cf6bd63 (patch)
tree55b17318254974378e289f01ec68031ea1f834ad
parent4c550c558dcb554b3ea1bc0246492e39e8532886 (diff)
parent6ab80f96b1ed14ccf96cae37ff207649a26a38ed (diff)
Merge branch 'release-0.5.2'0.5.2
-rw-r--r--CHANGELOG.rst34
-rw-r--r--Makefile2
-rw-r--r--data/images/countries/nl.pngbin0 -> 453 bytes
-rw-r--r--data/images/countries/tr.pngbin0 -> 492 bytes
-rw-r--r--data/images/countries/us.pngbin0 -> 609 bytes
-rw-r--r--data/resources/eipstatus.qrc7
-rw-r--r--docs/man/bitmask-root.1.rst61
-rw-r--r--docs/man/bitmask.1.rst22
-rw-r--r--docs/release_checklist.wiki1
-rwxr-xr-xpkg/linux/bitmask-root118
-rw-r--r--pkg/requirements.pip5
-rw-r--r--relnotes.txt18
-rwxr-xr-xsetup.py6
-rw-r--r--src/leap/bitmask/app.py105
-rw-r--r--src/leap/bitmask/backend.py948
-rw-r--r--src/leap/bitmask/backend_app.py0
-rw-r--r--src/leap/bitmask/config/flags.py2
-rw-r--r--src/leap/bitmask/config/providerconfig.py100
-rw-r--r--src/leap/bitmask/frontend_app.py0
-rw-r--r--src/leap/bitmask/gui/advanced_key_management.py247
-rw-r--r--src/leap/bitmask/gui/eip_status.py313
-rw-r--r--src/leap/bitmask/gui/loggerwindow.py2
-rw-r--r--src/leap/bitmask/gui/login.py5
-rw-r--r--src/leap/bitmask/gui/mail_status.py2
-rw-r--r--src/leap/bitmask/gui/mainwindow.py1001
-rw-r--r--src/leap/bitmask/gui/preferenceswindow.py209
-rw-r--r--src/leap/bitmask/gui/statemachines.py17
-rw-r--r--src/leap/bitmask/gui/ui/eip_status.ui40
-rw-r--r--src/leap/bitmask/gui/wizard.py102
-rw-r--r--src/leap/bitmask/logs/__init__.py3
-rw-r--r--src/leap/bitmask/logs/leap_log_handler.py (renamed from src/leap/bitmask/util/leap_log_handler.py)2
-rw-r--r--src/leap/bitmask/logs/log_silencer.py (renamed from src/leap/bitmask/util/log_silencer.py)0
-rw-r--r--src/leap/bitmask/logs/streamtologger.py (renamed from src/leap/bitmask/util/streamtologger.py)0
-rw-r--r--src/leap/bitmask/logs/tests/test_leap_log_handler.py (renamed from src/leap/bitmask/util/tests/test_leap_log_handler.py)2
-rw-r--r--src/leap/bitmask/logs/tests/test_streamtologger.py (renamed from src/leap/bitmask/util/tests/test_streamtologger.py)2
-rw-r--r--src/leap/bitmask/logs/utils.py92
-rw-r--r--src/leap/bitmask/platform_init/initializers.py72
-rw-r--r--src/leap/bitmask/services/eip/conductor.py321
-rw-r--r--src/leap/bitmask/services/eip/darwinvpnlauncher.py2
-rw-r--r--src/leap/bitmask/services/eip/eipconfig.py28
-rw-r--r--src/leap/bitmask/services/eip/linuxvpnlauncher.py8
-rw-r--r--src/leap/bitmask/services/eip/vpnlauncher.py9
-rw-r--r--src/leap/bitmask/services/eip/vpnprocess.py88
-rw-r--r--src/leap/bitmask/services/mail/conductor.py135
-rw-r--r--src/leap/bitmask/services/mail/imapcontroller.py103
-rw-r--r--src/leap/bitmask/services/mail/smtpbootstrapper.py26
-rw-r--r--src/leap/bitmask/services/soledad/soledadbootstrapper.py104
-rw-r--r--src/leap/bitmask/util/__init__.py5
-rw-r--r--src/leap/bitmask/util/credentials.py (renamed from src/leap/bitmask/util/password.py)31
49 files changed, 2918 insertions, 1482 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index b429595b..4faceb98 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -6,6 +6,40 @@ History
2014
====
+0.5.2 June 6 -- the "are we there yet" release:
++++++++++++++++++++++++++++++++++++++++++++++++
+
+- Unblock local multicast IPs from linux firewall, to allow SSDP and
+ Bonjour/mDNS to work.
+- Add support for gnome-shell polkit agent. Closes #4144, #4218.
+- Update username regex to support the same as webapp. Closes #5965.
+- Wrong error message for username too short. Fixes #5697.
+- Cleanup and refactor username/password validators.
+- Fix EIP autostart failing. Closes #5721.
+- Block ipv6 traffic for the moment. Closes #5693
+- Fix bug with ipv6 blocking that caused block to not get removed from
+ firewall when Bitmask quit.
+- Bring firewall down when switching EIP off. Closes #5687
+- Add OPENVPN_BIN_PATH for OSX so that EIP starts properly.
+- Allow usernames to end in a digit.
+- Improve signal handling in the mainwindow and wizard.
+- Enable UI when OpenVPN bin is not found, plus check before starting
+ EIP. Fixes #5619.
+- Properly set the userid for SMTP.
+- Update EIP UI if it fails to download the config.
+- Make use of cmdline in psutil backwards-compatible. Closes #5689
+- Add versioning support to bitmask-root.
+- Show flag of country for eip exit node, if available. Related #1232
+- Fix nameserver restoring. Closes #5692
+- Warn user if resolvconf cannot be found.
+- Refactor Keymanager to backend. Closes #5711.
+- Cleanup backend from hacks. Closes #5698.
+- Improve wait and quit process.
+- Move soledad password change to backend.
+- Move Mail logic to backend.
+- Separate imap/smtp logic from conductor.
+- Refactor SoledadBootstrapper to backend. Closes #5481.
+
0.5.1 May 16 -- the "lil less leaky" release:
+++++++++++++++++++++++++++++++++++++++++++++
diff --git a/Makefile b/Makefile
index 358af126..73437449 100644
--- a/Makefile
+++ b/Makefile
@@ -22,7 +22,7 @@ PROJFILE = data/bitmask.pro
#UI files to compile
UI_FILES = loggerwindow.ui mainwindow.ui wizard.ui login.ui preferences.ui eip_status.ui mail_status.ui eippreferences.ui advanced_key_management.ui
#Qt resource files to compile
-RESOURCES = locale.qrc loggerwindow.qrc mainwindow.qrc icons.qrc
+RESOURCES = locale.qrc loggerwindow.qrc mainwindow.qrc icons.qrc eipstatus.qrc
#pyuic4 and pyrcc4 binaries
PYUIC = pyside-uic
diff --git a/data/images/countries/nl.png b/data/images/countries/nl.png
new file mode 100644
index 00000000..fe44791e
--- /dev/null
+++ b/data/images/countries/nl.png
Binary files differ
diff --git a/data/images/countries/tr.png b/data/images/countries/tr.png
new file mode 100644
index 00000000..be32f77e
--- /dev/null
+++ b/data/images/countries/tr.png
Binary files differ
diff --git a/data/images/countries/us.png b/data/images/countries/us.png
new file mode 100644
index 00000000..10f451fe
--- /dev/null
+++ b/data/images/countries/us.png
Binary files differ
diff --git a/data/resources/eipstatus.qrc b/data/resources/eipstatus.qrc
new file mode 100644
index 00000000..5d0f2924
--- /dev/null
+++ b/data/resources/eipstatus.qrc
@@ -0,0 +1,7 @@
+<RCC>
+ <qresource prefix="/">
+ <file>../images/countries/nl.png</file>
+ <file>../images/countries/tr.png</file>
+ <file>../images/countries/us.png</file>
+ </qresource>
+</RCC>
diff --git a/docs/man/bitmask-root.1.rst b/docs/man/bitmask-root.1.rst
new file mode 100644
index 00000000..c18cc4d6
--- /dev/null
+++ b/docs/man/bitmask-root.1.rst
@@ -0,0 +1,61 @@
+============
+bitmask-root
+============
+
+------------------------------------------------------------------------
+privileged helper for bitmask, the encrypted internet access toolkit.
+------------------------------------------------------------------------
+
+:Author: LEAP Encryption Access Project https://leap.se
+:Date: 2014-06-05
+:Copyright: GPLv3+
+:Version: 0.5.2
+:Manual section: 1
+:Manual group: General Commands Manual
+
+SYNOPSIS
+========
+
+bitmask-root [openvpn | firewall | version] [start | stop | isup] [ARGS]
+
+DESCRIPTION
+===========
+
+*bitmask-root* is a privileged helper for bitmask.
+
+It is used to start or stop openvpn and the bitmask firewall. To operate, it
+needs to be executed with root privileges.
+
+
+OPTIONS
+=======
+
+openvpn
+--------
+
+**start** [ARGS] Starts openvpn. All args are passed to openvpn, and
+ filtered against a list of allowed args. If the next
+ argument is `restart`, the firewall will not be teared
+ down in the case of errors lauching openvpn.
+
+**stop** Stops openvpn.
+
+
+firewall
+---------
+
+**start** [GATEWAYS] Starts the firewall. GATEWAYS is a list of EIP
+ gateways to allow in the firewall.
+
+**stop** Stops the firewall.
+
+version
+--------
+
+**version** Prints the `bitmask-root` version string.
+
+
+BUGS
+====
+
+Please report any bugs to https://leap.se/code
diff --git a/docs/man/bitmask.1.rst b/docs/man/bitmask.1.rst
index ed4f7133..6eae7ff5 100644
--- a/docs/man/bitmask.1.rst
+++ b/docs/man/bitmask.1.rst
@@ -7,9 +7,9 @@ graphical client to control LEAP, the encrypted internet access toolkit.
------------------------------------------------------------------------
:Author: LEAP Encryption Access Project https://leap.se
-:Date: 2013-08-23
+:Date: 2014-06-05
:Copyright: GPLv3+
-:Version: 0.3.1
+:Version: 0.5.2
:Manual section: 1
:Manual group: General Commands Manual
@@ -80,26 +80,20 @@ WARNING
This software is still in its early phases of testing. So don't trust your life to it!
-At the current time, Bitmask is not compatible with ``openresolv``, but it works with ``resolvconf``.
FILES
=====
-/etc/leap/resolv-update
------------------------
-Post up/down script passed to openvpn. It writes /etc/resolv.conf to avoid dns leaks, and restores the original resolv.conf on exit.
-/etc/leap/resolv-head
----------------------
-/etc/leap/resolv-tail
----------------------
+/usr/share/polkit-1/actions/se.leap.bitmask.policy
+-------------------------------------------------------
-Custom entries that will appear in the written resolv.conf
+PolicyKit policy file, used for granting access to bitmask-root without the need of entering a password each time.
-/usr/share/polkit-1/actions/net.openvpn.gui.leap.policy
--------------------------------------------------------
+/usr/sbin/bitmask-root
+------------------------
-PolicyKit policy file, used for granting access to openvpn without the need of entering a password each time.
+Helper to launch and stop openvpn and the bitmask firewall.
~/.config/leap/
---------------
diff --git a/docs/release_checklist.wiki b/docs/release_checklist.wiki
index fc99fdf0..075591a7 100644
--- a/docs/release_checklist.wiki
+++ b/docs/release_checklist.wiki
@@ -1,5 +1,6 @@
= Bitmask Release Checklist (*) =
* [ ] Check that all tests are passing!
+ * [ ] Check that the version in bitmask_client/pkg/linux/bitmask-root is bumped if needed.
* [ ] Tag everything
* Should be done for the following packages, in order:
* [ ] 1. leap.common
diff --git a/pkg/linux/bitmask-root b/pkg/linux/bitmask-root
index 136fd6a4..1929b51b 100755
--- a/pkg/linux/bitmask-root
+++ b/pkg/linux/bitmask-root
@@ -22,14 +22,15 @@ It should only be called by the Bitmask application.
USAGE:
bitmask-root firewall stop
- bitmask-root firewall start GATEWAY1 GATEWAY2 ...
+ bitmask-root firewall start [restart] GATEWAY1 GATEWAY2 ...
bitmask-root openvpn stop
bitmask-root openvpn start CONFIG1 CONFIG1 ...
All actions return exit code 0 for success, non-zero otherwise.
The `openvpn start` action is special: it calls exec on openvpn and replaces
-the current process.
+the current process. If the `restart` parameter is passed, the firewall will
+not be teared down in the case of an error during launch.
"""
# TODO should be tested with python3, which can be the default on some distro.
from __future__ import print_function
@@ -38,18 +39,19 @@ import os
import re
import signal
import socket
+import syslog
import subprocess
import sys
import time
import traceback
-
cmdcheck = subprocess.check_output
##
## CONSTANTS
##
+VERSION = "1"
SCRIPT = "bitmask-root"
NAMESERVER = "10.42.0.1"
BITMASK_CHAIN = "bitmask"
@@ -129,6 +131,8 @@ if DEBUG:
logger.setLevel(logging.DEBUG)
logger.addHandler(ch)
+syslog.openlog(SCRIPT)
+
##
## UTILITY
##
@@ -413,6 +417,7 @@ def bail(msg=None, exception=None):
"""
if msg is not None:
print("%s: %s" % (SCRIPT, msg))
+ syslog.syslog(syslog.LOG_ERR, msg)
if exception is not None:
traceback.print_exc()
exit(1)
@@ -566,7 +571,7 @@ class NameserverRestorer(Daemon):
A daemon that will restore the previous nameservers.
"""
- def run(self):
+ def run(self, *args):
"""
Run when daemonized.
"""
@@ -614,7 +619,7 @@ def get_default_device():
"""
routes = subprocess.check_output([IP, "route", "show"])
match = re.search("^default .*dev ([^\s]*) .*$", routes, flags=re.M)
- if match.groups():
+ if match and match.groups():
return match.group(1)
else:
bail("Could not find default device")
@@ -629,7 +634,7 @@ def get_local_network_ipv4(device):
"""
addresses = cmdcheck([IP, "-o", "address", "show", "dev", device])
match = re.search("^.*inet ([^ ]*) .*$", addresses, flags=re.M)
- if match.groups():
+ if match and match.groups():
return match.group(1)
else:
return None
@@ -644,7 +649,7 @@ def get_local_network_ipv6(device):
"""
addresses = cmdcheck([IP, "-o", "address", "show", "dev", device])
match = re.search("^.*inet6 ([^ ]*) .*$", addresses, flags=re.M)
- if match.groups():
+ if match and match.groups():
return match.group(1)
else:
return None
@@ -653,6 +658,7 @@ def get_local_network_ipv6(device):
def run_iptable_with_check(cmd, *args, **options):
"""
Run an iptables command checking to see if it should:
+ for --append: run only if rule does not already exist.
for --insert: run only if rule does not already exist.
for --delete: run only if rule does exist.
other commands are run normally.
@@ -662,6 +668,11 @@ def run_iptable_with_check(cmd, *args, **options):
check_code = run(cmd, *check_args, exitcode=True)
if check_code != 0:
run(cmd, *args, **options)
+ elif "--append" in args:
+ check_args = [arg.replace("--append", "--check") for arg in args]
+ check_code = run(cmd, *check_args, exitcode=True)
+ if check_code != 0:
+ run(cmd, *args, **options)
elif "--delete" in args:
check_args = [arg.replace("--delete", "--check") for arg in args]
check_code = run(cmd, *check_args, exitcode=True)
@@ -729,41 +740,74 @@ def firewall_start(args):
local_network_ipv6 = get_local_network_ipv6(default_device)
gateways = get_gateways(args)
- # add custom chain "bitmask"
+ # add custom chain "bitmask" to front of OUTPUT chain
if not ipv4_chain_exists(BITMASK_CHAIN):
ip4tables("--new-chain", BITMASK_CHAIN)
if not ipv6_chain_exists(BITMASK_CHAIN):
ip6tables("--new-chain", BITMASK_CHAIN)
iptables("--insert", "OUTPUT", "--jump", BITMASK_CHAIN)
- # reject everything
- iptables("--insert", BITMASK_CHAIN, "-o", default_device,
- "--jump", "REJECT")
+ # allow DNS over VPN
+ for allowed_dns in [NAMESERVER, "127.0.0.1", "127.0.1.1"]:
+ ip4tables("--append", BITMASK_CHAIN, "--protocol", "udp",
+ "--dport", "53", "--destination", allowed_dns,
+ "--jump", "ACCEPT")
- # allow traffic to gateways
- for gateway in gateways:
- ip4tables("--insert", BITMASK_CHAIN, "--destination", gateway,
- "-o", default_device, "--jump", "ACCEPT")
+ # block DNS requests to anyone but the service provider or localhost
+ # (when we actually route ipv6, we will need DNS rules for it too)
+ ip4tables("--append", BITMASK_CHAIN, "--protocol", "udp", "--dport", "53",
+ "--jump", "REJECT")
# allow traffic to IPs on local network
if local_network_ipv4:
- ip4tables("--insert", BITMASK_CHAIN,
+ ip4tables("--append", BITMASK_CHAIN,
"--destination", local_network_ipv4, "-o", default_device,
"--jump", "ACCEPT")
+ # allow multicast Simple Service Discovery Protocol
+ ip4tables("--append", BITMASK_CHAIN,
+ "--protocol", "udp",
+ "--destination", "239.255.255.250", "--dport", "1900",
+ "-o", default_device, "--jump", "RETURN")
+ # allow multicast Bonjour/mDNS
+ ip4tables("--append", BITMASK_CHAIN,
+ "--protocol", "udp",
+ "--destination", "224.0.0.251", "--dport", "5353",
+ "-o", default_device, "--jump", "RETURN")
if local_network_ipv6:
- ip6tables("--insert", BITMASK_CHAIN,
+ ip6tables("--append", BITMASK_CHAIN,
"--destination", local_network_ipv6, "-o", default_device,
"--jump", "ACCEPT")
+ # allow multicast Simple Service Discovery Protocol
+ ip6tables("--append", BITMASK_CHAIN,
+ "--protocol", "udp",
+ "--destination", "FF05::C", "--dport", "1900",
+ "-o", default_device, "--jump", "RETURN")
+ # allow multicast Bonjour/mDNS
+ ip6tables("--append", BITMASK_CHAIN,
+ "--protocol", "udp",
+ "--destination", "FF02::FB", "--dport", "5353",
+ "-o", default_device, "--jump", "RETURN")
+
+ # allow ipv4 traffic to gateways
+ for gateway in gateways:
+ ip4tables("--append", BITMASK_CHAIN, "--destination", gateway,
+ "-o", default_device, "--jump", "ACCEPT")
- # block DNS requests to anyone but the service provider or localhost
- # when we actually route ipv6, we will need dns rules for it too
- ip4tables("--insert", BITMASK_CHAIN, "--protocol", "udp", "--dport", "53",
- "--jump", "REJECT")
+ # log rejected packets to syslog
+ if DEBUG:
+ iptables("--append", BITMASK_CHAIN, "-o", default_device,
+ "--jump", "LOG", "--log-prefix", "iptables denied: ",
+ "--log-level", "7")
- for allowed_dns in [NAMESERVER, "127.0.0.1", "127.0.1.1"]:
- ip4tables("--insert", BITMASK_CHAIN, "--protocol", "udp",
- "--dport", "53", "--destination", allowed_dns,
- "--jump", "ACCEPT")
+ # for now, ensure all other ipv6 packets get rejected (regardless of
+ # device)
+ # (not sure why, but "-p any" doesn't work)
+ ip6tables("--append", BITMASK_CHAIN, "-p", "tcp", "--jump", "REJECT")
+ ip6tables("--append", BITMASK_CHAIN, "-p", "udp", "--jump", "REJECT")
+
+ # reject all other ipv4 sent over the default device
+ ip4tables("--append", BITMASK_CHAIN, "-o",
+ default_device, "--jump", "REJECT")
def firewall_stop():
@@ -784,10 +828,27 @@ def firewall_stop():
def main():
- if len(sys.argv) >= 3:
+ """
+ Entry point for cmdline execution.
+ """
+ # TODO use argparse instead.
+
+ if len(sys.argv) >= 2:
command = "_".join(sys.argv[1:3])
args = sys.argv[3:]
+ is_restart = False
+ if args and args[0] == "restart":
+ is_restart = True
+ args.remove('restart')
+
+ if command == "version":
+ print(VERSION)
+ exit(0)
+
+ if os.getuid() != 0:
+ bail("ERROR: must be run as root")
+
if command == "openvpn_start":
openvpn_start(args)
@@ -799,8 +860,9 @@ def main():
firewall_start(args)
nameserver_setter.start(NAMESERVER)
except Exception as ex:
- nameserver_restorer.start()
- firewall_stop()
+ if not is_restart:
+ nameserver_restorer.start()
+ firewall_stop()
bail("ERROR: could not start firewall", ex)
elif command == "firewall_stop":
diff --git a/pkg/requirements.pip b/pkg/requirements.pip
index 70427e63..3d6b33a3 100644
--- a/pkg/requirements.pip
+++ b/pkg/requirements.pip
@@ -11,10 +11,7 @@ srp>=1.0.2
pyopenssl
python-dateutil
-# since gnupg requires exactly 1.2.1, this chokes if we
-# don't specify a version. Selecting something lesser than
-# 2.0 is equivalent to pick 1.2.1. See #5489
-psutil<2.0
+psutil
ipaddr
twisted
diff --git a/relnotes.txt b/relnotes.txt
index a658a782..e95e8c15 100644
--- a/relnotes.txt
+++ b/relnotes.txt
@@ -1,8 +1,8 @@
-ANNOUNCING Bitmask, the Internet Encryption Toolkit, release 0.5.1
+ANNOUNCING Bitmask, the Internet Encryption Toolkit, release 0.5.2
The LEAP team is pleased to announce the immediate availability of
-version 0.5.1 of Bitmask, the Internet Encryption Toolkit, codename
-"lil less leaky".
+version 0.5.2 of Bitmask, the Internet Encryption Toolkit, codename
+"are we there yet".
https://downloads.leap.se/client/
@@ -43,13 +43,9 @@ NOT trust your life to it.
WHAT CAN THIS VERSION OF BITMASK DO FOR ME?
-Bitmask 0.5.1 improves greatly its mail support and stability in
-general, among other various bug fixes. You can refer to the CHANGELOG
-for the meat.
-
-As always, you can connect to the Encrypted Internet Proxy service
-offered by a provider of your choice, and enjoy a encrypted internet
-connection that the spying eyes can only track back to your provider.
+Bitmask 0.5.2 improves greatly its Encrypted internet support and
+stability in general, among other various bug fixes. You can refer to
+the CHANGELOG for the meat.
Encrypted Internet on Linux now helps you don't shoot yourself in the
foot by leaking traffic outside of the secure connection it
@@ -108,6 +104,6 @@ beyond any border.
The LEAP team,
-May 16, 2014
+June 6, 2014
Somewhere in the middle of the intertubes.
EOF
diff --git a/setup.py b/setup.py
index de31be4b..3d12db64 100755
--- a/setup.py
+++ b/setup.py
@@ -203,9 +203,9 @@ if IS_LINUX:
# globally. Or make specific install command. See #3805
data_files = [
("share/polkit-1/actions",
- ["pkg/linux/polkit/net.openvpn.gui.leap.policy"]),
- ("etc/leap/",
- ["pkg/linux/resolv-update"]),
+ ["pkg/linux/polkit/se.leap.bitmask.policy"]),
+ ("/usr/sbin",
+ ["pkg/linux/bitmask-root"]),
]
diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py
index e413ab4c..e965604a 100644
--- a/src/leap/bitmask/app.py
+++ b/src/leap/bitmask/app.py
@@ -39,7 +39,6 @@
# M:::::::::::~NMMM7???7MMMM:::::::::::::::::::::::NMMMI??I7MMMM:::::::::::::M
# M::::::::::::::7MMMMMMM+:::::::::::::::::::::::::::?MMMMMMMZ:::::::::::::::M
# (thanks to: http://www.glassgiant.com/ascii/)
-import logging
import signal
import sys
import os
@@ -50,10 +49,7 @@ from PySide import QtCore, QtGui
from leap.bitmask import __version__ as VERSION
from leap.bitmask.util import leap_argparse
-from leap.bitmask.util import log_silencer, LOG_FORMAT
-from leap.bitmask.util.leap_log_handler import LeapLogHandler
-from leap.bitmask.util.streamtologger import StreamToLogger
-from leap.bitmask.platform_init import IS_WIN
+from leap.bitmask.logs.utils import get_logger
from leap.bitmask.services.mail import plumber
from leap.common.events import server as event_server
from leap.mail import __version__ as MAIL_VERSION
@@ -76,6 +72,7 @@ def sigint_handler(*args, **kwargs):
mainwindow = args[0]
mainwindow.quit()
+
def sigterm_handler(*args, **kwargs):
"""
Signal handler for SIGTERM.
@@ -87,89 +84,6 @@ def sigterm_handler(*args, **kwargs):
mainwindow = args[0]
mainwindow.quit()
-def add_logger_handlers(debug=False, logfile=None, replace_stdout=True):
- """
- Create the logger and attach the handlers.
-
- :param debug: the level of the messages that we should log
- :type debug: bool
- :param logfile: the file name of where we should to save the logs
- :type logfile: str
- :return: the new logger with the attached handlers.
- :rtype: logging.Logger
- """
- # TODO: get severity from command line args
- if debug:
- level = logging.DEBUG
- else:
- level = logging.WARNING
-
- # Create logger and formatter
- logger = logging.getLogger(name='leap')
- logger.setLevel(level)
- formatter = logging.Formatter(LOG_FORMAT)
-
- # Console handler
- try:
- import coloredlogs
- console = coloredlogs.ColoredStreamHandler(level=level)
- except ImportError:
- console = logging.StreamHandler()
- console.setLevel(level)
- console.setFormatter(formatter)
- using_coloredlog = False
- else:
- using_coloredlog = True
-
- if using_coloredlog:
- replace_stdout = False
-
- silencer = log_silencer.SelectiveSilencerFilter()
- console.addFilter(silencer)
- logger.addHandler(console)
- logger.debug('Console handler plugged!')
-
- # LEAP custom handler
- leap_handler = LeapLogHandler()
- leap_handler.setLevel(level)
- leap_handler.addFilter(silencer)
- logger.addHandler(leap_handler)
- logger.debug('Leap handler plugged!')
-
- # File handler
- if logfile is not None:
- logger.debug('Setting logfile to %s ', logfile)
- fileh = logging.FileHandler(logfile)
- fileh.setLevel(logging.DEBUG)
- fileh.setFormatter(formatter)
- fileh.addFilter(silencer)
- logger.addHandler(fileh)
- logger.debug('File handler plugged!')
-
- if replace_stdout:
- replace_stdout_stderr_with_logging(logger)
-
- return logger
-
-
-def replace_stdout_stderr_with_logging(logger):
- """
- Replace:
- - the standard output
- - the standard error
- - the twisted log output
- with a custom one that writes to the logger.
- """
- # Disabling this on windows since it breaks ALL THE THINGS
- # The issue for this is #4149
- if not IS_WIN:
- sys.stdout = StreamToLogger(logger, logging.DEBUG)
- sys.stderr = StreamToLogger(logger, logging.ERROR)
-
- # Replace twisted's logger to use our custom output.
- from twisted.python import log
- log.startLogging(sys.stdout)
-
def do_display_version(opts):
"""
@@ -212,6 +126,14 @@ def main():
mail_logfile = opts.mail_log_file
start_hidden = opts.start_hidden
+ replace_stdout = True
+ if opts.repair or opts.import_maildir:
+ # We don't want too much clutter on the comand mode
+ # this could be more generic with a Command class.
+ replace_stdout = False
+
+ logger = get_logger(debug, logfile, replace_stdout)
+
#############################################################
# Given how paths and bundling works, we need to delay the imports
# of certain parts that depend on this path settings.
@@ -230,13 +152,6 @@ def main():
BaseConfig.standalone = standalone
- replace_stdout = True
- if opts.repair or opts.import_maildir:
- # We don't want too much clutter on the comand mode
- # this could be more generic with a Command class.
- replace_stdout = False
- logger = add_logger_handlers(debug, logfile, replace_stdout)
-
# ok, we got logging in place, we can satisfy mail plumbing requests
# and show logs there. it normally will exit there if we got that path.
do_mail_plumbing(opts)
diff --git a/src/leap/bitmask/backend.py b/src/leap/bitmask/backend.py
index a2df465d..3c97c797 100644
--- a/src/leap/bitmask/backend.py
+++ b/src/leap/bitmask/backend.py
@@ -17,13 +17,13 @@
"""
Backend for everything
"""
-import commands
import logging
import os
import time
from functools import partial
from Queue import Queue, Empty
+from threading import Condition
from twisted.internet import reactor
from twisted.internet import threads, defer
@@ -31,46 +31,40 @@ from twisted.internet.task import LoopingCall
from twisted.python import log
import zope.interface
+import zope.proxy
from leap.bitmask.config.providerconfig import ProviderConfig
from leap.bitmask.crypto.srpauth import SRPAuth
from leap.bitmask.crypto.srpregister import SRPRegister
from leap.bitmask.platform_init import IS_LINUX
-from leap.bitmask.provider import get_provider_path
from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper
+from leap.bitmask.services import get_supported
from leap.bitmask.services.eip import eipconfig
from leap.bitmask.services.eip import get_openvpn_management
from leap.bitmask.services.eip.eipbootstrapper import EIPBootstrapper
from leap.bitmask.services.eip import vpnlauncher, vpnprocess
from leap.bitmask.services.eip import linuxvpnlauncher, darwinvpnlauncher
+from leap.bitmask.services.eip import get_vpn_launcher
-from leap.common import certs as leap_certs
-
-# Frontend side
-from PySide import QtCore
+from leap.bitmask.services.mail.imapcontroller import IMAPController
+from leap.bitmask.services.mail.smtpbootstrapper import SMTPBootstrapper
+from leap.bitmask.services.mail.smtpconfig import SMTPConfig
-logger = logging.getLogger(__name__)
+from leap.bitmask.services.soledad.soledadbootstrapper import \
+ SoledadBootstrapper
+from leap.common import certs as leap_certs
-def get_provider_config(config, domain):
- """
- Return the ProviderConfig object for the given domain.
- If it is already loaded in `config`, then don't reload.
+from leap.keymanager import openpgp
+from leap.keymanager.errors import KeyAddressMismatch, KeyFingerprintMismatch
- :param config: a ProviderConfig object
- :type conig: ProviderConfig
- :param domain: the domain which config is required.
- :type domain: unicode
+from leap.soledad.client import NoStorageSecret, PassphraseTooShort
- :returns: True if the config was loaded successfully, False otherwise.
- :rtype: bool
- """
- # TODO: see ProviderConfig.get_provider_config
- if (not config.loaded() or config.get_domain() != domain):
- config.load(get_provider_path(domain))
+# Frontend side
+from PySide import QtCore
- return config.loaded()
+logger = logging.getLogger(__name__)
class ILEAPComponent(zope.interface.Interface):
@@ -86,13 +80,13 @@ class ILEAPService(ILEAPComponent):
Interface that every Service needs to implement
"""
- def start(self):
+ def start(self, *args, **kwargs):
"""
Start the service.
"""
pass
- def stop(self):
+ def stop(self, *args, **kwargs):
"""
Stops the service.
"""
@@ -156,6 +150,7 @@ class Provider(object):
:type bypass_checks: bool
"""
self.key = "provider"
+ self._signaler = signaler
self._provider_bootstrapper = ProviderBootstrapper(signaler,
bypass_checks)
self._download_provider_defer = None
@@ -197,11 +192,11 @@ class Provider(object):
"""
d = None
- config = self._provider_config
- if get_provider_config(config, provider):
+ config = ProviderConfig.get_provider_config(provider)
+ self._provider_config = config
+ if config is not None:
d = self._provider_bootstrapper.run_provider_setup_checks(
- self._provider_config,
- download_if_needed=True)
+ config, download_if_needed=True)
else:
if self._signaler is not None:
self._signaler.signal(
@@ -213,6 +208,73 @@ class Provider(object):
d = defer.Deferred()
return d
+ def _get_services(self, domain):
+ """
+ Returns a list of services provided by the given provider.
+
+ :param domain: the provider to get the services from.
+ :type domain: str
+
+ :rtype: list of str
+ """
+ services = []
+ provider_config = ProviderConfig.get_provider_config(domain)
+ if provider_config is not None:
+ services = provider_config.get_services()
+
+ return services
+
+ def get_supported_services(self, domain):
+ """
+ Signal a list of supported services provided by the given provider.
+
+ :param domain: the provider to get the services from.
+ :type domain: str
+
+ Signals:
+ prov_get_supported_services -> list of unicode
+ """
+ services = get_supported(self._get_services(domain))
+
+ self._signaler.signal(
+ self._signaler.PROV_GET_SUPPORTED_SERVICES, services)
+
+ def get_all_services(self, providers):
+ """
+ Signal a list of services provided by all the configured providers.
+
+ :param providers: the list of providers to get the services.
+ :type providers: list
+
+ Signals:
+ prov_get_all_services -> list of unicode
+ """
+ services_all = set()
+
+ for domain in providers:
+ services = self._get_services(domain)
+ services_all = services_all.union(set(services))
+
+ self._signaler.signal(
+ self._signaler.PROV_GET_ALL_SERVICES, services_all)
+
+ def get_details(self, domain, lang=None):
+ """
+ Signal a ProviderConfigLight object with the current ProviderConfig
+ settings.
+
+ :param domain: the domain name of the provider.
+ :type domain: str
+ :param lang: the language to use for localized strings.
+ :type lang: str
+
+ Signals:
+ prov_get_details -> ProviderConfigLight
+ """
+ self._signaler.signal(
+ self._signaler.PROV_GET_DETAILS,
+ self._provider_config.get_light_config(domain, lang))
+
class Register(object):
"""
@@ -246,8 +308,9 @@ class Register(object):
:returns: the defer for the operation running in a thread.
:rtype: twisted.internet.defer.Deferred
"""
- config = ProviderConfig()
- if get_provider_config(config, domain):
+ config = ProviderConfig.get_provider_config(domain)
+ self._provider_config = config
+ if config is not None:
srpregister = SRPRegister(signaler=self._signaler,
provider_config=config)
return threads.deferToThread(
@@ -294,8 +357,9 @@ class EIP(object):
:returns: the defer for the operation running in a thread.
:rtype: twisted.internet.defer.Deferred
"""
- config = self._provider_config
- if get_provider_config(config, domain):
+ config = ProviderConfig.get_provider_config(domain)
+ self._provider_config = config
+ if config is not None:
if skip_network:
return defer.Deferred()
eb = self._eip_bootstrapper
@@ -314,9 +378,12 @@ class EIP(object):
if d is not None:
d.cancel()
- def _start_eip(self):
+ def _start_eip(self, restart=False):
"""
Start EIP
+
+ :param restart: whether is is a restart.
+ :type restart: bool
"""
provider_config = self._provider_config
eip_config = eipconfig.EIPConfig()
@@ -325,6 +392,11 @@ class EIP(object):
loaded = eipconfig.load_eipconfig_if_needed(
provider_config, eip_config, domain)
+ if not self._can_start(domain):
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.EIP_CONNECTION_ABORTED)
+ return
+
if not loaded:
if self._signaler is not None:
self._signaler.signal(self._signaler.EIP_CONNECTION_ABORTED)
@@ -335,9 +407,10 @@ class EIP(object):
host, port = get_openvpn_management()
self._vpn.start(eipconfig=eip_config,
providerconfig=provider_config,
- socket_host=host, socket_port=port)
+ socket_host=host, socket_port=port,
+ restart=restart)
- def start(self):
+ def start(self, *args, **kwargs):
"""
Start the service.
"""
@@ -350,7 +423,7 @@ class EIP(object):
return
try:
- self._start_eip()
+ self._start_eip(*args, **kwargs)
except vpnprocess.OpenVPNAlreadyRunning:
signaler.signal(signaler.EIP_OPENVPN_ALREADY_RUNNING)
except vpnprocess.AlienOpenVPNAlreadyRunning:
@@ -370,17 +443,22 @@ class EIP(object):
except Exception as e:
logger.error("Unexpected problem: {0!r}".format(e))
else:
- # TODO: are we connected here?
- signaler.signal(signaler.EIP_CONNECTED)
+ logger.debug('EIP: no errors')
- def stop(self, shutdown=False):
+ def _do_stop(self, shutdown=False, restart=False):
"""
- Stop the service.
+ Stop the service. This is run in a thread to avoid blocking.
"""
- self._vpn.terminate(shutdown)
+ self._vpn.terminate(shutdown, restart)
if IS_LINUX:
self._wait_for_firewall_down()
+ def stop(self, shutdown=False, restart=False):
+ """
+ Stop the service.
+ """
+ return threads.deferToThread(self._do_stop, shutdown, restart)
+
def _wait_for_firewall_down(self):
"""
Wait for the firewall to come down.
@@ -393,15 +471,16 @@ class EIP(object):
MAX_FW_WAIT_RETRIES = 25
FW_WAIT_STEP = 0.5
- retry = 0
-
- fw_up_cmd = "pkexec /usr/sbin/bitmask-root firewall isup"
- fw_is_down = lambda: commands.getstatusoutput(fw_up_cmd)[0] == 256
+ retry = 1
- while retry < MAX_FW_WAIT_RETRIES:
- if fw_is_down():
+ while retry <= MAX_FW_WAIT_RETRIES:
+ if self._vpn.is_fw_down():
+ self._signaler.signal(self._signaler.EIP_STOPPED)
return
else:
+ #msg = "Firewall is not down yet, waiting... {0} of {1}"
+ #msg = msg.format(retry, MAX_FW_WAIT_RETRIES)
+ #logger.debug(msg)
time.sleep(FW_WAIT_STEP)
retry += 1
logger.warning("After waiting, firewall is not down... "
@@ -459,6 +538,12 @@ class EIP(object):
self._signaler.signal(self._signaler.EIP_GET_INITIALIZED_PROVIDERS,
filtered_domains)
+ def tear_fw_down(self):
+ """
+ Tear the firewall down.
+ """
+ self._vpn.tear_down_firewall()
+
def get_gateways_list(self, domain):
"""
Signal a list of gateways for the given provider.
@@ -497,6 +582,46 @@ class EIP(object):
self._signaler.signal(
self._signaler.EIP_GET_GATEWAYS_LIST, gateways)
+ def _can_start(self, domain):
+ """
+ Returns True if it has everything that is needed to run EIP,
+ False otherwise
+
+ :param domain: the domain for the provider to check
+ :type domain: str
+ """
+ eip_config = eipconfig.EIPConfig()
+ provider_config = ProviderConfig.get_provider_config(domain)
+
+ api_version = provider_config.get_api_version()
+ eip_config.set_api_version(api_version)
+ eip_loaded = eip_config.load(eipconfig.get_eipconfig_path(domain))
+
+ launcher = get_vpn_launcher()
+ if not os.path.isfile(launcher.OPENVPN_BIN_PATH):
+ logger.error("Cannot start OpenVPN, binary not found")
+ return False
+
+ # check for other problems
+ if not eip_loaded or provider_config is None:
+ logger.error("Cannot load provider and eip config, cannot "
+ "autostart")
+ return False
+
+ client_cert_path = eip_config.\
+ get_client_cert_path(provider_config, about_to_download=False)
+
+ if leap_certs.should_redownload(client_cert_path):
+ logger.error("The client should redownload the certificate,"
+ " cannot autostart")
+ return False
+
+ if not os.path.isfile(client_cert_path):
+ logger.error("Can't find the certificate, cannot autostart")
+ return False
+
+ return True
+
def can_start(self, domain):
"""
Signal whether it has everything that is needed to run EIP or not
@@ -508,35 +633,363 @@ class EIP(object):
eip_can_start
eip_cannot_start
"""
- try:
- eip_config = eipconfig.EIPConfig()
- provider_config = ProviderConfig.get_provider_config(domain)
+ if self._can_start(domain):
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.EIP_CAN_START)
+ else:
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.EIP_CANNOT_START)
- api_version = provider_config.get_api_version()
- eip_config.set_api_version(api_version)
- eip_loaded = eip_config.load(eipconfig.get_eipconfig_path(domain))
- # check for other problems
- if not eip_loaded or provider_config is None:
- raise Exception("Cannot load provider and eip config, cannot "
- "autostart")
+class Soledad(object):
+ """
+ Interfaces with setup of Soledad.
+ """
+ zope.interface.implements(ILEAPComponent)
- client_cert_path = eip_config.\
- get_client_cert_path(provider_config, about_to_download=False)
+ def __init__(self, soledad_proxy, keymanager_proxy, signaler=None):
+ """
+ Constructor for the Soledad component.
- if leap_certs.should_redownload(client_cert_path):
- raise Exception("The client should redownload the certificate,"
- " cannot autostart")
+ :param soledad_proxy: proxy to pass around a Soledad object.
+ :type soledad_proxy: zope.ProxyBase
+ :param keymanager_proxy: proxy to pass around a Keymanager object.
+ :type keymanager_proxy: zope.ProxyBase
+ :param signaler: Object in charge of handling communication
+ back to the frontend
+ :type signaler: Signaler
+ """
+ self.key = "soledad"
+ self._soledad_proxy = soledad_proxy
+ self._keymanager_proxy = keymanager_proxy
+ self._signaler = signaler
+ self._soledad_bootstrapper = SoledadBootstrapper(signaler)
+ self._soledad_defer = None
- if not os.path.isfile(client_cert_path):
- raise Exception("Can't find the certificate, cannot autostart")
+ def bootstrap(self, username, domain, password):
+ """
+ Bootstrap Soledad with the user credentials.
+ Signals:
+ soledad_download_config
+ soledad_gen_key
+
+ :param user: user's login
+ :type user: unicode
+ :param domain: the domain that we are using.
+ :type domain: unicode
+ :param password: user's password
+ :type password: unicode
+ """
+ provider_config = ProviderConfig.get_provider_config(domain)
+ if provider_config is not None:
+ self._soledad_defer = threads.deferToThread(
+ self._soledad_bootstrapper.run_soledad_setup_checks,
+ provider_config, username, password,
+ download_if_needed=True)
+ self._soledad_defer.addCallback(self._set_proxies_cb)
+ else:
if self._signaler is not None:
- self._signaler.signal(self._signaler.EIP_CAN_START)
- except Exception as e:
- logger.exception(e)
- if self._signaler is not None:
- self._signaler.signal(self._signaler.EIP_CANNOT_START)
+ self._signaler.signal(self._signaler.SOLEDAD_BOOTSTRAP_FAILED)
+ logger.error("Could not load provider configuration.")
+
+ return self._soledad_defer
+
+ def _set_proxies_cb(self, _):
+ """
+ Update the soledad and keymanager proxies to reference the ones created
+ in the bootstrapper.
+ """
+ zope.proxy.setProxiedObject(self._soledad_proxy,
+ self._soledad_bootstrapper.soledad)
+ zope.proxy.setProxiedObject(self._keymanager_proxy,
+ self._soledad_bootstrapper.keymanager)
+
+ def load_offline(self, username, password, uuid):
+ """
+ Load the soledad database in offline mode.
+
+ :param username: full user id (user@provider)
+ :type username: str or unicode
+ :param password: the soledad passphrase
+ :type password: unicode
+ :param uuid: the user uuid
+ :type uuid: str or unicode
+
+ Signals:
+ Signaler.soledad_offline_finished
+ Signaler.soledad_offline_failed
+ """
+ self._soledad_bootstrapper.load_offline_soledad(
+ username, password, uuid)
+
+ def cancel_bootstrap(self):
+ """
+ Cancel the ongoing soledad bootstrap (if any).
+ """
+ if self._soledad_defer is not None:
+ logger.debug("Cancelling soledad defer.")
+ self._soledad_defer.cancel()
+ self._soledad_defer = None
+ zope.proxy.setProxiedObject(self._soledad_proxy, None)
+
+ def close(self):
+ """
+ Close soledad database.
+ """
+ if not zope.proxy.sameProxiedObjects(self._soledad_proxy, None):
+ self._soledad_proxy.close()
+ zope.proxy.setProxiedObject(self._soledad_proxy, None)
+
+ def _change_password_ok(self, _):
+ """
+ Password change callback.
+ """
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.SOLEDAD_PASSWORD_CHANGE_OK)
+
+ def _change_password_error(self, failure):
+ """
+ Password change errback.
+
+ :param failure: failure object containing problem.
+ :type failure: twisted.python.failure.Failure
+ """
+ if failure.check(NoStorageSecret):
+ logger.error("No storage secret for password change in Soledad.")
+ if failure.check(PassphraseTooShort):
+ logger.error("Passphrase too short.")
+
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.SOLEDAD_PASSWORD_CHANGE_ERROR)
+
+ def change_password(self, new_password):
+ """
+ Change the database's password.
+
+ :param new_password: the new password.
+ :type new_password: unicode
+
+ :returns: a defer to interact with.
+ :rtype: twisted.internet.defer.Deferred
+ """
+ d = threads.deferToThread(self._soledad_proxy.change_passphrase,
+ new_password)
+ d.addCallback(self._change_password_ok)
+ d.addErrback(self._change_password_error)
+
+
+class Keymanager(object):
+ """
+ Interfaces with KeyManager.
+ """
+ zope.interface.implements(ILEAPComponent)
+
+ def __init__(self, keymanager_proxy, signaler=None):
+ """
+ Constructor for the Keymanager component.
+
+ :param keymanager_proxy: proxy to pass around a Keymanager object.
+ :type keymanager_proxy: zope.ProxyBase
+ :param signaler: Object in charge of handling communication
+ back to the frontend
+ :type signaler: Signaler
+ """
+ self.key = "keymanager"
+ self._keymanager_proxy = keymanager_proxy
+ self._signaler = signaler
+
+ def import_keys(self, username, filename):
+ """
+ Imports the username's key pair.
+ Those keys need to be ascii armored.
+
+ :param username: the user that will have the imported pair of keys.
+ :type username: str
+ :param filename: the name of the file where the key pair is stored.
+ :type filename: str
+ """
+ # NOTE: This feature is disabled right now since is dangerous
+ return
+
+ new_key = ''
+ signal = None
+ try:
+ with open(filename, 'r') as keys_file:
+ new_key = keys_file.read()
+ except IOError as e:
+ logger.error("IOError importing key. {0!r}".format(e))
+ signal = self._signaler.KEYMANAGER_IMPORT_IOERROR
+ self._signaler.signal(signal)
+ return
+
+ keymanager = self._keymanager_proxy
+ try:
+ public_key, private_key = keymanager.parse_openpgp_ascii_key(
+ new_key)
+ except (KeyAddressMismatch, KeyFingerprintMismatch) as e:
+ logger.error(repr(e))
+ signal = self._signaler.KEYMANAGER_IMPORT_DATAMISMATCH
+ self._signaler.signal(signal)
+ return
+
+ if public_key is None or private_key is None:
+ signal = self._signaler.KEYMANAGER_IMPORT_MISSINGKEY
+ self._signaler.signal(signal)
+ return
+
+ current_public_key = keymanager.get_key(username, openpgp.OpenPGPKey)
+ if public_key.address != current_public_key.address:
+ logger.error("The key does not match the ID")
+ signal = self._signaler.KEYMANAGER_IMPORT_ADDRESSMISMATCH
+ self._signaler.signal(signal)
+ return
+
+ keymanager.delete_key(self._key)
+ keymanager.delete_key(self._key_priv)
+ keymanager.put_key(public_key)
+ keymanager.put_key(private_key)
+ keymanager.send_key(openpgp.OpenPGPKey)
+
+ logger.debug('Import ok')
+ signal = self._signaler.KEYMANAGER_IMPORT_OK
+
+ self._signaler.signal(signal)
+
+ def export_keys(self, username, filename):
+ """
+ Export the given username's keys to a file.
+
+ :param username: the username whos keys we need to export.
+ :type username: str
+ :param filename: the name of the file where we want to save the keys.
+ :type filename: str
+ """
+ keymanager = self._keymanager_proxy
+
+ public_key = keymanager.get_key(username, openpgp.OpenPGPKey)
+ private_key = keymanager.get_key(username, openpgp.OpenPGPKey,
+ private=True)
+ try:
+ with open(filename, 'w') as keys_file:
+ keys_file.write(public_key.key_data)
+ keys_file.write(private_key.key_data)
+
+ logger.debug('Export ok')
+ self._signaler.signal(self._signaler.KEYMANAGER_EXPORT_OK)
+ except IOError as e:
+ logger.error("IOError exporting key. {0!r}".format(e))
+ self._signaler.signal(self._signaler.KEYMANAGER_EXPORT_ERROR)
+
+ def list_keys(self):
+ """
+ List all the keys stored in the local DB.
+ """
+ keys = self._keymanager_proxy.get_all_keys_in_local_db()
+ self._signaler.signal(self._signaler.KEYMANAGER_KEYS_LIST, keys)
+
+ def get_key_details(self, username):
+ """
+ List all the keys stored in the local DB.
+ """
+ public_key = self._keymanager_proxy.get_key(username,
+ openpgp.OpenPGPKey)
+ details = (public_key.key_id, public_key.fingerprint)
+ self._signaler.signal(self._signaler.KEYMANAGER_KEY_DETAILS, details)
+
+
+class Mail(object):
+ """
+ Interfaces with setup and launch of Mail.
+ """
+ # We give each service some time to come to a halt before forcing quit
+ SERVICE_STOP_TIMEOUT = 20
+
+ zope.interface.implements(ILEAPComponent)
+
+ def __init__(self, soledad_proxy, keymanager_proxy, signaler=None):
+ """
+ Constructor for the Mail component.
+
+ :param soledad_proxy: proxy to pass around a Soledad object.
+ :type soledad_proxy: zope.ProxyBase
+ :param keymanager_proxy: proxy to pass around a Keymanager object.
+ :type keymanager_proxy: zope.ProxyBase
+ :param signaler: Object in charge of handling communication
+ back to the frontend
+ :type signaler: Signaler
+ """
+ self.key = "mail"
+ self._signaler = signaler
+ self._soledad_proxy = soledad_proxy
+ self._keymanager_proxy = keymanager_proxy
+ self._imap_controller = IMAPController(self._soledad_proxy,
+ self._keymanager_proxy)
+ self._smtp_bootstrapper = SMTPBootstrapper()
+ self._smtp_config = SMTPConfig()
+
+ def start_smtp_service(self, full_user_id, download_if_needed=False):
+ """
+ Start the SMTP service.
+
+ :param full_user_id: user id, in the form "user@provider"
+ :type full_user_id: str
+ :param download_if_needed: True if it should check for mtime
+ for the file
+ :type download_if_needed: bool
+
+ :returns: a defer to interact with.
+ :rtype: twisted.internet.defer.Deferred
+ """
+ return threads.deferToThread(
+ self._smtp_bootstrapper.start_smtp_service,
+ self._keymanager_proxy, full_user_id, download_if_needed)
+
+ def start_imap_service(self, full_user_id, offline=False):
+ """
+ Start the IMAP service.
+
+ :param full_user_id: user id, in the form "user@provider"
+ :type full_user_id: str
+ :param offline: whether imap should start in offline mode or not.
+ :type offline: bool
+
+ :returns: a defer to interact with.
+ :rtype: twisted.internet.defer.Deferred
+ """
+ return threads.deferToThread(
+ self._imap_controller.start_imap_service,
+ full_user_id, offline)
+
+ def stop_smtp_service(self):
+ """
+ Stop the SMTP service.
+
+ :returns: a defer to interact with.
+ :rtype: twisted.internet.defer.Deferred
+ """
+ return threads.deferToThread(self._smtp_bootstrapper.stop_smtp_service)
+
+ def _stop_imap_service(self):
+ """
+ Stop imap and wait until the service is stopped to signal that is done.
+ """
+ cv = Condition()
+ cv.acquire()
+ threads.deferToThread(self._imap_controller.stop_imap_service, cv)
+ logger.debug('Waiting for imap service to stop.')
+ cv.wait(self.SERVICE_STOP_TIMEOUT)
+ logger.debug('IMAP stopped')
+ self._signaler.signal(self._signaler.IMAP_STOPPED)
+
+ def stop_imap_service(self):
+ """
+ Stop imap service (fetcher, factory and port).
+
+ :returns: a defer to interact with.
+ :rtype: twisted.internet.defer.Deferred
+ """
+ return threads.deferToThread(self._stop_imap_service)
class Authenticate(object):
@@ -556,6 +1009,7 @@ class Authenticate(object):
"""
self.key = "authenticate"
self._signaler = signaler
+ self._login_defer = None
self._srp_auth = SRPAuth(ProviderConfig(), self._signaler)
def login(self, domain, username, password):
@@ -572,8 +1026,8 @@ class Authenticate(object):
:returns: the defer for the operation running in a thread.
:rtype: twisted.internet.defer.Deferred
"""
- config = ProviderConfig()
- if get_provider_config(config, domain):
+ config = ProviderConfig.get_provider_config(domain)
+ if config is not None:
self._srp_auth = SRPAuth(config, self._signaler)
self._login_defer = self._srp_auth.authenticate(username, password)
return self._login_defer
@@ -670,6 +1124,10 @@ class Signaler(QtCore.QObject):
prov_unsupported_client = QtCore.Signal(object)
prov_unsupported_api = QtCore.Signal(object)
+ prov_get_all_services = QtCore.Signal(object)
+ prov_get_supported_services = QtCore.Signal(object)
+ prov_get_details = QtCore.Signal(object)
+
prov_cancelled_setup = QtCore.Signal(object)
# Signals for SRPRegister
@@ -703,6 +1161,7 @@ class Signaler(QtCore.QObject):
eip_disconnected = QtCore.Signal(object)
eip_connection_died = QtCore.Signal(object)
eip_connection_aborted = QtCore.Signal(object)
+ eip_stopped = QtCore.Signal(object)
# EIP problems
eip_no_polkit_agent_error = QtCore.Signal(object)
@@ -727,11 +1186,38 @@ class Signaler(QtCore.QObject):
eip_state_changed = QtCore.Signal(dict)
eip_status_changed = QtCore.Signal(dict)
eip_process_finished = QtCore.Signal(int)
+ eip_tear_fw_down = QtCore.Signal(object)
# signals whether the needed files to start EIP exist or not
eip_can_start = QtCore.Signal(object)
eip_cannot_start = QtCore.Signal(object)
+ # Signals for Soledad
+ soledad_bootstrap_failed = QtCore.Signal(object)
+ soledad_bootstrap_finished = QtCore.Signal(object)
+ soledad_offline_failed = QtCore.Signal(object)
+ soledad_offline_finished = QtCore.Signal(object)
+ soledad_invalid_auth_token = QtCore.Signal(object)
+ soledad_cancelled_bootstrap = QtCore.Signal(object)
+ soledad_password_change_ok = QtCore.Signal(object)
+ soledad_password_change_error = QtCore.Signal(object)
+
+ # Keymanager signals
+ keymanager_export_ok = QtCore.Signal(object)
+ keymanager_export_error = QtCore.Signal(object)
+ keymanager_keys_list = QtCore.Signal(object)
+
+ keymanager_import_ioerror = QtCore.Signal(object)
+ keymanager_import_datamismatch = QtCore.Signal(object)
+ keymanager_import_missingkey = QtCore.Signal(object)
+ keymanager_import_addressmismatch = QtCore.Signal(object)
+ keymanager_import_ok = QtCore.Signal(object)
+
+ keymanager_key_details = QtCore.Signal(object)
+
+ # mail related signals
+ imap_stopped = QtCore.Signal(object)
+
# This signal is used to warn the backend user that is doing something
# wrong
backend_bad_call = QtCore.Signal(object)
@@ -751,6 +1237,9 @@ class Signaler(QtCore.QObject):
PROV_UNSUPPORTED_CLIENT = "prov_unsupported_client"
PROV_UNSUPPORTED_API = "prov_unsupported_api"
PROV_CANCELLED_SETUP = "prov_cancelled_setup"
+ PROV_GET_ALL_SERVICES = "prov_get_all_services"
+ PROV_GET_SUPPORTED_SERVICES = "prov_get_supported_services"
+ PROV_GET_DETAILS = "prov_get_details"
SRP_REGISTRATION_FINISHED = "srp_registration_finished"
SRP_REGISTRATION_FAILED = "srp_registration_failed"
@@ -777,6 +1266,8 @@ class Signaler(QtCore.QObject):
EIP_DISCONNECTED = "eip_disconnected"
EIP_CONNECTION_DIED = "eip_connection_died"
EIP_CONNECTION_ABORTED = "eip_connection_aborted"
+ EIP_STOPPED = "eip_stopped"
+
EIP_NO_POLKIT_AGENT_ERROR = "eip_no_polkit_agent_error"
EIP_NO_TUN_KEXT_ERROR = "eip_no_tun_kext_error"
EIP_NO_PKEXEC_ERROR = "eip_no_pkexec_error"
@@ -797,10 +1288,35 @@ class Signaler(QtCore.QObject):
EIP_STATE_CHANGED = "eip_state_changed"
EIP_STATUS_CHANGED = "eip_status_changed"
EIP_PROCESS_FINISHED = "eip_process_finished"
+ EIP_TEAR_FW_DOWN = "eip_tear_fw_down"
EIP_CAN_START = "eip_can_start"
EIP_CANNOT_START = "eip_cannot_start"
+ SOLEDAD_BOOTSTRAP_FAILED = "soledad_bootstrap_failed"
+ SOLEDAD_BOOTSTRAP_FINISHED = "soledad_bootstrap_finished"
+ SOLEDAD_OFFLINE_FAILED = "soledad_offline_failed"
+ SOLEDAD_OFFLINE_FINISHED = "soledad_offline_finished"
+ SOLEDAD_INVALID_AUTH_TOKEN = "soledad_invalid_auth_token"
+
+ SOLEDAD_PASSWORD_CHANGE_OK = "soledad_password_change_ok"
+ SOLEDAD_PASSWORD_CHANGE_ERROR = "soledad_password_change_error"
+
+ SOLEDAD_CANCELLED_BOOTSTRAP = "soledad_cancelled_bootstrap"
+
+ KEYMANAGER_EXPORT_OK = "keymanager_export_ok"
+ KEYMANAGER_EXPORT_ERROR = "keymanager_export_error"
+ KEYMANAGER_KEYS_LIST = "keymanager_keys_list"
+
+ KEYMANAGER_IMPORT_IOERROR = "keymanager_import_ioerror"
+ KEYMANAGER_IMPORT_DATAMISMATCH = "keymanager_import_datamismatch"
+ KEYMANAGER_IMPORT_MISSINGKEY = "keymanager_import_missingkey"
+ KEYMANAGER_IMPORT_ADDRESSMISMATCH = "keymanager_import_addressmismatch"
+ KEYMANAGER_IMPORT_OK = "keymanager_import_ok"
+ KEYMANAGER_KEY_DETAILS = "keymanager_key_details"
+
+ IMAP_STOPPED = "imap_stopped"
+
BACKEND_BAD_CALL = "backend_bad_call"
def __init__(self):
@@ -821,6 +1337,9 @@ class Signaler(QtCore.QObject):
self.PROV_UNSUPPORTED_CLIENT,
self.PROV_UNSUPPORTED_API,
self.PROV_CANCELLED_SETUP,
+ self.PROV_GET_ALL_SERVICES,
+ self.PROV_GET_SUPPORTED_SERVICES,
+ self.PROV_GET_DETAILS,
self.SRP_REGISTRATION_FINISHED,
self.SRP_REGISTRATION_FAILED,
@@ -834,6 +1353,8 @@ class Signaler(QtCore.QObject):
self.EIP_DISCONNECTED,
self.EIP_CONNECTION_DIED,
self.EIP_CONNECTION_ABORTED,
+ self.EIP_STOPPED,
+
self.EIP_NO_POLKIT_AGENT_ERROR,
self.EIP_NO_TUN_KEXT_ERROR,
self.EIP_NO_PKEXEC_ERROR,
@@ -872,6 +1393,29 @@ class Signaler(QtCore.QObject):
self.SRP_STATUS_LOGGED_IN,
self.SRP_STATUS_NOT_LOGGED_IN,
+ self.SOLEDAD_BOOTSTRAP_FAILED,
+ self.SOLEDAD_BOOTSTRAP_FINISHED,
+ self.SOLEDAD_OFFLINE_FAILED,
+ self.SOLEDAD_OFFLINE_FINISHED,
+ self.SOLEDAD_INVALID_AUTH_TOKEN,
+ self.SOLEDAD_CANCELLED_BOOTSTRAP,
+
+ self.SOLEDAD_PASSWORD_CHANGE_OK,
+ self.SOLEDAD_PASSWORD_CHANGE_ERROR,
+
+ self.KEYMANAGER_EXPORT_OK,
+ self.KEYMANAGER_EXPORT_ERROR,
+ self.KEYMANAGER_KEYS_LIST,
+
+ self.KEYMANAGER_IMPORT_IOERROR,
+ self.KEYMANAGER_IMPORT_DATAMISMATCH,
+ self.KEYMANAGER_IMPORT_MISSINGKEY,
+ self.KEYMANAGER_IMPORT_ADDRESSMISMATCH,
+ self.KEYMANAGER_IMPORT_OK,
+ self.KEYMANAGER_KEY_DETAILS,
+
+ self.IMAP_STOPPED,
+
self.BACKEND_BAD_CALL,
]
@@ -926,11 +1470,24 @@ class Backend(object):
# Signaler object to translate commands into Qt signals
self._signaler = Signaler()
+ # Objects needed by several components, so we make a proxy and pass
+ # them around
+ self._soledad_proxy = zope.proxy.ProxyBase(None)
+ self._keymanager_proxy = zope.proxy.ProxyBase(None)
+
# Component registration
self._register(Provider(self._signaler, bypass_checks))
self._register(Register(self._signaler))
self._register(Authenticate(self._signaler))
self._register(EIP(self._signaler))
+ self._register(Soledad(self._soledad_proxy,
+ self._keymanager_proxy,
+ self._signaler))
+ self._register(Keymanager(self._keymanager_proxy,
+ self._signaler))
+ self._register(Mail(self._soledad_proxy,
+ self._keymanager_proxy,
+ self._signaler))
# We have a looping call on a thread executing all the
# commands in queue. Right now this queue is an actual Queue
@@ -952,7 +1509,7 @@ class Backend(object):
"""
Starts the looping call
"""
- log.msg("Starting worker...")
+ logger.debug("Starting worker...")
self._lc.start(0.01)
def stop(self):
@@ -965,14 +1522,17 @@ class Backend(object):
"""
Delayed stopping of worker. Called from `stop`.
"""
- log.msg("Stopping worker...")
+ logger.debug("Stopping worker...")
if self._lc.running:
self._lc.stop()
else:
logger.warning("Looping call is not running, cannot stop")
+
+ logger.debug("Cancelling ongoing defers...")
while len(self._ongoing_defers) > 0:
d = self._ongoing_defers.pop()
d.cancel()
+ logger.debug("Defers cancelled.")
def _register(self, component):
"""
@@ -986,8 +1546,7 @@ class Backend(object):
try:
self._components[component.key] = component
except Exception:
- log.msg("There was a problem registering %s" % (component,))
- log.err()
+ logger.error("There was a problem registering %s" % (component,))
def _signal_back(self, _, signal):
"""
@@ -1015,19 +1574,19 @@ class Backend(object):
# A call might not have a callback signal, but if it does,
# we add it to the chain
if cmd[2] is not None:
- d.addCallbacks(self._signal_back, log.err, cmd[2])
- d.addCallbacks(self._done_action, log.err,
+ d.addCallbacks(self._signal_back, logger.error, cmd[2])
+ d.addCallbacks(self._done_action, logger.error,
callbackKeywords={"d": d})
- d.addErrback(log.err)
+ d.addErrback(logger.error)
self._ongoing_defers.append(d)
except Empty:
# If it's just empty we don't have anything to do.
pass
except defer.CancelledError:
logger.debug("defer cancelled somewhere (CancelledError).")
- except Exception:
+ except Exception as e:
# But we log the rest
- log.err()
+ logger.exception("Unexpected exception: {0!r}".format(e))
def _done_action(self, _, d):
"""
@@ -1044,7 +1603,7 @@ class Backend(object):
# this in two processes, the methods bellow can be changed to
# send_multipart and this backend class will be really simple.
- def setup_provider(self, provider):
+ def provider_setup(self, provider):
"""
Initiate the setup for a provider.
@@ -1060,7 +1619,7 @@ class Backend(object):
"""
self._call_queue.put(("provider", "setup_provider", None, provider))
- def cancel_setup_provider(self):
+ def provider_cancel_setup(self):
"""
Cancel the ongoing setup provider (if any).
"""
@@ -1081,7 +1640,48 @@ class Backend(object):
"""
self._call_queue.put(("provider", "bootstrap", None, provider))
- def register_user(self, provider, username, password):
+ def provider_get_supported_services(self, domain):
+ """
+ Signal a list of supported services provided by the given provider.
+
+ :param domain: the provider to get the services from.
+ :type domain: str
+
+ Signals:
+ prov_get_supported_services -> list of unicode
+ """
+ self._call_queue.put(("provider", "get_supported_services", None,
+ domain))
+
+ def provider_get_all_services(self, providers):
+ """
+ Signal a list of services provided by all the configured providers.
+
+ :param providers: the list of providers to get the services.
+ :type providers: list
+
+ Signals:
+ prov_get_all_services -> list of unicode
+ """
+ self._call_queue.put(("provider", "get_all_services", None,
+ providers))
+
+ def provider_get_details(self, domain, lang):
+ """
+ Signal a ProviderConfigLight object with the current ProviderConfig
+ settings.
+
+ :param domain: the domain name of the provider.
+ :type domain: str
+ :param lang: the language to use for localized strings.
+ :type lang: str
+
+ Signals:
+ prov_get_details -> ProviderConfigLight
+ """
+ self._call_queue.put(("provider", "get_details", None, domain, lang))
+
+ def user_register(self, provider, username, password):
"""
Register a user using the domain and password given as parameters.
@@ -1100,7 +1700,7 @@ class Backend(object):
self._call_queue.put(("register", "register_user", None, provider,
username, password))
- def setup_eip(self, provider, skip_network=False):
+ def eip_setup(self, provider, skip_network=False):
"""
Initiate the setup for a provider
@@ -1118,13 +1718,13 @@ class Backend(object):
self._call_queue.put(("eip", "setup_eip", None, provider,
skip_network))
- def cancel_setup_eip(self):
+ def eip_cancel_setup(self):
"""
Cancel the ongoing setup EIP (if any).
"""
self._call_queue.put(("eip", "cancel_setup_eip", None))
- def start_eip(self):
+ def eip_start(self, restart=False):
"""
Start the EIP service.
@@ -1145,19 +1745,25 @@ class Backend(object):
eip_state_changed -> str
eip_status_changed -> tuple of str (download, upload)
eip_vpn_launcher_exception
+
+ :param restart: whether is is a restart.
+ :type restart: bool
"""
- self._call_queue.put(("eip", "start", None))
+ self._call_queue.put(("eip", "start", None, restart))
- def stop_eip(self, shutdown=False):
+ def eip_stop(self, shutdown=False, restart=False, failed=False):
"""
Stop the EIP service.
- :param shutdown:
+ :param shutdown: whether this is the final shutdown.
:type shutdown: bool
+
+ :param restart: whether this is part of a restart.
+ :type restart: bool
"""
- self._call_queue.put(("eip", "stop", None, shutdown))
+ self._call_queue.put(("eip", "stop", None, shutdown, restart))
- def terminate_eip(self):
+ def eip_terminate(self):
"""
Terminate the EIP service, not necessarily in a nice way.
"""
@@ -1209,7 +1815,13 @@ class Backend(object):
self._call_queue.put(("eip", "can_start",
None, domain))
- def login(self, provider, username, password):
+ def tear_fw_down(self):
+ """
+ Signal the need to tear the fw down.
+ """
+ self._call_queue.put(("eip", "tear_fw_down", None))
+
+ def user_login(self, provider, username, password):
"""
Execute the whole authentication process for a user
@@ -1231,7 +1843,7 @@ class Backend(object):
self._call_queue.put(("authenticate", "login", None, provider,
username, password))
- def logout(self):
+ def user_logout(self):
"""
Log out the current session.
@@ -1242,13 +1854,13 @@ class Backend(object):
"""
self._call_queue.put(("authenticate", "logout", None))
- def cancel_login(self):
+ def user_cancel_login(self):
"""
Cancel the ongoing login (if any).
"""
self._call_queue.put(("authenticate", "cancel_login", None))
- def change_password(self, current_password, new_password):
+ def user_change_password(self, current_password, new_password):
"""
Change the user's password.
@@ -1266,7 +1878,23 @@ class Backend(object):
self._call_queue.put(("authenticate", "change_password", None,
current_password, new_password))
- def get_logged_in_status(self):
+ def soledad_change_password(self, new_password):
+ """
+ Change the database's password.
+
+ :param new_password: the new password for the user.
+ :type new_password: unicode
+
+ Signals:
+ srp_not_logged_in_error
+ srp_password_change_ok
+ srp_password_change_badpw
+ srp_password_change_error
+ """
+ self._call_queue.put(("soledad", "change_password", None,
+ new_password))
+
+ def user_get_logged_in_status(self):
"""
Signal if the user is currently logged in or not.
@@ -1276,10 +1904,126 @@ class Backend(object):
"""
self._call_queue.put(("authenticate", "get_logged_in_status", None))
- ###########################################################################
- # XXX HACK: this section is meant to be a place to hold methods and
- # variables needed in the meantime while we migrate all to the backend.
+ def soledad_bootstrap(self, username, domain, password):
+ """
+ Bootstrap the soledad database.
+
+ :param username: the user name
+ :type username: unicode
+ :param domain: the domain that we are using.
+ :type domain: unicode
+ :param password: the password for the username
+ :type password: unicode
+
+ Signals:
+ soledad_bootstrap_finished
+ soledad_bootstrap_failed
+ soledad_invalid_auth_token
+ """
+ self._call_queue.put(("soledad", "bootstrap", None,
+ username, domain, password))
+
+ def soledad_load_offline(self, username, password, uuid):
+ """
+ Load the soledad database in offline mode.
+
+ :param username: full user id (user@provider)
+ :type username: str or unicode
+ :param password: the soledad passphrase
+ :type password: unicode
+ :param uuid: the user uuid
+ :type uuid: str or unicode
+
+ Signals:
+ """
+ self._call_queue.put(("soledad", "load_offline", None,
+ username, password, uuid))
+
+ def soledad_cancel_bootstrap(self):
+ """
+ Cancel the ongoing soledad bootstrapping process (if any).
+ """
+ self._call_queue.put(("soledad", "cancel_bootstrap", None))
+
+ def soledad_close(self):
+ """
+ Close soledad database.
+ """
+ self._call_queue.put(("soledad", "close", None))
+
+ def keymanager_list_keys(self):
+ """
+ Signal a list of public keys locally stored.
+
+ Signals:
+ keymanager_keys_list -> list
+ """
+ self._call_queue.put(("keymanager", "list_keys", None))
+
+ def keymanager_export_keys(self, username, filename):
+ """
+ Export the given username's keys to a file.
+
+ :param username: the username whos keys we need to export.
+ :type username: str
+ :param filename: the name of the file where we want to save the keys.
+ :type filename: str
+
+ Signals:
+ keymanager_export_ok
+ keymanager_export_error
+ """
+ self._call_queue.put(("keymanager", "export_keys", None,
+ username, filename))
- def get_provider_config(self):
- provider_config = self._components["provider"]._provider_config
- return provider_config
+ def keymanager_get_key_details(self, username):
+ """
+ Signal the given username's key details.
+
+ :param username: the username whos keys we need to get details.
+ :type username: str
+
+ Signals:
+ keymanager_key_details
+ """
+ self._call_queue.put(("keymanager", "get_key_details", None, username))
+
+ def smtp_start_service(self, full_user_id, download_if_needed=False):
+ """
+ Start the SMTP service.
+
+ :param full_user_id: user id, in the form "user@provider"
+ :type full_user_id: str
+ :param download_if_needed: True if it should check for mtime
+ for the file
+ :type download_if_needed: bool
+ """
+ self._call_queue.put(("mail", "start_smtp_service", None,
+ full_user_id, download_if_needed))
+
+ def imap_start_service(self, full_user_id, offline=False):
+ """
+ Start the IMAP service.
+
+ :param full_user_id: user id, in the form "user@provider"
+ :type full_user_id: str
+ :param offline: whether imap should start in offline mode or not.
+ :type offline: bool
+ """
+ self._call_queue.put(("mail", "start_imap_service", None,
+ full_user_id, offline))
+
+ def smtp_stop_service(self):
+ """
+ Stop the SMTP service.
+ """
+ self._call_queue.put(("mail", "stop_smtp_service", None))
+
+ def imap_stop_service(self):
+ """
+ Stop imap service.
+
+ Signals:
+ imap_stopped
+ """
+ self._call_queue.put(("mail", "stop_imap_service", None))
diff --git a/src/leap/bitmask/backend_app.py b/src/leap/bitmask/backend_app.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/leap/bitmask/backend_app.py
diff --git a/src/leap/bitmask/config/flags.py b/src/leap/bitmask/config/flags.py
index 6b70659d..2f3fdde4 100644
--- a/src/leap/bitmask/config/flags.py
+++ b/src/leap/bitmask/config/flags.py
@@ -55,3 +55,5 @@ OPENVPN_VERBOSITY = 1
# Skip the checks in the wizard, use for testing purposes only!
SKIP_WIZARD_CHECKS = False
+
+CURRENT_VPN_COUNTRY = None
diff --git a/src/leap/bitmask/config/providerconfig.py b/src/leap/bitmask/config/providerconfig.py
index 2ebe05ce..cf31b3b2 100644
--- a/src/leap/bitmask/config/providerconfig.py
+++ b/src/leap/bitmask/config/providerconfig.py
@@ -38,6 +38,35 @@ class MissingCACert(Exception):
pass
+class ProviderConfigLight(object):
+ """
+ A light config object to hold some provider settings needed by the GUI.
+ """
+ def __init__(self):
+ """
+ Define the public attributes.
+ """
+ self.domain = ""
+ self.name = ""
+ self.description = ""
+ self.enrollment_policy = ""
+ self.services = []
+
+ @property
+ def services_string(self):
+ """
+ Return a comma separated list of serices provided by this provider.
+
+ :rtype: str
+ """
+ services = []
+ for service in self.services:
+ services.append(get_service_display_name(service))
+
+ services_str = ", ".join(services)
+ return services_str
+
+
class ProviderConfig(BaseConfig):
"""
Provider configuration abstraction class
@@ -45,6 +74,29 @@ class ProviderConfig(BaseConfig):
def __init__(self):
BaseConfig.__init__(self)
+ def get_light_config(self, domain, lang=None):
+ """
+ Return a ProviderConfigLight object with the data for the loaded
+ object.
+
+ :param domain: the domain name of the provider.
+ :type domain: str
+ :param lang: the language to use for localized strings.
+ :type lang: str
+
+ :rtype: ProviderConfigLight or None if the ProviderConfig isn't loaded.
+ """
+ config = self.get_provider_config(domain)
+ details = ProviderConfigLight()
+
+ details.domain = config.get_domain()
+ details.name = config.get_name(lang=lang)
+ details.description = config.get_description(lang=lang)
+ details.enrollment_policy = config.get_enrollment_policy()
+ details.services = config.get_services()
+
+ return details
+
@classmethod
def get_provider_config(self, domain):
"""
@@ -144,18 +196,6 @@ class ProviderConfig(BaseConfig):
services = self._safe_get_value("services")
return services
- def get_services_string(self):
- """
- Returns a string with the available services in the current
- provider, ready to be shown to the user.
- """
- services = []
- for service in self.get_services():
- services.append(get_service_display_name(service))
-
- services_str = ", ".join(services)
- return services_str
-
def get_ca_cert_path(self, about_to_download=False):
"""
Returns the path to the certificate for the current provider.
@@ -199,39 +239,3 @@ class ProviderConfig(BaseConfig):
:rtype: bool
"""
return "mx" in self.get_services()
-
-
-if __name__ == "__main__":
- logger = logging.getLogger(name='leap')
- logger.setLevel(logging.DEBUG)
- console = logging.StreamHandler()
- console.setLevel(logging.DEBUG)
- formatter = logging.Formatter(
- '%(asctime)s '
- '- %(name)s - %(levelname)s - %(message)s')
- console.setFormatter(formatter)
- logger.addHandler(console)
-
- provider = ProviderConfig()
-
- try:
- provider.get_api_version()
- except Exception as e:
- assert isinstance(e, AssertionError), "Expected an assert"
- print "Safe value getting is working"
-
- # standalone minitest
- #if provider.load("provider_bad.json"):
- if provider.load("leap/providers/bitmask.net/provider.json"):
- print provider.get_api_version()
- print provider.get_ca_cert_fingerprint()
- print provider.get_ca_cert_uri()
- print provider.get_default_language()
- print provider.get_description()
- print provider.get_description(lang="asd")
- print provider.get_domain()
- print provider.get_enrollment_policy()
- print provider.get_languages()
- print provider.get_name()
- print provider.get_services()
- print provider.get_services_string()
diff --git a/src/leap/bitmask/frontend_app.py b/src/leap/bitmask/frontend_app.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/leap/bitmask/frontend_app.py
diff --git a/src/leap/bitmask/gui/advanced_key_management.py b/src/leap/bitmask/gui/advanced_key_management.py
index be6b4410..b3a4ed8e 100644
--- a/src/leap/bitmask/gui/advanced_key_management.py
+++ b/src/leap/bitmask/gui/advanced_key_management.py
@@ -19,11 +19,8 @@ Advanced Key Management
"""
import logging
-from PySide import QtGui
-from zope.proxy import sameProxiedObjects
+from PySide import QtCore, QtGui
-from leap.keymanager import openpgp
-from leap.keymanager.errors import KeyAddressMismatch, KeyFingerprintMismatch
from leap.bitmask.services import get_service_display_name, MX_SERVICE
from ui_advanced_key_management import Ui_AdvancedKeyManagement
@@ -34,7 +31,7 @@ class AdvancedKeyManagement(QtGui.QDialog):
"""
Advanced Key Management
"""
- def __init__(self, parent, has_mx, user, keymanager, soledad):
+ def __init__(self, parent, has_mx, user, backend, soledad_started):
"""
:param parent: parent object of AdvancedKeyManagement.
:parent type: QWidget
@@ -43,10 +40,10 @@ class AdvancedKeyManagement(QtGui.QDialog):
:type has_mx: bool
:param user: the current logged in user.
:type user: unicode
- :param keymanager: the existing keymanager instance
- :type keymanager: KeyManager
- :param soledad: a loaded instance of Soledad
- :type soledad: Soledad
+ :param backend: Backend being used
+ :type backend: Backend
+ :param soledad_started: whether soledad has started or not
+ :type soledad_started: bool
"""
QtGui.QDialog.__init__(self, parent)
@@ -56,7 +53,6 @@ class AdvancedKeyManagement(QtGui.QDialog):
# XXX: Temporarily disable the key import.
self.ui.pbImportKeys.setVisible(False)
- # if Soledad is not started yet
if not has_mx:
msg = self.tr("The provider that you are using "
"does not support {0}.")
@@ -64,8 +60,7 @@ class AdvancedKeyManagement(QtGui.QDialog):
self._disable_ui(msg)
return
- # if Soledad is not started yet
- if sameProxiedObjects(soledad, None):
+ if not soledad_started:
msg = self.tr("To use this, you need to enable/start {0}.")
msg = msg.format(get_service_display_name(MX_SERVICE))
self._disable_ui(msg)
@@ -78,17 +73,12 @@ class AdvancedKeyManagement(QtGui.QDialog):
# "existing e-mails.")
# self.ui.lblStatus.setText(msg)
- self._keymanager = keymanager
- self._soledad = soledad
-
- self._key = keymanager.get_key(user, openpgp.OpenPGPKey)
- self._key_priv = keymanager.get_key(
- user, openpgp.OpenPGPKey, private=True)
+ self._user = user
+ self._backend = backend
+ self._backend_connect()
# show current key information
self.ui.leUser.setText(user)
- self.ui.leKeyID.setText(self._key.key_id)
- self.ui.leFingerprint.setText(self._key.fingerprint)
# set up connections
self.ui.pbImportKeys.clicked.connect(self._import_keys)
@@ -98,7 +88,15 @@ class AdvancedKeyManagement(QtGui.QDialog):
self.ui.twPublicKeys.horizontalHeader().setResizeMode(
0, QtGui.QHeaderView.Stretch)
- self._list_keys()
+ self._backend.keymanager_get_key_details(user)
+ self._backend.keymanager_list_keys()
+
+ def _keymanager_key_details(self, details):
+ """
+ Set the current user's key details into the gui.
+ """
+ self.ui.leKeyID.setText(details[0])
+ self.ui.leFingerprint.setText(details[1])
def _disable_ui(self, msg):
"""
@@ -117,53 +115,11 @@ class AdvancedKeyManagement(QtGui.QDialog):
Imports the user's key pair.
Those keys need to be ascii armored.
"""
- fileName, filtr = QtGui.QFileDialog.getOpenFileName(
+ file_name, filtr = QtGui.QFileDialog.getOpenFileName(
self, self.tr("Open keys file"),
options=QtGui.QFileDialog.DontUseNativeDialog)
- if fileName:
- new_key = ''
- try:
- with open(fileName, 'r') as keys_file:
- new_key = keys_file.read()
- except IOError as e:
- logger.error("IOError importing key. {0!r}".format(e))
- QtGui.QMessageBox.critical(
- self, self.tr("Input/Output error"),
- self.tr("There was an error accessing the file.\n"
- "Import canceled."))
- return
-
- keymanager = self._keymanager
- try:
- public_key, private_key = keymanager.parse_openpgp_ascii_key(
- new_key)
- except (KeyAddressMismatch, KeyFingerprintMismatch) as e:
- logger.error(repr(e))
- QtGui.QMessageBox.warning(
- self, self.tr("Data mismatch"),
- self.tr("The public and private key should have the "
- "same address and fingerprint.\n"
- "Import canceled."))
- return
-
- if public_key is None or private_key is None:
- QtGui.QMessageBox.warning(
- self, self.tr("Missing key"),
- self.tr("You need to provide the public AND private "
- "key in the same file.\n"
- "Import canceled."))
- return
-
- if public_key.address != self._key.address:
- logger.error("The key does not match the ID")
- QtGui.QMessageBox.warning(
- self, self.tr("Address mismatch"),
- self.tr("The identity for the key needs to be the same "
- "as your user address.\n"
- "Import canceled."))
- return
-
+ if file_name:
question = self.tr("Are you sure that you want to replace "
"the current key pair whith the imported?")
res = QtGui.QMessageBox.question(
@@ -171,61 +127,152 @@ class AdvancedKeyManagement(QtGui.QDialog):
QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
QtGui.QMessageBox.No) # default No
- if res == QtGui.QMessageBox.No:
- return
+ if res == QtGui.QMessageBox.Yes:
+ self._backend.keymanager_import_keys(self._user, file_name)
+ else:
+ logger.debug('Import canceled by the user.')
- keymanager.delete_key(self._key)
- keymanager.delete_key(self._key_priv)
- keymanager.put_key(public_key)
- keymanager.put_key(private_key)
- keymanager.send_key(openpgp.OpenPGPKey)
+ @QtCore.Slot()
+ def _keymanager_import_ok(self):
+ """
+ TRIGGERS:
+ Signaler.keymanager_import_ok
- logger.debug('Import ok')
+ Notify the user that the key import went OK.
+ """
+ QtGui.QMessageBox.information(
+ self, self.tr("Import Successful"),
+ self.tr("The key pair was imported successfully."))
- QtGui.QMessageBox.information(
- self, self.tr("Import Successful"),
- self.tr("The key pair was imported successfully."))
- else:
- logger.debug('Import canceled by the user.')
+ @QtCore.Slot()
+ def _import_ioerror(self):
+ """
+ TRIGGERS:
+ Signaler.keymanager_import_ioerror
+
+ Notify the user that the key import had an IOError problem.
+ """
+ QtGui.QMessageBox.critical(
+ self, self.tr("Input/Output error"),
+ self.tr("There was an error accessing the file.\n"
+ "Import canceled."))
+
+ @QtCore.Slot()
+ def _import_datamismatch(self):
+ """
+ TRIGGERS:
+ Signaler.keymanager_import_datamismatch
+
+ Notify the user that the key import had an data mismatch problem.
+ """
+ QtGui.QMessageBox.warning(
+ self, self.tr("Data mismatch"),
+ self.tr("The public and private key should have the "
+ "same address and fingerprint.\n"
+ "Import canceled."))
+
+ @QtCore.Slot()
+ def _import_missingkey(self):
+ """
+ TRIGGERS:
+ Signaler.keymanager_import_missingkey
+
+ Notify the user that the key import failed due a missing key.
+ """
+ QtGui.QMessageBox.warning(
+ self, self.tr("Missing key"),
+ self.tr("You need to provide the public AND private "
+ "key in the same file.\n"
+ "Import canceled."))
+
+ @QtCore.Slot()
+ def _import_addressmismatch(self):
+ """
+ TRIGGERS:
+ Signaler.keymanager_import_addressmismatch
+
+ Notify the user that the key import failed due an address mismatch.
+ """
+ QtGui.QMessageBox.warning(
+ self, self.tr("Address mismatch"),
+ self.tr("The identity for the key needs to be the same "
+ "as your user address.\n"
+ "Import canceled."))
def _export_keys(self):
"""
Exports the user's key pair.
"""
- fileName, filtr = QtGui.QFileDialog.getSaveFileName(
+ file_name, filtr = QtGui.QFileDialog.getSaveFileName(
self, self.tr("Save keys file"),
options=QtGui.QFileDialog.DontUseNativeDialog)
- if fileName:
- try:
- with open(fileName, 'w') as keys_file:
- keys_file.write(self._key.key_data)
- keys_file.write(self._key_priv.key_data)
-
- logger.debug('Export ok')
- QtGui.QMessageBox.information(
- self, self.tr("Export Successful"),
- self.tr("The key pair was exported successfully.\n"
- "Please, store your private key in a safe place."))
- except IOError as e:
- logger.error("IOError exporting key. {0!r}".format(e))
- QtGui.QMessageBox.critical(
- self, self.tr("Input/Output error"),
- self.tr("There was an error accessing the file.\n"
- "Export canceled."))
- return
+ if file_name:
+ self._backend.keymanager_export_keys(self._user, file_name)
else:
logger.debug('Export canceled by the user.')
- def _list_keys(self):
+ @QtCore.Slot()
+ def _keymanager_export_ok(self):
+ """
+ TRIGGERS:
+ Signaler.keymanager_export_ok
+
+ Notify the user that the key export went OK.
"""
- Loads all the public keys stored in the local db to the keys table.
+ QtGui.QMessageBox.information(
+ self, self.tr("Export Successful"),
+ self.tr("The key pair was exported successfully.\n"
+ "Please, store your private key in a safe place."))
+
+ @QtCore.Slot()
+ def _keymanager_export_error(self):
+ """
+ TRIGGERS:
+ Signaler.keymanager_export_error
+
+ Notify the user that the key export didn't go well.
+ """
+ QtGui.QMessageBox.critical(
+ self, self.tr("Input/Output error"),
+ self.tr("There was an error accessing the file.\n"
+ "Export canceled."))
+
+ @QtCore.Slot()
+ def _keymanager_keys_list(self, keys):
"""
- keys = self._keymanager.get_all_keys_in_local_db()
+ TRIGGERS:
+ Signaler.keymanager_keys_list
+ Load the keys given as parameter in the table.
+
+ :param keys: the list of keys to load.
+ :type keys: list
+ """
keys_table = self.ui.twPublicKeys
+
for key in keys:
row = keys_table.rowCount()
keys_table.insertRow(row)
keys_table.setItem(row, 0, QtGui.QTableWidgetItem(key.address))
keys_table.setItem(row, 1, QtGui.QTableWidgetItem(key.key_id))
+
+ def _backend_connect(self):
+ """
+ Connect to backend signals.
+ """
+ sig = self._backend.signaler
+
+ sig.keymanager_export_ok.connect(self._keymanager_export_ok)
+ sig.keymanager_export_error.connect(self._keymanager_export_error)
+ sig.keymanager_keys_list.connect(self._keymanager_keys_list)
+
+ sig.keymanager_key_details.connect(self._keymanager_key_details)
+
+ sig.keymanager_import_ok.connect(self._keymanager_import_ok)
+
+ sig.keymanager_import_ioerror.connect(self._import_ioerror)
+ sig.keymanager_import_datamismatch.connect(self._import_datamismatch)
+ sig.keymanager_import_missingkey.connect(self._import_missingkey)
+ sig.keymanager_import_addressmismatch.connect(
+ self._import_addressmismatch)
diff --git a/src/leap/bitmask/gui/eip_status.py b/src/leap/bitmask/gui/eip_status.py
index ca28b8bf..8b9f2d44 100644
--- a/src/leap/bitmask/gui/eip_status.py
+++ b/src/leap/bitmask/gui/eip_status.py
@@ -24,7 +24,7 @@ from functools import partial
from PySide import QtCore, QtGui
-from leap.bitmask.services.eip.connection import EIPConnection
+from leap.bitmask.config import flags
from leap.bitmask.services import get_service_display_name, EIP_SERVICE
from leap.bitmask.platform_init import IS_LINUX
from leap.bitmask.util.averages import RateMovingAverage
@@ -32,6 +32,7 @@ from leap.common.check import leap_assert_type
from ui_eip_status import Ui_EIPStatus
+QtDelayedCall = QtCore.QTimer.singleShot
logger = logging.getLogger(__name__)
@@ -43,9 +44,14 @@ class EIPStatusWidget(QtGui.QWidget):
RATE_STR = "%1.2f KB/s"
TOTAL_STR = "%1.2f Kb"
- eip_connection_connected = QtCore.Signal()
+ def __init__(self, parent=None, eip_conductor=None):
+ """
+ :param parent: the parent of the widget.
+ :type parent: QObject
- def __init__(self, parent=None):
+ :param eip_conductor: an EIPConductor object.
+ :type eip_conductor: EIPConductor
+ """
QtGui.QWidget.__init__(self, parent)
self._systray = None
@@ -54,13 +60,17 @@ class EIPStatusWidget(QtGui.QWidget):
self.ui = Ui_EIPStatus()
self.ui.setupUi(self)
- self.eipconnection = EIPConnection()
+ self.eip_conductor = eip_conductor
+ self.eipconnection = eip_conductor.eip_connection
# set systray tooltip status
self._eip_status = ""
self._service_name = get_service_display_name(EIP_SERVICE)
self.ui.eip_bandwidth.hide()
+ self.hide_fw_down_button()
+ self.ui.btnFwDown.clicked.connect(
+ self._on_fw_down_button_clicked)
# Set the EIP status icons
self.CONNECTING_ICON = None
@@ -75,11 +85,43 @@ class EIPStatusWidget(QtGui.QWidget):
self._make_status_clickable()
self._provider = ""
+ self.is_restart = False
+ self.is_cold_start = True
# Action for the systray
self._eip_disabled_action = QtGui.QAction(
"{0} is {1}".format(self._service_name, self.tr("disabled")), self)
+ def connect_backend_signals(self):
+ """
+ Connect backend signals.
+ """
+ signaler = self.eip_conductor._backend.signaler
+
+ signaler.eip_openvpn_already_running.connect(
+ self._on_eip_openvpn_already_running)
+ signaler.eip_alien_openvpn_already_running.connect(
+ self._on_eip_alien_openvpn_already_running)
+ signaler.eip_openvpn_not_found_error.connect(
+ self._on_eip_openvpn_not_found_error)
+ signaler.eip_vpn_launcher_exception.connect(
+ self._on_eip_vpn_launcher_exception)
+ signaler.eip_no_polkit_agent_error.connect(
+ self._on_eip_no_polkit_agent_error)
+ signaler.eip_connection_aborted.connect(
+ self._on_eip_connection_aborted)
+ signaler.eip_no_pkexec_error.connect(self._on_eip_no_pkexec_error)
+ signaler.eip_no_tun_kext_error.connect(self._on_eip_no_tun_kext_error)
+
+ signaler.eip_state_changed.connect(self.update_vpn_state)
+ signaler.eip_status_changed.connect(self.update_vpn_status)
+
+ # XXX we cannot connect this signal now because
+ # it interferes with the proper notifications during restarts
+ # without available network.
+ #signaler.eip_network_unreachable.connect(
+ #self._on_eip_network_unreachable)
+
def _make_status_clickable(self):
"""
Makes upload and download figures clickable.
@@ -208,7 +250,7 @@ class EIPStatusWidget(QtGui.QWidget):
def set_action_eip_startstop(self, action_eip_startstop):
"""
- Sets the action_eip_startstop to use.
+ Set the action_eip_startstop to use.
:param action_eip_startstop: action_eip_status to be used
:type action_eip_startstop: QtGui.QAction
@@ -238,9 +280,11 @@ class EIPStatusWidget(QtGui.QWidget):
def eip_pre_up(self):
"""
Triggered when the app activates eip.
- Hides the status box and disables the start/stop button.
+ Disables the start/stop button.
"""
self.set_startstop_enabled(False)
+ msg = self.tr("Encrypted Internet is starting")
+ self.set_eip_message(msg)
@QtCore.Slot()
def disable_eip_start(self):
@@ -248,7 +292,7 @@ class EIPStatusWidget(QtGui.QWidget):
Triggered when a default provider_config has not been found.
Disables the start button and adds instructions to the user.
"""
- #logger.debug('Hiding EIP start button')
+ logger.debug('Hiding EIP start button')
# you might be tempted to change this for a .setEnabled(False).
# it won't work. it's under the claws of the state machine.
# probably the best thing would be to make a conditional
@@ -282,10 +326,19 @@ class EIPStatusWidget(QtGui.QWidget):
if self.isVisible():
self._eip_status_menu.menuAction().setVisible(True)
- # XXX disable (later) --------------------------
+ def set_eip_message(self, message):
+ """
+ Set the EIP Widget main message.
+
+ :param message: the message to set in the widget
+ :type message: str or unicode
+ """
+ self.ui.lblEIPMessage.setText(message)
+ self.ui.lblEIPMessage.show()
+
def set_eip_status(self, status, error=False):
"""
- Sets the status label at the VPN stage to status
+ Set the status label at the VPN stage to status.
:param status: status message
:type status: str or unicode
@@ -326,29 +379,80 @@ class EIPStatusWidget(QtGui.QWidget):
Sets the state of the widget to how it should look after EIP
has started
"""
- self.ui.btnEipStartStop.setText(self.tr("Turn OFF"))
self.ui.btnEipStartStop.disconnect(self)
self.ui.btnEipStartStop.clicked.connect(
self.eipconnection.qtsigs.do_connect_signal)
- # XXX disable -----------------------------
- def eip_stopped(self):
+ def hide_fw_down_button(self):
+ """
+ Hide firewall-down button.
+ """
+ self.ui.btnFwDown.hide()
+
+ def show_fw_down_button(self):
+ """
+ Enable firewall-down button.
"""
+ retry_msg = self.tr("Retry")
+ self.ui.btnEipStartStop.setText(retry_msg)
+ self._action_eip_startstop.setText(retry_msg)
+ self.ui.btnFwDown.show()
+
+ def _on_fw_down_button_clicked(self):
+ """
+ Raise a signal for tearing down the firewall, and hide the button
+ afterwards.
+ """
+ self.eip_conductor._backend.tear_fw_down()
+ QtDelayedCall(50, self.hide_fw_down_button)
+
+ # XXX do actual check
+ msg = "Traffic is being routed in the clear."
+ self.ui.btnEipStartStop.setText(self.tr("Turn ON"))
+ self.set_eip_message(msg)
+ self.set_eip_status("")
+
+ @QtCore.Slot(dict)
+ def eip_stopped(self, restart=False, failed=False):
+ """
+ TRIGGERS:
+ EIPConductor.qtsigs.disconnected_signal
+
Sets the state of the widget to how it should look after EIP
has stopped
"""
- # XXX should connect this to EIPConnection.disconnected_signal
+ self.set_country_code("")
self._reset_traffic_rates()
- # XXX disable -----------------------------
- self.ui.btnEipStartStop.setText(self.tr("Turn ON"))
- self.ui.btnEipStartStop.disconnect(self)
- self.ui.btnEipStartStop.clicked.connect(
- self.eipconnection.qtsigs.do_disconnect_signal)
-
self.ui.eip_bandwidth.hide()
- self.ui.lblEIPMessage.setText(
- self.tr("Traffic is being routed in the clear"))
+
+ # This is assuming the firewall works correctly, but we should test fw
+ # status positively.
+ # Or better call it from the conductor...
+
+ clear_traffic = self.tr("Traffic is being routed in the clear.")
+ unreachable_net = self.tr("Network is unreachable.")
+ failed_msg = self.tr("Error connecting")
+
+ if restart:
+ msg = unreachable_net
+ elif failed:
+ msg = failed_msg
+ else:
+ msg = clear_traffic
+ self.set_eip_message(msg)
+ self.ui.lblEIPStatus.show()
+ self.show()
+
+ def eip_failed_to_connect(self):
+ """
+ Update EIP messages with error during (re)connection.
+ """
+ msg = self.tr("Error connecting.")
+ self.ui.lblEIPMessage.setText(msg)
self.ui.lblEIPStatus.show()
+ self.set_eip_status(self.tr("Bitmask is blocking "
+ "unencrypted traffic."))
+ self.show_fw_down_button()
@QtCore.Slot(dict)
def update_vpn_status(self, data=None):
@@ -407,11 +511,20 @@ class EIPStatusWidget(QtGui.QWidget):
self.ui.lblEIPStatus.hide()
# XXX should be handled by the state machine too.
- self.eip_connection_connected.emit()
+ # --- is this currently being sent?
+ self.eipconnection.qtsigs.connected_signal.emit()
+ self._on_eip_connected()
+ self.is_cold_start = False
# XXX should lookup vpn_state map in EIPConnection
elif vpn_state == "AUTH":
self.set_eip_status(self.tr("Authenticating..."))
+ # we wipe up any previous error info in the EIP message
+ # when we detect vpn authentication is happening
+ msg = self.tr("Encrypted Internet is starting")
+ self.set_eip_message(msg)
+ # on the first-run path, we hadn't showed the button yet.
+ self.eip_button.show()
elif vpn_state == "GET_CONFIG":
self.set_eip_status(self.tr("Retrieving configuration..."))
elif vpn_state == "WAIT":
@@ -423,10 +536,11 @@ class EIPStatusWidget(QtGui.QWidget):
elif vpn_state == "ALREADYRUNNING":
# Put the following calls in Qt's event queue, otherwise
# the UI won't update properly
- QtCore.QTimer.singleShot(
- 0, self.eipconnection.qtsigs.do_disconnect_signal)
+ #self.send_disconnect_signal()
+ QtDelayedCall(
+ 0, self.eipconnection.qtsigns.do_disconnect_signal.emit)
msg = self.tr("Unable to start VPN, it's already running.")
- QtCore.QTimer.singleShot(0, partial(self.set_eip_status, msg))
+ QtDelayedCall(0, partial(self.set_eip_status, msg))
else:
self.set_eip_status(vpn_state)
@@ -468,5 +582,152 @@ class EIPStatusWidget(QtGui.QWidget):
def set_provider(self, provider):
self._provider = provider
+
self.ui.lblEIPMessage.setText(
- self.tr("Route traffic through: {0}").format(self._provider))
+ self.tr("Routing traffic through: <b>{0}</b>").format(
+ provider))
+
+ ccode = flags.CURRENT_VPN_COUNTRY
+ if ccode is not None:
+ self.set_country_code(ccode)
+
+ def set_country_code(self, code):
+ """
+ Set the pixmap of the given country code
+
+ :param code: the country code
+ :type code: str
+ """
+ if code is not None and len(code) == 2:
+ img = ":/images/countries/%s.png" % (code.lower(),)
+ else:
+ img = None
+ cc = self.ui.lblGatewayCountryCode
+ cc.setPixmap(QtGui.QPixmap(img))
+ cc.setToolTip(code)
+
+ def aborted(self):
+ """
+ Notify the state machine that EIP was aborted for some reason.
+ """
+ # signal connection_aborted to state machine:
+ qtsigs = self.eipconnection.qtsigs
+ qtsigs.connection_aborted_signal.emit()
+
+ #
+ # Slots for signals
+ #
+
+ @QtCore.Slot()
+ def _on_eip_connection_aborted(self):
+ """
+ TRIGGERS:
+ Signaler.eip_connection_aborted
+ """
+ # TODO this name is very misleading, since there's a generic signal
+ # that's called connection_aborted / connection_died...
+ # should rename to something more specific about missing config.
+ logger.error("Tried to start EIP but cannot find any "
+ "available provider!")
+
+ eip_status_label = self.tr("Could not load {0} configuration.")
+ eip_status_label = eip_status_label.format(
+ self.eip_conductor.eip_name)
+ self.set_eip_status(eip_status_label, error=True)
+
+ self.aborted()
+
+ def _on_eip_openvpn_already_running(self):
+ self.set_eip_status(
+ self.tr("Another openvpn instance is already running, and "
+ "could not be stopped."),
+ error=True)
+ self.set_eipstatus_off()
+
+ self.aborted()
+
+ def _on_eip_alien_openvpn_already_running(self):
+ self.set_eip_status(
+ self.tr("Another openvpn instance is already running, and "
+ "could not be stopped because it was not launched by "
+ "Bitmask. Please stop it and try again."),
+ error=True)
+ self.set_eipstatus_off()
+
+ self.aborted()
+
+ def _on_eip_openvpn_not_found_error(self):
+ self.set_eip_status(
+ self.tr("We could not find openvpn binary."),
+ error=True)
+ self.set_eipstatus_off()
+
+ self.aborted()
+
+ def _on_eip_vpn_launcher_exception(self):
+ # XXX We should implement again translatable exceptions so
+ # we can pass a translatable string to the panel (usermessage attr)
+ self.set_eip_status("VPN Launcher error.", error=True)
+ self.set_eipstatus_off()
+
+ self.aborted()
+
+ def _on_eip_no_polkit_agent_error(self):
+ self.set_eip_status(
+ # XXX this should change to polkit-kde where
+ # applicable.
+ self.tr("We could not find any authentication agent in your "
+ "system.<br/>Make sure you have"
+ "<b>polkit-gnome-authentication-agent-1</b> running and"
+ "try again."),
+ error=True)
+ self.set_eipstatus_off()
+
+ self.aborted()
+
+ def _on_eip_no_pkexec_error(self):
+ self.set_eip_status(
+ self.tr("We could not find <b>pkexec</b> in your system."),
+ error=True)
+ self.set_eipstatus_off()
+
+ self.aborted()
+
+ def _on_eip_no_tun_kext_error(self):
+ self.set_eip_status(
+ self.tr("{0} cannot be started because the tuntap extension is "
+ "not installed properly in your "
+ "system.").format(self.eip_conductor.eip_name))
+ self.set_eipstatus_off()
+
+ self.aborted()
+
+ def _on_eip_connected(self):
+ """
+ Reconnect the disconnecting signal when we are just connected,
+ so that we restore the disconnecting -> stop behaviour.
+ """
+ self.eip_conductor.reconnect_stop_signal()
+
+ @QtCore.Slot()
+ def _on_eip_network_unreachable(self):
+ """
+ TRIGGERS:
+ self._eip_connection.qtsigs.network_unreachable
+
+ Displays a "network unreachable" error in the EIP status panel.
+ """
+ self.set_eip_status(self.tr("Network is unreachable"),
+ error=True)
+ self.set_eip_status_icon("error")
+
+ def set_eipstatus_off(self, error=True):
+ # XXX this should be handled by the state machine.
+ """
+ Sets eip status to off
+ """
+ self.set_eip_status("", error=error)
+ self.set_eip_status_icon("error")
+
+import eipstatus_rc
+assert(eipstatus_rc)
diff --git a/src/leap/bitmask/gui/loggerwindow.py b/src/leap/bitmask/gui/loggerwindow.py
index f19b172f..3a8354b1 100644
--- a/src/leap/bitmask/gui/loggerwindow.py
+++ b/src/leap/bitmask/gui/loggerwindow.py
@@ -27,7 +27,7 @@ from twisted.internet import threads
from ui_loggerwindow import Ui_LoggerWindow
from leap.bitmask.util.constants import PASTEBIN_API_DEV_KEY
-from leap.bitmask.util.leap_log_handler import LeapLogHandler
+from leap.bitmask.logs.leap_log_handler import LeapLogHandler
from leap.bitmask.util import pastebin
from leap.common.check import leap_assert, leap_assert_type
diff --git a/src/leap/bitmask/gui/login.py b/src/leap/bitmask/gui/login.py
index ac7ad878..f66e71d9 100644
--- a/src/leap/bitmask/gui/login.py
+++ b/src/leap/bitmask/gui/login.py
@@ -24,6 +24,7 @@ from ui_login import Ui_LoginWidget
from leap.bitmask.config import flags
from leap.bitmask.util import make_address
+from leap.bitmask.util.credentials import USERNAME_REGEX
from leap.bitmask.util.keyring_helpers import has_keyring
from leap.bitmask.util.keyring_helpers import get_keyring
from leap.common.check import leap_assert_type
@@ -48,8 +49,6 @@ class LoginWidget(QtGui.QWidget):
MAX_STATUS_WIDTH = 40
- BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$"
-
# Keyring
KEYRING_KEY = "bitmask"
@@ -87,7 +86,7 @@ class LoginWidget(QtGui.QWidget):
self.ui.btnLogout.clicked.connect(
self.logout)
- username_re = QtCore.QRegExp(self.BARE_USERNAME_REGEX)
+ username_re = QtCore.QRegExp(USERNAME_REGEX)
self.ui.lnUser.setValidator(
QtGui.QRegExpValidator(username_re, self))
diff --git a/src/leap/bitmask/gui/mail_status.py b/src/leap/bitmask/gui/mail_status.py
index d3346780..5caef745 100644
--- a/src/leap/bitmask/gui/mail_status.py
+++ b/src/leap/bitmask/gui/mail_status.py
@@ -188,7 +188,7 @@ class MailStatusWidget(QtGui.QWidget):
def set_soledad_failed(self):
"""
TRIGGERS:
- SoledadBootstrapper.soledad_failed
+ Signaler.soledad_bootstrap_failed
This method is called whenever soledad has a failure.
"""
diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py
index e3848c46..3ef994b1 100644
--- a/src/leap/bitmask/gui/mainwindow.py
+++ b/src/leap/bitmask/gui/mainwindow.py
@@ -19,22 +19,17 @@ Main window for Bitmask.
"""
import logging
import socket
-import time
-from threading import Condition
from datetime import datetime
from PySide import QtCore, QtGui
-from zope.proxy import ProxyBase, setProxiedObject
from twisted.internet import reactor, threads
from leap.bitmask import __version__ as VERSION
from leap.bitmask import __version_hash__ as VERSION_HASH
from leap.bitmask.config import flags
from leap.bitmask.config.leapsettings import LeapSettings
-from leap.bitmask.config.providerconfig import ProviderConfig
-from leap.bitmask.gui import statemachines
from leap.bitmask.gui.advanced_key_management import AdvancedKeyManagement
from leap.bitmask.gui.eip_preferenceswindow import EIPPreferencesWindow
from leap.bitmask.gui.eip_status import EIPStatusWidget
@@ -45,30 +40,24 @@ from leap.bitmask.gui.preferenceswindow import PreferencesWindow
from leap.bitmask.gui.systray import SysTray
from leap.bitmask.gui.wizard import Wizard
-from leap.bitmask import provider
from leap.bitmask.platform_init import IS_WIN, IS_MAC, IS_LINUX
from leap.bitmask.platform_init.initializers import init_platform
from leap.bitmask import backend
-from leap.bitmask.services import get_service_display_name
-
+from leap.bitmask.services.eip import conductor as eip_conductor
from leap.bitmask.services.mail import conductor as mail_conductor
from leap.bitmask.services import EIP_SERVICE, MX_SERVICE
-from leap.bitmask.services.eip.connection import EIPConnection
-from leap.bitmask.services.soledad.soledadbootstrapper import \
- SoledadBootstrapper
from leap.bitmask.util import make_address
from leap.bitmask.util.keyring_helpers import has_keyring
-from leap.bitmask.util.leap_log_handler import LeapLogHandler
+from leap.bitmask.logs.leap_log_handler import LeapLogHandler
if IS_WIN:
from leap.bitmask.platform_init.locks import WindowsLock
from leap.bitmask.platform_init.locks import raise_window_ack
-from leap.common.check import leap_assert
from leap.common.events import register
from leap.common.events import events_pb2 as proto
@@ -76,6 +65,7 @@ from leap.mail.imap.service.imap import IMAP_PORT
from ui_mainwindow import Ui_MainWindow
+QtDelayedCall = QtCore.QTimer.singleShot
logger = logging.getLogger(__name__)
@@ -89,17 +79,17 @@ class MainWindow(QtGui.QMainWindow):
new_updates = QtCore.Signal(object)
raise_window = QtCore.Signal([])
soledad_ready = QtCore.Signal([])
- mail_client_logged_in = QtCore.Signal([])
logout = QtCore.Signal([])
+ all_services_stopped = QtCore.Signal()
# We use this flag to detect abnormal terminations
user_stopped_eip = False
# We give EIP some time to come up before starting soledad anyway
- EIP_TIMEOUT = 60000 # in milliseconds
+ EIP_START_TIMEOUT = 60000 # in milliseconds
- # We give each service some time to come to a halt before forcing quit
- SERVICE_STOP_TIMEOUT = 20
+ # We give the services some time to a halt before forcing quit.
+ SERVICES_STOP_TIMEOUT = 20
def __init__(self, quit_callback, bypass_checks=False, start_hidden=False):
"""
@@ -125,9 +115,6 @@ class MainWindow(QtGui.QMainWindow):
register(signal=proto.RAISE_WINDOW,
callback=self._on_raise_window_event,
reqcbk=lambda req, resp: None) # make rpc call async
- register(signal=proto.IMAP_CLIENT_LOGIN,
- callback=self._on_mail_client_logged_in,
- reqcbk=lambda req, resp: None) # make rpc call async
# end register leap events ####################################
self._quit_callback = quit_callback
@@ -142,11 +129,16 @@ class MainWindow(QtGui.QMainWindow):
self._settings = LeapSettings()
+ # Login Widget
self._login_widget = LoginWidget(
self._settings,
self)
self.ui.loginLayout.addWidget(self._login_widget)
+ # Mail Widget
+ self._mail_status = MailStatusWidget(self)
+ self.ui.mailLayout.addWidget(self._mail_status)
+
# Qt Signal Connections #####################################
# TODO separate logic from ui signals.
@@ -155,67 +147,45 @@ class MainWindow(QtGui.QMainWindow):
self._login_widget.show_wizard.connect(self._launch_wizard)
self._login_widget.logout.connect(self._logout)
- self._eip_status = EIPStatusWidget(self)
- self.ui.eipLayout.addWidget(self._eip_status)
- self._login_widget.logged_in_signal.connect(
- self._eip_status.enable_eip_start)
- self._login_widget.logged_in_signal.connect(
- self._enable_eip_start_action)
+ # EIP Control redux #########################################
+ self._eip_conductor = eip_conductor.EIPConductor(
+ self._settings, self._backend)
+ self._eip_status = EIPStatusWidget(self, self._eip_conductor)
- self._mail_status = MailStatusWidget(self)
- self.ui.mailLayout.addWidget(self._mail_status)
-
- self._eip_connection = EIPConnection()
-
- # XXX this should be handled by EIP Conductor
- self._eip_connection.qtsigs.connecting_signal.connect(
- self._start_EIP)
- self._eip_connection.qtsigs.disconnecting_signal.connect(
- self._stop_eip)
+ self.ui.eipLayout.addWidget(self._eip_status)
+ self._eip_conductor.add_eip_widget(self._eip_status)
- self._eip_status.eip_connection_connected.connect(
+ self._eip_conductor.connect_signals()
+ self._eip_conductor.qtsigs.connected_signal.connect(
self._on_eip_connection_connected)
- self._eip_status.eip_connection_connected.connect(
+ self._eip_conductor.qtsigs.connected_signal.connect(
self._maybe_run_soledad_setup_checks)
+
self.offline_mode_bypass_login.connect(
self._maybe_run_soledad_setup_checks)
self.eip_needs_login.connect(self._eip_status.disable_eip_start)
self.eip_needs_login.connect(self._disable_eip_start_action)
+ self._already_started_eip = False
self._trying_to_start_eip = False
- # This is loaded only once, there's a bug when doing that more
- # than once
- # XXX HACK!! But we need it as long as we are using
- # provider_config in here
- self._provider_config = self._backend.get_provider_config()
-
- # Used for automatic start of EIP
- self._provisional_provider_config = ProviderConfig()
-
self._already_started_eip = False
- self._already_started_soledad = False
+ self._soledad_started = False
# This is created once we have a valid provider config
self._srp_auth = None
self._logged_user = None
self._logged_in_offline = False
- self._backend_connected_signals = {}
- self._backend_connect()
+ # Set used to track the services being stopped and need wait.
+ self._services_being_stopped = {}
- self._soledad_bootstrapper = SoledadBootstrapper()
- self._soledad_bootstrapper.download_config.connect(
- self._soledad_intermediate_stage)
- self._soledad_bootstrapper.gen_key.connect(
- self._soledad_bootstrapped_stage)
- self._soledad_bootstrapper.local_only_ready.connect(
- self._soledad_bootstrapped_stage)
- self._soledad_bootstrapper.soledad_invalid_auth_token.connect(
- self._mail_status.set_soledad_invalid_auth_token)
- self._soledad_bootstrapper.soledad_failed.connect(
- self._mail_status.set_soledad_failed)
+ # timeout object used to trigger quit
+ self._quit_timeout_callater = None
+
+ self._backend_connected_signals = []
+ self._backend_connect()
self.ui.action_preferences.triggered.connect(self._show_preferences)
self.ui.action_eip_preferences.triggered.connect(
@@ -241,8 +211,7 @@ class MainWindow(QtGui.QMainWindow):
self._systray = None
- # XXX separate actions into a different
- # module.
+ # XXX separate actions into a different module.
self._action_mail_status = QtGui.QAction(self.tr("Mail is OFF"), self)
self._mail_status.set_action_mail_status(self._action_mail_status)
@@ -260,6 +229,8 @@ class MainWindow(QtGui.QMainWindow):
self._ui_mx_visible = True
self._ui_eip_visible = True
+ self._provider_details = None
+
# last minute UI manipulations
self._center_window()
@@ -280,10 +251,6 @@ class MainWindow(QtGui.QMainWindow):
# XXX should connect to mail_conductor.start_mail_service instead
self.soledad_ready.connect(self._start_smtp_bootstrapping)
self.soledad_ready.connect(self._start_imap_service)
- self.mail_client_logged_in.connect(self._fetch_incoming_mail)
- self.logout.connect(self._stop_imap_service)
- self.logout.connect(self._stop_smtp_service)
-
################################# end Qt Signals connection ########
init_platform()
@@ -296,18 +263,11 @@ class MainWindow(QtGui.QMainWindow):
self._bypass_checks = bypass_checks
self._start_hidden = start_hidden
- # We initialize Soledad and Keymanager instances as
- # transparent proxies, so we can pass the reference freely
- # around.
- self._soledad = ProxyBase(None)
- self._keymanager = ProxyBase(None)
-
- self._soledad_defer = None
-
- self._mail_conductor = mail_conductor.MailConductor(
- self._soledad, self._keymanager)
+ self._mail_conductor = mail_conductor.MailConductor(self._backend)
self._mail_conductor.connect_mail_signals(self._mail_status)
+ self.logout.connect(self._mail_conductor.stop_mail_services)
+
# Eip machine is a public attribute where the state machine for
# the eip connection will be available to the different components.
# Remember that this will not live in the +1600LOC mainwindow for
@@ -315,20 +275,19 @@ class MainWindow(QtGui.QMainWindow):
# the EIPConductor or some other clever component that we will
# instantiate from here.
- self.eip_machine = None
# start event machines
- self.start_eip_machine()
+ # TODO should encapsulate all actions into one object
+ self._eip_conductor.start_eip_machine(
+ action=self._action_eip_startstop)
self._mail_conductor.start_mail_machine()
- self._eip_name = get_service_display_name(EIP_SERVICE)
-
if self._first_run():
self._wizard_firstrun = True
self._disconnect_and_untrack()
self._wizard = Wizard(backend=self._backend,
bypass_checks=bypass_checks)
# Give this window time to finish init and then show the wizard
- QtCore.QTimer.singleShot(1, self._launch_wizard)
+ QtDelayedCall(1, self._launch_wizard)
self._wizard.accepted.connect(self._finish_init)
self._wizard.rejected.connect(self._rejected_wizard)
else:
@@ -357,7 +316,7 @@ class MainWindow(QtGui.QMainWindow):
:param method: the method to call when the signal is triggered.
:type method: callable, Slot or Signal
"""
- self._backend_connected_signals[signal] = method
+ self._backend_connected_signals.append((signal, method))
signal.connect(method)
def _backend_bad_call(self, data):
@@ -370,97 +329,102 @@ class MainWindow(QtGui.QMainWindow):
logger.error("Bad call to the backend:")
logger.error(data)
- def _backend_connect(self):
+ def _backend_connect(self, only_tracked=False):
"""
- Helper to connect to backend signals
- """
- sig = self._backend.signaler
+ Connect to backend signals.
- sig.backend_bad_call.connect(self._backend_bad_call)
+ We track some signals in order to disconnect them on demand.
+ For instance, in the wizard we need to connect to some signals that are
+ already connected in the mainwindow, so to avoid conflicts we do:
+ - disconnect signals needed in wizard (`_disconnect_and_untrack`)
+ - use wizard
+ - reconnect disconnected signals (we use the `only_tracked` param)
- self._connect_and_track(sig.prov_name_resolution,
- self._intermediate_stage)
- self._connect_and_track(sig.prov_https_connection,
- self._intermediate_stage)
- self._connect_and_track(sig.prov_download_ca_cert,
- self._intermediate_stage)
+ :param only_tracked: whether or not we should connect only the signals
+ that we are tracking to disconnect later.
+ :type only_tracked: bool
+ """
+ sig = self._backend.signaler
+ conntrack = self._connect_and_track
+ auth_err = self._authentication_error
- self._connect_and_track(sig.prov_download_provider_info,
- self._load_provider_config)
- self._connect_and_track(sig.prov_check_api_certificate,
- self._provider_config_loaded)
+ conntrack(sig.prov_name_resolution, self._intermediate_stage)
+ conntrack(sig.prov_https_connection, self._intermediate_stage)
+ conntrack(sig.prov_download_ca_cert, self._intermediate_stage)
+ conntrack(sig.prov_download_provider_info, self._load_provider_config)
+ conntrack(sig.prov_check_api_certificate, self._provider_config_loaded)
+ conntrack(sig.prov_check_api_certificate, self._get_provider_details)
- self._connect_and_track(sig.prov_problem_with_provider,
- self._login_problem_provider)
+ conntrack(sig.prov_problem_with_provider, self._login_problem_provider)
+ conntrack(sig.prov_cancelled_setup, self._set_login_cancelled)
- self._connect_and_track(sig.prov_cancelled_setup,
- self._set_login_cancelled)
+ conntrack(sig.prov_get_details, self._provider_get_details)
# Login signals
- self._connect_and_track(sig.srp_auth_ok, self._authentication_finished)
+ conntrack(sig.srp_auth_ok, self._authentication_finished)
- auth_error = (
- lambda: self._authentication_error(self.tr("Unknown error.")))
- self._connect_and_track(sig.srp_auth_error, auth_error)
+ auth_error = lambda: auth_err(self.tr("Unknown error."))
+ conntrack(sig.srp_auth_error, auth_error)
- auth_server_error = (
- lambda: self._authentication_error(
- self.tr("There was a server problem with authentication.")))
- self._connect_and_track(sig.srp_auth_server_error, auth_server_error)
+ auth_server_error = lambda: auth_err(self.tr(
+ "There was a server problem with authentication."))
+ conntrack(sig.srp_auth_server_error, auth_server_error)
- auth_connection_error = (
- lambda: self._authentication_error(
- self.tr("Could not establish a connection.")))
- self._connect_and_track(sig.srp_auth_connection_error,
- auth_connection_error)
+ auth_connection_error = lambda: auth_err(self.tr(
+ "Could not establish a connection."))
+ conntrack(sig.srp_auth_connection_error, auth_connection_error)
- auth_bad_user_or_password = (
- lambda: self._authentication_error(
- self.tr("Invalid username or password.")))
- self._connect_and_track(sig.srp_auth_bad_user_or_password,
- auth_bad_user_or_password)
+ auth_bad_user_or_password = lambda: auth_err(self.tr(
+ "Invalid username or password."))
+ conntrack(sig.srp_auth_bad_user_or_password, auth_bad_user_or_password)
# Logout signals
- self._connect_and_track(sig.srp_logout_ok, self._logout_ok)
- self._connect_and_track(sig.srp_logout_error, self._logout_error)
-
- self._connect_and_track(sig.srp_not_logged_in_error,
- self._not_logged_in_error)
+ conntrack(sig.srp_logout_ok, self._logout_ok)
+ conntrack(sig.srp_logout_error, self._logout_error)
+ conntrack(sig.srp_not_logged_in_error, self._not_logged_in_error)
# EIP bootstrap signals
- self._connect_and_track(sig.eip_config_ready,
- self._eip_intermediate_stage)
- self._connect_and_track(sig.eip_client_certificate_ready,
- self._finish_eip_bootstrap)
+ conntrack(sig.eip_config_ready, self._eip_intermediate_stage)
+ conntrack(sig.eip_client_certificate_ready, self._finish_eip_bootstrap)
+
+ ###################################################
+ # Add tracked signals above this, untracked below!
+ ###################################################
+ if only_tracked:
+ return
# We don't want to disconnect some signals so don't track them:
+
+ sig.backend_bad_call.connect(self._backend_bad_call)
+
sig.prov_unsupported_client.connect(self._needs_update)
sig.prov_unsupported_api.connect(self._incompatible_api)
+ sig.prov_get_all_services.connect(self._provider_get_all_services)
- # EIP start signals
- sig.eip_openvpn_already_running.connect(
- self._on_eip_openvpn_already_running)
- sig.eip_alien_openvpn_already_running.connect(
- self._on_eip_alien_openvpn_already_running)
- sig.eip_openvpn_not_found_error.connect(
- self._on_eip_openvpn_not_found_error)
- sig.eip_vpn_launcher_exception.connect(
- self._on_eip_vpn_launcher_exception)
- sig.eip_no_polkit_agent_error.connect(
- self._on_eip_no_polkit_agent_error)
- sig.eip_no_pkexec_error.connect(self._on_eip_no_pkexec_error)
- sig.eip_no_tun_kext_error.connect(self._on_eip_no_tun_kext_error)
-
- sig.eip_state_changed.connect(self._eip_status.update_vpn_state)
- sig.eip_status_changed.connect(self._eip_status.update_vpn_status)
- sig.eip_process_finished.connect(self._eip_finished)
- sig.eip_network_unreachable.connect(self._on_eip_network_unreachable)
- sig.eip_process_restart_tls.connect(self._do_eip_restart)
- sig.eip_process_restart_ping.connect(self._do_eip_restart)
+ # EIP start signals ==============================================
+ self._eip_conductor.connect_backend_signals()
sig.eip_can_start.connect(self._backend_can_start_eip)
sig.eip_cannot_start.connect(self._backend_cannot_start_eip)
+ # ==================================================================
+
+ # Soledad signals
+ # TODO delegate connection to soledad bootstrapper
+ sig.soledad_bootstrap_failed.connect(
+ self._mail_status.set_soledad_failed)
+ sig.soledad_bootstrap_finished.connect(self._on_soledad_ready)
+
+ sig.soledad_offline_failed.connect(
+ self._mail_status.set_soledad_failed)
+ sig.soledad_offline_finished.connect(self._on_soledad_ready)
+
+ sig.soledad_invalid_auth_token.connect(
+ self._mail_status.set_soledad_invalid_auth_token)
+
+ # TODO: connect this with something
+ # sig.soledad_cancelled_bootstrap.connect()
+
def _disconnect_and_untrack(self):
"""
Helper to disconnect the tracked signals.
@@ -468,13 +432,13 @@ class MainWindow(QtGui.QMainWindow):
Some signals are emitted from the wizard, and we want to
ignore those.
"""
- for signal, method in self._backend_connected_signals.items():
+ for signal, method in self._backend_connected_signals:
try:
signal.disconnect(method)
except RuntimeError:
pass # Signal was not connected
- self._backend_connected_signals = {}
+ self._backend_connected_signals = []
@QtCore.Slot()
def _rejected_wizard(self):
@@ -497,7 +461,7 @@ class MainWindow(QtGui.QMainWindow):
# This happens if the user finishes the provider
# setup but does not register
self._wizard = None
- self._backend_connect()
+ self._backend_connect(only_tracked=True)
if self._wizard_firstrun:
self._finish_init()
@@ -586,13 +550,14 @@ class MainWindow(QtGui.QMainWindow):
domain = self._login_widget.get_selected_provider()
logged_user = "{0}@{1}".format(self._logged_user, domain)
- has_mx = True
- if self._logged_user is not None:
- provider_config = self._get_best_provider_config()
- has_mx = provider_config.provides_mx()
+ details = self._provider_details
+ mx_provided = False
+ if details is not None:
+ mx_provided = MX_SERVICE in details.services
- akm = AdvancedKeyManagement(
- self, has_mx, logged_user, self._keymanager, self._soledad)
+ # XXX: handle differently not logged in user?
+ akm = AdvancedKeyManagement(self, mx_provided, logged_user,
+ self._backend, self._soledad_started)
akm.show()
@QtCore.Slot()
@@ -604,11 +569,13 @@ class MainWindow(QtGui.QMainWindow):
Displays the preferences window.
"""
- user = self._login_widget.get_user()
- prov = self._login_widget.get_selected_provider()
- preferences = PreferencesWindow(
- self, self._backend, self._provider_config, self._soledad,
- user, prov)
+ user = self._logged_user
+ domain = self._login_widget.get_selected_provider()
+ mx_provided = False
+ if self._provider_details is not None:
+ mx_provided = MX_SERVICE in self._provider_details.services
+ preferences = PreferencesWindow(self, user, domain, self._backend,
+ self._soledad_started, mx_provided)
self.soledad_ready.connect(preferences.set_soledad_ready)
preferences.show()
@@ -630,7 +597,7 @@ class MainWindow(QtGui.QMainWindow):
default_provider = settings.get_defaultprovider()
if default_provider is None:
- logger.warning("Trying toupdate eip enabled status but there's no"
+ logger.warning("Trying to update eip enabled status but there's no"
" default provider. Disabling EIP for the time"
" being...")
self._backend_cannot_start_eip()
@@ -642,7 +609,7 @@ class MainWindow(QtGui.QMainWindow):
# If we don't want to start eip, we leave everything
# initialized to quickly start it
if not self._trying_to_start_eip:
- self._backend.setup_eip(default_provider, skip_network=True)
+ self._backend.eip_setup(default_provider, skip_network=True)
def _backend_can_start_eip(self):
"""
@@ -670,7 +637,6 @@ class MainWindow(QtGui.QMainWindow):
# so the user needs to log in first
self._eip_status.disable_eip_start()
else:
- self._stop_eip()
self._eip_status.disable_eip_start()
self._eip_status.set_eip_status(self.tr("Disabled"))
@@ -697,7 +663,6 @@ class MainWindow(QtGui.QMainWindow):
# so the user needs to log in first
self._eip_status.disable_eip_start()
else:
- self._stop_eip()
self._eip_status.disable_eip_start()
self._eip_status.set_eip_status(self.tr("Disabled"))
@@ -817,7 +782,7 @@ class MainWindow(QtGui.QMainWindow):
self.eip_needs_login.emit()
self._wizard = None
- self._backend_connect()
+ self._backend_connect(only_tracked=True)
else:
self._update_eip_enabled_status()
@@ -846,16 +811,9 @@ class MainWindow(QtGui.QMainWindow):
"""
providers = self._settings.get_configured_providers()
- services = set()
-
- for prov in providers:
- provider_config = ProviderConfig()
- loaded = provider_config.load(
- provider.get_provider_path(prov))
- if loaded:
- for service in provider_config.get_services():
- services.add(service)
+ self._backend.provider_get_all_services(providers)
+ def _provider_get_all_services(self, services):
self._set_eip_visible(EIP_SERVICE in services)
self._set_mx_visible(MX_SERVICE in services)
@@ -893,14 +851,11 @@ class MainWindow(QtGui.QMainWindow):
"""
Set the login label to reflect offline status.
"""
- if self._logged_in_offline:
- provider = ""
- else:
+ provider = ""
+ if not self._logged_in_offline:
provider = self.ui.lblLoginProvider.text()
- self.ui.lblLoginProvider.setText(
- provider +
- self.tr(" (offline mode)"))
+ self.ui.lblLoginProvider.setText(provider + self.tr(" (offline mode)"))
#
# systray
@@ -923,7 +878,8 @@ class MainWindow(QtGui.QMainWindow):
systrayMenu.addAction(self._action_visible)
systrayMenu.addSeparator()
- eip_status_label = "{0}: {1}".format(self._eip_name, self.tr("OFF"))
+ eip_status_label = "{0}: {1}".format(
+ self._eip_conductor.eip_name, self.tr("OFF"))
self._eip_menu = eip_menu = systrayMenu.addMenu(eip_status_label)
eip_menu.addAction(self._action_eip_startstop)
self._eip_status.set_eip_status_menu(eip_menu)
@@ -1005,7 +961,7 @@ class MainWindow(QtGui.QMainWindow):
# Wait a bit until the window visibility has changed so
# the menu is set with the correct value.
- QtCore.QTimer.singleShot(500, self._update_hideshow_menu)
+ QtDelayedCall(500, self._update_hideshow_menu)
def _center_window(self):
"""
@@ -1154,9 +1110,8 @@ class MainWindow(QtGui.QMainWindow):
provider configuration if it's not present, otherwise will
emit the corresponding signals inmediately
"""
- # XXX should rename this provider, name clash.
- provider = self._login_widget.get_selected_provider()
- self._backend.setup_provider(provider)
+ domain = self._login_widget.get_selected_provider()
+ self._backend.provider_setup(domain)
@QtCore.Slot(dict)
def _load_provider_config(self, data):
@@ -1164,12 +1119,11 @@ class MainWindow(QtGui.QMainWindow):
TRIGGERS:
self._backend.signaler.prov_download_provider_info
- Once the provider config has been downloaded, this loads the
- self._provider_config instance with it and starts the second
- part of the bootstrapping sequence
+ Once the provider config has been downloaded, start the second
+ part of the bootstrapping sequence.
:param data: result from the last stage of the
- run_provider_select_checks
+ backend.provider_setup()
:type data: dict
"""
if data[self._backend.PASSED_KEY]:
@@ -1211,7 +1165,6 @@ class MainWindow(QtGui.QMainWindow):
self._set_label_offline()
self.offline_mode_bypass_login.emit()
else:
- leap_assert(self._provider_config, "We need a provider config")
self.ui.action_create_new_account.setEnabled(False)
if self._login_widget.start_login():
self._download_provider_config()
@@ -1250,20 +1203,19 @@ class MainWindow(QtGui.QMainWindow):
Cancel the running defers to avoid app blocking.
"""
# XXX: Should we stop all the backend defers?
- self._backend.cancel_setup_provider()
- self._backend.cancel_login()
+ self._backend.provider_cancel_setup()
+ self._backend.user_cancel_login()
+ self._backend.soledad_cancel_bootstrap()
+ self._backend.soledad_close()
- if self._soledad_defer is not None:
- logger.debug("Cancelling soledad defer.")
- self._soledad_defer.cancel()
- self._soledad_defer = None
+ self._soledad_started = False
@QtCore.Slot()
def _set_login_cancelled(self):
"""
TRIGGERS:
Signaler.prov_cancelled_setup fired by
- self._backend.cancel_setup_provider()
+ self._backend.provider_cancel_setup()
This method re-enables the login widget and display a message for
the cancelled operation.
@@ -1280,16 +1232,14 @@ class MainWindow(QtGui.QMainWindow):
Once the provider configuration is loaded, this starts the SRP
authentication
"""
- leap_assert(self._provider_config, "We need a provider config!")
-
if data[self._backend.PASSED_KEY]:
username = self._login_widget.get_user()
password = self._login_widget.get_password()
self._show_hide_unsupported_services()
- domain = self._provider_config.get_domain()
- self._backend.login(domain, username, password)
+ domain = self._login_widget.get_selected_provider()
+ self._backend.user_login(domain, username, password)
else:
logger.error(data[self._backend.ERROR_KEY])
self._login_problem_provider()
@@ -1307,7 +1257,7 @@ class MainWindow(QtGui.QMainWindow):
self._logged_user = self._login_widget.get_user()
user = self._logged_user
- domain = self._provider_config.get_domain()
+ domain = self._login_widget.get_selected_provider()
full_user_id = make_address(user, domain)
self._mail_conductor.userid = full_user_id
self._start_eip_bootstrap()
@@ -1317,11 +1267,11 @@ class MainWindow(QtGui.QMainWindow):
if MX_SERVICE in self._enabled_services:
btn_enabled = self._login_widget.set_logout_btn_enabled
btn_enabled(False)
- self.soledad_ready.connect(lambda: btn_enabled(True))
- self._soledad_bootstrapper.soledad_failed.connect(
- lambda: btn_enabled(True))
+ sig = self._backend.signaler
+ sig.soledad_bootstrap_failed.connect(lambda: btn_enabled(True))
+ sig.soledad_bootstrap_finished.connect(lambda: btn_enabled(True))
- if not self._get_best_provider_config().provides_mx():
+ if not MX_SERVICE in self._provider_details.services:
self._set_mx_visible(False)
def _start_eip_bootstrap(self):
@@ -1331,11 +1281,10 @@ class MainWindow(QtGui.QMainWindow):
"""
self._login_widget.logged_in()
- provider = self._provider_config.get_domain()
- self.ui.lblLoginProvider.setText(provider)
+ domain = self._login_widget.get_selected_provider()
+ self.ui.lblLoginProvider.setText(domain)
- self._enabled_services = self._settings.get_enabled_services(
- self._provider_config.get_domain())
+ self._enabled_services = self._settings.get_enabled_services(domain)
# TODO separate UI from logic.
if self._provides_mx_and_enabled():
@@ -1345,6 +1294,30 @@ class MainWindow(QtGui.QMainWindow):
self._maybe_start_eip()
+ @QtCore.Slot()
+ def _get_provider_details(self):
+ """
+ TRIGGERS:
+ prov_check_api_certificate
+
+ Set the attributes to know if the EIP and MX services are supported
+ and enabled.
+ This is triggered right after the provider has been set up.
+ """
+ domain = self._login_widget.get_selected_provider()
+ lang = QtCore.QLocale.system().name()
+ self._backend.provider_get_details(domain, lang)
+
+ @QtCore.Slot()
+ def _provider_get_details(self, details):
+ """
+ Set the details for the just downloaded provider.
+
+ :param details: the details of the provider.
+ :type details: ProviderConfigLight
+ """
+ self._provider_details = details
+
def _provides_mx_and_enabled(self):
"""
Defines if the current provider provides mx and if we have it enabled.
@@ -1352,9 +1325,15 @@ class MainWindow(QtGui.QMainWindow):
:returns: True if provides and is enabled, False otherwise
:rtype: bool
"""
- provider_config = self._get_best_provider_config()
- return (provider_config.provides_mx() and
- MX_SERVICE in self._enabled_services)
+ domain = self._login_widget.get_selected_provider()
+ enabled_services = self._settings.get_enabled_services(domain)
+
+ mx_enabled = MX_SERVICE in enabled_services
+ mx_provided = False
+ if self._provider_details is not None:
+ mx_provided = MX_SERVICE in self._provider_details.services
+
+ return mx_enabled and mx_provided
def _provides_eip_and_enabled(self):
"""
@@ -1363,33 +1342,30 @@ class MainWindow(QtGui.QMainWindow):
:returns: True if provides and is enabled, False otherwise
:rtype: bool
"""
- provider_config = self._get_best_provider_config()
- return (provider_config.provides_eip() and
- EIP_SERVICE in self._enabled_services)
+ domain = self._login_widget.get_selected_provider()
+ enabled_services = self._settings.get_enabled_services(domain)
+
+ eip_enabled = EIP_SERVICE in enabled_services
+ eip_provided = False
+ if self._provider_details is not None:
+ eip_provided = EIP_SERVICE in self._provider_details.services
+
+ return eip_enabled and eip_provided
def _maybe_run_soledad_setup_checks(self):
"""
Conditionally start Soledad.
"""
# TODO split.
- if self._already_started_soledad is True:
- return
-
- if not self._provides_mx_and_enabled():
+ if not self._provides_mx_and_enabled() and not flags.OFFLINE:
+ logger.debug("Provider does not offer MX, but it is enabled.")
return
username = self._login_widget.get_user()
password = unicode(self._login_widget.get_password())
provider_domain = self._login_widget.get_selected_provider()
- sb = self._soledad_bootstrapper
- if flags.OFFLINE is True:
- provider_domain = self._login_widget.get_selected_provider()
- sb._password = password
-
- self._provisional_provider_config.load(
- provider.get_provider_path(provider_domain))
-
+ if flags.OFFLINE:
full_user_id = make_address(username, provider_domain)
uuid = self._settings.get_uuid(full_user_id)
self._mail_conductor.userid = full_user_id
@@ -1399,74 +1375,26 @@ class MainWindow(QtGui.QMainWindow):
# this is mostly for internal use/debug for now.
logger.warning("Sorry! Log-in at least one time.")
return
- fun = sb.load_offline_soledad
- fun(full_user_id, password, uuid)
+ self._backend.soledad_load_offline(full_user_id, password, uuid)
else:
- provider_config = self._provider_config
-
if self._logged_user is not None:
- self._soledad_defer = sb.run_soledad_setup_checks(
- provider_config, username, password,
- download_if_needed=True)
+ domain = self._login_widget.get_selected_provider()
+ self._backend.soledad_bootstrap(username, domain, password)
###################################################################
# Service control methods: soledad
- @QtCore.Slot(dict)
- def _soledad_intermediate_stage(self, data):
- # TODO missing param docstring
- """
- TRIGGERS:
- self._soledad_bootstrapper.download_config
-
- If there was a problem, displays it, otherwise it does nothing.
- This is used for intermediate bootstrapping stages, in case
- they fail.
- """
- passed = data[self._soledad_bootstrapper.PASSED_KEY]
- if not passed:
- # TODO display in the GUI:
- # should pass signal to a slot in status_panel
- # that sets the global status
- logger.error("Soledad failed to start: %s" %
- (data[self._soledad_bootstrapper.ERROR_KEY],))
-
- @QtCore.Slot(dict)
- def _soledad_bootstrapped_stage(self, data):
+ @QtCore.Slot()
+ def _on_soledad_ready(self):
"""
TRIGGERS:
- self._soledad_bootstrapper.gen_key
- self._soledad_bootstrapper.local_only_ready
-
- If there was a problem, displays it, otherwise it does nothing.
- This is used for intermediate bootstrapping stages, in case
- they fail.
+ Signaler.soledad_bootstrap_finished
- :param data: result from the bootstrapping stage for Soledad
- :type data: dict
+ Actions to take when Soledad is ready.
"""
- passed = data[self._soledad_bootstrapper.PASSED_KEY]
- if not passed:
- # TODO should actually *display* on the panel.
- logger.debug("ERROR on soledad bootstrapping:")
- logger.error("%r" % data[self._soledad_bootstrapper.ERROR_KEY])
- return
-
logger.debug("Done bootstrapping Soledad")
- # Update the proxy objects to point to
- # the initialized instances.
- setProxiedObject(self._soledad,
- self._soledad_bootstrapper.soledad)
- setProxiedObject(self._keymanager,
- self._soledad_bootstrapper.keymanager)
-
- # Ok, now soledad is ready, so we can allow other things that
- # depend on soledad to start.
- self._soledad_defer = None
-
- # this will trigger start_imap_service
- # and start_smtp_boostrapping
+ self._soledad_started = True
self.soledad_ready.emit()
###################################################################
@@ -1483,19 +1411,7 @@ class MainWindow(QtGui.QMainWindow):
return
if self._provides_mx_and_enabled():
- self._mail_conductor.start_smtp_service(self._provider_config,
- download_if_needed=True)
-
- # XXX --- should remove from here, and connecte directly to the state
- # machine.
- @QtCore.Slot()
- def _stop_smtp_service(self):
- """
- TRIGGERS:
- self.logout
- """
- # TODO call stop_mail_service
- self._mail_conductor.stop_smtp_service()
+ self._mail_conductor.start_smtp_service(download_if_needed=True)
###################################################################
# Service control methods: imap
@@ -1509,69 +1425,14 @@ class MainWindow(QtGui.QMainWindow):
# TODO in the OFFLINE mode we should also modify the rules
# in the mail state machine so it shows that imap is active
# (but not smtp since it's not yet ready for offline use)
- start_fun = self._mail_conductor.start_imap_service
- if flags.OFFLINE is True:
- provider_domain = self._login_widget.get_selected_provider()
- self._provider_config.load(
- provider.get_provider_path(provider_domain))
- provides_mx = self._provider_config.provides_mx()
-
- if flags.OFFLINE is True and provides_mx:
- start_fun()
- return
-
- if self._provides_mx_and_enabled():
- start_fun()
-
- def _on_mail_client_logged_in(self, req):
- """
- Triggers qt signal when client login event is received.
- """
- self.mail_client_logged_in.emit()
-
- @QtCore.Slot()
- def _fetch_incoming_mail(self):
- """
- TRIGGERS:
- self.mail_client_logged_in
- """
- # TODO connect signal directly!!!
- self._mail_conductor.fetch_incoming_mail()
-
- @QtCore.Slot()
- def _stop_imap_service(self):
- """
- TRIGGERS:
- self.logout
- """
- cv = Condition()
- cv.acquire()
- # TODO call stop_mail_service
- threads.deferToThread(self._mail_conductor.stop_imap_service, cv)
- # and wait for it to be stopped
- logger.debug('Waiting for imap service to stop.')
- cv.wait(self.SERVICE_STOP_TIMEOUT)
+ if self._provides_mx_and_enabled() or flags.OFFLINE:
+ self._mail_conductor.start_imap_service()
# end service control methods (imap)
###################################################################
# Service control methods: eip
- def start_eip_machine(self):
- """
- Initializes and starts the EIP state machine
- """
- button = self._eip_status.eip_button
- action = self._action_eip_startstop
- label = self._eip_status.eip_label
- builder = statemachines.ConnectionMachineBuilder(self._eip_connection)
- eip_machine = builder.make_machine(button=button,
- action=action,
- label=label)
- self.eip_machine = eip_machine
- self.eip_machine.start()
- logger.debug('eip machine started')
-
@QtCore.Slot()
def _disable_eip_start_action(self):
"""
@@ -1585,32 +1446,32 @@ class MainWindow(QtGui.QMainWindow):
Enables the EIP start action in the systray menu.
"""
self._action_eip_startstop.setEnabled(True)
+ self._eip_status.enable_eip_start()
@QtCore.Slot()
def _on_eip_connection_connected(self):
"""
TRIGGERS:
- self._eip_status.eip_connection_connected
-
- Emits the EIPConnection.qtsigs.connected_signal
+ self._eip_conductor.qtsigs.connected_signal
This is a little workaround for connecting the vpn-connected
signal that currently is beeing processed under status_panel.
After the refactor to EIPConductor this should not be necessary.
"""
- self._eip_connection.qtsigs.connected_signal.emit()
-
- provider_config = self._get_best_provider_config()
- domain = provider_config.get_domain()
+ domain = self._login_widget.get_selected_provider()
self._eip_status.set_provider(domain)
self._settings.set_defaultprovider(domain)
self._already_started_eip = True
# check for connectivity
+ # we might want to leave a little time here...
self._check_name_resolution(domain)
def _check_name_resolution(self, domain):
+ # FIXME this has to be moved to backend !!!
+ # Should move to netchecks module.
+ # and separate qt from reactor...
"""
Check if we can resolve the given domain name.
@@ -1641,7 +1502,7 @@ class MainWindow(QtGui.QMainWindow):
"missing some helper files that are needed to securely use "
"DNS while {1} is active. To install these helper files, quit "
"this application and start it again."
- ).format(domain, self._eip_name)
+ ).format(domain, self._eip_conductor.eip_name)
show_err = lambda: QtGui.QMessageBox.critical(
self, self.tr("Connection Error"), msg)
@@ -1663,245 +1524,42 @@ class MainWindow(QtGui.QMainWindow):
self._enabled_services = settings.get_enabled_services(
default_provider)
- loaded = self._provisional_provider_config.load(
- provider.get_provider_path(default_provider))
- if loaded and settings.get_autostart_eip():
- # XXX I think we should not try to re-download config every time,
- # it adds some delay.
- # Maybe if it's the first run in a session,
- # or we can try only if it fails.
- self._maybe_start_eip()
- elif settings.get_autostart_eip():
- # XXX: Display a proper message to the user
- self.eip_needs_login.emit()
- logger.error("Unable to load %s config, cannot autostart." %
- (default_provider,))
-
- @QtCore.Slot()
- def _start_EIP(self):
- """
- Starts EIP
- """
- self._eip_status.eip_pre_up()
- self.user_stopped_eip = False
-
- # Until we set an option in the preferences window, we'll assume that
- # by default we try to autostart. If we switch it off manually, it
- # won't try the next time.
- self._settings.set_autostart_eip(True)
-
- self._backend.start_eip()
-
- @QtCore.Slot()
- def _on_eip_connection_aborted(self):
- """
- TRIGGERS:
- Signaler.eip_connection_aborted
- """
- logger.error("Tried to start EIP but cannot find any "
- "available provider!")
-
- eip_status_label = self.tr("Could not load {0} configuration.")
- eip_status_label = eip_status_label.format(self._eip_name)
- self._eip_status.set_eip_status(eip_status_label, error=True)
-
- # signal connection_aborted to state machine:
- qtsigs = self._eip_connection.qtsigs
- qtsigs.connection_aborted_signal.emit()
-
- def _on_eip_openvpn_already_running(self):
- self._eip_status.set_eip_status(
- self.tr("Another openvpn instance is already running, and "
- "could not be stopped."),
- error=True)
- self._set_eipstatus_off()
-
- def _on_eip_alien_openvpn_already_running(self):
- self._eip_status.set_eip_status(
- self.tr("Another openvpn instance is already running, and "
- "could not be stopped because it was not launched by "
- "Bitmask. Please stop it and try again."),
- error=True)
- self._set_eipstatus_off()
-
- def _on_eip_openvpn_not_found_error(self):
- self._eip_status.set_eip_status(
- self.tr("We could not find openvpn binary."),
- error=True)
- self._set_eipstatus_off()
-
- def _on_eip_vpn_launcher_exception(self):
- # XXX We should implement again translatable exceptions so
- # we can pass a translatable string to the panel (usermessage attr)
- self._eip_status.set_eip_status("VPN Launcher error.", error=True)
- self._set_eipstatus_off()
-
- def _on_eip_no_polkit_agent_error(self):
- self._eip_status.set_eip_status(
- # XXX this should change to polkit-kde where
- # applicable.
- self.tr("We could not find any authentication agent in your "
- "system.<br/>Make sure you have"
- "<b>polkit-gnome-authentication-agent-1</b> running and"
- "try again."),
- error=True)
- self._set_eipstatus_off()
-
- def _on_eip_no_pkexec_error(self):
- self._eip_status.set_eip_status(
- self.tr("We could not find <b>pkexec</b> in your system."),
- error=True)
- self._set_eipstatus_off()
-
- def _on_eip_no_tun_kext_error(self):
- self._eip_status.set_eip_status(
- self.tr("{0} cannot be started because the tuntap extension is "
- "not installed properly in your "
- "system.").format(self._eip_name))
- self._set_eipstatus_off()
-
- @QtCore.Slot()
- def _stop_eip(self):
- """
- TRIGGERS:
- self._eip_connection.qtsigs.do_disconnect_signal (via state machine)
-
- Stops vpn process and makes gui adjustments to reflect
- the change of state.
-
- :param abnormal: whether this was an abnormal termination.
- :type abnormal: bool
- """
- self.user_stopped_eip = True
- self._backend.stop_eip()
-
- self._set_eipstatus_off(False)
- self._already_started_eip = False
-
- logger.debug('Setting autostart to: False')
- self._settings.set_autostart_eip(False)
-
- if self._logged_user:
- self._eip_status.set_provider(
- make_address(
- self._logged_user,
- self._get_best_provider_config().get_domain()))
- self._eip_status.eip_stopped()
-
- @QtCore.Slot()
- def _on_eip_network_unreachable(self):
- # XXX Should move to EIP Conductor
- """
- TRIGGERS:
- self._eip_connection.qtsigs.network_unreachable
-
- Displays a "network unreachable" error in the EIP status panel.
- """
- self._eip_status.set_eip_status(self.tr("Network is unreachable"),
- error=True)
- self._eip_status.set_eip_status_icon("error")
-
- @QtCore.Slot()
- def _do_eip_restart(self):
- # XXX Should move to EIP Conductor
- """
- TRIGGERS:
- self._eip_connection.qtsigs.process_restart
-
- Restart the connection.
- """
- # for some reason, emitting the do_disconnect/do_connect
- # signals hangs the UI.
- self._stop_eip()
- QtCore.QTimer.singleShot(2000, self._start_EIP)
-
- def _set_eipstatus_off(self, error=True):
- """
- Sets eip status to off
- """
- # XXX this should be handled by the state machine.
- self._eip_status.set_eip_status("", error=error)
- self._eip_status.set_eip_status_icon("error")
-
- @QtCore.Slot(int)
- def _eip_finished(self, exitCode):
- """
- TRIGGERS:
- Signaler.eip_process_finished
-
- Triggered when the EIP/VPN process finishes to set the UI
- accordingly.
-
- Ideally we would have the right exit code here,
- but the use of different wrappers (pkexec, cocoasudo) swallows
- the openvpn exit code so we get zero exit in some cases where we
- shouldn't. As a workaround we just use a flag to indicate
- a purposeful switch off, and mark everything else as unexpected.
-
- In the near future we should trigger a native notification from here,
- since the user really really wants to know she is unprotected asap.
- And the right thing to do will be to fail-close.
-
- :param exitCode: the exit code of the eip process.
- :type exitCode: int
- """
- # TODO move to EIPConductor.
- # TODO Add error catching to the openvpn log observer
- # so we can have a more precise idea of which type
- # of error did we have (server side, local problem, etc)
-
- logger.info("VPN process finished with exitCode %s..."
- % (exitCode,))
-
- qtsigs = self._eip_connection.qtsigs
- signal = qtsigs.disconnected_signal
-
- # XXX check if these exitCodes are pkexec/cocoasudo specific
- if exitCode in (126, 127):
- eip_status_label = self.tr(
- "{0} could not be launched "
- "because you did not authenticate properly.")
- eip_status_label = eip_status_label.format(self._eip_name)
- self._eip_status.set_eip_status(eip_status_label, error=True)
- signal = qtsigs.connection_aborted_signal
- self._backend.terminate_eip()
-
- elif exitCode != 0 or not self.user_stopped_eip:
- eip_status_label = self.tr("{0} finished in an unexpected manner!")
- eip_status_label = eip_status_label.format(self._eip_name)
- self._eip_status.eip_stopped()
- self._eip_status.set_eip_status_icon("error")
- self._eip_status.set_eip_status(eip_status_label, error=True)
- signal = qtsigs.connection_died_signal
-
- if exitCode == 0 and IS_MAC:
- # XXX remove this warning after I fix cocoasudo.
- logger.warning("The above exit code MIGHT BE WRONG.")
-
- # We emit signals to trigger transitions in the state machine:
- signal.emit()
+ if settings.get_autostart_eip():
+ self._maybe_start_eip(autostart=True)
# eip boostrapping, config etc...
- def _maybe_start_eip(self):
+ def _maybe_start_eip(self, autostart=False):
"""
Start the EIP bootstrapping sequence if the client is configured to
do so.
+
+ :param autostart: we are autostarting EIP when this is True
+ :type autostart: bool
"""
- if self._provides_eip_and_enabled() and not self._already_started_eip:
+ # during autostart we assume that the provider provides EIP
+ if autostart:
+ should_start = EIP_SERVICE in self._enabled_services
+ else:
+ should_start = self._provides_eip_and_enabled()
+
+ if should_start and not self._already_started_eip:
+ if self._eip_status.is_cold_start:
+ self._backend.tear_fw_down()
# XXX this should be handled by the state machine.
+ self._enable_eip_start_action()
self._eip_status.set_eip_status(
self.tr("Starting..."))
+ self._eip_status.eip_button.setEnabled(False)
domain = self._login_widget.get_selected_provider()
- self._backend.setup_eip(domain)
+ self._backend.eip_setup(domain)
self._already_started_eip = True
# we want to start soledad anyway after a certain timeout if eip
# fails to come up
- QtCore.QTimer.singleShot(
- self.EIP_TIMEOUT,
- self._maybe_run_soledad_setup_checks)
+ QtDelayedCall(self.EIP_START_TIMEOUT,
+ self._maybe_run_soledad_setup_checks)
else:
if not self._already_started_eip:
if EIP_SERVICE in self._enabled_services:
@@ -1920,8 +1578,8 @@ class MainWindow(QtGui.QMainWindow):
TRIGGERS:
self._backend.signaler.eip_client_certificate_ready
- Starts the VPN thread if the eip configuration is properly
- loaded
+ Start the VPN thread if the eip configuration is properly
+ loaded.
"""
passed = data[self._backend.PASSED_KEY]
@@ -1933,11 +1591,11 @@ class MainWindow(QtGui.QMainWindow):
return
# DO START EIP Connection!
- self._eip_connection.qtsigs.do_connect_signal.emit()
+ self._eip_conductor.do_connect()
@QtCore.Slot(dict)
def _eip_intermediate_stage(self, data):
- # TODO missing param
+ # TODO missing param documentation
"""
TRIGGERS:
self._backend.signaler.eip_config_ready
@@ -1952,33 +1610,10 @@ class MainWindow(QtGui.QMainWindow):
self.tr("Unable to connect: Problem with provider"))
logger.error(data[self._backend.ERROR_KEY])
self._already_started_eip = False
+ self._eip_status.aborted()
# end of EIP methods ---------------------------------------------
- def _get_best_provider_config(self):
- """
- Returns the best ProviderConfig to use at a moment. We may
- have to use self._provider_config or
- self._provisional_provider_config depending on the start
- status.
-
- :rtype: ProviderConfig
- """
- # TODO move this out of gui.
- leap_assert(self._provider_config is not None or
- self._provisional_provider_config is not None,
- "We need a provider config")
-
- provider_config = None
- if self._provider_config.loaded():
- provider_config = self._provider_config
- elif self._provisional_provider_config.loaded():
- provider_config = self._provisional_provider_config
- else:
- leap_assert(False, "We could not find any usable ProviderConfig.")
-
- return provider_config
-
@QtCore.Slot()
def _logout(self):
"""
@@ -1987,16 +1622,11 @@ class MainWindow(QtGui.QMainWindow):
Starts the logout sequence
"""
- setProxiedObject(self._soledad, None)
-
self._cancel_ongoing_defers()
- # reset soledad status flag
- self._already_started_soledad = False
-
# XXX: If other defers are doing authenticated stuff, this
# might conflict with those. CHECK!
- self._backend.logout()
+ self._backend.user_logout()
self.logout.emit()
@QtCore.Slot()
@@ -2080,59 +1710,37 @@ class MainWindow(QtGui.QMainWindow):
# cleanup and quit methods
#
- def _cleanup_pidfiles(self):
- """
- Removes lockfiles on a clean shutdown.
-
- Triggered after aboutToQuit signal.
+ def _stop_services(self):
"""
- if IS_WIN:
- WindowsLock.release_all_locks()
-
- def _cleanup_and_quit(self):
- """
- Call all the cleanup actions in a serialized way.
- Should be called from the quit function.
+ Stop services and cancel ongoing actions (if any).
"""
- logger.debug('About to quit, doing cleanup...')
-
- self._stop_imap_service()
-
- if self._logged_user is not None:
- self._backend.logout()
+ logger.debug('About to quit, doing cleanup.')
- if self._soledad_bootstrapper.soledad is not None:
- logger.debug("Closing soledad...")
- self._soledad_bootstrapper.soledad.close()
- else:
- logger.error("No instance of soledad was found.")
+ self._cancel_ongoing_defers()
- logger.debug('Terminating vpn')
- self._backend.stop_eip(shutdown=True)
+ self._services_being_stopped = {'imap', 'eip'}
- # We need to give some time to the ongoing signals for shutdown
- # to come into action. This needs to be solved using
- # back-communication from backend.
- QtCore.QTimer.singleShot(3000, self._shutdown)
+ imap_stopped = lambda: self._remove_service('imap')
+ self._backend.signaler.imap_stopped.connect(imap_stopped)
- def _shutdown(self):
- """
- Actually shutdown.
- """
- self._cancel_ongoing_defers()
+ eip_stopped = lambda: self._remove_service('eip')
+ self._backend.signaler.eip_stopped.connect(eip_stopped)
- # TODO missing any more cancels?
+ logger.debug('Stopping mail services')
+ self._backend.imap_stop_service()
+ self._backend.smtp_stop_service()
- logger.debug('Cleaning pidfiles')
- self._cleanup_pidfiles()
- if self._quit_callback:
- self._quit_callback()
+ if self._logged_user is not None:
+ logger.debug("Doing logout")
+ self._backend.user_logout()
- logger.debug('Bye.')
+ logger.debug('Terminating vpn')
+ self._backend.eip_stop(shutdown=True)
def quit(self):
"""
- Cleanup and tidely close the main window before quitting.
+ Start the quit sequence and wait for services to finish.
+ Cleanup and close the main window before quitting.
"""
# TODO separate the shutting down of services from the
# UI stuff.
@@ -2142,25 +1750,72 @@ class MainWindow(QtGui.QMainWindow):
if self._systray is not None:
self._systray.showMessage(
self.tr('Quitting...'),
- self.tr('The app is quitting, please wait.'))
+ self.tr('Bitmask is quitting, please wait.'))
# explicitly process events to display tooltip immediately
- QtCore.QCoreApplication.processEvents()
+ QtCore.QCoreApplication.processEvents(0, 10)
+
+ # Close other windows if any.
+ if self._wizard:
+ self._wizard.close()
+
+ if self._logger_window:
+ self._logger_window.close()
# Set this in case that the app is hidden
QtGui.QApplication.setQuitOnLastWindowClosed(True)
- self._cleanup_and_quit()
+ self._stop_services()
- # We queue the call to stop since we need to wait until EIP is stopped.
- # Otherwise we may exit leaving an unmanaged openvpn process.
- reactor.callLater(0, self._backend.stop)
self._really_quit = True
- if self._wizard:
- self._wizard.close()
+ # call final quit when all the services are stopped
+ self.all_services_stopped.connect(self.final_quit)
+ # or if we reach the timeout
+ self._quit_timeout_callater = reactor.callLater(
+ self.SERVICES_STOP_TIMEOUT, self.final_quit)
- if self._logger_window:
- self._logger_window.close()
+ @QtCore.Slot()
+ def _remove_service(self, service):
+ """
+ Remove the given service from the waiting list and check if we have
+ running services that we need to wait until we quit.
+ Emit self.all_services_stopped signal if we don't need to keep waiting.
+ :param service: the service that we want to remove
+ :type service: str
+ """
+ self._services_being_stopped.discard(service)
+
+ if not self._services_being_stopped:
+ logger.debug("All services stopped.")
+ self.all_services_stopped.emit()
+
+ @QtCore.Slot()
+ def final_quit(self):
+ """
+ Final steps to quit the app, starting from here we don't care about
+ running services or user interaction, just quitting.
+ """
+ logger.debug('Final quit...')
+
+ try:
+ # disconnect signal if we get here due a timeout.
+ self.all_services_stopped.disconnect(self.final_quit)
+ except RuntimeError:
+ pass # Signal was not connected
+
+ # Cancel timeout to avoid being called if we reached here through the
+ # signal
+ if self._quit_timeout_callater.active():
+ self._quit_timeout_callater.cancel()
+
+ # Remove lockfiles on a clean shutdown.
+ logger.debug('Cleaning pidfiles')
+ if IS_WIN:
+ WindowsLock.release_all_locks()
+
+ self._backend.stop()
self.close()
+
+ reactor.callLater(1, self._quit_callback)
diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py
index 2947c5db..a3b81d38 100644
--- a/src/leap/bitmask/gui/preferenceswindow.py
+++ b/src/leap/bitmask/gui/preferenceswindow.py
@@ -23,15 +23,10 @@ import logging
from functools import partial
from PySide import QtCore, QtGui
-from zope.proxy import sameProxiedObjects
-from leap.bitmask.provider import get_provider_path
from leap.bitmask.config.leapsettings import LeapSettings
from leap.bitmask.gui.ui_preferences import Ui_Preferences
-from leap.soledad.client import NoStorageSecret
-from leap.bitmask.util.password import basic_password_checks
-from leap.bitmask.services import get_supported
-from leap.bitmask.config.providerconfig import ProviderConfig
+from leap.bitmask.util.credentials import password_checks
from leap.bitmask.services import get_service_display_name, MX_SERVICE
logger = logging.getLogger(__name__)
@@ -43,32 +38,31 @@ class PreferencesWindow(QtGui.QDialog):
"""
preferences_saved = QtCore.Signal()
- def __init__(self, parent, backend, provider_config,
- soledad, username, domain):
+ def __init__(self, parent, username, domain, backend, soledad_started, mx):
"""
:param parent: parent object of the PreferencesWindow.
:parent type: QWidget
- :param backend: Backend being used
- :type backend: Backend
- :param provider_config: ProviderConfig object.
- :type provider_config: ProviderConfig
- :param soledad: Soledad instance
- :type soledad: Soledad
:param username: the user set in the login widget
:type username: unicode
:param domain: the selected domain in the login widget
:type domain: unicode
+ :param backend: Backend being used
+ :type backend: Backend
+ :param soledad_started: whether soledad has started or not
+ :type soledad_started: bool
+ :param mx: whether the current provider provides mx or not.
+ :type mx: bool
"""
QtGui.QDialog.__init__(self, parent)
self.AUTOMATIC_GATEWAY_LABEL = self.tr("Automatic")
- self._backend = backend
- self._settings = LeapSettings()
- self._soledad = soledad
- self._provider_config = provider_config
self._username = username
self._domain = domain
+ self._backend = backend
+ self._soledad_started = soledad_started
+ self._mx_provided = mx
+ self._settings = LeapSettings()
self._backend_connect()
# Load UI
@@ -89,50 +83,17 @@ class PreferencesWindow(QtGui.QDialog):
else:
self._add_configured_providers()
- self._backend.get_logged_in_status()
+ if self._username is None:
+ self._not_logged_in()
+ else:
+ self.ui.gbPasswordChange.setEnabled(True)
+ if self._mx_provided:
+ self._provides_mx()
self._select_provider_by_name(domain)
- @QtCore.Slot()
- def _is_logged_in(self):
- """
- TRIGGERS:
- Signaler.srp_status_logged_in
-
- Actions to perform is the user is logged in.
- """
- settings = self._settings
- pw_enabled = True
-
- # check if provider has 'mx' ...
- # TODO: we should move this to the backend.
- if self._provider_config.provides_mx():
- enabled_services = settings.get_enabled_services(self._domain)
- mx_name = get_service_display_name(MX_SERVICE)
-
- # ... and if the user have it enabled
- if MX_SERVICE not in enabled_services:
- msg = self.tr("You need to enable {0} in order to change "
- "the password.".format(mx_name))
- self._set_password_change_status(msg, error=True)
- pw_enabled = False
- else:
- # check if Soledad is bootstrapped
- if sameProxiedObjects(self._soledad, None):
- msg = self.tr(
- "You need to wait until {0} is ready in "
- "order to change the password.".format(mx_name))
- self._set_password_change_status(msg)
- pw_enabled = False
-
- self.ui.gbPasswordChange.setEnabled(pw_enabled)
-
- @QtCore.Slot()
def _not_logged_in(self):
"""
- TRIGGERS:
- Signaler.srp_status_not_logged_in
-
Actions to perform if the user is not logged in.
"""
msg = self.tr(
@@ -140,6 +101,30 @@ class PreferencesWindow(QtGui.QDialog):
self._set_password_change_status(msg)
self.ui.gbPasswordChange.setEnabled(False)
+ def _provides_mx(self):
+ """
+ Actions to perform if the provider provides MX.
+ """
+ pw_enabled = True
+ enabled_services = self._settings.get_enabled_services(self._domain)
+ mx_name = get_service_display_name(MX_SERVICE)
+
+ if MX_SERVICE not in enabled_services:
+ msg = self.tr("You need to enable {0} in order to change "
+ "the password.".format(mx_name))
+ self._set_password_change_status(msg, error=True)
+ pw_enabled = False
+ else:
+ # check if Soledad is bootstrapped
+ if not self._soledad_started:
+ msg = self.tr(
+ "You need to wait until {0} is ready in "
+ "order to change the password.".format(mx_name))
+ self._set_password_change_status(msg)
+ pw_enabled = False
+
+ self.ui.gbPasswordChange.setEnabled(pw_enabled)
+
@QtCore.Slot()
def set_soledad_ready(self):
"""
@@ -200,7 +185,7 @@ class PreferencesWindow(QtGui.QDialog):
new_password = self.ui.leNewPassword.text()
new_password2 = self.ui.leNewPassword2.text()
- ok, msg = basic_password_checks(username, new_password, new_password2)
+ ok, msg = password_checks(username, new_password, new_password2)
if not ok:
self._set_changing_password(False)
@@ -209,10 +194,10 @@ class PreferencesWindow(QtGui.QDialog):
return
self._set_changing_password(True)
- self._backend.change_password(current_password, new_password)
+ self._backend.user_change_password(current_password, new_password)
@QtCore.Slot()
- def _change_password_ok(self):
+ def _srp_change_password_ok(self):
"""
TRIGGERS:
self._backend.signaler.srp_password_change_ok
@@ -221,12 +206,44 @@ class PreferencesWindow(QtGui.QDialog):
"""
new_password = self.ui.leNewPassword.text()
logger.debug("SRP password changed successfully.")
- try:
- self._soledad.change_passphrase(new_password)
- logger.debug("Soledad password changed successfully.")
- except NoStorageSecret:
- logger.debug(
- "No storage secret for password change in Soledad.")
+
+ if self._mx_provided:
+ self._backend.soledad_change_password(new_password)
+ else:
+ self._change_password_success()
+
+ @QtCore.Slot(unicode)
+ def _srp_change_password_problem(self, msg):
+ """
+ TRIGGERS:
+ self._backend.signaler.srp_password_change_error
+ self._backend.signaler.srp_password_change_badpw
+
+ Callback used to display an error on changing password.
+
+ :param msg: the message to show to the user.
+ :type msg: unicode
+ """
+ logger.error("Error changing password")
+ self._set_password_change_status(msg, error=True)
+ self._set_changing_password(False)
+
+ @QtCore.Slot()
+ def _soledad_change_password_ok(self):
+ """
+ TRIGGERS:
+ Signaler.soledad_password_change_ok
+
+ Soledad password change went OK.
+ """
+ logger.debug("Soledad password changed successfully.")
+ self._change_password_success()
+
+ def _change_password_success(self):
+ """
+ Callback used to display a successfully changed password.
+ """
+ logger.debug("Soledad password changed successfully.")
self._set_password_change_status(
self.tr("Password changed successfully."), success=True)
@@ -234,18 +251,17 @@ class PreferencesWindow(QtGui.QDialog):
self._set_changing_password(False)
@QtCore.Slot(unicode)
- def _change_password_problem(self, msg):
+ def _soledad_change_password_problem(self, msg):
"""
TRIGGERS:
- self._backend.signaler.srp_password_change_error
- self._backend.signaler.srp_password_change_badpw
+ Signaler.soledad_password_change_error
Callback used to display an error on changing password.
:param msg: the message to show to the user.
:type msg: unicode
"""
- logger.error("Error changing password")
+ logger.error("Error changing soledad password")
self._set_password_change_status(msg, error=True)
self._set_changing_password(False)
@@ -321,8 +337,7 @@ class PreferencesWindow(QtGui.QDialog):
TRIGGERS:
self.ui.cbProvidersServices.currentIndexChanged[unicode]
- Loads the services that the provider provides into the UI for
- the user to enable or disable.
+ Fill the services list with the selected provider's services.
:param domain: the domain of the provider to load services from.
:type domain: str
@@ -333,10 +348,6 @@ class PreferencesWindow(QtGui.QDialog):
if not domain:
return
- provider_config = self._get_provider_config(domain)
- if provider_config is None:
- return
-
# set the proper connection for the 'save' button
try:
self.ui.pbSaveServices.clicked.disconnect()
@@ -346,7 +357,21 @@ class PreferencesWindow(QtGui.QDialog):
save_services = partial(self._save_enabled_services, domain)
self.ui.pbSaveServices.clicked.connect(save_services)
- services = get_supported(provider_config.get_services())
+ self._backend.provider_get_supported_services(domain)
+
+ @QtCore.Slot(str)
+ def _load_services(self, services):
+ """
+ TRIGGERS:
+ self.ui.cbProvidersServices.currentIndexChanged[unicode]
+
+ Loads the services that the provider provides into the UI for
+ the user to enable or disable.
+
+ :param domain: the domain of the provider to load services from.
+ :type domain: str
+ """
+ domain = self.ui.cbProvidersServices.currentText()
services_conf = self._settings.get_enabled_services(domain)
# discard changes if other provider is selected
@@ -394,36 +419,26 @@ class PreferencesWindow(QtGui.QDialog):
self._set_providers_services_status(msg, success=True)
self.preferences_saved.emit()
- def _get_provider_config(self, domain):
- """
- Helper to return a valid Provider Config from the domain name.
-
- :param domain: the domain name of the provider.
- :type domain: str
-
- :rtype: ProviderConfig or None if there is a problem loading the config
- """
- provider_config = ProviderConfig()
- if not provider_config.load(get_provider_path(domain)):
- provider_config = None
-
- return provider_config
-
def _backend_connect(self):
"""
Helper to connect to backend signals
"""
sig = self._backend.signaler
- sig.srp_status_logged_in.connect(self._is_logged_in)
- sig.srp_status_not_logged_in.connect(self._not_logged_in)
+ sig.prov_get_supported_services.connect(self._load_services)
- sig.srp_password_change_ok.connect(self._change_password_ok)
+ sig.srp_password_change_ok.connect(self._srp_change_password_ok)
- pwd_change_error = lambda: self._change_password_problem(
+ pwd_change_error = lambda: self._srp_change_password_problem(
self.tr("There was a problem changing the password."))
sig.srp_password_change_error.connect(pwd_change_error)
- pwd_change_badpw = lambda: self._change_password_problem(
+ pwd_change_badpw = lambda: self._srp_change_password_problem(
self.tr("You did not enter a correct current password."))
sig.srp_password_change_badpw.connect(pwd_change_badpw)
+
+ sig.soledad_password_change_ok.connect(
+ self._soledad_change_password_ok)
+
+ sig.soledad_password_change_error.connect(
+ self._soledad_change_password_problem)
diff --git a/src/leap/bitmask/gui/statemachines.py b/src/leap/bitmask/gui/statemachines.py
index 31938a70..00a1387e 100644
--- a/src/leap/bitmask/gui/statemachines.py
+++ b/src/leap/bitmask/gui/statemachines.py
@@ -504,6 +504,11 @@ class ConnectionMachineBuilder(object):
conn.qtsigs.connection_died_signal,
states[_OFF])
+ # XXX adding this---------------------
+ states[_ON].addTransition(
+ conn.qtsigs.do_disconnect_signal,
+ states[_DIS])
+
# * If we receive the connection_aborted, we transition
# from connecting to the off state
states[_CON].addTransition(
@@ -551,7 +556,8 @@ class ConnectionMachineBuilder(object):
# TODO add tooltip
# OFF State ----------------------
- off = QState()
+ off = SignallingState(
+ None, name=conn.name)
off_label = _tr("Turn {0}").format(
conn.Connected.short_label)
if button:
@@ -559,11 +565,15 @@ class ConnectionMachineBuilder(object):
button, 'text', off_label)
off.assignProperty(
button, 'enabled', True)
+ off.assignProperty(
+ button, 'visible', True)
if action:
off.assignProperty(
action, 'text', off_label)
off.assignProperty(
action, 'enabled', True)
+ off.assignProperty(
+ action, 'visible', True)
off.setObjectName(_OFF)
states[_OFF] = off
@@ -587,7 +597,10 @@ class ConnectionMachineBuilder(object):
states[_CON] = connecting
# ON State ------------------------
- on = QState()
+ on = SignallingState(
+ None, name=conn.name)
+ on_label = _tr("Turn {0}").format(
+ conn.Disconnected.short_label)
if button:
on.assignProperty(
button, 'text', on_label)
diff --git a/src/leap/bitmask/gui/ui/eip_status.ui b/src/leap/bitmask/gui/ui/eip_status.ui
index 64821ad6..7216bb0a 100644
--- a/src/leap/bitmask/gui/ui/eip_status.ui
+++ b/src/leap/bitmask/gui/ui/eip_status.ui
@@ -28,7 +28,7 @@
<property name="verticalSpacing">
<number>0</number>
</property>
- <item row="0" column="2">
+ <item row="0" column="4">
<widget class="QPushButton" name="btnEipStartStop">
<property name="text">
<string>Turn On</string>
@@ -51,7 +51,7 @@
</property>
</widget>
</item>
- <item row="3" column="1">
+ <item row="3" column="2">
<widget class="QLabel" name="lblEIPStatus">
<property name="maximumSize">
<size>
@@ -70,7 +70,7 @@
</property>
</widget>
</item>
- <item row="0" column="1">
+ <item row="0" column="2">
<widget class="QLabel" name="lblEIPMessage">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
@@ -86,7 +86,7 @@
</property>
</widget>
</item>
- <item row="0" column="3">
+ <item row="0" column="5">
<widget class="QLabel" name="lblVPNStatusIcon">
<property name="maximumSize">
<size>
@@ -105,7 +105,7 @@
</property>
</widget>
</item>
- <item row="1" column="1">
+ <item row="1" column="2">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@@ -118,7 +118,7 @@
</property>
</spacer>
</item>
- <item row="2" column="1" colspan="3">
+ <item row="2" column="2" colspan="4">
<widget class="QWidget" name="eip_bandwidth" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
@@ -161,12 +161,13 @@
<property name="text">
<string>0.0 KB/s</string>
</property>
+ <property name="icon">
+ <iconset resource="../../../../../data/resources/mainwindow.qrc">
+ <normaloff>:/images/black/32/arrow-down.png</normaloff>:/images/black/32/arrow-down.png</iconset>
+ </property>
<property name="flat">
<bool>true</bool>
</property>
- <property name="icon">
- <pixmap resource="../../../../../data/resources/icons.qrc">:/images/light/16/down-arrow.png</pixmap>
- </property>
</widget>
</item>
<item>
@@ -211,12 +212,13 @@
<property name="text">
<string>0.0 KB/s</string>
</property>
+ <property name="icon">
+ <iconset resource="../../../../../data/resources/mainwindow.qrc">
+ <normaloff>:/images/black/32/arrow-up.png</normaloff>:/images/black/32/arrow-up.png</iconset>
+ </property>
<property name="flat">
<bool>true</bool>
</property>
- <property name="icon">
- <pixmap resource="../../../../../data/resources/icons.qrc">:/images/light/16/up-arrow.png</pixmap>
- </property>
</widget>
</item>
<item>
@@ -237,6 +239,20 @@
</layout>
</widget>
</item>
+ <item row="0" column="3">
+ <widget class="QPushButton" name="btnFwDown">
+ <property name="text">
+ <string>Turn Off</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="lblGatewayCountryCode">
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
</layout>
</item>
</layout>
diff --git a/src/leap/bitmask/gui/wizard.py b/src/leap/bitmask/gui/wizard.py
index 020a58e2..4d774907 100644
--- a/src/leap/bitmask/gui/wizard.py
+++ b/src/leap/bitmask/gui/wizard.py
@@ -26,11 +26,10 @@ from PySide import QtCore, QtGui
from leap.bitmask.config import flags
from leap.bitmask.config.leapsettings import LeapSettings
-from leap.bitmask.config.providerconfig import ProviderConfig
-from leap.bitmask.provider import get_provider_path
from leap.bitmask.services import get_service_display_name, get_supported
+from leap.bitmask.util.credentials import password_checks, username_checks
+from leap.bitmask.util.credentials import USERNAME_REGEX
from leap.bitmask.util.keyring_helpers import has_keyring
-from leap.bitmask.util.password import basic_password_checks
from ui_wizard import Ui_Wizard
@@ -49,8 +48,6 @@ class Wizard(QtGui.QWizard):
REGISTER_USER_PAGE = 4
SERVICES_PAGE = 5
- BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$"
-
def __init__(self, backend, bypass_checks=False):
"""
Constructor for the main Wizard.
@@ -89,10 +86,9 @@ class Wizard(QtGui.QWizard):
self._backend_connect()
self._domain = None
- # HACK!! We need provider_config for the time being, it'll be
- # removed
- self._provider_config = (
- self._backend._components["provider"]._provider_config)
+
+ # this details are set when the provider download is complete.
+ self._provider_details = None
# We will store a reference to the defers for eventual use
# (eg, to cancel them) but not doing anything with them right now.
@@ -118,7 +114,7 @@ class Wizard(QtGui.QWizard):
self.ui.rbExistingProvider.toggled.connect(self._skip_provider_checks)
- usernameRe = QtCore.QRegExp(self.BARE_USERNAME_REGEX)
+ usernameRe = QtCore.QRegExp(USERNAME_REGEX)
self.ui.lblUser.setValidator(
QtGui.QRegExpValidator(usernameRe, self))
@@ -231,6 +227,12 @@ class Wizard(QtGui.QWizard):
if reset:
self._reset_provider_check()
+ def _focus_username(self):
+ """
+ Focus at the username lineedit for the registration page
+ """
+ self.ui.lblUser.setFocus()
+
def _focus_password(self):
"""
Focuses at the password lineedit for the registration page
@@ -253,16 +255,22 @@ class Wizard(QtGui.QWizard):
password = self.ui.lblPassword.text()
password2 = self.ui.lblPassword2.text()
- ok, msg = basic_password_checks(username, password, password2)
- if ok:
+ user_ok, msg = username_checks(username)
+ if user_ok:
+ pass_ok, msg = password_checks(username, password, password2)
+
+ if user_ok and pass_ok:
self._set_register_status(self.tr("Starting registration..."))
- self._backend.register_user(self._domain, username, password)
+ self._backend.user_register(self._domain, username, password)
self._username = username
self._password = password
else:
+ if user_ok:
+ self._focus_password()
+ else:
+ self._focus_username()
self._set_register_status(msg, error=True)
- self._focus_password()
self.ui.btnRegister.setEnabled(True)
def _set_registration_fields_visibility(self, visible):
@@ -406,7 +414,7 @@ class Wizard(QtGui.QWizard):
self.ui.lblNameResolution.setPixmap(self.QUESTION_ICON)
self._provider_select_defer = self._backend.\
- setup_provider(self._domain)
+ provider_setup(self._domain)
@QtCore.Slot(bool)
def _skip_provider_checks(self, skip):
@@ -502,10 +510,12 @@ class Wizard(QtGui.QWizard):
check. Since this check is the last of this set, it also
completes the page if passed
"""
- if self._provider_config.load(get_provider_path(self._domain)):
+ if data[self._backend.PASSED_KEY]:
self._complete_task(data, self.ui.lblProviderInfo,
True, self.SELECT_PROVIDER_PAGE)
self._provider_checks_ok = True
+ lang = QtCore.QLocale.system().name()
+ self._backend.provider_get_details(self._domain, lang)
else:
new_data = {
self._backend.PASSED_KEY: False,
@@ -527,6 +537,16 @@ class Wizard(QtGui.QWizard):
else:
self.ui.cbProviders.setEnabled(True)
+ @QtCore.Slot()
+ def _provider_get_details(self, details):
+ """
+ Set the details for the just downloaded provider.
+
+ :param details: the details of the provider.
+ :type details: ProviderConfigLight
+ """
+ self._provider_details = details
+
@QtCore.Slot(dict)
def _download_ca_cert(self, data):
"""
@@ -594,11 +614,9 @@ class Wizard(QtGui.QWizard):
the user to enable or disable.
"""
self.ui.grpServices.setTitle(
- self.tr("Services by %s") %
- (self._provider_config.get_name(),))
+ self.tr("Services by {0}").format(self._provider_details.name))
- services = get_supported(
- self._provider_config.get_services())
+ services = get_supported(self._provider_details.services)
for service in services:
try:
@@ -641,38 +659,31 @@ class Wizard(QtGui.QWizard):
if not self._provider_setup_ok:
self._reset_provider_setup()
sub_title = self.tr("Gathering configuration options for {0}")
- sub_title = sub_title.format(self._provider_config.get_name())
+ sub_title = sub_title.format(self._provider_details.name)
self.page(pageId).setSubTitle(sub_title)
self.ui.lblDownloadCaCert.setPixmap(self.QUESTION_ICON)
self._provider_setup_defer = self._backend.\
provider_bootstrap(self._domain)
if pageId == self.PRESENT_PROVIDER_PAGE:
- self.page(pageId).setSubTitle(self.tr("Description of services "
- "offered by %s") %
- (self._provider_config
- .get_name(),))
-
- lang = QtCore.QLocale.system().name()
- self.ui.lblProviderName.setText(
- "<b>%s</b>" %
- (self._provider_config.get_name(lang=lang),))
- self.ui.lblProviderURL.setText(
- "https://%s" % (self._provider_config.get_domain(),))
- self.ui.lblProviderDesc.setText(
- "<i>%s</i>" %
- (self._provider_config.get_description(lang=lang),))
-
- self.ui.lblServicesOffered.setText(self._provider_config
- .get_services_string())
- self.ui.lblProviderPolicy.setText(self._provider_config
- .get_enrollment_policy())
+ sub_title = self.tr("Description of services offered by {0}")
+ sub_title = sub_title.format(self._provider_details.name)
+ self.page(pageId).setSubTitle(sub_title)
+
+ details = self._provider_details
+ name = "<b>{0}</b>".format(details.name)
+ domain = "https://{0}".format(details.domain)
+ description = "<i>{0}</i>".format(details.description)
+ self.ui.lblProviderName.setText(name)
+ self.ui.lblProviderURL.setText(domain)
+ self.ui.lblProviderDesc.setText(description)
+ self.ui.lblServicesOffered.setText(details.services_string)
+ self.ui.lblProviderPolicy.setText(details.enrollment_policy)
if pageId == self.REGISTER_USER_PAGE:
- self.page(pageId).setSubTitle(self.tr("Register a new user with "
- "%s") %
- (self._provider_config
- .get_name(),))
+ sub_title = self.tr("Register a new user with {0}")
+ sub_title = sub_title.format(self._provider_details.name)
+ self.page(pageId).setSubTitle(sub_title)
self.ui.chkRemember.setVisible(False)
if pageId == self.SERVICES_PAGE:
@@ -695,8 +706,6 @@ class Wizard(QtGui.QWizard):
if self.currentPage() == self.page(self.SELECT_PROVIDER_PAGE):
if self._use_existing_provider:
self._domain = self.ui.cbProviders.currentText()
- self._provider_config = ProviderConfig.get_provider_config(
- self._domain)
if self._show_register:
return self.REGISTER_USER_PAGE
else:
@@ -721,6 +730,7 @@ class Wizard(QtGui.QWizard):
sig.prov_name_resolution.connect(self._name_resolution)
sig.prov_https_connection.connect(self._https_connection)
sig.prov_download_provider_info.connect(self._download_provider_info)
+ sig.prov_get_details.connect(self._provider_get_details)
sig.prov_download_ca_cert.connect(self._download_ca_cert)
sig.prov_check_ca_fingerprint.connect(self._check_ca_fingerprint)
diff --git a/src/leap/bitmask/logs/__init__.py b/src/leap/bitmask/logs/__init__.py
new file mode 100644
index 00000000..0516b304
--- /dev/null
+++ b/src/leap/bitmask/logs/__init__.py
@@ -0,0 +1,3 @@
+# levelname length == 8, since 'CRITICAL' is the longest
+LOG_FORMAT = ('%(asctime)s - %(levelname)-8s - '
+ 'L#%(lineno)-4s : %(name)s:%(funcName)s() - %(message)s')
diff --git a/src/leap/bitmask/util/leap_log_handler.py b/src/leap/bitmask/logs/leap_log_handler.py
index 807e53d4..24141638 100644
--- a/src/leap/bitmask/util/leap_log_handler.py
+++ b/src/leap/bitmask/logs/leap_log_handler.py
@@ -21,7 +21,7 @@ import logging
from PySide import QtCore
-from leap.bitmask.util import LOG_FORMAT
+from leap.bitmask.logs import LOG_FORMAT
class LogHandler(logging.Handler):
diff --git a/src/leap/bitmask/util/log_silencer.py b/src/leap/bitmask/logs/log_silencer.py
index 56b290e4..56b290e4 100644
--- a/src/leap/bitmask/util/log_silencer.py
+++ b/src/leap/bitmask/logs/log_silencer.py
diff --git a/src/leap/bitmask/util/streamtologger.py b/src/leap/bitmask/logs/streamtologger.py
index 25a06718..25a06718 100644
--- a/src/leap/bitmask/util/streamtologger.py
+++ b/src/leap/bitmask/logs/streamtologger.py
diff --git a/src/leap/bitmask/util/tests/test_leap_log_handler.py b/src/leap/bitmask/logs/tests/test_leap_log_handler.py
index 518fd35b..20b09aef 100644
--- a/src/leap/bitmask/util/tests/test_leap_log_handler.py
+++ b/src/leap/bitmask/logs/tests/test_leap_log_handler.py
@@ -24,7 +24,7 @@ except ImportError:
import logging
-from leap.bitmask.util.leap_log_handler import LeapLogHandler
+from leap.bitmask.logs.leap_log_handler import LeapLogHandler
from leap.bitmask.util.pyside_tests_helper import BasicPySlotCase
from leap.common.testing.basetest import BaseLeapTest
diff --git a/src/leap/bitmask/util/tests/test_streamtologger.py b/src/leap/bitmask/logs/tests/test_streamtologger.py
index fc97b794..9bbadde8 100644
--- a/src/leap/bitmask/util/tests/test_streamtologger.py
+++ b/src/leap/bitmask/logs/tests/test_streamtologger.py
@@ -26,7 +26,7 @@ except ImportError:
import logging
import sys
-from leap.bitmask.util.streamtologger import StreamToLogger
+from leap.bitmask.logs.streamtologger import StreamToLogger
from leap.common.testing.basetest import BaseLeapTest
diff --git a/src/leap/bitmask/logs/utils.py b/src/leap/bitmask/logs/utils.py
new file mode 100644
index 00000000..06959c45
--- /dev/null
+++ b/src/leap/bitmask/logs/utils.py
@@ -0,0 +1,92 @@
+import logging
+import sys
+
+from leap.bitmask.logs import LOG_FORMAT
+from leap.bitmask.logs.log_silencer import SelectiveSilencerFilter
+from leap.bitmask.logs.leap_log_handler import LeapLogHandler
+from leap.bitmask.logs.streamtologger import StreamToLogger
+from leap.bitmask.platform_init import IS_WIN
+
+
+def get_logger(debug=False, logfile=None, replace_stdout=True):
+ """
+ Create the logger and attach the handlers.
+
+ :param debug: the level of the messages that we should log
+ :type debug: bool
+ :param logfile: the file name of where we should to save the logs
+ :type logfile: str
+ :return: the new logger with the attached handlers.
+ :rtype: logging.Logger
+ """
+ # TODO: get severity from command line args
+ if debug:
+ level = logging.DEBUG
+ else:
+ level = logging.WARNING
+
+ # Create logger and formatter
+ logger = logging.getLogger(name='leap')
+ logger.setLevel(level)
+ formatter = logging.Formatter(LOG_FORMAT)
+
+ # Console handler
+ try:
+ import coloredlogs
+ console = coloredlogs.ColoredStreamHandler(level=level)
+ except ImportError:
+ console = logging.StreamHandler()
+ console.setLevel(level)
+ console.setFormatter(formatter)
+ using_coloredlog = False
+ else:
+ using_coloredlog = True
+
+ if using_coloredlog:
+ replace_stdout = False
+
+ silencer = SelectiveSilencerFilter()
+ console.addFilter(silencer)
+ logger.addHandler(console)
+ logger.debug('Console handler plugged!')
+
+ # LEAP custom handler
+ leap_handler = LeapLogHandler()
+ leap_handler.setLevel(level)
+ leap_handler.addFilter(silencer)
+ logger.addHandler(leap_handler)
+ logger.debug('Leap handler plugged!')
+
+ # File handler
+ if logfile is not None:
+ logger.debug('Setting logfile to %s ', logfile)
+ fileh = logging.FileHandler(logfile)
+ fileh.setLevel(logging.DEBUG)
+ fileh.setFormatter(formatter)
+ fileh.addFilter(silencer)
+ logger.addHandler(fileh)
+ logger.debug('File handler plugged!')
+
+ if replace_stdout:
+ replace_stdout_stderr_with_logging(logger)
+
+ return logger
+
+
+def replace_stdout_stderr_with_logging(logger):
+ """
+ Replace:
+ - the standard output
+ - the standard error
+ - the twisted log output
+ with a custom one that writes to the logger.
+ """
+ # Disabling this on windows since it breaks ALL THE THINGS
+ # The issue for this is #4149
+ if not IS_WIN:
+ sys.stdout = StreamToLogger(logger, logging.DEBUG)
+ sys.stderr = StreamToLogger(logger, logging.ERROR)
+
+ # Replace twisted's logger to use our custom output.
+ from twisted.python import log
+ log.startLogging(sys.stdout)
diff --git a/src/leap/bitmask/platform_init/initializers.py b/src/leap/bitmask/platform_init/initializers.py
index f2710c58..b282a229 100644
--- a/src/leap/bitmask/platform_init/initializers.py
+++ b/src/leap/bitmask/platform_init/initializers.py
@@ -14,15 +14,14 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
"""
-Platform dependant initializing code
+Platform-dependant initialization code.
"""
-
import logging
import os
import platform
import stat
+import sys
import subprocess
import tempfile
@@ -33,7 +32,6 @@ from leap.bitmask.services.eip import get_vpn_launcher
from leap.bitmask.services.eip.linuxvpnlauncher import LinuxVPNLauncher
from leap.bitmask.services.eip.darwinvpnlauncher import DarwinVPNLauncher
from leap.bitmask.util import first
-from leap.bitmask.util import privilege_policies
logger = logging.getLogger(__name__)
@@ -48,7 +46,7 @@ _system = platform.system()
def init_platform():
"""
- Returns the right initializer for the platform we are running in, or
+ Return the right initializer for the platform we are running in, or
None if no proper initializer is found
"""
initializer = None
@@ -80,7 +78,7 @@ UPDOWN_BADEXEC_MSG = BADEXEC_MSG % (
def get_missing_updown_dialog():
"""
- Creates a dialog for notifying of missing updown scripts.
+ Create a dialog for notifying of missing updown scripts.
Returns that dialog.
:rtype: QtGui.QMessageBox instance
@@ -102,7 +100,7 @@ def get_missing_updown_dialog():
def check_missing():
"""
- Checks for the need of installing missing scripts, and
+ Check for the need of installing missing scripts, and
raises a dialog to ask user for permission to do it.
"""
config = LeapSettings()
@@ -150,7 +148,7 @@ def check_missing():
def _windows_has_tap_device():
"""
- Loops over the windows registry trying to find if the tap0901 tap driver
+ Loop over the windows registry trying to find if the tap0901 tap driver
has been installed on this machine.
"""
import _winreg as reg
@@ -176,7 +174,7 @@ def _windows_has_tap_device():
def WindowsInitializer():
"""
- Raises a dialog in case that the windows tap driver has not been found
+ Raise a dialog in case that the windows tap driver has not been found
in the registry, asking the user for permission to install the driver
"""
if not _windows_has_tap_device():
@@ -220,7 +218,7 @@ def WindowsInitializer():
def _darwin_has_tun_kext():
"""
- Returns True only if we found a directory under the system kext folder
+ Return True only if we found a directory under the system kext folder
containing a kext named tun.kext, AND we found a startup item named 'tun'
"""
# XXX we should be smarter here and use kextstats output.
@@ -236,7 +234,7 @@ def _darwin_has_tun_kext():
def _darwin_install_missing_scripts(badexec, notfound):
"""
- Tries to install the missing up/down scripts.
+ Try to install the missing up/down scripts.
:param badexec: error for notifying execution error during command.
:type badexec: str
@@ -291,7 +289,7 @@ def _darwin_install_missing_scripts(badexec, notfound):
def DarwinInitializer():
"""
- Raises a dialog in case that the osx tuntap driver has not been found
+ Raise a dialog in case that the osx tuntap driver has not been found
in the registry, asking the user for permission to install the driver
"""
# XXX split this function into several
@@ -345,9 +343,49 @@ def DarwinInitializer():
#
# Linux initializers
#
+
+def _get_missing_resolvconf_dialog():
+ """
+ Create a dialog for notifying about missing openresolv.
+
+ :rtype: QtGui.QMessageBox instance
+ """
+ NO_RESOLVCONF = (
+ "Could not find <b>resolvconf</b> installed in your system.\n"
+ "Do you want to quit Bitmask now?")
+
+ EXPLAIN = (
+ "Encrypted Internet needs resolvconf installed to work properly.\n"
+ "Please use your package manager to install it.\n")
+
+ msg = QtGui.QMessageBox()
+ msg.setWindowTitle(msg.tr("Missing resolvconf framework"))
+ msg.setText(msg.tr(NO_RESOLVCONF))
+ # but maybe the user really deserve to know more
+ msg.setInformativeText(msg.tr(EXPLAIN))
+ msg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No)
+ msg.setDefaultButton(QtGui.QMessageBox.Yes)
+ return msg
+
+
+def _linux_check_resolvconf():
+ """
+ Raise a dialog warning about the lack of the resolvconf framework.
+ """
+ RESOLVCONF_PATH = "/sbin/resolvconf"
+ missing = not os.path.isfile(RESOLVCONF_PATH)
+
+ if missing:
+ msg = _get_missing_resolvconf_dialog()
+ ret = msg.exec_()
+
+ if ret == QtGui.QMessageBox.Yes:
+ sys.exit()
+
+
def _linux_install_missing_scripts(badexec, notfound):
"""
- Tries to install the missing up/down scripts.
+ Try to install the missing up/down scripts.
:param badexec: error for notifying execution error during command.
:type badexec: str
@@ -398,7 +436,11 @@ def _linux_install_missing_scripts(badexec, notfound):
def LinuxInitializer():
"""
- Raises a dialog in case that either updown scripts or policykit file
- are missing or they have incorrect permissions.
+ Raise a dialog if needed files are missing.
+
+ Missing files can be either system-wide resolvconf, bitmask-root, or
+ policykit file. The dialog will also be raised if some of those files are
+ found to have incorrect permissions.
"""
+ _linux_check_resolvconf()
check_missing()
diff --git a/src/leap/bitmask/services/eip/conductor.py b/src/leap/bitmask/services/eip/conductor.py
new file mode 100644
index 00000000..a8821160
--- /dev/null
+++ b/src/leap/bitmask/services/eip/conductor.py
@@ -0,0 +1,321 @@
+# -*- coding: utf-8 -*-
+# conductor.py
+# Copyright (C) 2014 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+EIP Conductor module.
+"""
+import logging
+
+from PySide import QtCore
+
+from leap.bitmask.gui import statemachines
+from leap.bitmask.services import EIP_SERVICE
+from leap.bitmask.services import get_service_display_name
+from leap.bitmask.services.eip.connection import EIPConnection
+from leap.bitmask.platform_init import IS_MAC
+
+QtDelayedCall = QtCore.QTimer.singleShot
+logger = logging.getLogger(__name__)
+
+
+class EIPConductor(object):
+
+ def __init__(self, settings, backend, **kwargs):
+ """
+ Initializes EIP Conductor.
+
+ :param settings:
+ :type settings:
+
+ :param backend:
+ :type backend:
+ """
+ self.eip_connection = EIPConnection()
+ self.eip_name = get_service_display_name(EIP_SERVICE)
+ self._settings = settings
+ self._backend = backend
+
+ self._eip_status = None
+
+ @property
+ def qtsigs(self):
+ return self.eip_connection.qtsigs
+
+ def add_eip_widget(self, widget):
+ """
+ Keep a reference to the passed eip status widget.
+
+ :param widget: the EIP Status widget.
+ :type widget: QWidget
+ """
+ self._eip_status = widget
+
+ def connect_signals(self):
+ """
+ Connect signals.
+ """
+ self.qtsigs.connecting_signal.connect(self._start_eip)
+
+ self.qtsigs.disconnecting_signal.connect(self._stop_eip)
+ self.qtsigs.disconnected_signal.connect(self._eip_status.eip_stopped)
+
+ def connect_backend_signals(self):
+ """
+ Connect to backend signals.
+ """
+ signaler = self._backend.signaler
+
+ # for conductor
+ signaler.eip_process_restart_tls.connect(self._do_eip_restart)
+ signaler.eip_process_restart_tls.connect(self._do_eip_failed)
+ signaler.eip_process_restart_ping.connect(self._do_eip_restart)
+ signaler.eip_process_finished.connect(self._eip_finished)
+
+ # for widget
+ self._eip_status.connect_backend_signals()
+
+ def start_eip_machine(self, action):
+ """
+ Initializes and starts the EIP state machine.
+ Needs the reference to the eip_status widget not to be empty.
+
+ :action: QtAction
+ """
+ action = action
+ button = self._eip_status.eip_button
+ label = self._eip_status.eip_label
+
+ builder = statemachines.ConnectionMachineBuilder(self.eip_connection)
+ eip_machine = builder.make_machine(button=button,
+ action=action,
+ label=label)
+ self.eip_machine = eip_machine
+ self.eip_machine.start()
+ logger.debug('eip machine started')
+
+ def do_connect(self):
+ """
+ Start the connection procedure.
+ Emits a signal that triggers the OFF -> Connecting sequence.
+ This will call _start_eip via the state machine.
+ """
+ self.qtsigs.do_connect_signal.emit()
+
+ def tear_fw_down(self):
+ """
+ Tear the firewall down.
+ """
+ self._backend.tear_fw_down()
+
+ @QtCore.Slot()
+ def _start_eip(self):
+ """
+ Starts EIP.
+ """
+ st = self._eip_status
+ is_restart = st and st.is_restart
+
+ def reconnect():
+ self.qtsigs.disconnecting_signal.connect(self._stop_eip)
+
+ if is_restart:
+ QtDelayedCall(0, reconnect)
+ else:
+ self._eip_status.eip_pre_up()
+ self.user_stopped_eip = False
+ self._eip_status.hide_fw_down_button()
+
+ # Until we set an option in the preferences window, we'll assume that
+ # by default we try to autostart. If we switch it off manually, it
+ # won't try the next time.
+ self._settings.set_autostart_eip(True)
+ self._eip_status.is_restart = False
+
+ # DO the backend call!
+ self._backend.eip_start(restart=is_restart)
+
+ def reconnect_stop_signal(self):
+ """
+ Restore the original behaviour associated with the disconnecting
+ signal, this is, trigger a normal stop, and not a restart one.
+ """
+
+ def do_stop(*args):
+ self._stop_eip(restart=False)
+
+ self.qtsigs.disconnecting_signal.disconnect()
+ self.qtsigs.disconnecting_signal.connect(do_stop)
+
+ @QtCore.Slot()
+ def _stop_eip(self, restart=False, failed=False):
+ """
+ TRIGGERS:
+ self.qsigs.do_disconnect_signal (via state machine)
+
+ Stops vpn process and makes gui adjustments to reflect
+ the change of state.
+
+ :param restart: whether this is part of a eip restart.
+ :type restart: bool
+
+ :param failed: whether this is the final step of a retry sequence
+ :type failed: bool
+ """
+ self._eip_status.is_restart = restart
+ self.user_stopped_eip = not restart and not failed
+
+ def on_disconnected_do_restart():
+ # hard restarts
+ logger.debug("HARD RESTART")
+ eip_status_label = self._eip_status.tr("{0} is restarting")
+ eip_status_label = eip_status_label.format(self.eip_name)
+ self._eip_status.eip_stopped(restart=True)
+ self._eip_status.set_eip_status(eip_status_label, error=False)
+
+ QtDelayedCall(2000, self.do_connect)
+
+ def plug_restart_on_disconnected():
+ self.qtsigs.disconnected_signal.connect(on_disconnected_do_restart)
+
+ def reconnect_disconnected_signal():
+ self.qtsigs.disconnected_signal.disconnect(
+ on_disconnected_do_restart)
+
+ def do_stop(*args):
+ self._stop_eip(restart=False)
+
+ if restart:
+ # we bypass the on_eip_disconnected here
+ plug_restart_on_disconnected()
+ self.qtsigs.disconnected_signal.emit()
+ #QtDelayedCall(0, self.qtsigs.disconnected_signal.emit)
+ # ...and reconnect the original signal again, after having used the
+ # diversion
+ QtDelayedCall(500, reconnect_disconnected_signal)
+
+ elif failed:
+ self.qtsigs.disconnected_signal.emit()
+
+ else:
+ logger.debug('Setting autostart to: False')
+ self._settings.set_autostart_eip(False)
+
+ # Call to the backend.
+ self._backend.eip_stop(restart=restart)
+
+ # ... and inform the status widget
+ self._eip_status.set_eipstatus_off(False)
+ self._eip_status.eip_stopped(restart=restart, failed=failed)
+
+ self._already_started_eip = False
+
+ # XXX needed?
+ if restart:
+ QtDelayedCall(2000, self.reconnect_stop_signal)
+
+ @QtCore.Slot()
+ def _do_eip_restart(self):
+ """
+ TRIGGERS:
+ self._eip_connection.qtsigs.process_restart
+
+ Restart the connection.
+ """
+ if self._eip_status is not None:
+ self._eip_status.is_restart = True
+
+ def do_stop(*args):
+ self._stop_eip(restart=True)
+
+ try:
+ self.qtsigs.disconnecting_signal.disconnect()
+ except Exception:
+ logger.error("cannot disconnect signals")
+
+ self.qtsigs.disconnecting_signal.connect(do_stop)
+ self.qtsigs.do_disconnect_signal.emit()
+
+ @QtCore.Slot()
+ def _do_eip_failed(self):
+ """
+ Stop EIP after a failure to start.
+
+ TRIGGERS
+ signaler.eip_process_restart_tls
+ """
+ logger.debug("TLS Error: eip_stop (failed)")
+ self.qtsigs.connection_died_signal.emit()
+ QtDelayedCall(1000, self._eip_status.eip_failed_to_connect)
+
+ @QtCore.Slot(int)
+ def _eip_finished(self, exitCode):
+ """
+ TRIGGERS:
+ Signaler.eip_process_finished
+
+ Triggered when the EIP/VPN process finishes to set the UI
+ accordingly.
+
+ Ideally we would have the right exit code here,
+ but the use of different wrappers (pkexec, cocoasudo) swallows
+ the openvpn exit code so we get zero exit in some cases where we
+ shouldn't. As a workaround we just use a flag to indicate
+ a purposeful switch off, and mark everything else as unexpected.
+
+ :param exitCode: the exit code of the eip process.
+ :type exitCode: int
+ """
+ # TODO Add error catching to the openvpn log observer
+ # so we can have a more precise idea of which type
+ # of error did we have (server side, local problem, etc)
+
+ logger.info("VPN process finished with exitCode %s..."
+ % (exitCode,))
+
+ signal = self.qtsigs.disconnected_signal
+
+ # XXX check if these exitCodes are pkexec/cocoasudo specific
+ if exitCode in (126, 127):
+ eip_status_label = self._eip_status.tr(
+ "{0} could not be launched "
+ "because you did not authenticate properly.")
+ eip_status_label = eip_status_label.format(self.eip_name)
+ self._eip_status.set_eip_status(eip_status_label, error=True)
+ signal = self.qtsigs.connection_aborted_signal
+ self._backend.eip_terminate()
+
+ # XXX FIXME --- check exitcode is != 0 really.
+ # bitmask-root is masking the exitcode, so we might need
+ # to fix it on that side.
+ #if exitCode != 0 and not self.user_stopped_eip:
+ if not self.user_stopped_eip:
+ eip_status_label = self._eip_status.tr(
+ "{0} finished in an unexpected manner!")
+ eip_status_label = eip_status_label.format(self.eip_name)
+ self._eip_status.eip_stopped()
+ self._eip_status.set_eip_status_icon("error")
+ self._eip_status.set_eip_status(eip_status_label,
+ error=True)
+ signal = self.qtsigs.connection_died_signal
+ self._eip_status.show_fw_down_button()
+ self._eip_status.eip_failed_to_connect()
+
+ if exitCode == 0 and IS_MAC:
+ # XXX remove this warning after I fix cocoasudo.
+ logger.warning("The above exit code MIGHT BE WRONG.")
+
+ # We emit signals to trigger transitions in the state machine:
+ signal.emit()
diff --git a/src/leap/bitmask/services/eip/darwinvpnlauncher.py b/src/leap/bitmask/services/eip/darwinvpnlauncher.py
index a03bfc44..41d75052 100644
--- a/src/leap/bitmask/services/eip/darwinvpnlauncher.py
+++ b/src/leap/bitmask/services/eip/darwinvpnlauncher.py
@@ -52,6 +52,8 @@ class DarwinVPNLauncher(VPNLauncher):
OPENVPN_PATH = "%s/Contents/Resources/openvpn" % (INSTALL_PATH,)
OPENVPN_PATH_ESCAPED = "%s/Contents/Resources/openvpn" % (
INSTALL_PATH_ESCAPED,)
+ OPENVPN_BIN_PATH = "%s/Contents/Resources/%s" % (INSTALL_PATH,
+ OPENVPN_BIN)
UP_SCRIPT = "%s/client.up.sh" % (OPENVPN_PATH,)
DOWN_SCRIPT = "%s/client.down.sh" % (OPENVPN_PATH,)
diff --git a/src/leap/bitmask/services/eip/eipconfig.py b/src/leap/bitmask/services/eip/eipconfig.py
index 09a3d257..e7419b22 100644
--- a/src/leap/bitmask/services/eip/eipconfig.py
+++ b/src/leap/bitmask/services/eip/eipconfig.py
@@ -110,7 +110,7 @@ class VPNGatewaySelector(object):
def get_gateways_list(self):
"""
- Returns the existing gateways, sorted by timezone proximity.
+ Return the existing gateways, sorted by timezone proximity.
:rtype: list of tuples (location, ip)
(str, IPv4Address or IPv6Address object)
@@ -148,16 +148,36 @@ class VPNGatewaySelector(object):
def get_gateways(self):
"""
- Returns the 4 best gateways, sorted by timezone proximity.
+ Return the 4 best gateways, sorted by timezone proximity.
:rtype: list of IPv4Address or IPv6Address object.
"""
gateways = [ip for location, ip in self.get_gateways_list()][:4]
return gateways
+ def get_gateways_country_code(self):
+ """
+ Return a dict with ipaddress -> country code mapping.
+
+ :rtype: dict
+ """
+ country_codes = {}
+
+ locations = self._eipconfig.get_locations()
+ gateways = self._eipconfig.get_gateways()
+
+ for idx, gateway in enumerate(gateways):
+ gateway_location = gateway.get('location')
+
+ ip = self._eipconfig.get_gateway_ip(idx)
+ if gateway_location is not None:
+ ccode = locations[gateway['location']]['country_code']
+ country_codes[ip] = ccode
+ return country_codes
+
def _get_timezone_distance(self, offset):
'''
- Returns the distance between the local timezone and
+ Return the distance between the local timezone and
the one with offset 'offset'.
:param offset: the distance of a timezone to GMT.
@@ -179,7 +199,7 @@ class VPNGatewaySelector(object):
def _get_local_offset(self):
'''
- Returns the distance between GMT and the local timezone.
+ Return the distance between GMT and the local timezone.
:rtype: int
'''
diff --git a/src/leap/bitmask/services/eip/linuxvpnlauncher.py b/src/leap/bitmask/services/eip/linuxvpnlauncher.py
index 1f0813e0..955768d1 100644
--- a/src/leap/bitmask/services/eip/linuxvpnlauncher.py
+++ b/src/leap/bitmask/services/eip/linuxvpnlauncher.py
@@ -63,14 +63,20 @@ def _is_auth_agent_running():
:return: True if it's running, False if it's not.
:rtype: boolean
"""
+ # Note that gnome-shell does not uses a separate process for the
+ # polkit-agent, it uses a polkit-agent within its own process so we can't
+ # ps-grep a polkit process, we can ps-grep gnome-shell itself.
+
# the [x] thing is to avoid grep match itself
polkit_options = [
'ps aux | grep "polkit-[g]nome-authentication-agent-1"',
'ps aux | grep "polkit-[k]de-authentication-agent-1"',
'ps aux | grep "polkit-[m]ate-authentication-agent-1"',
- 'ps aux | grep "[l]xpolkit"'
+ 'ps aux | grep "[l]xpolkit"',
+ 'ps aux | grep "[g]nome-shell"',
]
is_running = [commands.getoutput(cmd) for cmd in polkit_options]
+
return any(is_running)
diff --git a/src/leap/bitmask/services/eip/vpnlauncher.py b/src/leap/bitmask/services/eip/vpnlauncher.py
index dcb48e8a..9629afae 100644
--- a/src/leap/bitmask/services/eip/vpnlauncher.py
+++ b/src/leap/bitmask/services/eip/vpnlauncher.py
@@ -25,6 +25,7 @@ import stat
from abc import ABCMeta, abstractmethod
from functools import partial
+from leap.bitmask.config import flags
from leap.bitmask.config.leapsettings import LeapSettings
from leap.bitmask.config.providerconfig import ProviderConfig
from leap.bitmask.platform_init import IS_LINUX
@@ -122,9 +123,9 @@ class VPNLauncher(object):
leap_settings = LeapSettings()
domain = providerconfig.get_domain()
gateway_conf = leap_settings.get_selected_gateway(domain)
+ gateway_selector = VPNGatewaySelector(eipconfig)
if gateway_conf == leap_settings.GATEWAY_AUTOMATIC:
- gateway_selector = VPNGatewaySelector(eipconfig)
gateways = gateway_selector.get_gateways()
else:
gateways = [gateway_conf]
@@ -133,6 +134,12 @@ class VPNLauncher(object):
logger.error('No gateway was found!')
raise VPNLauncherException('No gateway was found!')
+ # this only works for selecting the first gateway, as we're
+ # currently doing.
+ ccodes = gateway_selector.get_gateways_country_code()
+ gateway_ccode = ccodes[gateways[0]]
+ flags.CURRENT_VPN_COUNTRY = gateway_ccode
+
logger.debug("Using gateways ips: {0}".format(', '.join(gateways)))
return gateways
diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py
index 1559ea8b..f56d464e 100644
--- a/src/leap/bitmask/services/eip/vpnprocess.py
+++ b/src/leap/bitmask/services/eip/vpnprocess.py
@@ -17,6 +17,7 @@
"""
VPN Manager, spawned in a custom processProtocol.
"""
+import commands
import logging
import os
import shutil
@@ -30,9 +31,11 @@ import psutil
try:
# psutil < 2.0.0
from psutil.error import AccessDenied as psutil_AccessDenied
+ PSUTIL_2 = False
except ImportError:
# psutil >= 2.0.0
from psutil import AccessDenied as psutil_AccessDenied
+ PSUTIL_2 = True
from leap.bitmask.config import flags
from leap.bitmask.config.providerconfig import ProviderConfig
@@ -67,7 +70,7 @@ class VPNObserver(object):
'NETWORK_UNREACHABLE': (
'Network is unreachable (code=101)',),
'PROCESS_RESTART_TLS': (
- "SIGUSR1[soft,tls-error]",),
+ "SIGTERM[soft,tls-error]",),
'PROCESS_RESTART_PING': (
"SIGTERM[soft,ping-restart]",),
'INITIALIZATION_COMPLETED': (
@@ -113,10 +116,12 @@ class VPNObserver(object):
:returns: a Signaler signal or None
:rtype: str or None
"""
+ sig = self._signaler
signals = {
- "network_unreachable": self._signaler.EIP_NETWORK_UNREACHABLE,
- "process_restart_tls": self._signaler.EIP_PROCESS_RESTART_TLS,
- "process_restart_ping": self._signaler.EIP_PROCESS_RESTART_PING,
+ "network_unreachable": sig.EIP_NETWORK_UNREACHABLE,
+ "process_restart_tls": sig.EIP_PROCESS_RESTART_TLS,
+ "process_restart_ping": sig.EIP_PROCESS_RESTART_PING,
+ "initialization_completed": sig.EIP_CONNECTED
}
return signals.get(event.lower())
@@ -178,6 +183,8 @@ class VPN(object):
kwargs['openvpn_verb'] = self._openvpn_verb
kwargs['signaler'] = self._signaler
+ restart = kwargs.pop('restart', False)
+
# start the main vpn subprocess
vpnproc = VPNProcess(*args, **kwargs)
@@ -188,8 +195,9 @@ class VPN(object):
# we try to bring the firewall up
if IS_LINUX:
gateways = vpnproc.getGateways()
- firewall_up = self._launch_firewall(gateways)
- if not firewall_up:
+ firewall_up = self._launch_firewall(gateways,
+ restart=restart)
+ if not restart and not firewall_up:
logger.error("Could not bring firewall up, "
"aborting openvpn launch.")
return
@@ -211,7 +219,7 @@ class VPN(object):
self._pollers.extend(poll_list)
self._start_pollers()
- def _launch_firewall(self, gateways):
+ def _launch_firewall(self, gateways, restart=False):
"""
Launch the firewall using the privileged wrapper.
@@ -226,11 +234,24 @@ class VPN(object):
# XXX could check that the iptables rules are in place.
BM_ROOT = linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT
- exitCode = subprocess.call(["pkexec",
- BM_ROOT, "firewall", "start"] + gateways)
+ cmd = ["pkexec", BM_ROOT, "firewall", "start"]
+ if restart:
+ cmd.append("restart")
+ exitCode = subprocess.call(cmd + gateways)
return True if exitCode is 0 else False
- def _tear_down_firewall(self):
+ def is_fw_down(self):
+ """
+ Return whether the firewall is down or not.
+
+ :rtype: bool
+ """
+ BM_ROOT = linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT
+ fw_up_cmd = "pkexec {0} firewall isup".format(BM_ROOT)
+ fw_is_down = lambda: commands.getstatusoutput(fw_up_cmd)[0] == 256
+ return fw_is_down()
+
+ def tear_down_firewall(self):
"""
Tear the firewall down using the privileged wrapper.
"""
@@ -254,7 +275,7 @@ class VPN(object):
# we try to tear the firewall down
if IS_LINUX and self._user_stopped:
- firewall_down = self._tear_down_firewall()
+ firewall_down = self.tear_down_firewall()
if firewall_down:
logger.debug("Firewall down")
else:
@@ -286,22 +307,28 @@ class VPN(object):
self._vpnproc.aborted = True
self._vpnproc.killProcess()
- def terminate(self, shutdown=False):
+ def terminate(self, shutdown=False, restart=False):
"""
Stops the openvpn subprocess.
Attempts to send a SIGTERM first, and after a timeout
it sends a SIGKILL.
+
+ :param shutdown: whether this is the final shutdown
+ :type shutdown: bool
+ :param restart: whether this stop is part of a hard restart.
+ :type restart: bool
"""
from twisted.internet import reactor
self._stop_pollers()
- # We assume that the only valid shutodowns are initiated
- # by an user action.
- self._user_stopped = shutdown
-
# First we try to be polite and send a SIGTERM...
- if self._vpnproc:
+ if self._vpnproc is not None:
+ # We assume that the only valid stops are initiated
+ # by an user action, not hard restarts
+ self._user_stopped = not restart
+ self._vpnproc.is_restart = restart
+
self._sentterm = True
self._vpnproc.terminate_openvpn(shutdown=shutdown)
@@ -310,13 +337,12 @@ class VPN(object):
reactor.callLater(
self.TERMINATE_WAIT, self._kill_if_left_alive)
- if shutdown:
- if IS_LINUX and self._user_stopped:
- firewall_down = self._tear_down_firewall()
- if firewall_down:
- logger.debug("Firewall down")
- else:
- logger.warning("Could not tear firewall down")
+ if IS_LINUX and self._user_stopped:
+ firewall_down = self.tear_down_firewall()
+ if firewall_down:
+ logger.debug("Firewall down")
+ else:
+ logger.warning("Could not tear firewall down")
def _start_pollers(self):
"""
@@ -676,7 +702,13 @@ class VPNManager(object):
# we need to be able to filter out arguments in the form
# --openvpn-foo, since otherwise we are shooting ourselves
# in the feet.
- if any(map(lambda s: s.find("LEAPOPENVPN") != -1, p.cmdline)):
+
+ if PSUTIL_2:
+ cmdline = p.cmdline()
+ else:
+ cmdline = p.cmdline
+ if any(map(lambda s: s.find(
+ "LEAPOPENVPN") != -1, cmdline)):
openvpn_process = p
break
except psutil_AccessDenied:
@@ -731,7 +763,7 @@ class VPNManager(object):
# However, that should be a rare case right now.
self._send_command("signal SIGTERM")
self._close_management_socket(announce=True)
- except Exception as e:
+ except (Exception, AssertionError) as e:
logger.warning("Problem trying to terminate OpenVPN: %r"
% (e,))
else:
@@ -800,6 +832,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):
self._openvpn_verb = openvpn_verb
self._vpn_observer = VPNObserver(signaler)
+ self.is_restart = False
# processProtocol methods
@@ -835,7 +868,8 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):
exit_code = reason.value.exitCode
if isinstance(exit_code, int):
logger.debug("processExited, status %d" % (exit_code,))
- self._signaler.signal(self._signaler.EIP_PROCESS_FINISHED, exit_code)
+ self._signaler.signal(
+ self._signaler.EIP_PROCESS_FINISHED, exit_code)
self._alive = False
def processEnded(self, reason):
diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py
index 1766a39d..98b40929 100644
--- a/src/leap/bitmask/services/mail/conductor.py
+++ b/src/leap/bitmask/services/mail/conductor.py
@@ -19,15 +19,10 @@ Mail Services Conductor
"""
import logging
-from zope.proxy import sameProxiedObjects
-
+from leap.bitmask.config import flags
from leap.bitmask.gui import statemachines
from leap.bitmask.services.mail import connection as mail_connection
-from leap.bitmask.services.mail import imap
-from leap.bitmask.services.mail.smtpbootstrapper import SMTPBootstrapper
-from leap.bitmask.services.mail.smtpconfig import SMTPConfig
-from leap.common.check import leap_assert
from leap.common.events import events_pb2 as leap_events
from leap.common.events import register as leap_register
@@ -44,9 +39,6 @@ class IMAPControl(object):
Initializes smtp variables.
"""
self.imap_machine = None
- self.imap_service = None
- self.imap_port = None
- self.imap_factory = None
self.imap_connection = None
leap_register(signal=leap_events.IMAP_SERVICE_STARTED,
@@ -55,10 +47,13 @@ class IMAPControl(object):
leap_register(signal=leap_events.IMAP_SERVICE_FAILED_TO_START,
callback=self._handle_imap_events,
reqcbk=lambda req, resp: None)
+ leap_register(signal=leap_events.IMAP_CLIENT_LOGIN,
+ callback=self._handle_imap_events,
+ reqcbk=lambda req, resp: None)
def set_imap_connection(self, imap_connection):
"""
- Sets the imap connection to an initialized connection.
+ Set the imap connection to an initialized connection.
:param imap_connection: an initialized imap connection
:type imap_connection: IMAPConnection instance.
@@ -67,67 +62,18 @@ class IMAPControl(object):
def start_imap_service(self):
"""
- Starts imap service.
+ Start imap service.
"""
- from leap.bitmask.config import flags
-
- logger.debug('Starting imap service')
- leap_assert(sameProxiedObjects(self._soledad, None)
- is not True,
- "We need a non-null soledad for initializing imap service")
- leap_assert(sameProxiedObjects(self._keymanager, None)
- is not True,
- "We need a non-null keymanager for initializing imap "
- "service")
-
- offline = flags.OFFLINE
- self.imap_service, self.imap_port, \
- self.imap_factory = imap.start_imap_service(
- self._soledad,
- self._keymanager,
- userid=self.userid,
- offline=offline)
+ self._backend.imap_start_service(self.userid, flags.OFFLINE)
- if offline is False:
- logger.debug("Starting loop")
- self.imap_service.start_loop()
-
- def stop_imap_service(self, cv):
+ def stop_imap_service(self):
"""
- Stops imap service (fetcher, factory and port).
-
- :param cv: A condition variable to which we can signal when imap
- indeed stops.
- :type cv: threading.Condition
+ Stop imap service.
"""
self.imap_connection.qtsigs.disconnecting_signal.emit()
- # TODO We should homogenize both services.
- if self.imap_service is not None:
- logger.debug('Stopping imap service.')
- # Stop the loop call in the fetcher
- self.imap_service.stop()
- self.imap_service = None
- # Stop listening on the IMAP port
- self.imap_port.stopListening()
- # Stop the protocol
- self.imap_factory.theAccount.closed = True
- self.imap_factory.doStop(cv)
- else:
- # main window does not have to wait because there's no service to
- # be stopped, so we release the condition variable
- cv.acquire()
- cv.notify()
- cv.release()
-
- def fetch_incoming_mail(self):
- """
- Fetches incoming mail.
- """
- if self.imap_service:
- logger.debug('Client connected, fetching mail...')
- self.imap_service.fetch()
-
- # handle events
+ logger.debug('Stopping imap service.')
+
+ self._backend.imap_stop_service()
def _handle_imap_events(self, req):
"""
@@ -137,25 +83,31 @@ class IMAPControl(object):
:type req: leap.common.events.events_pb2.SignalRequest
"""
if req.event == leap_events.IMAP_SERVICE_STARTED:
- self.on_imap_connected()
+ self._on_imap_connected()
elif req.event == leap_events.IMAP_SERVICE_FAILED_TO_START:
- self.on_imap_failed()
+ self._on_imap_failed()
+ elif req.event == leap_events.IMAP_CLIENT_LOGIN:
+ self._on_mail_client_logged_in()
- # emit connection signals
+ def _on_mail_client_logged_in(self):
+ """
+ On mail client logged in, fetch incoming mail.
+ """
+ self._controller.imap_service_fetch()
- def on_imap_connecting(self):
+ def _on_imap_connecting(self):
"""
Callback for IMAP connecting state.
"""
self.imap_connection.qtsigs.connecting_signal.emit()
- def on_imap_connected(self):
+ def _on_imap_connected(self):
"""
Callback for IMAP connected state.
"""
self.imap_connection.qtsigs.connected_signal.emit()
- def on_imap_failed(self):
+ def _on_imap_failed(self):
"""
Callback for IMAP failed state.
"""
@@ -167,12 +119,9 @@ class SMTPControl(object):
"""
Initializes smtp variables.
"""
- self.smtp_config = SMTPConfig()
self.smtp_connection = None
self.smtp_machine = None
- self.smtp_bootstrapper = SMTPBootstrapper()
-
leap_register(signal=leap_events.SMTP_SERVICE_STARTED,
callback=self._handle_smtp_events,
reqcbk=lambda req, resp: None)
@@ -188,29 +137,23 @@ class SMTPControl(object):
"""
self.smtp_connection = smtp_connection
- def start_smtp_service(self, provider_config, download_if_needed=False):
+ def start_smtp_service(self, download_if_needed=False):
"""
Starts the SMTP service.
- :param provider_config: Provider configuration
- :type provider_config: ProviderConfig
:param download_if_needed: True if it should check for mtime
for the file
:type download_if_needed: bool
"""
self.smtp_connection.qtsigs.connecting_signal.emit()
- self.smtp_bootstrapper.start_smtp_service(
- provider_config, self.smtp_config, self._keymanager,
- self.userid, download_if_needed)
+ self._backend.smtp_start_service(self.userid, download_if_needed)
def stop_smtp_service(self):
"""
Stops the SMTP service.
"""
self.smtp_connection.qtsigs.disconnecting_signal.emit()
- self.smtp_bootstrapper.stop_smtp_service()
-
- # handle smtp events
+ self._backend.smtp_stop_service()
def _handle_smtp_events(self, req):
"""
@@ -224,8 +167,6 @@ class SMTPControl(object):
elif req.event == leap_events.SMTP_SERVICE_FAILED_TO_START:
self.on_smtp_failed()
- # emit connection signals
-
def on_smtp_connecting(self):
"""
Callback for SMTP connecting state.
@@ -253,22 +194,17 @@ class MailConductor(IMAPControl, SMTPControl):
"""
# XXX We could consider to use composition instead of inheritance here.
- def __init__(self, soledad, keymanager):
+ def __init__(self, backend):
"""
Initializes the mail conductor.
- :param soledad: a transparent proxy that eventually will point to a
- Soledad Instance.
- :type soledad: zope.proxy.ProxyBase
-
- :param keymanager: a transparent proxy that eventually will point to a
- Keymanager Instance.
- :type keymanager: zope.proxy.ProxyBase
+ :param backend: Backend being used
+ :type backend: Backend
"""
IMAPControl.__init__(self)
SMTPControl.__init__(self)
- self._soledad = soledad
- self._keymanager = keymanager
+
+ self._backend = backend
self._mail_machine = None
self._mail_connection = mail_connection.MailConnection()
@@ -309,6 +245,13 @@ class MailConductor(IMAPControl, SMTPControl):
self._smtp_machine = smtp
self._smtp_machine.start()
+ def stop_mail_services(self):
+ """
+ Stop the IMAP and SMTP services.
+ """
+ self.stop_imap_service()
+ self.stop_smtp_service()
+
def connect_mail_signals(self, widget):
"""
Connects the mail signals to the mail_status widget slots.
diff --git a/src/leap/bitmask/services/mail/imapcontroller.py b/src/leap/bitmask/services/mail/imapcontroller.py
new file mode 100644
index 00000000..d0bf4c34
--- /dev/null
+++ b/src/leap/bitmask/services/mail/imapcontroller.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+# imapcontroller.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+IMAP service controller.
+"""
+import logging
+
+from leap.bitmask.services.mail import imap
+
+
+logger = logging.getLogger(__name__)
+
+
+class IMAPController(object):
+ """
+ IMAP Controller.
+ """
+ def __init__(self, soledad, keymanager):
+ """
+ Initialize IMAP variables.
+
+ :param soledad: a transparent proxy that eventually will point to a
+ Soledad Instance.
+ :type soledad: zope.proxy.ProxyBase
+ :param keymanager: a transparent proxy that eventually will point to a
+ Keymanager Instance.
+ :type keymanager: zope.proxy.ProxyBase
+ """
+ self._soledad = soledad
+ self._keymanager = keymanager
+
+ self.imap_service = None
+ self.imap_port = None
+ self.imap_factory = None
+
+ def start_imap_service(self, userid, offline=False):
+ """
+ Start IMAP service.
+
+ :param userid: user id, in the form "user@provider"
+ :type userid: str
+ :param offline: whether imap should start in offline mode or not.
+ :type offline: bool
+ """
+ logger.debug('Starting imap service')
+
+ self.imap_service, self.imap_port, \
+ self.imap_factory = imap.start_imap_service(
+ self._soledad,
+ self._keymanager,
+ userid=userid,
+ offline=offline)
+
+ if offline is False:
+ logger.debug("Starting loop")
+ self.imap_service.start_loop()
+
+ def stop_imap_service(self, cv):
+ """
+ Stop IMAP service (fetcher, factory and port).
+
+ :param cv: A condition variable to which we can signal when imap
+ indeed stops.
+ :type cv: threading.Condition
+ """
+ if self.imap_service is not None:
+ # Stop the loop call in the fetcher
+ self.imap_service.stop()
+ self.imap_service = None
+
+ # Stop listening on the IMAP port
+ self.imap_port.stopListening()
+
+ # Stop the protocol
+ self.imap_factory.theAccount.closed = True
+ self.imap_factory.doStop(cv)
+ else:
+ # Release the condition variable so the caller doesn't have to wait
+ cv.acquire()
+ cv.notify()
+ cv.release()
+
+ def fetch_incoming_mail(self):
+ """
+ Fetch incoming mail.
+ """
+ if self.imap_service:
+ logger.debug('Client connected, fetching mail...')
+ self.imap_service.fetch()
diff --git a/src/leap/bitmask/services/mail/smtpbootstrapper.py b/src/leap/bitmask/services/mail/smtpbootstrapper.py
index 7ecf8134..3ef755e8 100644
--- a/src/leap/bitmask/services/mail/smtpbootstrapper.py
+++ b/src/leap/bitmask/services/mail/smtpbootstrapper.py
@@ -28,7 +28,7 @@ from leap.bitmask.services.mail.smtpconfig import SMTPConfig
from leap.bitmask.util import is_file
from leap.common import certs as leap_certs
-from leap.common.check import leap_assert, leap_assert_type
+from leap.common.check import leap_assert
from leap.common.files import check_and_fix_urw_only
logger = logging.getLogger(__name__)
@@ -38,6 +38,10 @@ class NoSMTPHosts(Exception):
"""This is raised when there is no SMTP host to use."""
+class MalformedUserId(Exception):
+ """This is raised when an userid does not have the form user@provider."""
+
+
class SMTPBootstrapper(AbstractBootstrapper):
"""
SMTP init procedure
@@ -126,15 +130,10 @@ class SMTPBootstrapper(AbstractBootstrapper):
smtp_key=client_cert_path,
encrypted_only=False)
- def start_smtp_service(self, provider_config, smtp_config, keymanager,
- userid, download_if_needed=False):
+ def start_smtp_service(self, keymanager, userid, download_if_needed=False):
"""
Starts the SMTP service.
- :param provider_config: Provider configuration
- :type provider_config: ProviderConfig
- :param smtp_config: SMTP configuration to populate
- :type smtp_config: SMTPConfig
:param keymanager: a transparent proxy that eventually will point to a
Keymanager Instance.
:type keymanager: zope.proxy.ProxyBase
@@ -144,13 +143,16 @@ class SMTPBootstrapper(AbstractBootstrapper):
for the file
:type download_if_needed: bool
"""
- leap_assert_type(provider_config, ProviderConfig)
- leap_assert_type(smtp_config, SMTPConfig)
+ try:
+ username, domain = userid.split('@')
+ except ValueError:
+ logger.critical("Malformed userid parameter!")
+ raise MalformedUserId()
- self._provider_config = provider_config
+ self._provider_config = ProviderConfig.get_provider_config(domain)
self._keymanager = keymanager
- self._smtp_config = smtp_config
- self._useid = userid
+ self._smtp_config = SMTPConfig()
+ self._userid = userid
self._download_if_needed = download_if_needed
try:
diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py
index 6bb7c036..db12fd80 100644
--- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py
+++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py
@@ -25,7 +25,6 @@ import sys
from ssl import SSLError
from sqlite3 import ProgrammingError as sqlite_ProgrammingError
-from PySide import QtCore
from u1db import errors as u1db_errors
from twisted.internet import threads
from zope.proxy import sameProxiedObjects
@@ -134,16 +133,11 @@ class SoledadBootstrapper(AbstractBootstrapper):
MAX_INIT_RETRIES = 10
MAX_SYNC_RETRIES = 10
- # All dicts returned are of the form
- # {"passed": bool, "error": str}
- download_config = QtCore.Signal(dict)
- gen_key = QtCore.Signal(dict)
- local_only_ready = QtCore.Signal(dict)
- soledad_invalid_auth_token = QtCore.Signal()
- soledad_failed = QtCore.Signal()
+ def __init__(self, signaler=None):
+ AbstractBootstrapper.__init__(self, signaler)
- def __init__(self):
- AbstractBootstrapper.__init__(self)
+ if signaler is not None:
+ self._cancel_signal = signaler.SOLEDAD_CANCELLED_BOOTSTRAP
self._provider_config = None
self._soledad_config = None
@@ -181,16 +175,23 @@ class SoledadBootstrapper(AbstractBootstrapper):
Instantiate Soledad for offline use.
:param username: full user id (user@provider)
- :type username: basestring
+ :type username: str or unicode
:param password: the soledad passphrase
:type password: unicode
:param uuid: the user uuid
- :type uuid: basestring
+ :type uuid: str or unicode
"""
print "UUID ", uuid
self._address = username
+ self._password = password
self._uuid = uuid
- return self.load_and_sync_soledad(uuid, offline=True)
+ try:
+ self.load_and_sync_soledad(uuid, offline=True)
+ self._signaler.signal(self._signaler.SOLEDAD_OFFLINE_FINISHED)
+ except Exception as e:
+ # TODO: we should handle more specific exceptions in here
+ logger.exception(e)
+ self._signaler.signal(self._signaler.SOLEDAD_OFFLINE_FAILED)
def _get_soledad_local_params(self, uuid, offline=False):
"""
@@ -245,7 +246,7 @@ class SoledadBootstrapper(AbstractBootstrapper):
def _do_soledad_init(self, uuid, secrets_path, local_db_path,
server_url, cert_file, token):
"""
- Initialize soledad, retry if necessary and emit soledad_failed if we
+ Initialize soledad, retry if necessary and raise an exception if we
can't succeed.
:param uuid: user identifier
@@ -263,19 +264,22 @@ class SoledadBootstrapper(AbstractBootstrapper):
:param auth token: auth token
:type auth_token: str
"""
- init_tries = self.MAX_INIT_RETRIES
- while init_tries > 0:
+ init_tries = 1
+ while init_tries <= self.MAX_INIT_RETRIES:
try:
+ logger.debug("Trying to init soledad....")
self._try_soledad_init(
uuid, secrets_path, local_db_path,
server_url, cert_file, token)
logger.debug("Soledad has been initialized.")
return
except Exception:
- init_tries -= 1
+ init_tries += 1
+ msg = "Init failed, retrying... (retry {0} of {1})".format(
+ init_tries, self.MAX_INIT_RETRIES)
+ logger.warning(msg)
continue
- self.soledad_failed.emit()
raise SoledadInitError()
def load_and_sync_soledad(self, uuid=None, offline=False):
@@ -306,9 +310,8 @@ class SoledadBootstrapper(AbstractBootstrapper):
leap_assert(not sameProxiedObjects(self._soledad, None),
"Null soledad, error while initializing")
- if flags.OFFLINE is True:
+ if flags.OFFLINE:
self._init_keymanager(self._address, token)
- self.local_only_ready.emit({self.PASSED_KEY: True})
else:
try:
address = make_address(
@@ -353,9 +356,10 @@ class SoledadBootstrapper(AbstractBootstrapper):
Do several retries to get an initial soledad sync.
"""
# and now, let's sync
- sync_tries = self.MAX_SYNC_RETRIES
- while sync_tries > 0:
+ sync_tries = 1
+ while sync_tries <= self.MAX_SYNC_RETRIES:
try:
+ logger.debug("Trying to sync soledad....")
self._try_soledad_sync()
logger.debug("Soledad has been synced.")
# so long, and thanks for all the fish
@@ -368,19 +372,20 @@ class SoledadBootstrapper(AbstractBootstrapper):
# retry strategy can be pushed to u1db, or at least
# it's something worthy to talk about with the
# ubuntu folks.
- sync_tries -= 1
+ sync_tries += 1
+ msg = "Sync failed, retrying... (retry {0} of {1})".format(
+ sync_tries, self.MAX_SYNC_RETRIES)
+ logger.warning(msg)
continue
except InvalidAuthTokenError:
- self.soledad_invalid_auth_token.emit()
+ self._signaler.signal(
+ self._signaler.SOLEDAD_INVALID_AUTH_TOKEN)
raise
except Exception as e:
logger.exception("Unhandled error while syncing "
"soledad: %r" % (e,))
break
- # reached bottom, failed to sync
- # and there's nothing we can do...
- self.soledad_failed.emit()
raise SoledadSyncError()
def _try_soledad_init(self, uuid, secrets_path, local_db_path,
@@ -443,7 +448,6 @@ class SoledadBootstrapper(AbstractBootstrapper):
Raises SoledadSyncError if not successful.
"""
try:
- logger.debug("trying to sync soledad....")
self._soledad.sync()
except SSLError as exc:
logger.error("%r" % (exc,))
@@ -467,7 +471,6 @@ class SoledadBootstrapper(AbstractBootstrapper):
"""
Download the Soledad config for the given provider
"""
-
leap_assert(self._provider_config,
"We need a provider configuration!")
logger.debug("Downloading Soledad config for %s" %
@@ -480,14 +483,6 @@ class SoledadBootstrapper(AbstractBootstrapper):
self._session,
self._download_if_needed)
- # soledad config is ok, let's proceed to load and sync soledad
- # XXX but honestly, this is a pretty strange entry point for that.
- # it feels like it should be the other way around:
- # load_and_sync, and from there, if needed, call download_config
-
- uuid = self.srpauth.get_uuid()
- self.load_and_sync_soledad(uuid)
-
def _get_gpg_bin_path(self):
"""
Return the path to gpg binary.
@@ -574,7 +569,7 @@ class SoledadBootstrapper(AbstractBootstrapper):
logger.exception(exc)
# but we do not raise
- def _gen_key(self, _):
+ def _gen_key(self):
"""
Generates the key pair if needed, uploads it to the webapp and
nickserver
@@ -613,10 +608,7 @@ class SoledadBootstrapper(AbstractBootstrapper):
logger.debug("Key generated successfully.")
- def run_soledad_setup_checks(self,
- provider_config,
- user,
- password,
+ def run_soledad_setup_checks(self, provider_config, user, password,
download_if_needed=False):
"""
Starts the checks needed for a new soledad setup
@@ -640,9 +632,27 @@ class SoledadBootstrapper(AbstractBootstrapper):
self._user = user
self._password = password
- cb_chain = [
- (self._download_config, self.download_config),
- (self._gen_key, self.gen_key)
- ]
+ if flags.OFFLINE:
+ signal_finished = self._signaler.SOLEDAD_OFFLINE_FINISHED
+ signal_failed = self._signaler.SOLEDAD_OFFLINE_FAILED
+ else:
+ signal_finished = self._signaler.SOLEDAD_BOOTSTRAP_FINISHED
+ signal_failed = self._signaler.SOLEDAD_BOOTSTRAP_FAILED
- return self.addCallbackChain(cb_chain)
+ try:
+ self._download_config()
+
+ # soledad config is ok, let's proceed to load and sync soledad
+ uuid = self.srpauth.get_uuid()
+ self.load_and_sync_soledad(uuid)
+
+ if not flags.OFFLINE:
+ self._gen_key()
+
+ self._signaler.signal(signal_finished)
+ except Exception as e:
+ # TODO: we should handle more specific exceptions in here
+ self._soledad = None
+ self._keymanager = None
+ logger.exception("Error while bootstrapping Soledad: %r" % (e, ))
+ self._signaler.signal(signal_failed)
diff --git a/src/leap/bitmask/util/__init__.py b/src/leap/bitmask/util/__init__.py
index 2b2cd874..c35be99e 100644
--- a/src/leap/bitmask/util/__init__.py
+++ b/src/leap/bitmask/util/__init__.py
@@ -28,11 +28,6 @@ from leap.common.config import get_path_prefix as common_get_path_prefix
# We'll give your money back if it does not alleviate the eye strain, at least.
-# levelname length == 8, since 'CRITICAL' is the longest
-LOG_FORMAT = ('%(asctime)s - %(levelname)-8s - '
- 'L#%(lineno)-4s : %(name)s:%(funcName)s() - %(message)s')
-
-
def first(things):
"""
Return the head of a collection.
diff --git a/src/leap/bitmask/util/password.py b/src/leap/bitmask/util/credentials.py
index 73659f0d..07ded17b 100644
--- a/src/leap/bitmask/util/password.py
+++ b/src/leap/bitmask/util/credentials.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# password.py
+# credentials.py
# Copyright (C) 2013 LEAP
#
# This program is free software: you can redistribute it and/or modify
@@ -16,14 +16,34 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
-Password utilities
+Credentials utilities
"""
-from PySide import QtCore
+from PySide import QtCore, QtGui
WEAK_PASSWORDS = ("123456", "qweasd", "qwerty", "password")
+USERNAME_REGEX = r"^[A-Za-z][A-Za-z\d_\-\.]+[A-Za-z\d]$"
+USERNAME_VALIDATOR = QtGui.QRegExpValidator(QtCore.QRegExp(USERNAME_REGEX))
-def basic_password_checks(username, password, password2):
+
+def username_checks(username):
+ # translation helper
+ _tr = QtCore.QObject().tr
+
+ message = None
+
+ if message is None and len(username) < 2:
+ message = _tr("Username must have at least 2 characters")
+
+ valid = USERNAME_VALIDATOR.validate(username, 0)
+ valid_username = valid[0] == QtGui.QValidator.State.Acceptable
+ if message is None and not valid_username:
+ message = _tr("Invalid username")
+
+ return message is None, message
+
+
+def password_checks(username, password, password2):
"""
Performs basic password checks to avoid really easy passwords.
@@ -46,6 +66,9 @@ def basic_password_checks(username, password, password2):
if message is None and password != password2:
message = _tr("Passwords don't match")
+ if message is None and not password:
+ message = _tr("You can't use an empty password")
+
if message is None and len(password) < 6:
message = _tr("Password too short")