diff options
Diffstat (limited to 'src/leap')
-rw-r--r-- | src/leap/app.py | 9 | ||||
-rw-r--r-- | src/leap/base/config.py | 1 | ||||
-rw-r--r-- | src/leap/baseapp/leap_app.py | 54 | ||||
-rw-r--r-- | src/leap/baseapp/mainwindow.py | 32 | ||||
-rw-r--r-- | src/leap/crypto/__init__.py | 0 | ||||
-rw-r--r-- | src/leap/crypto/leapkeyring.py | 64 | ||||
-rw-r--r-- | src/leap/eip/checks.py | 3 | ||||
-rwxr-xr-x | src/leap/gui/firstrunwizard.py | 489 | ||||
-rw-r--r-- | src/leap/gui/tests/integration/fake_user_signup.py | 80 |
9 files changed, 727 insertions, 5 deletions
diff --git a/src/leap/app.py b/src/leap/app.py index 3170de4a..341f6a6e 100644 --- a/src/leap/app.py +++ b/src/leap/app.py @@ -3,6 +3,7 @@ import logging # This is only needed for Python v2 but is harmless for Python v3. import sip sip.setapi('QVariant', 2) +sip.setapi('QString', 2) from PyQt4.QtGui import (QApplication, QSystemTrayIcon, QMessageBox) from leap import __version__ as VERSION @@ -50,6 +51,14 @@ def main(): logger.info('Starting app') app = QApplication(sys.argv) + # needed for initializing qsettings + # it will write .config/leap/leap.conf + # top level app settings + # in a platform independent way + app.setOrganizationName("leap") + app.setApplicationName("leap") + app.setOrganizationDomain("leap.se") + if not QSystemTrayIcon.isSystemTrayAvailable(): QMessageBox.critical(None, "Systray", "I couldn't detect" diff --git a/src/leap/base/config.py b/src/leap/base/config.py index dc047f80..57f9f1b7 100644 --- a/src/leap/base/config.py +++ b/src/leap/base/config.py @@ -149,6 +149,7 @@ class JSONLeapConfig(BaseLeapConfig): if not fetcher: fetcher = self.fetcher logger.debug('verify: %s', verify) + logger.debug('uri: %s', uri) request = fetcher.get(uri, verify=verify) # XXX should send a if-modified-since header diff --git a/src/leap/baseapp/leap_app.py b/src/leap/baseapp/leap_app.py index 98ca292e..460d1269 100644 --- a/src/leap/baseapp/leap_app.py +++ b/src/leap/baseapp/leap_app.py @@ -1,5 +1,9 @@ import logging +import sip +sip.setapi('QVariant', 2) + +from PyQt4 import QtCore from PyQt4 import QtGui from leap.gui import mainwindow_rc @@ -23,9 +27,9 @@ class MainWindowMixin(object): widget = QtGui.QWidget() self.setCentralWidget(widget) + mainLayout = QtGui.QVBoxLayout() # add widgets to layout #self.createWindowHeader() - mainLayout = QtGui.QVBoxLayout() #mainLayout.addWidget(self.headerBox) mainLayout.addWidget(self.statusIconBox) if self.debugmode: @@ -33,11 +37,51 @@ class MainWindowMixin(object): mainLayout.addWidget(self.loggerBox) widget.setLayout(mainLayout) + self.createMainActions() + self.createMainMenus() + self.setWindowTitle("LEAP Client") self.set_app_icon() - self.resize(400, 300) self.set_statusbarMessage('ready') - logger.debug('set ready.........') + + def createMainActions(self): + #self.openAct = QtGui.QAction("&Open...", self, shortcut="Ctrl+O", + #triggered=self.open) + + self.firstRunWizardAct = QtGui.QAction( + "&First run wizard...", self, + triggered=self.launch_first_run_wizard) + self.aboutAct = QtGui.QAction("&About", self, triggered=self.about) + + #self.aboutQtAct = QtGui.QAction("About &Qt", self, + #triggered=QtGui.qApp.aboutQt) + + def createMainMenus(self): + self.connMenu = QtGui.QMenu("&Connections", self) + #self.viewMenu.addSeparator() + self.connMenu.addAction(self.quitAction) + + self.settingsMenu = QtGui.QMenu("&Settings", self) + self.settingsMenu.addAction(self.firstRunWizardAct) + + self.helpMenu = QtGui.QMenu("&Help", self) + self.helpMenu.addAction(self.aboutAct) + #self.helpMenu.addAction(self.aboutQtAct) + + self.menuBar().addMenu(self.connMenu) + self.menuBar().addMenu(self.settingsMenu) + self.menuBar().addMenu(self.helpMenu) + + def launch_first_run_wizard(self): + settings = QtCore.QSettings() + settings.setValue('FirstRunWizardDone', False) + logger.debug('should run first run wizard again...') + + from leap.gui.firstrunwizard import FirstRunWizard + wizard = FirstRunWizard( + parent=self, + success_cb=self.initReady.emit) + wizard.show() def set_app_icon(self): icon = QtGui.QIcon(APP_LOGO) @@ -88,6 +132,10 @@ class MainWindowMixin(object): """ cleans state before shutting down app. """ + # save geometry for restoring + settings = QtCore.QSettings() + settings.setValue("Geometry", self.saveGeometry()) + # TODO:make sure to shutdown all child process / threads # in conductor # XXX send signal instead? diff --git a/src/leap/baseapp/mainwindow.py b/src/leap/baseapp/mainwindow.py index 55be55f7..1accac30 100644 --- a/src/leap/baseapp/mainwindow.py +++ b/src/leap/baseapp/mainwindow.py @@ -26,18 +26,26 @@ class LeapWindow(QtGui.QMainWindow, newLogLine = QtCore.pyqtSignal([str]) statusChange = QtCore.pyqtSignal([object]) + mainappReady = QtCore.pyqtSignal([]) + initReady = QtCore.pyqtSignal([]) def __init__(self, opts): logger.debug('init leap window') self.debugmode = getattr(opts, 'debug', False) - super(LeapWindow, self).__init__() if self.debugmode: self.createLogBrowser() + EIPConductorAppMixin.__init__(self, opts=opts) StatusAwareTrayIconMixin.__init__(self) MainWindowMixin.__init__(self) + settings = QtCore.QSettings() + geom = settings.value("Geometry") + if geom: + self.restoreGeometry(geom) + self.wizard_done = settings.value("FirstRunWizardDone") + self.initchecks = InitChecksThread(self.run_eip_checks) # bind signals @@ -51,8 +59,28 @@ class LeapWindow(QtGui.QMainWindow, self.timer.timeout.connect( lambda: self.onTimerTick()) - # ... all ready. go! + # do frwizard and init signals + self.mainappReady.connect(self.do_first_run_wizard_check) + self.initReady.connect(self.runchecks_and_eipconnect) + # ... all ready. go! + # calls do_first_run_wizard_check + self.mainappReady.emit() + + def do_first_run_wizard_check(self): + logger.debug('first run wizard check...') + if self.wizard_done: + self.initReady.emit() + else: + # need to run first-run-wizard + logger.debug('running first run wizard') + from leap.gui.firstrunwizard import FirstRunWizard + wizard = FirstRunWizard( + parent=self, + success_cb=self.initReady.emit) + wizard.show() + + def runchecks_and_eipconnect(self): self.initchecks.begin() diff --git a/src/leap/crypto/__init__.py b/src/leap/crypto/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/leap/crypto/__init__.py diff --git a/src/leap/crypto/leapkeyring.py b/src/leap/crypto/leapkeyring.py new file mode 100644 index 00000000..bb0ca147 --- /dev/null +++ b/src/leap/crypto/leapkeyring.py @@ -0,0 +1,64 @@ +import os + +import keyring + +############# +# Disclaimer +############# +# This currently is not a keyring, it's more like a joke. +# No, seriously. +# We're affected by this **bug** + +# https://bitbucket.org/kang/python-keyring-lib/ +# issue/65/dbusexception-method-opensession-with + +# so using the gnome keyring does not seem feasible right now. +# I thought this was the next best option to store secrets in plain sight. + +# in the future we should move to use the gnome/kde/macosx/win keyrings. + + +class LeapCryptedFileKeyring(keyring.backend.CryptedFileKeyring): + + filename = os.path.expanduser("~/.config/leap/.secrets") + + def __init__(self, seed=None): + self.seed = seed + + def _get_new_password(self): + # XXX every time this method is called, + # $deity kills a kitten. + return "secret%s" % self.seed + + def _init_file(self): + self.keyring_key = self._get_new_password() + self.set_password('keyring_setting', 'pass_ref', 'pass_ref_value') + + def _unlock(self): + self.keyring_key = self._get_new_password() + print 'keyring key ', self.keyring_key + try: + ref_pw = self.get_password( + 'keyring_setting', + 'pass_ref') + print 'ref pw ', ref_pw + assert ref_pw == "pass_ref_value" + except AssertionError: + self._lock() + raise ValueError('Incorrect password') + + +def leap_set_password(key, value, seed="xxx"): + keyring.set_keyring(LeapCryptedFileKeyring(seed=seed)) + keyring.set_password('leap', key, value) + + +def leap_get_password(key, seed="xxx"): + keyring.set_keyring(LeapCryptedFileKeyring(seed=seed)) + return keyring.get_password('leap', key) + + +if __name__ == "__main__": + leap_set_password('test', 'bar') + passwd = leap_get_password('test') + assert passwd == 'bar' diff --git a/src/leap/eip/checks.py b/src/leap/eip/checks.py index f79d47f5..413a3467 100644 --- a/src/leap/eip/checks.py +++ b/src/leap/eip/checks.py @@ -232,6 +232,9 @@ class ProviderCertChecker(object): # verify=verify # Workaround for #638. return to verification # when That's done!!! + + # XXX HOOK SRP here... + # will have to be more generic in the future. req = self.fetcher.get(uri, verify=False) req.raise_for_status() except requests.exceptions.SSLError: diff --git a/src/leap/gui/firstrunwizard.py b/src/leap/gui/firstrunwizard.py new file mode 100755 index 00000000..abdff7cf --- /dev/null +++ b/src/leap/gui/firstrunwizard.py @@ -0,0 +1,489 @@ +#!/usr/bin/env python +import logging +import json +import socket + +import sip +sip.setapi('QString', 2) +sip.setapi('QVariant', 2) + +from PyQt4 import QtCore +from PyQt4 import QtGui + +from leap.crypto import leapkeyring +from leap.gui import mainwindow_rc + +logger = logging.getLogger(__name__) + +APP_LOGO = ':/images/leap-color-small.png' + +# registration ###################### +# move to base/ +import binascii + +import requests +import srp + +from leap.base import constants as baseconstants + +SIGNUP_TIMEOUT = getattr(baseconstants, 'SIGNUP_TIMEOUT', 5) + + +class LeapSRPRegister(object): + + def __init__(self, + schema="https", + provider=None, + port=None, + register_path="1/users.json", + method="POST", + fetcher=requests, + srp=srp, + hashfun=srp.SHA256, + ng_constant=srp.NG_1024): + + self.schema = schema + self.provider = provider + self.port = port + self.register_path = register_path + self.method = method + self.fetcher = fetcher + self.srp = srp + self.HASHFUN = hashfun + self.NG = ng_constant + + self.init_session() + + def init_session(self): + self.session = self.fetcher.session() + + def get_registration_uri(self): + # XXX assert is https! + # use urlparse + if self.port: + uri = "%s://%s:%s/%s" % ( + self.schema, + self.provider, + self.port, + self.register_path) + else: + uri = "%s://%s/%s" % ( + self.schema, + self.provider, + self.register_path) + + return uri + + def register_user(self, username, password, keep=False): + """ + @rtype: tuple + @rvalue: (ok, request) + """ + salt, vkey = self.srp.create_salted_verification_key( + username, + password, + self.HASHFUN, + self.NG) + + user_data = { + 'user[login]': username, + 'user[password_verifier]': binascii.hexlify(vkey), + 'user[password_salt]': binascii.hexlify(salt)} + + uri = self.get_registration_uri() + logger.debug('post to uri: %s' % uri) + + # XXX get self.method + req = self.session.post( + uri, data=user_data, + timeout=SIGNUP_TIMEOUT) + logger.debug(req) + logger.debug('user_data: %s', user_data) + #logger.debug('response: %s', req.text) + # we catch it in the form + #req.raise_for_status() + return (req.ok, req) + +###################################### + +ErrorLabelStyleSheet = """ +QLabel { color: red; + font-weight: bold} +""" + + +class FirstRunWizard(QtGui.QWizard): + + def __init__( + self, parent=None, providers=None, + success_cb=None): + super(FirstRunWizard, self).__init__( + parent, + QtCore.Qt.WindowStaysOnTopHint) + + # XXX hardcoded for tests + if not providers: + providers = ('springbok',) + self.providers = providers + + # success callback + self.success_cb = success_cb + + self.addPage(IntroPage()) + self.addPage(SelectProviderPage(providers=providers)) + + self.addPage(RegisterUserPage(wizard=self)) + #self.addPage(GlobalEIPSettings()) + self.addPage(LastPage()) + + self.setPixmap( + QtGui.QWizard.BannerPixmap, + QtGui.QPixmap(':/images/banner.png')) + self.setPixmap( + QtGui.QWizard.BackgroundPixmap, + QtGui.QPixmap(':/images/background.png')) + + self.setWindowTitle("First Run Wizard") + + # TODO: set style for MAC / windows ... + #self.setWizardStyle() + + def setWindowFlags(self, flags): + logger.debug('setting window flags') + QtGui.QWizard.setWindowFlags(self, flags) + + def focusOutEvent(self, event): + # needed ? + self.setFocus(True) + self.activateWindow() + self.raise_() + self.show() + + def accept(self): + """ + final step in the wizard. + gather the info, update settings + and call the success callback. + """ + provider = self.get_provider() + username = self.field('userName') + password = self.field('userPassword') + remember_pass = self.field('rememberPassword') + + logger.debug('chosen provider: %s', provider) + logger.debug('username: %s', username) + logger.debug('remember password: %s', remember_pass) + super(FirstRunWizard, self).accept() + + settings = QtCore.QSettings() + settings.setValue("FirstRunWizardDone", True) + settings.setValue( + "eip_%s_username" % provider, + username) + settings.setValue("%s_remember_pass" % provider, remember_pass) + + seed = self.get_random_str(10) + settings.setValue("%s_seed" % provider, seed) + + leapkeyring.leap_set_password(username, password, seed=seed) + + logger.debug('First Run Wizard Done.') + cb = self.success_cb + if cb and callable(cb): + self.success_cb() + + def get_provider(self): + provider = self.field('provider_index') + return self.providers[provider] + + def get_random_str(self, n): + from string import (ascii_uppercase, ascii_lowercase, digits) + from random import choice + return ''.join(choice( + ascii_uppercase + + ascii_lowercase + + digits) for x in range(n)) + + +class IntroPage(QtGui.QWizardPage): + def __init__(self, parent=None): + super(IntroPage, self).__init__(parent) + + self.setTitle("First run wizard.") + + #self.setPixmap( + #QtGui.QWizard.WatermarkPixmap, + #QtGui.QPixmap(':/images/watermark1.png')) + + label = QtGui.QLabel( + "Now we will guide you through " + "some configuration that is needed before you " + "can connect for the first time.<br><br>" + "If you ever need to modify these options again, " + "you can find the wizard in the '<i>Settings</i>' menu from the " + "main window of the Leap App.") + + label.setWordWrap(True) + + layout = QtGui.QVBoxLayout() + layout.addWidget(label) + self.setLayout(layout) + + +class SelectProviderPage(QtGui.QWizardPage): + def __init__(self, parent=None, providers=None): + super(SelectProviderPage, self).__init__(parent) + + self.setTitle("Select Provider") + self.setSubTitle( + "Please select which provider do you want " + "to use for your connection." + ) + self.setPixmap( + QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(APP_LOGO)) + + providerNameLabel = QtGui.QLabel("&Provider:") + + providercombo = QtGui.QComboBox() + if providers: + for provider in providers: + providercombo.addItem(provider) + providerNameSelect = providercombo + + providerNameLabel.setBuddy(providerNameSelect) + + self.registerField('provider_index', providerNameSelect) + + layout = QtGui.QGridLayout() + layout.addWidget(providerNameLabel, 0, 0) + layout.addWidget(providerNameSelect, 0, 1) + self.setLayout(layout) + + +class RegisterUserPage(QtGui.QWizardPage): + setSigningUpStatus = QtCore.pyqtSignal([]) + + def __init__(self, parent=None, wizard=None): + super(RegisterUserPage, self).__init__(parent) + + # bind wizard page signals + self.setSigningUpStatus.connect( + self.set_status_validating) + + # XXX check for no wizard pased + # getting provider from previous step + provider = wizard.get_provider() + + self.setTitle("User registration") + self.setSubTitle( + "Register a new user with provider %s." % + provider) + self.setPixmap( + QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(APP_LOGO)) + + rememberPasswordCheckBox = QtGui.QCheckBox( + "&Remember password.") + rememberPasswordCheckBox.setChecked(True) + + userNameLabel = QtGui.QLabel("User &name:") + userNameLineEdit = QtGui.QLineEdit() + userNameLineEdit.cursorPositionChanged.connect( + self.reset_validation_status) + userNameLabel.setBuddy(userNameLineEdit) + + # add regex validator + usernameRe = QtCore.QRegExp(r"^[A-Za-z\d_]+$") + userNameLineEdit.setValidator( + QtGui.QRegExpValidator(usernameRe, self)) + self.userNameLineEdit = userNameLineEdit + + userPasswordLabel = QtGui.QLabel("&Password:") + self.userPasswordLineEdit = QtGui.QLineEdit() + self.userPasswordLineEdit.setEchoMode( + QtGui.QLineEdit.Password) + + userPasswordLabel.setBuddy(self.userPasswordLineEdit) + + self.registerField('userName', self.userNameLineEdit) + self.registerField('userPassword', self.userPasswordLineEdit) + self.registerField('rememberPassword', rememberPasswordCheckBox) + + layout = QtGui.QGridLayout() + layout.setColumnMinimumWidth(0, 20) + + validationMsg = QtGui.QLabel("") + validationMsg.setStyleSheet(ErrorLabelStyleSheet) + + self.validationMsg = validationMsg + + layout.addWidget(validationMsg, 0, 3) + + layout.addWidget(userNameLabel, 1, 0) + layout.addWidget(self.userNameLineEdit, 1, 3) + + layout.addWidget(userPasswordLabel, 2, 0) + layout.addWidget(self.userPasswordLineEdit, 2, 3) + + layout.addWidget(rememberPasswordCheckBox, 3, 3, 3, 4) + self.setLayout(layout) + + def reset_validation_status(self): + """ + empty the validation msg + """ + self.validationMsg.setText('') + + def set_status_validating(self): + """ + set validation msg to 'registering...' + """ + # XXX this is NOT WORKING. + # My guess is that, even if we are using + # signals to trigger this, it does + # not show until the validate function + # returns. + # I guess it is because there is no delay... + logger.debug('registering........') + self.validationMsg.setText('registering...') + # need to call update somehow??? + + def set_status_invalid_username(self): + """ + set validation msg to + not available user + """ + self.validationMsg.setText('Username not available.') + + def set_status_server_500(self): + """ + set validation msg to + internal server error + """ + self.validationMsg.setText("Error during registration (500)") + + def set_status_timeout(self): + """ + set validation msg to + timeout + """ + self.validationMsg.setText("Error connecting to provider (timeout)") + + def set_status_unknown_error(self): + """ + set validation msg to + unknown error + """ + self.validationMsg.setText("Error during signup") + + # overwritten methods + + def initializePage(self): + """ + inits wizard page + """ + self.validationMsg.setText('') + + def validatePage(self): + """ + validation + we initialize the srp protocol register + and try to register user. if error + returned we write validation error msg + above the form. + """ + # the slot for this signal is not doing + # what's expected. Investigate why, + # right now we're not giving any feedback + # to the user re. what's going on. The only + # thing I can see as a workaround is setting + # a low timeout. + self.setSigningUpStatus.emit() + + username = self.userNameLineEdit.text() + password = self.userPasswordLineEdit.text() + + # XXX TODO -- remove debug info + # XXX get from provider info + # XXX enforce https + # and pass a verify value + + signup = LeapSRPRegister( + schema="http", + provider="springbok", + + #provider="localhost", + #register_path="timeout", + #port=8000 + ) + try: + ok, req = signup.register_user(username, password) + except socket.timeout: + self.set_status_timeout() + return False + + if ok: + return True + + # something went wrong. + # not registered, let's catch what. + # get timeout + # ... + if req.status_code == 500: + self.set_status_server_500() + return False + + validation_msgs = json.loads(req.content) + logger.debug('validation errors: %s' % validation_msgs) + errors = validation_msgs.get('errors', None) + if errors and errors.get('login', None): + self.set_status_invalid_username() + else: + self.set_status_unknown_error() + return False + + +class GlobalEIPSettings(QtGui.QWizardPage): + def __init__(self, parent=None): + super(GlobalEIPSettings, self).__init__(parent) + + +class LastPage(QtGui.QWizardPage): + def __init__(self, parent=None): + super(LastPage, self).__init__(parent) + + self.setTitle("Ready to go!") + + #self.setPixmap( + #QtGui.QWizard.WatermarkPixmap, + #QtGui.QPixmap(':/images/watermark2.png')) + + self.label = QtGui.QLabel() + self.label.setWordWrap(True) + + layout = QtGui.QVBoxLayout() + layout.addWidget(self.label) + self.setLayout(layout) + + def initializePage(self): + finishText = self.wizard().buttonText( + QtGui.QWizard.FinishButton) + finishText = finishText.replace('&', '') + self.label.setText( + "Click '<i>%s</i>' to end the wizard and start " + "encrypting your connection." % finishText) + + +if __name__ == '__main__': + # standalone test + import sys + import logging + logging.basicConfig() + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + + app = QtGui.QApplication(sys.argv) + wizard = FirstRunWizard() + wizard.show() + sys.exit(app.exec_()) diff --git a/src/leap/gui/tests/integration/fake_user_signup.py b/src/leap/gui/tests/integration/fake_user_signup.py new file mode 100644 index 00000000..12f18966 --- /dev/null +++ b/src/leap/gui/tests/integration/fake_user_signup.py @@ -0,0 +1,80 @@ +""" +simple server to test registration and +authentication + +To test: + +curl -d login=python_test_user -d password_salt=54321\ + -d password_verifier=12341234 \ + http://localhost:8000/users.json + +""" +from BaseHTTPServer import HTTPServer +from BaseHTTPServer import BaseHTTPRequestHandler +import cgi +import urlparse + +HOST = "localhost" +PORT = 8000 + +LOGIN_ERROR = """{"errors":{"login":["has already been taken"]}}""" + + +class request_handler(BaseHTTPRequestHandler): + responses = { + '/': ['ok\n'], + '/users.json': ['ok\n'], + '/timeout': ['ok\n'] + } + + def do_GET(self): + path = urlparse.urlparse(self.path) + message = '\n'.join( + self.responses.get( + path.path, None)) + self.send_response(200) + self.end_headers() + self.wfile.write(message) + + def do_POST(self): + form = cgi.FieldStorage( + fp=self.rfile, + headers=self.headers, + environ={'REQUEST_METHOD': 'POST', + 'CONTENT_TYPE': self.headers['Content-Type'], + }) + data = dict( + (key, form[key].value) for key in form.keys()) + path = urlparse.urlparse(self.path) + message = '\n'.join( + self.responses.get( + path.path, '')) + + login = data.get('login', None) + #password_salt = data.get('password_salt', None) + #password_verifier = data.get('password_verifier', None) + + if path.geturl() == "/timeout": + print 'timeout' + self.send_response(200) + self.end_headers() + self.wfile.write(message) + import time + time.sleep(10) + return + + ok = True if (login == "python_test_user") else False + if ok: + self.send_response(200) + self.end_headers() + self.wfile.write(message) + + else: + self.send_response(500) + self.end_headers() + self.wfile.write(LOGIN_ERROR) + + +if __name__ == "__main__": + server = HTTPServer((HOST, PORT), request_handler) + server.serve_forever() |