diff options
author | kali <kali@leap.se> | 2012-10-08 07:19:09 +0900 |
---|---|---|
committer | kali <kali@leap.se> | 2012-10-08 07:19:09 +0900 |
commit | 3a77603eae5fea0b1efb226860e0264ccf96f41b (patch) | |
tree | 19dced0267d7840d142ff9d771548f3c3f39fcbb /src/leap/gui | |
parent | c7cfb8fcef773b84738e36b6d0c27487cf8e952a (diff) | |
parent | 8a594577660d78a4f0f9d3c5f5902dff3911e010 (diff) |
Merge branch 'feature/firstrun-wizard' into develop
Diffstat (limited to 'src/leap/gui')
-rwxr-xr-x | src/leap/gui/firstrunwizard.py | 489 | ||||
-rw-r--r-- | src/leap/gui/tests/integration/fake_user_signup.py | 80 |
2 files changed, 569 insertions, 0 deletions
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() |