diff options
Diffstat (limited to 'src/leap/gui')
22 files changed, 2275 insertions, 1100 deletions
diff --git a/src/leap/gui/__init__.py b/src/leap/gui/__init__.py index e69de29b..6ecd665f 100644 --- a/src/leap/gui/__init__.py +++ b/src/leap/gui/__init__.py @@ -0,0 +1,3 @@ +import firstrun + +__all__ = ['firstrun'] diff --git a/src/leap/gui/constants.py b/src/leap/gui/constants.py new file mode 100644 index 00000000..277f3540 --- /dev/null +++ b/src/leap/gui/constants.py @@ -0,0 +1,13 @@ +import time + +APP_LOGO = ':/images/leap-color-small.png' + +# bare is the username portion of a JID +# full includes the "at" and some extra chars +# that can be allowed for fqdn + +BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$" +FULL_USERNAME_REGEX = r"^[A-Za-z\d_@.-]+$" + +GUI_PAUSE_FOR_USER_SECONDS = 1 +pause_for_user = lambda: time.sleep(GUI_PAUSE_FOR_USER_SECONDS) diff --git a/src/leap/gui/firstrun/__init__.py b/src/leap/gui/firstrun/__init__.py new file mode 100644 index 00000000..477e7269 --- /dev/null +++ b/src/leap/gui/firstrun/__init__.py @@ -0,0 +1,26 @@ +import sip +sip.setapi('QString', 2) +sip.setapi('QVariant', 2) + +import connect +import intro +import last +import login +import mixins +import providerinfo +import providerselect +import providersetup +import register +import regvalidation + +__all__ = [ + 'connect', + 'intro', + 'last', + 'login', + 'mixins', + 'providerinfo', + 'providerselect', + 'providersetup', + 'register', + 'regvalidation'] diff --git a/src/leap/gui/firstrun/connect.py b/src/leap/gui/firstrun/connect.py new file mode 100644 index 00000000..283e81b2 --- /dev/null +++ b/src/leap/gui/firstrun/connect.py @@ -0,0 +1,226 @@ +""" +Connecting Page, used in First Run Wizard +""" +import logging + +from PyQt4 import QtGui + +logger = logging.getLogger(__name__) + +from leap.base import auth + +from leap.gui.constants import APP_LOGO +from leap.gui.styles import ErrorLabelStyleSheet + + +class ConnectingPage(QtGui.QWizardPage): + + # XXX change to a ValidationPage + + def __init__(self, parent=None): + super(ConnectingPage, self).__init__(parent) + + self.setTitle("Connecting") + self.setSubTitle('Connecting to provider.') + + self.setPixmap( + QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(APP_LOGO)) + + self.status = QtGui.QLabel("") + self.status.setWordWrap(True) + self.progress = QtGui.QProgressBar() + self.progress.setMaximum(100) + self.progress.hide() + + # for pre-checks + self.status_line_1 = QtGui.QLabel() + self.status_line_2 = QtGui.QLabel() + self.status_line_3 = QtGui.QLabel() + self.status_line_4 = QtGui.QLabel() + + # for connecting signals... + self.status_line_5 = QtGui.QLabel() + + layout = QtGui.QGridLayout() + layout.addWidget(self.status, 0, 1) + layout.addWidget(self.progress, 5, 1) + layout.addWidget(self.status_line_1, 8, 1) + layout.addWidget(self.status_line_2, 9, 1) + layout.addWidget(self.status_line_3, 10, 1) + layout.addWidget(self.status_line_4, 11, 1) + + # XXX to be used? + #self.validation_status = QtGui.QLabel("") + #self.validation_status.setStyleSheet( + #ErrorLabelStyleSheet) + #self.validation_msg = QtGui.QLabel("") + + self.setLayout(layout) + + self.goto_login_again = False + + def set_status(self, status): + self.status.setText(status) + self.status.setWordWrap(True) + + def set_status_line(self, line, status): + line = getattr(self, 'status_line_%s' % line) + if line: + line.setText(status) + + def set_validation_status(self, status): + # Do not remember if we're using + # status lines > 3 now... + # if we are, move below + self.status_line_3.setStyleSheet( + ErrorLabelStyleSheet) + self.status_line_3.setText(status) + + def set_validation_message(self, message): + self.status_line_4.setText(message) + self.status_line_4.setWordWrap(True) + + def get_donemsg(self, msg): + return "%s ... done" % msg + + def run_eip_checks_for_provider_and_connect(self, domain): + wizard = self.wizard() + conductor = wizard.conductor + start_eip_signal = getattr( + wizard, + 'start_eipconnection_signal', None) + + if conductor: + conductor.set_provider_domain(domain) + conductor.run_checks() + self.conductor = conductor + errors = self.eip_error_check() + if not errors and start_eip_signal: + start_eip_signal.emit() + + else: + logger.warning( + "No conductor found. This means that " + "probably the wizard has been launched " + "in an stand-alone way") + + def eip_error_check(self): + """ + a version of the main app error checker, + but integrated within the connecting page of the wizard. + consumes the conductor error queue. + pops errors, and add those to the wizard page + """ + logger.debug('eip error check from connecting page') + errq = self.conductor.error_queue + # XXX missing! + + def fetch_and_validate(self): + # XXX MOVE TO validate function in register-validation + import time + domain = self.field('provider_domain') + wizard = self.wizard() + #pconfig = wizard.providerconfig + eipconfigchecker = wizard.eipconfigchecker() + pCertChecker = wizard.providercertchecker( + domain=domain) + + # username and password are in different fields + # if they were stored in log_in or sign_up pages. + from_login = self.wizard().from_login + unamek_base = 'userName' + passwk_base = 'userPassword' + unamek = 'login_%s' % unamek_base if from_login else unamek_base + passwk = 'login_%s' % passwk_base if from_login else passwk_base + + username = self.field(unamek) + password = self.field(passwk) + credentials = username, password + + self.progress.show() + + fetching_eip_conf_msg = 'Fetching eip service configuration' + self.set_status(fetching_eip_conf_msg) + self.progress.setValue(30) + + # Fetching eip service + eipconfigchecker.fetch_eip_service_config( + domain=domain) + + self.status_line_1.setText( + self.get_donemsg(fetching_eip_conf_msg)) + + getting_client_cert_msg = 'Getting client certificate' + self.set_status(getting_client_cert_msg) + self.progress.setValue(66) + + # Download cert + try: + pCertChecker.download_new_client_cert( + credentials=credentials, + # FIXME FIXME FIXME + # XXX FIX THIS!!!!! + # BUG #638. remove verify + # FIXME FIXME FIXME + verify=False) + except auth.SRPAuthenticationError as exc: + self.set_validation_status( + "Authentication error: %s" % exc.message) + return False + + time.sleep(2) + self.status_line_2.setText( + self.get_donemsg(getting_client_cert_msg)) + + validating_clientcert_msg = 'Validating client certificate' + self.set_status(validating_clientcert_msg) + self.progress.setValue(90) + time.sleep(2) + self.status_line_3.setText( + self.get_donemsg(validating_clientcert_msg)) + + self.progress.setValue(100) + time.sleep(3) + + # here we go! :) + self.run_eip_checks_for_provider_and_connect(domain) + + #self.validation_block = self.wait_for_validation_block() + + # XXX signal timeout! + return True + + # + # wizardpage methods + # + + def nextId(self): + wizard = self.wizard() + # XXX this does not work because + # page login has already been met + #if self.goto_login_again: + #next_ = "login" + #else: + #next_ = "lastpage" + next_ = "lastpage" + return wizard.get_page_index(next_) + + def initializePage(self): + # XXX if we're coming from signup page + # we could say something like + # 'registration successful!' + self.status.setText( + "We have " + "all we need to connect with the provider.<br><br> " + "Click <i>next</i> to continue. ") + self.progress.setValue(0) + self.progress.hide() + self.status_line_1.setText('') + self.status_line_2.setText('') + self.status_line_3.setText('') + + def validatePage(self): + # XXX remove + validated = self.fetch_and_validate() + return validated diff --git a/src/leap/gui/firstrun/constants.py b/src/leap/gui/firstrun/constants.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/leap/gui/firstrun/constants.py diff --git a/src/leap/gui/firstrun/intro.py b/src/leap/gui/firstrun/intro.py new file mode 100644 index 00000000..4bb008c7 --- /dev/null +++ b/src/leap/gui/firstrun/intro.py @@ -0,0 +1,68 @@ +""" +Intro page used in first run wizard +""" + +from PyQt4 import QtGui + +from leap.gui.constants import APP_LOGO + + +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')) + + self.setPixmap( + QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(APP_LOGO)) + + 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.<br><br>" + "Do you want to <b>sign up</b> for a new account, or <b>log " + "in</b> with an already existing username?<br>") + label.setWordWrap(True) + + radiobuttonGroup = QtGui.QGroupBox() + + self.sign_up = QtGui.QRadioButton( + "Sign up for a new account.") + self.sign_up.setChecked(True) + self.log_in = QtGui.QRadioButton( + "Log In with my credentials.") + + radiobLayout = QtGui.QVBoxLayout() + radiobLayout.addWidget(self.sign_up) + radiobLayout.addWidget(self.log_in) + radiobuttonGroup.setLayout(radiobLayout) + + layout = QtGui.QVBoxLayout() + layout.addWidget(label) + layout.addWidget(radiobuttonGroup) + self.setLayout(layout) + + self.registerField('is_signup', self.sign_up) + + def validatePage(self): + return True + + def nextId(self): + """ + returns next id + in a non-linear wizard + """ + if self.sign_up.isChecked(): + next_ = 'providerselection' + if self.log_in.isChecked(): + next_ = 'login' + wizard = self.wizard() + return wizard.get_page_index(next_) diff --git a/src/leap/gui/firstrun/last.py b/src/leap/gui/firstrun/last.py new file mode 100644 index 00000000..6f9abbb5 --- /dev/null +++ b/src/leap/gui/firstrun/last.py @@ -0,0 +1,89 @@ +""" +Last Page, used in First Run Wizard +""" +import logging + +from PyQt4 import QtGui + +from leap.util.coroutines import coroutine +from leap.gui.constants import APP_LOGO + +logger = logging.getLogger(__name__) + + +class LastPage(QtGui.QWizardPage): + def __init__(self, parent=None): + super(LastPage, self).__init__(parent) + + self.setTitle("Connecting to Encrypted Internet Proxy service...") + + self.setPixmap( + QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(APP_LOGO)) + + #self.setPixmap( + #QtGui.QWizard.WatermarkPixmap, + #QtGui.QPixmap(':/images/watermark2.png')) + + self.label = QtGui.QLabel() + self.label.setWordWrap(True) + + self.status_line_1 = QtGui.QLabel() + self.status_line_2 = QtGui.QLabel() + self.status_line_3 = QtGui.QLabel() + self.status_line_4 = QtGui.QLabel() + + layout = QtGui.QVBoxLayout() + layout.addWidget(self.label) + + # make loop + layout.addWidget(self.status_line_1) + layout.addWidget(self.status_line_2) + layout.addWidget(self.status_line_3) + layout.addWidget(self.status_line_4) + + self.setLayout(layout) + + def set_status_line(self, line, status): + statusline = getattr(self, 'status_line_%s' % line) + if statusline: + statusline.setText(status) + + def set_finished_status(self): + self.setTitle('You are now using an encrypted connection!') + finishText = self.wizard().buttonText( + QtGui.QWizard.FinishButton) + finishText = finishText.replace('&', '') + self.label.setText( + "Click '<i>%s</i>' to end the wizard and " + "save your settings." % finishText) + + @coroutine + def eip_status_handler(self): + # XXX this can be changed to use + # signals. See progress.py + logger.debug('logging status in last page') + self.validation_done = False + status_count = 0 + try: + while True: + status = (yield) + status_count += 1 + # XXX add to line... + logger.debug('status --> %s', status) + self.set_status_line(status_count, status) + if status == "connected": + self.set_finished_status() + break + except GeneratorExit: + pass + + def initializePage(self): + wizard = self.wizard() + if not wizard: + return + eip_status_handler = self.eip_status_handler() + eip_statuschange_signal = wizard.eip_statuschange_signal + if eip_statuschange_signal: + eip_statuschange_signal.connect( + lambda status: eip_status_handler.send(status)) diff --git a/src/leap/gui/firstrun/login.py b/src/leap/gui/firstrun/login.py new file mode 100644 index 00000000..ae4b23c6 --- /dev/null +++ b/src/leap/gui/firstrun/login.py @@ -0,0 +1,132 @@ +""" +LogIn Page, used inf First Run Wizard +""" +from PyQt4 import QtCore +from PyQt4 import QtGui + +import requests + +from leap.gui.firstrun.mixins import UserFormMixIn + +from leap.gui.constants import APP_LOGO, FULL_USERNAME_REGEX +from leap.gui.styles import ErrorLabelStyleSheet + + +class LogInPage(QtGui.QWizardPage, UserFormMixIn): + def __init__(self, parent=None): + super(LogInPage, self).__init__(parent) + + self.setTitle("Log In") + self.setSubTitle("Log in with your credentials.") + + self.setPixmap( + QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(APP_LOGO)) + + userNameLabel = QtGui.QLabel("User &name:") + userNameLineEdit = QtGui.QLineEdit() + userNameLineEdit.cursorPositionChanged.connect( + self.reset_validation_status) + userNameLabel.setBuddy(userNameLineEdit) + + # let's add regex validator + usernameRe = QtCore.QRegExp(FULL_USERNAME_REGEX) + 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('login_userName*', self.userNameLineEdit) + self.registerField('login_userPassword*', self.userPasswordLineEdit) + + 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) + + self.setLayout(layout) + + #self.registerField('is_login_wizard') + + def onUserNameEdit(self, *args): + if self.initial_username_sample: + self.userNameLineEdit.setText('') + self.initial_username_sample = None + + # pagewizard methods + + def nextId(self): + wizard = self.wizard() + if not wizard: + return + if wizard.is_provider_setup is True: + next_ = 'connecting' + if wizard.is_provider_setup is False: + next_ = 'providersetup' + return wizard.get_page_index(next_) + + def initializePage(self): + self.userNameLineEdit.setText('username@provider.example.org') + self.userNameLineEdit.cursorPositionChanged.connect( + self.onUserNameEdit) + self.initial_username_sample = True + + def validatePage(self): + wizard = self.wizard() + eipconfigchecker = wizard.eipconfigchecker() + + full_username = self.userNameLineEdit.text() + password = self.userPasswordLineEdit.text() + if full_username.count('@') != 1: + self.set_validation_status( + "Username must be in the username@provider form.") + return False + + username, domain = full_username.split('@') + self.setField('provider_domain', domain) + self.setField('login_userName', username) + self.setField('login_userPassword', password) + + # Able to contact domain? + # can get definition? + # two-by-one + try: + eipconfigchecker.fetch_definition(domain=domain) + + # we're using requests here for all + # the possible error cases that it catches. + except requests.exceptions.ConnectionError as exc: + self.set_validation_status(exc.message[1]) + return False + except requests.exceptions.HTTPError as exc: + self.set_validation_status(exc.message) + return False + wizard.set_providerconfig( + eipconfigchecker.defaultprovider.config) + + # XXX validate user? or we leave that for later? + # I think the best thing to do for that is + # continue to provider setup page, and if + # we catch authentication error there, redirect + # again to this page (by clicking "next" to + # come here). + # Rationale is that we need to verify server certs + # and so on. + + # mark that we came from login page. + self.wizard().from_login = True + + return True diff --git a/src/leap/gui/firstrun/mixins.py b/src/leap/gui/firstrun/mixins.py new file mode 100644 index 00000000..c4731893 --- /dev/null +++ b/src/leap/gui/firstrun/mixins.py @@ -0,0 +1,18 @@ +""" +mixins used in First Run Wizard +""" + + +class UserFormMixIn(object): + + def reset_validation_status(self): + """ + empty the validation msg + """ + self.validationMsg.setText('') + + def set_validation_status(self, msg): + """ + set generic validation status + """ + self.validationMsg.setText(msg) diff --git a/src/leap/gui/firstrun/providerinfo.py b/src/leap/gui/firstrun/providerinfo.py new file mode 100644 index 00000000..4df477a7 --- /dev/null +++ b/src/leap/gui/firstrun/providerinfo.py @@ -0,0 +1,214 @@ +""" +Provider Info Page, used in First run Wizard +""" +import logging + +from PyQt4 import QtCore +from PyQt4 import QtGui + +import requests + +from leap.base import exceptions as baseexceptions +from leap.crypto import certs +from leap.eip import exceptions as eipexceptions + +from leap.gui.progress import ValidationPage +from leap.util.web import get_https_domain_and_port + +from leap.gui.constants import APP_LOGO, pause_for_user + +logger = logging.getLogger(__name__) + + +class ProviderInfoPage(ValidationPage): + def __init__(self, parent=None): + super(ProviderInfoPage, self).__init__(parent) + + self.setTitle("Provider Info") + #self.setSubTitle("Available information about chosen provider.") + + self.setPixmap( + QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(APP_LOGO)) + + def create_info_panel(self): + displayName = QtGui.QLabel("") + description = QtGui.QLabel("") + enrollment_policy = QtGui.QLabel("") + # XXX set stylesheet... + # prettify a little bit. + # bigger fonts and so on... + self.displayName = displayName + self.description = description + self.enrollment_policy = enrollment_policy + + # this trick allows us to reparent + QtCore.QObjectCleanupHandler().add(self.layout) + layout = QtGui.QGridLayout() + + layout.addWidget(displayName, 0, 1) + layout.addWidget(description, 1, 1) + layout.addWidget(enrollment_policy, 2, 1) + + self.setLayout(layout) + self.update() + + def show_provider_info(self): + + # XXX get multilingual objects + # directly from the config object + + lang = "en" + pconfig = self.wizard().providerconfig + + dn = pconfig.get('display_name') + display_name = dn[lang] if dn else '' + self.displayName.setText( + "<b>%s</b>" % display_name) + + desc = pconfig.get('description') + description_text = desc[lang] if desc else '' + self.description.setText( + "<i>%s</i>" % description_text) + + enroll = pconfig.get('enrollment_policy') + if enroll: + self.enrollment_policy.setText( + 'enrollment policy: %s' % enroll) + + def _do_checks(self, update_signal=None): + """ + executes actual checks in a separate thread + """ + def pause_and_finish(): + update_signal.emit("end_sentinel", 100) + pause_for_user() + + wizard = self.wizard() + prevpage = "providerselection" + netchecker = wizard.netchecker() + providercertchecker = wizard.providercertchecker() + eipconfigchecker = wizard.eipconfigchecker() + + full_domain = self.field('provider_domain') + + # we check if we have a port in the domain string. + domain, port = get_https_domain_and_port(full_domain) + _domain = u"%s:%s" % (domain, port) if port != 443 else unicode(domain) + + update_signal.emit("head_sentinel", 0) + pause_for_user() + + ######################## + # 1) try name resolution + ######################## + update_signal.emit("Checking that server is reachable", 20) + logger.debug('checking name resolution') + try: + netchecker.check_name_resolution( + domain) + + except baseexceptions.LeapException as exc: + logger.error(exc.message) + wizard.set_validation_error( + prevpage, exc.usermessage) + pause_and_finish() + return False + + ######################### + # 2) try https connection + ######################### + update_signal.emit("Checking secure connection to provider", 40) + logger.debug('checking https connection') + try: + providercertchecker.is_https_working( + "https://%s" % _domain, + verify=True) + + except eipexceptions.HttpsBadCertError as exc: + logger.debug('exception') + # XXX skipping for now... + ############################################## + # We had this validation logic + # in the provider selection page before + ############################################## + #if self.trustProviderCertCheckBox.isChecked(): + #pass + #else: + wizard.set_validation_error( + prevpage, exc.usermessage) + #fingerprint = certs.get_cert_fingerprint( + #domain=domain, sep=" ") + + # it's ok if we've trusted this fgprt before + #trustedcrts = wizard.trusted_certs + #if trustedcrts and fingerprint.replace(' ', '') in trustedcrts: + #pass + #else: + # let your user face panick :P + #self.add_cert_info(fingerprint) + #self.did_cert_check = True + #self.completeChanged.emit() + #return False + pause_and_finish() + return False + + except baseexceptions.LeapException as exc: + wizard.set_validation_error( + prevpage, exc.usermessage) + pause_and_finish() + return False + + ################################## + # 3) try download provider info... + ################################## + + update_signal.emit("Downloading provider info", 70) + try: + eipconfigchecker.fetch_definition(domain=_domain) + wizard.set_providerconfig( + eipconfigchecker.defaultprovider.config) + except requests.exceptions.SSLError: + # XXX we should have catched this before. + # but cert checking is broken. + wizard.set_validation_error( + prevpage, + "Could not get info from provider.") + pause_and_finish() + return False + except requests.exceptions.ConnectionError: + wizard.set_validation_error( + prevpage, + "Could not download provider info " + "(refused conn.).") + pause_and_finish() + return False + # XXX catch more errors... + + # We're done! + pause_and_finish() + + def _do_validation(self): + """ + called after _do_checks has finished + (connected to checker thread finished signal) + """ + print 'validation...' + prevpage = "providerselection" + errors = self.wizard().get_validation_error(prevpage) + + if not errors: + self.progress.hide() + self.stepsTableWidget.hide() + self.create_info_panel() + self.show_provider_info() + + else: + logger.debug('going back with errors') + logger.debug('ERRORS: %s' % errors) + self.go_back() + + def nextId(self): + wizard = self.wizard() + next_ = "providersetupvalidation" + return wizard.get_page_index(next_) diff --git a/src/leap/gui/firstrun/providerselect.py b/src/leap/gui/firstrun/providerselect.py new file mode 100644 index 00000000..a20f40b3 --- /dev/null +++ b/src/leap/gui/firstrun/providerselect.py @@ -0,0 +1,183 @@ +""" +Select Provider Page, used in First Run Wizard +""" +import logging + +from PyQt4 import QtCore +from PyQt4 import QtGui + +#from leap.base import exceptions as baseexceptions +#from leap.crypto import certs +#from leap.eip import exceptions as eipexceptions + +from leap.gui.constants import APP_LOGO +from leap.gui.styles import ErrorLabelStyleSheet + +logger = logging.getLogger(__name__) + + +class SelectProviderPage(QtGui.QWizardPage): + def __init__(self, parent=None, providers=None): + super(SelectProviderPage, self).__init__(parent) + + self.setTitle("Enter Provider") + self.setSubTitle( + "Please enter the domain of the provider you want " + "to use for your connection." + ) + self.setPixmap( + QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(APP_LOGO)) + + self.did_cert_check = False + self.current_page = 'providerselection' + + providerNameLabel = QtGui.QLabel("h&ttps://") + # note that we expect the bare domain name + # we will add the scheme later + providerNameEdit = QtGui.QLineEdit() + providerNameEdit.cursorPositionChanged.connect( + self.reset_validation_status) + providerNameLabel.setBuddy(providerNameEdit) + + # add regex validator + providerDomainRe = QtCore.QRegExp(r"^[a-z\d_-.]+$") + providerNameEdit.setValidator( + QtGui.QRegExpValidator(providerDomainRe, self)) + self.providerNameEdit = providerNameEdit + + # Eventually we will seed a list of + # well known providers here. + + #providercombo = QtGui.QComboBox() + #if providers: + #for provider in providers: + #providercombo.addItem(provider) + #providerNameSelect = providercombo + + self.registerField("provider_domain*", self.providerNameEdit) + #self.registerField('provider_name_index', providerNameSelect) + + validationMsg = QtGui.QLabel("") + validationMsg.setStyleSheet(ErrorLabelStyleSheet) + self.validationMsg = validationMsg + + # cert info + + # this is used in the callback + # for the checkbox changes. + # tricky, since the first time came + # from the exception message. + # should get string from exception too! + self.bad_cert_status = "Server certificate could not be verified." + + self.certInfo = QtGui.QLabel("") + self.certInfo.setWordWrap(True) + self.certWarning = QtGui.QLabel("") + self.trustProviderCertCheckBox = QtGui.QCheckBox( + "&Trust this provider certificate.") + + self.trustProviderCertCheckBox.stateChanged.connect( + self.onTrustCheckChanged) + self.providerNameEdit.textChanged.connect( + self.onProviderChanged) + + layout = QtGui.QGridLayout() + layout.addWidget(validationMsg, 0, 2) + layout.addWidget(providerNameLabel, 1, 1) + layout.addWidget(providerNameEdit, 1, 2) + + # XXX get a groupbox or something.... + certinfoGroup = QtGui.QGroupBox("Certificate validation") + certinfoLayout = QtGui.QVBoxLayout() + certinfoLayout.addWidget(self.certInfo) + certinfoLayout.addWidget(self.certWarning) + certinfoLayout.addWidget(self.trustProviderCertCheckBox) + certinfoGroup.setLayout(certinfoLayout) + + layout.addWidget(certinfoGroup, 4, 1, 4, 2) + self.certinfoGroup = certinfoGroup + self.certinfoGroup.hide() + + self.setLayout(layout) + + def is_insecure_cert_trusted(self): + return self.trustProviderCertCheckBox.isChecked() + + def onTrustCheckChanged(self, state): + checked = False + if state == 2: + checked = True + + if checked: + self.reset_validation_status() + else: + self.set_validation_status(self.bad_cert_status) + + # trigger signal to redraw next button + self.completeChanged.emit() + + def onProviderChanged(self, text): + self.completeChanged.emit() + + def reset_validation_status(self): + """ + empty the validation msg + """ + self.validationMsg.setText('') + + #def set_validation_status(selF, STATUS): + #self.validationMsg.setText(status) + + def add_cert_info(self, certinfo): + self.certWarning.setText( + "Do you want to <b>trust this provider certificate?</b>") + self.certInfo.setText( + 'SHA-256 fingerprint: <i>%s</i><br>' % certinfo) + self.certInfo.setWordWrap(True) + self.certinfoGroup.show() + + # pagewizard methods + + def isComplete(self): + provider = self.providerNameEdit.text() + + if not provider: + return False + else: + if self.is_insecure_cert_trusted(): + return True + if not self.did_cert_check: + return True + return False + + def populateErrors(self): + # XXX could move this to ValidationMixin + + #logger.debug('getting errors') + errors = self.wizard().get_validation_error( + self.current_page) + if errors: + #logger.debug('errors! -> %s', errors) + self.validationMsg.setText(errors) + + def paintEvent(self, event): + """ + we hook our populate errors + on paintEvent because we need it to catch + when user enters the page coming from next, + and initializePage does not cover that case. + Maybe there's a better event to hook upon. + """ + super(SelectProviderPage, self).paintEvent(event) + self.populateErrors() + + def initializePage(self): + self.validationMsg.setText('') + self.certinfoGroup.hide() + + def nextId(self): + wizard = self.wizard() + if not wizard: + return + return wizard.get_page_index('providerinfo') diff --git a/src/leap/gui/firstrun/providersetup.py b/src/leap/gui/firstrun/providersetup.py new file mode 100644 index 00000000..2609629a --- /dev/null +++ b/src/leap/gui/firstrun/providersetup.py @@ -0,0 +1,119 @@ +""" +Provider Setup Validation Page, +used if First Run Wizard +""" + +from PyQt4 import QtGui + +from leap.gui.progress import ValidationPage + +from leap.gui.constants import APP_LOGO + + +class ProviderSetupValidationPage(ValidationPage): + def __init__(self, parent=None): + super(ProviderSetupValidationPage, self).__init__(parent) + self.setTitle("Setting up provider") + #self.setSubTitle( + #"auto configuring provider...") + + self.setPixmap( + QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(APP_LOGO)) + + def _do_checks(self, update_signal=None): + """ + executes actual checks in a separate thread + """ + import time + domain = self.field('provider_domain') + wizard = self.wizard() + pconfig = wizard.providerconfig + + pCertChecker = wizard.providercertchecker + certchecker = pCertChecker(domain=domain) + + update_signal.emit('Fetching CA certificate', 30) + + if pconfig: + ca_cert_uri = pconfig.get('ca_cert_uri').geturl() + else: + ca_cert_uri = None + + # XXX check scheme == "https" + # XXX passing verify == False because + # we have trusted right before. + # We should check it's the same domain!!! + # (Check with the trusted fingerprints dict + # or something smart) + + #certchecker.download_ca_cert( + #uri=ca_cert_uri, + #verify=False) + + time.sleep(2) + + update_signal.emit('Checking CA fingerprint', 66) + #ca_cert_fingerprint = pconfig.get('ca_cert_fingerprint', None) + + # XXX get fingerprint dict (types) + #sha256_fpr = ca_cert_fingerprint.split('=')[1] + + #validate_fpr = certchecker.check_ca_cert_fingerprint( + #fingerprint=sha256_fpr) + time.sleep(0.5) + #if not validate_fpr: + # XXX update validationMsg + # should catch exception + #return False + + update_signal.emit('Validating api certificate', 90) + + #api_uri = pconfig.get('api_uri', None) + #try: + #api_cert_verified = certchecker.verify_api_https(api_uri) + #except requests.exceptions.SSLError as exc: + #logger.error('BUG #638. %s' % exc.message) + # XXX RAISE! See #638 + # bypassing until the hostname is fixed. + # We probably should raise yet-another-warning + # here saying user that the hostname "XX.XX.XX.XX' does not + # match 'foo.bar.baz' + #api_cert_verified = True + + #if not api_cert_verified: + # XXX update validationMsg + # should catch exception + #return False + time.sleep(0.5) + #ca_cert_path = checker.ca_cert_path + + update_signal.emit('end_sentinel', 100) + time.sleep(1) + + def _do_validation(self): + """ + called after _do_checks has finished + (connected to checker thread finished signal) + """ + wizard = self.wizard() + if self.errors: + print 'going back with errors' + wizard.set_validation_error( + 'providerselection', + 'error on provider setup') + self.go_back() + else: + print 'going next' + self.go_next() + + def nextId(self): + wizard = self.wizard() + if not wizard: + return + is_signup = self.field('is_signup') + if is_signup is True: + next_ = 'signup' + if is_signup is False: + next_ = 'connecting' + return wizard.get_page_index(next_) diff --git a/src/leap/gui/firstrun/register.py b/src/leap/gui/firstrun/register.py new file mode 100644 index 00000000..0a7ba34b --- /dev/null +++ b/src/leap/gui/firstrun/register.py @@ -0,0 +1,157 @@ +""" +Register User Page, used in First Run Wizard +""" +import logging + + +from PyQt4 import QtCore +from PyQt4 import QtGui + +from leap.gui.firstrun.mixins import UserFormMixIn + +logger = logging.getLogger(__name__) + +from leap.gui.constants import APP_LOGO, BARE_USERNAME_REGEX +from leap.gui.styles import ErrorLabelStyleSheet + + +class RegisterUserPage(QtGui.QWizardPage, UserFormMixIn): + + def __init__(self, parent=None): + + super(RegisterUserPage, self).__init__(parent) + + self.setTitle("Sign Up") + + self.setPixmap( + QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(APP_LOGO)) + + self.current_page = "signup" + + userNameLabel = QtGui.QLabel("User &name:") + userNameLineEdit = QtGui.QLineEdit() + userNameLineEdit.cursorPositionChanged.connect( + self.reset_validation_status) + userNameLabel.setBuddy(userNameLineEdit) + + # let's add regex validator + usernameRe = QtCore.QRegExp(BARE_USERNAME_REGEX) + 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) + + userPassword2Label = QtGui.QLabel("Password (again):") + self.userPassword2LineEdit = QtGui.QLineEdit() + self.userPassword2LineEdit.setEchoMode( + QtGui.QLineEdit.Password) + userPassword2Label.setBuddy(self.userPassword2LineEdit) + + rememberPasswordCheckBox = QtGui.QCheckBox( + "&Remember username and password.") + rememberPasswordCheckBox.setChecked(True) + + self.registerField('userName*', self.userNameLineEdit) + self.registerField('userPassword*', self.userPasswordLineEdit) + + # XXX missing password confirmation + # XXX validator! + + 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(userPassword2Label, 3, 0) + layout.addWidget(self.userPasswordLineEdit, 2, 3) + layout.addWidget(self.userPassword2LineEdit, 3, 3) + layout.addWidget(rememberPasswordCheckBox, 4, 3, 4, 4) + self.setLayout(layout) + + # pagewizard methods + + def populateErrors(self): + # XXX could move this to ValidationMixin + + #logger.debug('getting errors') + errors = self.wizard().get_validation_error( + self.current_page) + if errors: + #logger.debug('errors! -> %s', errors) + self.validationMsg.setText(errors) + + def paintEvent(self, event): + """ + we hook our populate errors + on paintEvent because we need it to catch + when user enters the page coming from next, + and initializePage does not cover that case. + Maybe there's a better event to hook upon. + """ + super(RegisterUserPage, self).paintEvent(event) + self.populateErrors() + + def validatePage(self): + """ + we only pre-validate here password weakness + stuff, or any other client side validation + that we think of. + real server validation is made on next page, + and if any errors are thrown there we come back + and re-display the validation label. + """ + + #username = self.userNameLineEdit.text() + password = self.userPasswordLineEdit.text() + password2 = self.userPassword2LineEdit.text() + + # we better have here + # some call to a password checker... + # to assess strenght and avoid silly stuff. + + if password != password2: + self.set_validation_status('Password does not match.') + return False + + if len(password) < 6: + self.set_validation_status('Password too short.') + return False + + if password == "123456": + # joking + self.set_validation_status('Password too obvious.') + return False + + return True + + def initializePage(self): + """ + inits wizard page + """ + provider = self.field('provider_domain') + self.setSubTitle( + "Register a new user with provider %s." % + provider) + self.validationMsg.setText('') + self.userPassword2LineEdit.setText('') + + def nextId(self): + wizard = self.wizard() + if not wizard: + return + return wizard.get_page_index('signupvalidation') diff --git a/src/leap/gui/firstrun/regvalidation.py b/src/leap/gui/firstrun/regvalidation.py new file mode 100644 index 00000000..f6d69a60 --- /dev/null +++ b/src/leap/gui/firstrun/regvalidation.py @@ -0,0 +1,236 @@ +""" +Provider Setup Validation Page, +used if First Run Wizard +""" +import logging +import json +import socket + +from PyQt4 import QtGui + +import requests + +from leap.gui.progress import ValidationPage +from leap.util.web import get_https_domain_and_port + +from leap.base import auth +from leap.gui.constants import APP_LOGO, pause_for_user + +logger = logging.getLogger(__name__) + + +class RegisterUserValidationPage(ValidationPage): + + def __init__(self, parent=None): + # XXX TODO: + # We should check if we come from signup + # or login, and change title / first step + # accordingly. + + super(RegisterUserValidationPage, self).__init__(parent) + self.setTitle("User Creation") + self.setSubTitle( + "Registering account with provider.") + + self.setPixmap( + QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(APP_LOGO)) + + def _do_checks(self, update_signal=None): + """ + executes actual checks in a separate thread + + we initialize the srp protocol register + and try to register user. + """ + wizard = self.wizard() + full_domain = self.field('provider_domain') + domain, port = get_https_domain_and_port(full_domain) + + # FIXME #BUG 638 FIXME FIXME FIXME + verify = False # !!!!!!!!!!!!!!!! + # FIXME #BUG 638 FIXME FIXME FIXME + + ########################################### + # Set Credentials. + # username and password are in different fields + # if they were stored in log_in or sign_up pages. + + from_login = self.wizard().from_login + unamek_base = 'userName' + passwk_base = 'userPassword' + unamek = 'login_%s' % unamek_base if from_login else unamek_base + passwk = 'login_%s' % passwk_base if from_login else passwk_base + + username = self.field(unamek) + password = self.field(passwk) + credentials = username, password + + eipconfigchecker = wizard.eipconfigchecker() + pCertChecker = wizard.providercertchecker( + domain=domain) + + ########################################### + # XXX this only should be setup + # if not from_login. + + if wizard and wizard.debug_server: + # We're debugging + # XXX remove this branch? + dbgsrv = wizard.debug_server + schema = dbgsrv.scheme + netloc = dbgsrv.netloc + port = None + netloc_split = netloc.split(':') + if len(netloc_split) > 1: + provider, port = netloc_split + else: + provider = netloc + + signup = auth.LeapSRPRegister( + scheme=schema, + provider=provider, + port=port, + verify=verify) + + else: + # this is the real thing + signup = auth.LeapSRPRegister( + schema="https", + port=port, + provider=domain, + verify=verify) + + update_signal.emit("head_sentinel", 0) + + ################################################## + # 1) register user + ################################################## + # XXX this only should be DONE + # if NOT from_login. + + step = "register" + update_signal.emit("registering with provider", 40) + logger.debug('registering user') + + try: + ok, req = signup.register_user( + username, password) + + except socket.timeout: + self.set_error( + step, + "Error connecting to provider (timeout)") + pause_for_user() + return False + + except requests.exceptions.ConnectionError as exc: + logger.error(exc.message) + self.set_error( + step, + "Error connecting to provider " + "(connection error)") + # XXX we should signal a BAD step + pause_for_user() + update_signal.emit("connection error!", 50) + pause_for_user() + return False + + # XXX check for != OK instead??? + + if req.status_code in (404, 500): + self.set_error( + step, + "Error during registration (%s)" % req.status_code) + pause_for_user() + return False + + validation_msgs = json.loads(req.content) + errors = validation_msgs.get('errors', None) + logger.debug('validation errors: %s' % validation_msgs) + + if errors and errors.get('login', None): + # XXX this sometimes catch the blank username + # but we're not allowing that (soon) + self.set_error( + step, + 'Username not available.') + pause_for_user() + return False + + pause_for_user() + + ################################################## + # 2) fetching eip service config + ################################################## + + step = "fetch_eipconf" + fetching_eipconf_msg = "Fetching eip service configuration" + update_signal.emit(fetching_eipconf_msg, 60) + try: + eipconfigchecker.fetch_eip_service_config( + domain=full_domain) + + # XXX get specific exception + except: + self.set_error( + step, + 'Could not download eip config.') + pause_for_user() + return False + pause_for_user() + + ################################################## + # 3) getting client certificate + ################################################## + + step = "fetch_eipcert" + fetching_clientcert_msg = "Fetching eip certificate" + update_signal.emit(fetching_clientcert_msg, 80) + + try: + pCertChecker.download_new_client_cert( + credentials=credentials, + verify=verify) + + except auth.SRPAuthenticationError as exc: + self.set_error( + step, + "Authentication error: %s" % exc.message) + return False + + pause_for_user() + + ################ + # end ! + ################ + + update_signal.emit("end_sentinel", 100) + pause_for_user() + + def _do_validation(self): + """ + called after _do_checks has finished + (connected to checker thread finished signal) + """ + is_signup = self.field("is_signup") + prevpage = "signup" if is_signup else "login" + + wizard = self.wizard() + if self.errors: + print 'going back with errors' + logger.error(self.errors) + name, first_error = self.pop_first_error() + wizard.set_validation_error( + prevpage, + first_error) + self.go_back() + else: + print 'going next' + self.go_next() + + def nextId(self): + wizard = self.wizard() + if not wizard: + return + return wizard.get_page_index('lastpage') diff --git a/src/leap/gui/firstrun/tests/integration/fake_provider.py b/src/leap/gui/firstrun/tests/integration/fake_provider.py new file mode 100755 index 00000000..27886d3b --- /dev/null +++ b/src/leap/gui/firstrun/tests/integration/fake_provider.py @@ -0,0 +1,132 @@ +#/usr/bin/env python +"""A server faking some of the provider resources and apis, +used for testing Leap Client requests. + +Right needs that you create a subfolder named 'certs', +and that you place the following files: + +[ ] certs/leaptestscert.pem +[ ] certs/leaptestskey.pem +[ ] certs/cacert.pem +[ ] certs/openvpn.pem + +[ ] provider.json +[ ] eip-service.json + +""" +import json +import os +import sys + +# GnuTLS Example -- is not working as expected +from gnutls import crypto +from gnutls.constants import COMP_LZO, COMP_DEFLATE, COMP_NULL +from gnutls.interfaces.twisted import X509Credentials + +# Going with OpenSSL as a workaround instead +# But we DO NOT want to introduce this dependency. +from OpenSSL import SSL + +from twisted.web.server import Site +from twisted.web.static import File +from twisted.web.resource import Resource +from twisted.internet import reactor + +# See +# http://twistedmatrix.com/documents/current/web/howto/web-in-60/index.htmln +# for more examples + + +class FakeSession(Resource): + def __init__(self, name): + self.name = name + + def render_GET(self, request): + return json.dumps({'errors': None}) + + def render_POST(self, request): + return json.dumps( + {'salt': 'deadbeef', 'B': 'deadbeef', 'errors': None}) + + def render_PUT(self, request): + return json.dumps( + {'M2': 'deadbeef', 'errors': None}) + + +class API_Sessions(Resource): + def getChild(self, name, request): + return FakeSession(name) + + +def get_certs_path(): + script_path = os.path.realpath(os.path.dirname(sys.argv[0])) + certs_path = os.path.join(script_path, 'certs') + return certs_path + + +def get_TLS_credentials(): + # XXX this is giving errors + # XXX REview! We want to use gnutls! + certs_path = get_certs_path() + + cert = crypto.X509Certificate( + open(certs_path + '/leaptestscert.pem').read()) + key = crypto.X509PrivateKey( + open(certs_path + '/leaptestskey.pem').read()) + ca = crypto.X509Certificate( + open(certs_path + '/cacert.pem').read()) + #crl = crypto.X509CRL(open(certs_path + '/crl.pem').read()) + #cred = crypto.X509Credentials(cert, key, [ca], [crl]) + cred = X509Credentials(cert, key, [ca]) + cred.verify_peer = True + cred.session_params.compressions = (COMP_LZO, COMP_DEFLATE, COMP_NULL) + return cred + + +class OpenSSLServerContextFactory: + # XXX workaround for broken TLS interface + # from gnuTLS. + + def getContext(self): + """Create an SSL context. + This is a sample implementation that loads a certificate from a file + called 'server.pem'.""" + certs_path = get_certs_path() + + ctx = SSL.Context(SSL.SSLv23_METHOD) + ctx.use_certificate_file(certs_path + '/leaptestscert.pem') + ctx.use_privatekey_file(certs_path + '/leaptestskey.pem') + return ctx + + +if __name__ == "__main__": + + from twisted.python import log + log.startLogging(sys.stdout) + + root = Resource() + root.putChild("provider.json", File("./provider.json")) + config = Resource() + config.putChild( + "eip-service.json", + File("./eip-service.json")) + apiv1 = Resource() + apiv1.putChild("config", config) + apiv1.putChild("sessions.json", API_Sessions()) + apiv1.putChild("cert", File(get_certs_path() + '/openvpn.pem')) + root.putChild("1", apiv1) + + cred = get_TLS_credentials() + + factory = Site(root) + + # regular http + reactor.listenTCP(8000, factory) + + # TLS with gnutls --- seems broken :( + #reactor.listenTLS(8003, factory, cred) + + # OpenSSL + reactor.listenSSL(8443, factory, OpenSSLServerContextFactory()) + + reactor.run() diff --git a/src/leap/gui/firstrun/wizard.py b/src/leap/gui/firstrun/wizard.py new file mode 100755 index 00000000..7a3d6973 --- /dev/null +++ b/src/leap/gui/firstrun/wizard.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python +import logging + +import sip +sip.setapi('QString', 2) +sip.setapi('QVariant', 2) + +from PyQt4 import QtCore +from PyQt4 import QtGui + +from leap.base import checks as basechecks +from leap.crypto import leapkeyring +from leap.eip import checks as eipchecks + +from leap.gui import firstrun + +from leap.gui import mainwindow_rc + +try: + from collections import OrderedDict +except ImportError: + # We must be in 2.6 + from leap.util.dicts import OrderedDict + +logger = logging.getLogger(__name__) + +""" +~~~~~~~~~~~~~~~~~~~~~~~~~~ +Work in progress! +~~~~~~~~~~~~~~~~~~~~~~~~~~ +This wizard still needs to be refactored out. + +TODO-ish: + +[X] Break file in wizard / pages files (and its own folder). +[ ] Separate presentation from logic. +[ ] Have a "manager" class for connections, that can be + dep-injected for testing. +[ ] Document signals used / expected. +[ ] Separate style from widgets. +[ ] Fix TOFU Widget for provider cert. +[ ] Refactor widgets out. +[ ] Follow more MVC style. +[ ] Maybe separate "first run wizard" into different wizards + that share some of the pages? +""" + + +class FirstRunWizard(QtGui.QWizard): + + def __init__( + self, + conductor_instance, + parent=None, + eip_username=None, + providers=None, + success_cb=None, is_provider_setup=False, + trusted_certs=None, + netchecker=basechecks.LeapNetworkChecker, + providercertchecker=eipchecks.ProviderCertChecker, + eipconfigchecker=eipchecks.EIPConfigChecker, + start_eipconnection_signal=None, + eip_statuschange_signal=None, + debug_server=None): + super(FirstRunWizard, self).__init__( + parent, + QtCore.Qt.WindowStaysOnTopHint) + + # we keep a reference to the conductor + # to be able to launch eip checks and connection + # in the connection page, before the wizard has ended. + self.conductor = conductor_instance + + self.eip_username = eip_username + self.providers = providers + + # success callback + self.success_cb = success_cb + + # is provider setup? + self.is_provider_setup = is_provider_setup + + # a dict with trusted fingerprints + # in the form {'nospacesfingerprint': ['host1', 'host2']} + self.trusted_certs = trusted_certs + + # Checkers + self.netchecker = netchecker + self.providercertchecker = providercertchecker + self.eipconfigchecker = eipconfigchecker + + # debug server + self.debug_server = debug_server + + # Signals + # will be emitted in connecting page + self.start_eipconnection_signal = start_eipconnection_signal + self.eip_statuschange_signal = eip_statuschange_signal + + self.providerconfig = None + # previously registered + # if True, jumps to LogIn page. + # by setting 1st page?? + #self.is_previously_registered = is_previously_registered + # XXX ??? ^v + self.is_previously_registered = bool(self.eip_username) + self.from_login = False + + pages_dict = OrderedDict(( + ('intro', firstrun.intro.IntroPage), + ('providerselection', + firstrun.providerselect.SelectProviderPage), + ('login', firstrun.login.LogInPage), + ('providerinfo', firstrun.providerinfo.ProviderInfoPage), + ('providersetupvalidation', + firstrun.providersetup.ProviderSetupValidationPage), + ('signup', firstrun.register.RegisterUserPage), + ('signupvalidation', + firstrun.regvalidation.RegisterUserValidationPage), + ('connecting', firstrun.connect.ConnectingPage), + ('lastpage', firstrun.last.LastPage) + )) + self.add_pages_from_dict(pages_dict) + + self.validation_errors = {} + + 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 add_pages_from_dict(self, pages_dict): + """ + @param pages_dict: the dictionary with pages, where + values are a tuple of InstanceofWizardPage, kwargs. + @type pages_dict: dict + """ + for name, page in pages_dict.items(): + # XXX check for is_previously registered + # and skip adding the signup branch if so + self.addPage(page()) + self.pages_dict = pages_dict + + def get_page_index(self, page_name): + """ + returns the index of the given page + @param page_name: the name of the desired page + @type page_name: str + @rparam: index of page in wizard + @rtype: int + """ + return self.pages_dict.keys().index(page_name) + + def set_validation_error(self, pagename, error): + self.validation_errors[pagename] = error + + def get_validation_error(self, pagename): + return self.validation_errors.get(pagename, None) + + def set_providerconfig(self, providerconfig): + self.providerconfig = providerconfig + + 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 if any has been passed. + """ + super(FirstRunWizard, self).accept() + + # username and password are in different fields + # if they were stored in log_in or sign_up pages. + from_login = self.from_login + unamek_base = 'userName' + passwk_base = 'userPassword' + unamek = 'login_%s' % unamek_base if from_login else unamek_base + passwk = 'login_%s' % passwk_base if from_login else passwk_base + + username = self.field(unamek) + password = self.field(passwk) + provider = self.field('provider_domain') + remember_pass = self.field('rememberPassword') + + logger.debug('chosen provider: %s', provider) + logger.debug('username: %s', username) + logger.debug('remember password: %s', remember_pass) + + # we are assuming here that we only remember one username + # in the form username@provider.domain + # We probably could extend this to support some form of + # profiles. + + settings = QtCore.QSettings() + + settings.setValue("FirstRunWizardDone", True) + settings.setValue("provider_domain", provider) + full_username = "%s@%s" % (username, provider) + + settings.setValue("remember_user_and_pass", remember_pass) + + if remember_pass: + settings.setValue("eip_username", full_username) + seed = self.get_random_str(10) + settings.setValue("%s_seed" % provider, seed) + + # XXX #744: comment out for 0.2.0 release + # if we need to have a version of python-keyring < 0.9 + leapkeyring.leap_set_password( + full_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_by_index(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)) + + +if __name__ == '__main__': + # standalone test + # it can be (somehow) run against + # gui/tests/integration/fake_user_signup.py + + import sys + import logging + logging.basicConfig() + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + + app = QtGui.QApplication(sys.argv) + server = sys.argv[1] if len(sys.argv) > 1 else None + + trusted_certs = { + "3DF83F316BFA0186" + "0A11A5C9C7FC24B9" + "18C62B941192CC1A" + "49AE62218B2A4B7C": ['springbok']} + + wizard = FirstRunWizard( + None, trusted_certs=trusted_certs, + debug_server=server) + wizard.show() + sys.exit(app.exec_()) diff --git a/src/leap/gui/firstrunwizard.py b/src/leap/gui/firstrunwizard.py deleted file mode 100755 index d9e33f7e..00000000 --- a/src/leap/gui/firstrunwizard.py +++ /dev/null @@ -1,1097 +0,0 @@ -#!/usr/bin/env python -import logging -import json -import socket - -import requests - -import sip -sip.setapi('QString', 2) -sip.setapi('QVariant', 2) - -from PyQt4 import QtCore -from PyQt4 import QtGui - -from leap.base.auth import LeapSRPRegister -from leap.base import checks as basechecks -from leap.base import exceptions as baseexceptions -from leap.crypto import certs -from leap.crypto import leapkeyring -from leap.eip import checks as eipchecks -from leap.eip import exceptions as eipexceptions -from leap.gui import mainwindow_rc - -try: - from collections import OrderedDict -except ImportError: - # We must be in 2.6 - from leap.util.dicts import OrderedDict - -# XXX DEBUG -logging.basicConfig() -logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) - -APP_LOGO = ':/images/leap-color-small.png' - - -ErrorLabelStyleSheet = """ -QLabel { color: red; - font-weight: bold} -""" - - -class FirstRunWizard(QtGui.QWizard): - - def __init__( - self, parent=None, providers=None, - success_cb=None, is_provider_setup=False, - is_previously_registered=False, - trusted_certs=None, - netchecker=basechecks.LeapNetworkChecker, - providercertchecker=eipchecks.ProviderCertChecker, - eipconfigchecker=eipchecks.EIPConfigChecker): - 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 - - # is provider setup? - self.is_provider_setup = is_provider_setup - - # previously registered - # if True, jumps to LogIn page. - self.is_previously_registered = is_previously_registered - - # a dict with trusted fingerprints - # in the form {'nospacesfingerprint': ['host1', 'host2']} - self.trusted_certs = trusted_certs - - # Checkers - self.netchecker = netchecker - self.providercertchecker = providercertchecker - self.eipconfigchecker = eipconfigchecker - - self.providerconfig = None - - # FIXME add param for previously_registered - # should start at login page. - - pages_dict = OrderedDict(( - # (name, WizardPage) - ('intro', IntroPage), - ('providerselection', - SelectProviderPage), - ('login', LogInPage), - ('providerinfo', ProviderInfoPage), - ('providersetup', ProviderSetupPage), - ('signup', RegisterUserPage), - ('connecting', ConnectingPage), - ('lastpage', LastPage) - )) - self.add_pages_from_dict(pages_dict) - - 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 add_pages_from_dict(self, pages_dict): - """ - @param pages_dict: the dictionary with pages, where - values are a tuple of InstanceofWizardPage, kwargs. - @type pages_dict: dict - """ - for name, page in pages_dict.items(): - # XXX check for is_previously registered - # and skip adding the signup branch if so - self.addPage(page()) - self.pages_dict = pages_dict - - def get_page_index(self, page_name): - """ - returns the index of the given page - @param page_name: the name of the desired page - @type page_name: str - @rparam: index of page in wizard - @rtype: int - """ - return self.pages_dict.keys().index(page_name) - - def set_providerconfig(self, providerconfig): - self.providerconfig = providerconfig - - 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.field('provider_domain') - 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) - - # Commenting out for 0.2.0 release - # since we did not fix #744 on time. - - #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_by_index(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')) - - self.setPixmap( - QtGui.QWizard.LogoPixmap, - QtGui.QPixmap(APP_LOGO)) - - 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.<br><br>" - "Do you want to <b>sign up</b> for a new account, or <b>log " - "in</b> with an already existing username?<br>") - label.setWordWrap(True) - - radiobuttonGroup = QtGui.QGroupBox() - - self.sign_up = QtGui.QRadioButton( - "Sign up for a new account.") - self.sign_up.setChecked(True) - self.log_in = QtGui.QRadioButton( - "Log In with my credentials.") - - radiobLayout = QtGui.QVBoxLayout() - radiobLayout.addWidget(self.sign_up) - radiobLayout.addWidget(self.log_in) - radiobuttonGroup.setLayout(radiobLayout) - - layout = QtGui.QVBoxLayout() - layout.addWidget(label) - layout.addWidget(radiobuttonGroup) - self.setLayout(layout) - - self.registerField('is_signup', self.sign_up) - - def validatePage(self): - return True - - def nextId(self): - """ - returns next id - in a non-linear wizard - """ - if self.sign_up.isChecked(): - next_ = 'providerselection' - if self.log_in.isChecked(): - next_ = 'login' - wizard = self.wizard() - return wizard.get_page_index(next_) - - -class SelectProviderPage(QtGui.QWizardPage): - def __init__(self, parent=None, providers=None): - super(SelectProviderPage, self).__init__(parent) - - self.setTitle("Enter Provider") - self.setSubTitle( - "Please enter the domain of the provider you want " - "to use for your connection." - ) - self.setPixmap( - QtGui.QWizard.LogoPixmap, - QtGui.QPixmap(APP_LOGO)) - - self.did_cert_check = False - - providerNameLabel = QtGui.QLabel("h&ttps://") - # note that we expect the bare domain name - # we will add the scheme later - providerNameEdit = QtGui.QLineEdit() - providerNameEdit.cursorPositionChanged.connect( - self.reset_validation_status) - providerNameLabel.setBuddy(providerNameEdit) - - # add regex validator - providerDomainRe = QtCore.QRegExp(r"^[a-z\d_-.]+$") - providerNameEdit.setValidator( - QtGui.QRegExpValidator(providerDomainRe, self)) - self.providerNameEdit = providerNameEdit - - # Eventually we will seed a list of - # well known providers here. - - #providercombo = QtGui.QComboBox() - #if providers: - #for provider in providers: - #providercombo.addItem(provider) - #providerNameSelect = providercombo - - self.registerField('provider_domain*', self.providerNameEdit) - #self.registerField('provider_name_index', providerNameSelect) - - validationMsg = QtGui.QLabel("") - validationMsg.setStyleSheet(ErrorLabelStyleSheet) - self.validationMsg = validationMsg - - # cert info - - # this is used in the callback - # for the checkbox changes. - # tricky, since the first time came - # from the exception message. - # should get string from exception too! - self.bad_cert_status = "Server certificate could not be verified." - - self.certInfo = QtGui.QLabel("") - self.certInfo.setWordWrap(True) - self.certWarning = QtGui.QLabel("") - self.trustProviderCertCheckBox = QtGui.QCheckBox( - "&Trust this provider certificate.") - - self.trustProviderCertCheckBox.stateChanged.connect( - self.onTrustCheckChanged) - - layout = QtGui.QGridLayout() - layout.addWidget(validationMsg, 0, 2) - layout.addWidget(providerNameLabel, 1, 1) - layout.addWidget(providerNameEdit, 1, 2) - - # XXX get a groupbox or something.... - certinfoGroup = QtGui.QGroupBox("Certificate validation") - certinfoLayout = QtGui.QVBoxLayout() - certinfoLayout.addWidget(self.certInfo) - certinfoLayout.addWidget(self.certWarning) - certinfoLayout.addWidget(self.trustProviderCertCheckBox) - certinfoGroup.setLayout(certinfoLayout) - - layout.addWidget(certinfoGroup, 4, 1, 4, 2) - self.certinfoGroup = certinfoGroup - self.certinfoGroup.hide() - - #layout.addWidget(self.certInfo, 4, 1, 4, 2) - #layout.addWidget(self.certWarning, 6, 1, 6, 2) - #layout.addWidget( - #self.trustProviderCertCheckBox, - #8, 1, 8, 2) - - #self.trustProviderCertCheckBox.hide() - self.setLayout(layout) - - def is_insecure_cert_trusted(self): - return self.trustProviderCertCheckBox.isChecked() - - def onTrustCheckChanged(self, state): - checked = False - if state == 2: - checked = True - - if checked: - self.reset_validation_status() - else: - self.set_validation_status(self.bad_cert_status) - - # trigger signal to redraw next button - self.completeChanged.emit() - - def reset_validation_status(self): - """ - empty the validation msg - """ - self.validationMsg.setText('') - - def set_validation_status(self, status): - self.validationMsg.setText(status) - - def add_cert_info(self, certinfo): - self.certWarning.setText( - "Do you want to <b>trust this provider certificate?</b>") - self.certInfo.setText( - 'SHA-256 fingerprint: <i>%s</i><br>' % certinfo) - self.certInfo.setWordWrap(True) - self.certinfoGroup.show() - - # pagewizard methods - - def isComplete(self): - if not self.did_cert_check: - return True - if self.is_insecure_cert_trusted(): - return True - return False - - def initializePage(self): - self.certinfoGroup.hide() - - def validatePage(self): - wizard = self.wizard() - netchecker = wizard.netchecker() - providercertchecker = wizard.providercertchecker() - eipconfigchecker = wizard.eipconfigchecker() - - domain = self.providerNameEdit.text() - - # try name resolution - try: - netchecker.check_name_resolution( - domain) - - except baseexceptions.LeapException as exc: - self.set_validation_status(exc.usermessage) - return False - - # try https connection - try: - providercertchecker.is_https_working( - "https://%s" % domain, - verify=True) - - except eipexceptions.HttpsBadCertError as exc: - if self.trustProviderCertCheckBox.isChecked(): - pass - else: - self.set_validation_status(exc.usermessage) - fingerprint = certs.get_cert_fingerprint( - domain=domain, sep=" ") - - # it's ok if we've trusted this fgprt before - trustedcrts = self.wizard().trusted_certs - if trustedcrts and fingerprint.replace(' ', '') in trustedcrts: - pass - else: - # let your user face panick :P - self.add_cert_info(fingerprint) - self.did_cert_check = True - self.completeChanged.emit() - return False - - except baseexceptions.LeapException as exc: - self.set_validation_status(exc.usermessage) - return False - - # try download provider info... - eipconfigchecker.fetch_definition(domain=domain) - wizard.set_providerconfig( - eipconfigchecker.defaultprovider.config) - - # all ok, go on... - return True - - def nextId(self): - wizard = self.wizard() - if not wizard: - return - return wizard.get_page_index('providerinfo') - - -class ProviderInfoPage(QtGui.QWizardPage): - def __init__(self, parent=None): - super(ProviderInfoPage, self).__init__(parent) - - self.setTitle("Provider Info") - self.setSubTitle("Available information about chosen provider.") - - self.setPixmap( - QtGui.QWizard.LogoPixmap, - QtGui.QPixmap(APP_LOGO)) - - displayName = QtGui.QLabel("") - description = QtGui.QLabel("") - enrollment_policy = QtGui.QLabel("") - # XXX set stylesheet... - # prettify a little bit. - # bigger fonts and so on... - self.displayName = displayName - self.description = description - self.enrollment_policy = enrollment_policy - - layout = QtGui.QGridLayout() - layout.addWidget(displayName, 0, 1) - layout.addWidget(description, 1, 1) - layout.addWidget(enrollment_policy, 2, 1) - - self.setLayout(layout) - - def initializePage(self): - # XXX get multilingual objects - # directly from the config object - - lang = "en" - pconfig = self.wizard().providerconfig - - dn = pconfig.get('display_name') - display_name = dn[lang] if dn else '' - self.displayName.setText( - "<b>%s</b>" % display_name) - - desc = pconfig.get('description') - description_text = desc[lang] if desc else '' - self.description.setText( - "<i>%s</i>" % description_text) - - enroll = pconfig.get('enrollment_policy') - if enroll: - self.enrollment_policy.setText( - 'enrollment policy: %s' % enroll) - - def nextId(self): - wizard = self.wizard() - if not wizard: - return - return wizard.get_page_index('providersetup') - - -class ProviderSetupPage(QtGui.QWizardPage): - def __init__(self, parent=None): - super(ProviderSetupPage, self).__init__(parent) - - self.setTitle("Provider Setup") - self.setSubTitle("Setting up provider.") - - self.setPixmap( - QtGui.QWizard.LogoPixmap, - QtGui.QPixmap(APP_LOGO)) - - self.status = QtGui.QLabel("") - self.progress = QtGui.QProgressBar() - self.progress.setMaximum(100) - self.progress.hide() - - layout = QtGui.QGridLayout() - layout.addWidget(self.status, 0, 1) - layout.addWidget(self.progress, 5, 1) - - self.setLayout(layout) - - def set_status(self, status): - self.status.setText(status) - self.status.setWordWrap(True) - - def fetch_and_validate(self): - # Fake... till you make it... - import time - domain = self.field('provider_domain') - wizard = self.wizard() - pconfig = wizard.providerconfig - pCertChecker = wizard.providercertchecker - certchecker = pCertChecker(domain=domain) - - self.set_status('Fetching CA certificate') - self.progress.setValue(30) - ca_cert_uri = pconfig.get('ca_cert_uri').geturl() - - # XXX check scheme == "https" - # XXX passing verify == False because - # we have trusted right before. - # We should check it's the same domain!!! - # (Check with the trusted fingerprints dict - # or something smart) - - certchecker.download_ca_cert( - uri=ca_cert_uri, - verify=False) - - self.set_status('Checking CA fingerprint') - self.progress.setValue(66) - ca_cert_fingerprint = pconfig.get('ca_cert_fingerprint', None) - - # XXX get fingerprint dict (types) - sha256_fpr = ca_cert_fingerprint.split('=')[1] - - validate_fpr = certchecker.check_ca_cert_fingerprint( - fingerprint=sha256_fpr) - time.sleep(0.5) - if not validate_fpr: - # XXX update validationMsg - # should catch exception - return False - - self.set_status('Validating api certificate') - self.progress.setValue(90) - - api_uri = pconfig.get('api_uri', None) - try: - api_cert_verified = certchecker.verify_api_https(api_uri) - except requests.exceptions.SSLError as exc: - logger.error('BUG #638. %s' % exc.message) - # XXX RAISE! See #638 - # bypassing until the hostname is fixed. - # We probably should raise yet-another-warning - # here saying user that the hostname "XX.XX.XX.XX' does not - # match 'foo.bar.baz' - api_cert_verified = True - - if not api_cert_verified: - # XXX update validationMsg - # should catch exception - return False - time.sleep(0.5) - #ca_cert_path = checker.ca_cert_path - - self.progress.setValue(100) - time.sleep(1) - - # pagewizard methods - - def initializePage(self): - self.set_status( - 'We are going to contact the provider to get ' - 'the certificates that will be used to stablish ' - 'a secure connection.<br><br>Click <i>next</i> to continue.') - self.progress.setValue(0) - self.progress.hide() - - def validatePage(self): - self.progress.show() - self.fetch_and_validate() - - return True - - def nextId(self): - wizard = self.wizard() - if not wizard: - return - is_signup = self.field('is_signup') - if is_signup is True: - next_ = 'signup' - if is_signup is False: - next_ = 'connecting' - return wizard.get_page_index(next_) - - -class UserFormMixIn(object): - - def reset_validation_status(self): - """ - empty the validation msg - """ - self.validationMsg.setText('') - - def set_validation_status(self, msg): - """ - set generic validation status - """ - self.validationMsg.setText(msg) - - # XXX Refactor all these validation msgs!!! - - 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... - self.validationMsg.setText('registering...') - - 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_connerror(self): - """ - set validation msg to - connection refused - """ - self.validationMsg.setText( - "Error connecting to provider " - "(connection error)") - - def set_status_unknown_error(self): - """ - set validation msg to - unknown error - """ - self.validationMsg.setText("Error during sign up") - - -class LogInPage(QtGui.QWizardPage, UserFormMixIn): - def __init__(self, parent=None): - super(LogInPage, self).__init__(parent) - - self.setTitle("Log In") - self.setSubTitle("Log in with your credentials.") - - self.setPixmap( - QtGui.QWizard.LogoPixmap, - QtGui.QPixmap(APP_LOGO)) - - 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('log_in_userName*', self.userNameLineEdit) - self.registerField('log_in_userPassword*', self.userPasswordLineEdit) - - 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) - - self.setLayout(layout) - - def nextId(self): - wizard = self.wizard() - if not wizard: - return - if wizard.is_provider_setup is True: - next_ = 'connecting' - if wizard.is_provider_setup is False: - next_ = 'providersetup' - return wizard.get_page_index(next_) - - -class RegisterUserPage(QtGui.QWizardPage, UserFormMixIn): - setSigningUpStatus = QtCore.pyqtSignal([]) - - def __init__(self, parent=None): - super(RegisterUserPage, self).__init__(parent) - - # bind wizard page signals - self.setSigningUpStatus.connect( - self.set_status_validating) - - self.setTitle("Sign Up") - - self.setPixmap( - QtGui.QWizard.LogoPixmap, - QtGui.QPixmap(APP_LOGO)) - - 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) - - userPassword2Label = QtGui.QLabel("Password (again):") - self.userPassword2LineEdit = QtGui.QLineEdit() - self.userPassword2LineEdit.setEchoMode( - QtGui.QLineEdit.Password) - userPassword2Label.setBuddy(self.userPassword2LineEdit) - - rememberPasswordCheckBox = QtGui.QCheckBox( - "&Remember username and password.") - rememberPasswordCheckBox.setChecked(True) - - self.registerField('userName*', self.userNameLineEdit) - self.registerField('userPassword*', self.userPasswordLineEdit) - - # XXX missing password confirmation - # XXX validator! - - 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(userPassword2Label, 3, 0) - layout.addWidget(self.userPasswordLineEdit, 2, 3) - layout.addWidget(self.userPassword2LineEdit, 3, 3) - layout.addWidget(rememberPasswordCheckBox, 4, 3, 4, 4) - self.setLayout(layout) - - # overwritten methods - - def initializePage(self): - """ - inits wizard page - """ - provider = self.field('provider_domain') - self.setSubTitle( - "Register a new user with provider %s." % - provider) - 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() - password2 = self.userPassword2LineEdit.text() - - # have some call to a password checker... - - if password != password2: - self.set_validation_status('Password does not match.') - return False - - if len(password) < 6: - self.set_validation_status('Password too short.') - return False - - if password == "123456": - # XD - self.set_validation_status('Password too obvious.') - return False - - # XXX TODO -- remove debug info - # XXX get from provider info - # XXX enforce https - # and pass a verify value - - domain = self.field('provider_domain') - - signup = LeapSRPRegister( - schema="http", - provider=domain, - - # debug ----- - #provider="localhost", - #register_path="timeout", - #port=8000 - ) - try: - ok, req = signup.register_user(username, password) - except socket.timeout: - self.set_status_timeout() - return False - - except requests.exceptions.ConnectionError as exc: - logger.error(exc) - self.set_status_connerror() - 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): - # XXX this sometimes catch the blank username - # but we're not allowing that (soon) - self.set_status_invalid_username() - else: - self.set_status_unknown_error() - return False - - def nextId(self): - wizard = self.wizard() - if not wizard: - return - return wizard.get_page_index('connecting') - - -class GlobalEIPSettings(QtGui.QWizardPage): - """ - not in use right now - """ - def __init__(self, parent=None): - super(GlobalEIPSettings, self).__init__(parent) - - -class ConnectingPage(QtGui.QWizardPage): - def __init__(self, parent=None): - super(ConnectingPage, self).__init__(parent) - - self.setTitle("Connecting") - self.setSubTitle('Connecting to provider.') - - self.setPixmap( - QtGui.QWizard.LogoPixmap, - QtGui.QPixmap(APP_LOGO)) - - self.status = QtGui.QLabel("") - self.status.setWordWrap(True) - self.progress = QtGui.QProgressBar() - self.progress.setMaximum(100) - self.progress.hide() - - self.status_line_1 = QtGui.QLabel() - self.status_line_2 = QtGui.QLabel() - self.status_line_3 = QtGui.QLabel() - self.status_line_4 = QtGui.QLabel() - - layout = QtGui.QGridLayout() - layout.addWidget(self.status, 0, 1) - layout.addWidget(self.progress, 5, 1) - layout.addWidget(self.status_line_1, 8, 1) - layout.addWidget(self.status_line_2, 9, 1) - layout.addWidget(self.status_line_3, 10, 1) - layout.addWidget(self.status_line_4, 11, 1) - - self.setLayout(layout) - - def set_status(self, status): - self.status.setText(status) - self.status.setWordWrap(True) - - def get_donemsg(self, msg): - return "%s ... done" % msg - - def fetch_and_validate(self): - # Fake... till you make it... - import time - domain = self.field('provider_domain') - wizard = self.wizard() - #pconfig = wizard.providerconfig - eipconfigchecker = wizard.eipconfigchecker() - pCertChecker = wizard.providercertchecker( - domain=domain) - - # XXX get from log_in page if we came that way - # instead - - username = self.field('userName') - password = self.field('userPassword') - - credentials = username, password - - self.progress.show() - - fetching_eip_conf_msg = 'Fetching eip service configuration' - self.set_status(fetching_eip_conf_msg) - self.progress.setValue(30) - - # Fetching eip service - eipconfigchecker.fetch_eip_service_config( - domain=domain) - - self.status_line_1.setText( - self.get_donemsg(fetching_eip_conf_msg)) - - getting_client_cert_msg = 'Getting client certificate' - self.set_status(getting_client_cert_msg) - self.progress.setValue(66) - - # Download cert - pCertChecker.download_new_client_cert( - credentials=credentials) - - time.sleep(2) - self.status_line_2.setText( - self.get_donemsg(getting_client_cert_msg)) - - validating_clientcert_msg = 'Validating client certificate' - self.set_status(validating_clientcert_msg) - self.progress.setValue(90) - time.sleep(2) - self.status_line_3.setText( - self.get_donemsg(validating_clientcert_msg)) - - self.progress.setValue(100) - time.sleep(3) - - return True - - # pagewizard methods - - def initializePage(self): - # XXX if we're coming from signup page - # we could say something like - # 'registration successful!' - self.status.setText( - "We have " - "all we need to connect with the provider.<br><br> " - "Click <i>next</i> to continue. ") - self.progress.setValue(0) - self.progress.hide() - self.status_line_1.setText('') - self.status_line_2.setText('') - self.status_line_3.setText('') - - def validatePage(self): - validated = self.fetch_and_validate() - return validated - - -class LastPage(QtGui.QWizardPage): - def __init__(self, parent=None): - super(LastPage, self).__init__(parent) - - self.setTitle("Ready to go!") - - self.setPixmap( - QtGui.QWizard.LogoPixmap, - QtGui.QPixmap(APP_LOGO)) - - #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) - - trusted_certs = { - "3DF83F316BFA0186" - "0A11A5C9C7FC24B9" - "18C62B941192CC1A" - "49AE62218B2A4B7C": ['springbok']} - - wizard = FirstRunWizard(trusted_certs=trusted_certs) - wizard.show() - sys.exit(app.exec_()) diff --git a/src/leap/gui/mainwindow_rc.py b/src/leap/gui/mainwindow_rc.py index be575159..63e9f6be 100644 --- a/src/leap/gui/mainwindow_rc.py +++ b/src/leap/gui/mainwindow_rc.py @@ -2,7 +2,7 @@ # Resource object code # -# Created: Thu Sep 13 16:12:58 2012 +# Created: Tue Nov 6 01:22:11 2012 # by: The Resource Compiler for PyQt (Qt v4.8.2) # # WARNING! All changes made in this file will be lost! @@ -1491,6 +1491,94 @@ qt_resource_data = "\ \xc3\x25\x0d\x25\x35\x01\xd7\x0f\x5b\xb5\x7e\x8e\x93\x83\xff\x0f\ \x92\x04\x28\x92\xfd\x58\xc9\xac\x00\x00\x00\x00\x49\x45\x4e\x44\ \xae\x42\x60\x82\ +\x00\x00\x05\x5f\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x18\x00\x00\x00\x18\x08\x06\x00\x00\x00\xe0\x77\x3d\xf8\ +\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x00\x8e\x00\x00\x00\x8e\ +\x01\x6b\xdf\xd6\xc9\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\ +\x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\ +\x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x04\xdc\x49\x44\ +\x41\x54\x48\x89\x8d\x95\x79\x6c\x54\x55\x14\xc6\x7f\xf7\xce\x4c\ +\x3b\xa5\xed\x14\x6b\x3b\x25\x42\x17\x18\x28\x65\xba\x90\x6e\x18\ +\xa3\xc5\x05\xa4\x86\x20\x21\xa0\x0d\x88\x24\x26\x98\x10\x28\x68\ +\x42\x40\x8c\x20\x1a\xa3\x16\x43\x48\x8c\x9a\x50\x08\xc6\x14\xad\ +\x9a\x22\x8d\xa2\xa6\x01\x42\x13\x20\x44\x49\x3b\x2c\x5d\x87\xd2\ +\x05\x64\xb1\xad\x85\x76\xa6\xed\x74\x9b\xf7\xae\x7f\xcc\x62\x9b\ +\x69\x2b\xe7\x9f\x97\x73\x5e\xce\xf7\x9d\x7b\xce\x77\xcf\x15\x4a\ +\x29\xa6\xb3\x95\x55\x0b\x2c\xe1\x26\x73\x8e\x80\x3c\x14\xb9\x20\ +\x24\x4a\xd4\x02\x0e\xa3\x34\x39\x2a\x96\xd7\xba\xa6\xcb\x17\x53\ +\x11\xac\x3b\x97\x91\xaf\x94\x2c\x45\x91\x0d\x88\x29\xf2\x15\xd0\ +\xa0\xeb\xa2\xf8\xe7\xc2\xeb\x17\x1f\x89\xa0\xe8\x44\x46\x98\x77\ +\xa6\xf8\x00\xe4\x1e\x83\x30\x18\x72\xe3\x0a\x48\x8d\x49\xc7\x66\ +\x49\xe7\x71\x73\x02\x12\xc9\x83\x91\x2e\x5a\x5d\x8d\xdc\x74\x37\ +\x52\xdb\x73\x01\xaf\x3e\xa6\x03\x9f\xc7\x98\xfa\xf7\x7e\xf3\x5c\ +\xc7\xf0\x94\x04\xaf\x9c\xc9\x4a\xd3\x05\x15\x40\x66\x72\x54\x2a\ +\xc5\xf6\xfd\xb4\xba\xae\xd2\xe6\xba\xc6\xbd\x81\x16\x06\x46\x7b\ +\x30\x00\xb1\x66\x2b\x89\x51\x69\x24\x5b\x32\x48\xb6\x64\x73\xd8\ +\xf9\x09\x6d\xee\x66\x80\x66\x29\xd5\xfa\x9f\x96\xd5\xd7\x85\x10\ +\xf8\x2a\x97\xb5\x52\xc8\xcc\x75\x29\x6f\x92\x1f\xbf\x94\x32\xe7\ +\xc7\x74\x79\xda\x31\x08\x81\x44\x60\x10\x02\x03\x4c\xf0\xe3\xcc\ +\x4f\xb0\x7a\xfe\x1e\xea\xfa\x1c\xfc\xd8\x76\x04\x4d\xd7\xda\xbc\ +\xa3\x61\x59\xa7\x56\xd5\x7a\x00\x64\x80\xc9\xd7\x16\x32\xd7\xa6\ +\x6c\x26\xde\x6c\xa5\xe4\xca\x66\xee\x0f\xb6\x4d\xd1\xfa\xff\xac\ +\x77\xf8\x6f\xca\x1b\x77\x32\x2b\x2c\x9a\x8d\xf3\x8b\x41\x60\x33\ +\x86\x8f\x95\x04\xfe\x4b\xf0\x0d\x14\xe4\x9e\xe4\xa8\x54\xf2\xe3\ +\x9f\xa5\xbc\xe5\x20\x9a\xd2\xfe\x17\x1c\x40\x20\x88\x09\x4f\xe0\ +\xcf\xfb\x15\x64\xc7\x2e\x21\x35\x26\x13\x50\x3b\xd6\x9c\x5e\x5c\ +\x00\x60\x04\x50\x4a\x96\x1a\x84\xc1\xb0\xd5\xbe\x9f\x63\xcd\x1f\ +\xa1\x29\x0d\x39\x95\x6e\xc6\xd9\x9c\xa8\x34\x5e\xb6\xed\x24\xc9\ +\x92\xc1\xe0\xd8\x43\xca\xea\xb7\xb3\xdd\xfe\x21\xbb\x2e\x6f\x14\ +\xa3\x8c\x94\x02\xe9\x72\x65\xd5\x02\x0b\x8a\xec\xdc\xb8\x02\x9c\ +\xbd\x0e\xee\x3e\x42\x5b\x00\x56\xcd\xdb\xc1\xb6\xec\x63\x24\x59\ +\x32\x00\xb8\xe3\x6e\xe0\xe1\xd0\x1d\x3a\x7a\x2f\xf1\x94\xf5\x05\ +\x00\x7b\x51\x55\x4e\xbc\x0c\x37\x99\x73\x00\x31\xdf\x92\x4e\x73\ +\xdf\x95\x10\xa0\xe4\xe8\x45\x21\xb1\xe5\x49\x6f\xb0\x74\xce\x6b\ +\x08\xff\x08\x3b\x07\x5b\x39\xd5\x7a\xc0\x4f\x74\x8d\x45\x33\xb3\ +\x00\xf0\x86\x69\xb9\x52\x40\x1e\xc0\x3c\x8b\x9d\x5b\xfd\xce\x09\ +\x40\x85\x49\x9b\x78\x2f\xef\x38\x6b\x6d\x6f\x05\x63\xb9\xd6\x97\ +\x28\x4c\xd9\x12\xf4\xbb\x06\xdb\x29\x6b\x78\x9b\x21\xaf\xdb\x47\ +\x36\xd0\xc2\xbc\x68\x3b\x00\x4a\x53\x79\x46\xdf\xf5\x87\x38\x73\ +\x02\xbd\x23\xff\xf8\x24\x28\x24\x9b\x52\xdf\x61\xd9\x9c\x22\x00\ +\x5e\x4c\xdc\x84\xa3\xeb\x0c\x46\x69\x60\x43\xda\x3e\x84\xff\x62\ +\x0f\x8e\xf5\xf1\x6d\xe3\x6e\x3c\x63\xae\xe0\xcc\x46\xb4\x41\x66\ +\x18\x23\x91\x42\xa2\x2b\x3d\xcf\x08\x42\x02\x08\x11\x54\x2c\x11\ +\xc6\x68\x22\x4d\xd1\x13\x4e\xf3\x42\xe2\x06\x52\x2c\x19\x18\x84\ +\x09\x00\x5d\x69\x7c\xdf\xbc\x8f\xbe\x91\x4e\x0c\x62\xa2\x22\x02\ +\x58\x42\x28\x19\x58\x5c\xf4\x0c\x77\x12\x1b\x6e\xf5\x57\xe6\xa2\ +\xb4\x71\x2f\xe5\x2d\x9f\xa1\xfb\xe5\xba\x24\x61\x25\xd6\x88\xa4\ +\x20\x48\x55\xc7\x61\xda\x5d\xa1\x33\x33\x1b\xa3\xf0\x78\x07\xd0\ +\x95\x0e\x88\x1a\x09\x38\x00\xda\xdd\x4d\xcc\xb5\x4c\x1c\x68\xf5\ +\xdd\x0a\xbe\xbb\x51\x12\x02\x72\x7f\xa0\x85\x8b\xf7\x7e\x08\x89\ +\x03\xcc\x8a\x5c\x48\x9b\xbb\xc9\x77\x02\x70\x48\xa3\x34\x39\x00\ +\xd5\xea\x6e\x62\x81\x25\x33\x24\xe1\x52\xe7\x29\x3a\x3d\x1d\x41\ +\x5f\xa1\xa8\x6c\x3d\xe8\xaf\x30\xd4\x66\x47\x67\xd0\xec\xaa\x07\ +\x40\x57\xa2\x56\xfa\xf7\x79\x83\xa3\xe7\x02\xf6\xd8\x7c\xe2\x23\ +\x66\x4f\x48\x50\x4a\xe7\xcc\x5f\xc7\x83\x7e\x4d\xe7\xef\xdc\x72\ +\xd7\x4f\x0a\x6e\x09\x8b\x67\xee\x63\x4f\x73\xb9\xbb\x1a\x50\x37\ +\x2b\x57\x5c\xef\x96\x00\xba\x2e\x8a\xbd\xfa\x98\x7e\xd4\xf9\x29\ +\x9b\xd3\xde\x0f\xaa\x24\x60\x8d\x0f\xff\xe0\xfc\xbd\x0a\xaa\xef\ +\x94\xf3\x6b\xfb\x57\x93\x82\x03\x14\xda\x76\x73\xc4\x59\xc2\xb0\ +\x36\x04\x8a\x62\x18\xb7\x4d\xd7\x9e\xcd\x3a\x04\xec\x5c\x6f\xdb\ +\x46\xa4\x21\x82\x93\xed\x5f\x22\x94\xf2\x6d\xd0\x69\xb6\xa9\xc1\ +\xff\x2d\x48\x7c\x9d\x11\x22\xf8\xba\xe5\x10\x42\x70\xf4\xe4\xf2\ +\xba\x2d\x30\x6e\x9b\xc6\x98\xfa\xf7\x02\xcd\x27\xda\x8f\x30\xac\ +\x8f\xf2\x6e\xf6\x31\x12\x66\x24\x4f\x59\x6d\xb0\x2d\xe1\x56\x36\ +\xd8\x0f\x32\x4c\x04\x65\xad\x5f\x00\xe2\xb6\xd7\x33\xba\x2b\xf0\ +\x7f\xe2\x83\x73\x2e\x33\x4b\xd7\x44\x25\x02\x5b\x6a\x4c\x26\x5b\ +\x17\xed\xa7\xee\xc1\x79\x5a\xfb\xae\x72\x77\xc0\xc9\x98\xe6\xc1\ +\x00\x44\x99\x2c\x24\x46\xa5\x31\x37\x26\x8b\x85\xb1\xcf\x70\xd4\ +\x79\x80\xa6\xbe\xab\x80\xb8\x2d\xa4\xf6\xea\xc9\x65\x0d\x35\x93\ +\x12\x00\xac\xfe\x2d\x6f\x86\x6f\x9f\xab\x1d\x61\x32\x5c\x3c\x69\ +\x7d\x9e\x85\x31\x59\xd8\x2c\x76\x22\x8c\x91\x48\x04\x43\x5e\x0f\ +\x6d\xfd\x8d\xdc\x70\x35\x70\xb9\xbb\x9a\x61\x6d\x08\x21\x38\xea\ +\xf5\x8c\xee\xfa\x65\xb5\xb3\x7f\x3c\xde\x94\x8f\xfe\x9a\xd3\x8b\ +\x0b\xa4\x54\xa5\x80\x3d\x10\x93\xfe\x1b\x3a\x51\xa2\xea\x26\x8a\ +\xe2\xca\x15\xf5\x67\x27\xc3\x99\x92\x20\x60\x45\x55\x39\xf1\xde\ +\x30\x2d\x57\x69\x2a\x4f\x40\x9e\x10\x4a\x82\xa8\x11\xe0\xd0\x95\ +\xa8\xad\x5c\x71\xbd\x7b\xba\xfc\x7f\x01\xe3\xf6\xed\xcb\x2c\x97\ +\xd8\xbf\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ " qt_resource_name = "\ @@ -1521,12 +1609,17 @@ qt_resource_name = "\ \x00\x6c\ \x00\x65\x00\x61\x00\x70\x00\x2d\x00\x63\x00\x6f\x00\x6c\x00\x6f\x00\x72\x00\x2d\x00\x73\x00\x6d\x00\x61\x00\x6c\x00\x6c\x00\x2e\ \x00\x70\x00\x6e\x00\x67\ +\x00\x0b\ +\x01\x64\x80\x07\ +\x00\x63\ +\x00\x68\x00\x65\x00\x63\x00\x6b\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ " qt_resource_struct = "\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x02\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x06\x00\x00\x00\x02\ \x00\x00\x00\xa8\x00\x00\x00\x00\x00\x01\x00\x00\x2d\x4e\ +\x00\x00\x00\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x5b\xd7\ \x00\x00\x00\x34\x00\x00\x00\x00\x00\x01\x00\x00\x0d\xf7\ \x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ \x00\x00\x00\x5e\x00\x00\x00\x00\x00\x01\x00\x00\x19\xd2\ diff --git a/src/leap/gui/progress.py b/src/leap/gui/progress.py new file mode 100644 index 00000000..b2e34e22 --- /dev/null +++ b/src/leap/gui/progress.py @@ -0,0 +1,274 @@ +""" +classes used in progress pages +from first run wizard +""" +try: + from collections import OrderedDict +except ImportError: + # We must be in 2.6 + from leap.util.dicts import OrderedDict + +import logging + +from PyQt4 import QtCore +from PyQt4 import QtGui + +from leap.baseapp.mainwindow import FunThread + +from leap.gui import mainwindow_rc + +CHECKMARK_IMG = ":/images/checked.png" + +logger = logging.getLogger(__name__) + + +class ImgWidget(QtGui.QWidget): + + # XXX move to widgets + + def __init__(self, parent=None, img=None): + super(ImgWidget, self).__init__(parent) + self.pic = QtGui.QPixmap(img) + + def paintEvent(self, event): + painter = QtGui.QPainter(self) + painter.drawPixmap(0, 0, self.pic) + + +class ProgressStep(object): + """ + Data model for sequential steps + to be used in a progress page in + connection wizard + """ + NAME = 0 + DONE = 1 + + def __init__(self, stepname, done, index=None): + """ + @param step: the name of the step + @type step: str + @param done: whether is completed or not + @type done: bool + """ + self.index = int(index) if index else 0 + self.name = unicode(stepname) + self.done = bool(done) + + @classmethod + def columns(self): + return ('name', 'done') + + +class ProgressStepContainer(object): + """ + a container for ProgressSteps objects + access data in the internal dict + """ + + def __init__(self): + self.dirty = False + self.steps = {} + + def step(self, identity): + return self.step.get(identity) + + def addStep(self, step): + self.steps[step.index] = step + + def removeStep(self, step): + del self.steps[step.index] + del step + self.dirty = True + + def removeAllSteps(self): + for item in iter(self): + self.removeStep(item) + + @property + def columns(self): + return ProgressStep.columns() + + def __len__(self): + return len(self.steps) + + def __iter__(self): + for step in self.steps.values(): + yield step + + +class StepsTableWidget(QtGui.QTableWidget): + """ + initializes a TableWidget + suitable for our display purposes, like removing + header info and grid display + """ + + def __init__(self, parent=None): + super(StepsTableWidget, self).__init__(parent) + + # remove headers and all edit/select behavior + self.horizontalHeader().hide() + self.verticalHeader().hide() + self.setEditTriggers( + QtGui.QAbstractItemView.NoEditTriggers) + self.setSelectionMode( + QtGui.QAbstractItemView.NoSelection) + width = self.width() + # WTF? Here init width is 100... + # but on populating is 456... :( + + # XXX do we need this initial? + logger.debug('init table. width=%s' % width) + self.horizontalHeader().resizeSection(0, width * 0.7) + + # this disables the table grid. + # we should add alignment to the ImgWidget (it's top-left now) + self.setShowGrid(False) + + # XXX change image for done to rc + + # Note about the "done" status painting: + # + # XXX currently we are setting the CellWidget + # for the whole table on a per-row basis + # (on add_status_line method on ValidationPage). + # However, a more generic solution might be + # to implement a custom Delegate that overwrites + # the paint method (so it paints a checked tickmark if + # done is True and some other thing if checking or false). + # What we have now is quick and works because + # I'm supposing that on first fail we will + # go back to previous wizard page to signal the failure. + # A more generic solution could be used for + # some failing tests if they are not critical. + + +class ValidationPage(QtGui.QWizardPage): + """ + class to be used as an intermediate + between two pages in a wizard. + shows feedback to the user and goes back if errors, + goes forward if ok. + initializePage triggers a one shot timer + that calls do_checks. + Derived classes should implement + _do_checks and + _do_validation + """ + + # signals + + stepChanged = QtCore.pyqtSignal([str, int]) + + def __init__(self, parent=None): + super(ValidationPage, self).__init__(parent) + + self.steps = ProgressStepContainer() + self.progress = QtGui.QProgressBar(self) + + # steps table widget + self.stepsTableWidget = StepsTableWidget(self) + + layout = QtGui.QVBoxLayout() + layout.addWidget(self.progress) + layout.addWidget(self.stepsTableWidget) + + self.setLayout(layout) + self.layout = layout + + self.timer = QtCore.QTimer() + + # connect the new step status + # signal to status handler + self.stepChanged.connect( + self.onStepStatusChanged) + + self.errors = OrderedDict() + + def set_error(self, name, error): + self.errors[name] = error + + def pop_first_error(self): + return list(reversed(self.errors.items())).pop() + + def populateStepsTable(self): + # from examples, + # but I guess it's not needed to re-populate + # the whole table. + table = self.stepsTableWidget + table.setRowCount(len(self.steps)) + columns = self.steps.columns + table.setColumnCount(len(columns)) + + for row, step in enumerate(self.steps): + item = QtGui.QTableWidgetItem(step.name) + item.setData(QtCore.Qt.UserRole, + long(id(step))) + table.setItem(row, columns.index('name'), item) + table.setItem(row, columns.index('done'), + QtGui.QTableWidgetItem(step.done)) + self.resizeTable() + self.update() + + def clearTable(self): + # ??? -- not sure what's the difference + #self.stepsTableWidget.clear() + self.stepsTableWidget.clearContents() + + def resizeTable(self): + # resize first column to ~80% + table = self.stepsTableWidget + FIRST_COLUMN_PERCENT = 0.75 + width = table.width() + logger.debug('populate table. width=%s' % width) + table.horizontalHeader().resizeSection(0, width * FIRST_COLUMN_PERCENT) + + def onStepStatusChanged(self, status, progress=None): + if status not in ("head_sentinel", "end_sentinel"): + self.add_status_line(status) + if progress: + self.progress.setValue(progress) + self.progress.update() + + def add_status_line(self, message): + index = len(self.steps) + step = ProgressStep(message, False, index=index) + self.steps.addStep(step) + self.populateStepsTable() + table = self.stepsTableWidget + + # setting cell widget. + # see note on StepsTableWidget about plans to + # change this for a better solution. + + table.setCellWidget( + index - 1, + ProgressStep.DONE, + ImgWidget(img=CHECKMARK_IMG)) + table.update() + + def go_back(self): + self.wizard().back() + + def go_next(self): + self.wizard().next() + + def initializePage(self): + self.steps.removeAllSteps() + self.clearTable() + self.resizeTable() + self.timer.singleShot(0, self.do_checks) + + def do_checks(self): + """ + launches a thread to do the checks + """ + signal = self.stepChanged + self.checks = FunThread( + self._do_checks(update_signal=signal)) + self.checks.finished.connect(self._do_validation) + self.checks.begin() + #logger.debug('check thread started!') + #logger.debug('waiting for it to terminate...') + self.checks.wait() diff --git a/src/leap/gui/styles.py b/src/leap/gui/styles.py new file mode 100644 index 00000000..759817ce --- /dev/null +++ b/src/leap/gui/styles.py @@ -0,0 +1,4 @@ +ErrorLabelStyleSheet = """ +QLabel { color: red; + font-weight: bold} +""" diff --git a/src/leap/gui/tests/integration/fake_user_signup.py b/src/leap/gui/tests/integration/fake_user_signup.py index 12f18966..78873749 100644 --- a/src/leap/gui/tests/integration/fake_user_signup.py +++ b/src/leap/gui/tests/integration/fake_user_signup.py @@ -12,6 +12,7 @@ curl -d login=python_test_user -d password_salt=54321\ from BaseHTTPServer import HTTPServer from BaseHTTPServer import BaseHTTPRequestHandler import cgi +import json import urlparse HOST = "localhost" @@ -19,12 +20,15 @@ PORT = 8000 LOGIN_ERROR = """{"errors":{"login":["has already been taken"]}}""" +from leap.base.tests.test_providers import EXPECTED_DEFAULT_CONFIG + class request_handler(BaseHTTPRequestHandler): responses = { '/': ['ok\n'], '/users.json': ['ok\n'], - '/timeout': ['ok\n'] + '/timeout': ['ok\n'], + '/provider.json': ['%s\n' % json.dumps(EXPECTED_DEFAULT_CONFIG)] } def do_GET(self): diff --git a/src/leap/gui/utils.py b/src/leap/gui/utils.py new file mode 100644 index 00000000..8b1e3630 --- /dev/null +++ b/src/leap/gui/utils.py @@ -0,0 +1,10 @@ +""" +utility functions to work with gui objects +""" + + +def layout_widgets(layout): + """ + return a generator with all widgets in a layout + """ + return (layout.itemAt(i) for i in range(layout.count())) |