From 304f0cc995f6d861edca19ebf7c0ee8f8c6a2ea1 Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Tue, 27 Dec 2016 02:08:19 +0100 Subject: [feature] pixelated UA integration a bit hacky and all, but this should launch the service and allow interacting from the default site (localhost:9090). this is the first example of a pyqt-js bridge, it's an interesting mechanism that we can use more in the future. no efforts made so far in authenticating the app. --- src/leap/bitmask/core/mail_services.py | 11 ++ src/leap/bitmask/gui/app.py | 104 ++++++++------ src/leap/bitmask/mail/outgoing/service.py | 4 +- src/leap/bitmask/pix.py | 220 ++++++++++++++++++++++++++++++ 4 files changed, 292 insertions(+), 47 deletions(-) create mode 100644 src/leap/bitmask/pix.py diff --git a/src/leap/bitmask/core/mail_services.py b/src/leap/bitmask/core/mail_services.py index 92712019..7d5d9533 100644 --- a/src/leap/bitmask/core/mail_services.py +++ b/src/leap/bitmask/core/mail_services.py @@ -34,6 +34,7 @@ from twisted.internet import task from twisted.logger import Logger from leap.common.files import check_and_fix_urw_only +from leap.bitmask import pix from leap.bitmask.hooks import HookableService from leap.bitmask.bonafide import config from leap.bitmask.keymanager import KeyManager @@ -499,6 +500,7 @@ class StandardMailService(service.MultiService, HookableService): d = soledad.get_or_create_service_token('mail_auth') d.addCallback(registerToken) d.addCallback(self._write_tokens_file, userid) + d.addCallback(self._maybe_start_pixelated, userid, soledad, keymanager) return d # hooks @@ -621,6 +623,8 @@ class StandardMailService(service.MultiService, HookableService): def get_keymanager_session(self, userid): return self._keymanager_sessions.get(userid) + # other helpers + def _write_tokens_file(self, token, userid): tokens_folder = os.path.join(tempfile.gettempdir(), "bitmask_tokens") if os.path.exists(tokens_folder): @@ -638,6 +642,13 @@ class StandardMailService(service.MultiService, HookableService): with open(tokens_path, 'w') as ftokens: json.dump(token_dict, ftokens) + def _maybe_start_pixelated(self, passthrough, userid, soledad, keymanager): + if pix.HAS_PIXELATED: + reactor.callFromThread( + pix.start_pixelated_user_agent, + userid, soledad, keymanager) + return passthrough + class IMAPService(service.Service): diff --git a/src/leap/bitmask/gui/app.py b/src/leap/bitmask/gui/app.py index 14025afc..e686498e 100644 --- a/src/leap/bitmask/gui/app.py +++ b/src/leap/bitmask/gui/app.py @@ -45,13 +45,16 @@ if platform.system() == 'Windows': else: from PyQt5 import QtCore, QtGui from PyQt5 import QtWebKit + from PyQt5.QtCore import QSize + from PyQt5.QtCore import QObject, pyqtSlot from PyQt5.QtWidgets import QDialog from PyQt5.QtWidgets import QApplication from PyQt5.QtWebKitWidgets import QWebView - from PyQt5.QtCore import QSize + from PyQt5.QtWebKit import QWebSettings BITMASK_URI = 'http://localhost:7070/' +PIXELATED_URI = 'http://localhost:9090/' IS_WIN = platform.system() == "Windows" DEBUG = os.environ.get("DEBUG", False) @@ -60,51 +63,40 @@ qApp = None bitmaskd = None -class BrowserWindow(QDialog): - - def __init__(self, parent): - super(BrowserWindow, self).__init__(parent) - if IS_WIN: - self.view = QWebView(self) - win_size = QSize(1024, 600) - self.setMinimumSize(win_size) - self.view.page().setViewportSize(win_size) - self.view.page().setPreferredContentsSize(win_size) - else: - self.view = QWebView(self) - win_size = QSize(800, 600) - self.win_size = win_size - self.resize(win_size) - - if DEBUG: - self.view.settings().setAttribute( - QtWebKit.QWebSettings.WebAttribute.DeveloperExtrasEnabled, - True) - self.inspector = QtWebKit.QWebInspector(self) - self.inspector.setPage(self.view.page()) - self.inspector.show() - self.splitter = QtGui.QSplitter() - self.splitter.addWidget(self.view) - self.splitter.addWidget(self.inspector) - # TODO add layout also in non-DEBUG mode - layout = QtGui.QVBoxLayout(self) - layout.addWidget(self.splitter) - - icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(":/mask-icon.png"), - QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.setWindowIcon(icon) - - self.setWindowTitle('Bitmask') - self.load_app() +class BrowserWindow(QWebView): + def __init__(self, *args, **kw): + url = kw.pop('url', None) + first = False + if not url: + url = "http://localhost:7070" + path = os.path.join(get_path_prefix(), 'leap', 'authtoken') + token = open(path).read().strip() + url += '#' + token + first = True + self.url = url self.closing = False - def load_app(self): - path = os.path.join(get_path_prefix(), 'leap', 'authtoken') - global_token = open(path).read().strip() - anchored_uri = BITMASK_URI + 'index.html#' + global_token - print('[bitmask] opening Browser with {0}'.format(anchored_uri)) - self.view.load(QtCore.QUrl(anchored_uri)) + super(QWebView, self).__init__(*args, **kw) + self.bitmask_browser = NewPageConnector(self) if first else None + self.loadPage(self.url) + + def loadPage(self, web_page): + self.settings().setAttribute( + QWebSettings.DeveloperExtrasEnabled, True) + + if os.environ.get('DEBUG'): + self.inspector = QWebInspector(self) + self.inspector.setPage(self.page()) + self.inspector.show() + + if os.path.isabs(web_page): + web_page = os.path.relpath(web_page) + + url = QtCore.QUrl(web_page) + self.load(url) + self.frame = self.page().mainFrame() + self.frame.addToJavaScriptWindowObject( + "bitmaskBrowser", self.bitmask_browser) def shutdown(self, *args): if self.closing: @@ -119,7 +111,13 @@ class BrowserWindow(QDialog): os.kill(pidno, signal.SIGTERM) print('[bitmask] shutting down gui...') try: - self.view.stop() + self.stop() + try: + global pixbrowser + pixbrowser.stop() + del pixbrowser + except: + pass QtCore.QTimer.singleShot(0, qApp.deleteLater) except Exception as ex: @@ -127,10 +125,25 @@ class BrowserWindow(QDialog): sys.exit(1) +pixbrowser = None + + +class NewPageConnector(QObject): + + @pyqtSlot() + def openPixelated(self): + global pixbrowser + pixbrowser = BrowserWindow(url=PIXELATED_URI) + pixbrowser.show() + + def _handle_kill(*args, **kw): win = kw.get('win') if win: QtCore.QTimer.singleShot(0, win.close) + global pixbrowser + if pixbrowser: + QtCore.QTimer.singleShot(0, pixbrowser.close) def launch_gui(): @@ -164,6 +177,7 @@ def launch_gui(): def start_app(): from leap.bitmask.util import STANDALONE + os.environ['QT_AUTO_SCREEN_SCALE_FACTOR'] = '1' # Allow the frozen binary in the bundle double as the cli entrypoint # Why have only a user interface when you can have two? diff --git a/src/leap/bitmask/mail/outgoing/service.py b/src/leap/bitmask/mail/outgoing/service.py index 249cabe4..bd578ed2 100644 --- a/src/leap/bitmask/mail/outgoing/service.py +++ b/src/leap/bitmask/mail/outgoing/service.py @@ -129,9 +129,9 @@ class OutgoingMail(object): leap_assert(host != '') leap_assert_type(port, int) leap_assert(port is not 0) - leap_assert_type(cert, unicode) + leap_assert_type(cert, basestring) leap_assert(cert != '') - leap_assert_type(key, unicode) + leap_assert_type(key, basestring) leap_assert(key != '') self._port = port diff --git a/src/leap/bitmask/pix.py b/src/leap/bitmask/pix.py new file mode 100644 index 00000000..519b7c15 --- /dev/null +++ b/src/leap/bitmask/pix.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- +# pix.py +# Copyright (C) 2016 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Pixelated plugin integration. +""" +import json +import os +import sys + +from twisted.internet import defer, reactor +from twisted.logger import Logger + +from leap.common.config import get_path_prefix +from leap.bitmask.mail.mail import Account +from leap.bitmask.keymanager import KeyNotFound + +try: + from pixelated.adapter.mailstore import LeapMailStore + from pixelated.adapter.welcome_mail import add_welcome_mail + from pixelated.application import SingleUserServicesFactory + from pixelated.application import UserAgentMode + from pixelated.application import start_site + from pixelated.bitmask_libraries.smtp import LeapSMTPConfig + from pixelated.config.sessions import SessionCache + from pixelated.config import services + from pixelated.resources.root_resource import RootResource + import pixelated_www + HAS_PIXELATED = True +except ImportError: + HAS_PIXELATED = False + + +log = Logger() + + +def start_pixelated_user_agent(userid, soledad, keymanager): + + leap_session = LeapSessionAdapter( + userid, soledad, keymanager) + + config = Config() + leap_home = os.path.join(get_path_prefix(), 'leap') + config.leap_home = leap_home + leap_session.config = config + + services_factory = SingleUserServicesFactory( + UserAgentMode(is_single_user=True)) + + if getattr(sys, 'frozen', False): + # we are running in a |PyInstaller| bundle + static_folder = os.path.join(sys._MEIPASS, 'pixelated_www') + else: + static_folder = os.path.abspath(pixelated_www.__path__[0]) + + resource = RootResource(services_factory, static_folder=static_folder) + + config.host = 'localhost' + config.port = 9090 + config.sslkey = None + config.sslcert = None + config.manhole = False + + d = leap_session.account.callWhenReady( + lambda _: _start_in_single_user_mode( + leap_session, config, + resource, services_factory)) + return d + + +def get_smtp_config(provider): + config_path = os.path.join( + get_path_prefix(), 'leap', 'providers', provider, 'smtp-service.json') + json_config = json.loads(open(config_path).read()) + chosen_host = json_config['hosts'].keys()[0] + hostname = json_config['hosts'][chosen_host]['hostname'] + port = json_config['hosts'][chosen_host]['port'] + + config = Config() + config.host = hostname + config.port = port + return config + + +class NickNym(object): + + def __init__(self, keymanager, userid): + self._email = userid + self.keymanager = keymanager + + @defer.inlineCallbacks + def generate_openpgp_key(self): + key_present = yield self._key_exists(self._email) + if not key_present: + yield self._gen_key() + yield self._send_key_to_leap() + + @defer.inlineCallbacks + def _key_exists(self, email): + try: + yield self.fetch_key(email, private=True, fetch_remote=False) + defer.returnValue(True) + except KeyNotFound: + defer.returnValue(False) + + def fetch_key(self, email, private=False, fetch_remote=True): + return self.keymanager.get_key( + email, private=private, fetch_remote=fetch_remote) + + def get_key(self, *args, **kw): + return self.keymanager.get_key(*args, **kw) + + def _gen_key(self): + return self.keymanager.gen_key() + + def _send_key_to_leap(self): + return self.keymanager.send_key() + + +class LeapSessionAdapter(object): + + def __init__(self, userid, soledad, keymanager): + + self.userid = userid + self.soledad = soledad + + # XXX this needs to be converged with our public apis. + _n = NickNym(keymanager, userid) + self.nicknym = self.keymanager = _n + self.mail_store = LeapMailStore(soledad) + + self.user_auth = Config() + self.user_auth.uuid = soledad.uuid + + self.fresh_account = False + self.incoming_mail_fetcher = None + self.account = Account(soledad, userid) + + username, provider = userid.split('@') + smtp_client_cert = os.path.join( + get_path_prefix(), + 'leap', 'providers', provider, 'keys', + 'client', + 'smtp_{username}.pem'.format( + username=username)) + + _prov = Config() + _prov.server_name = provider + self.provider = _prov + + assert(os.path.isfile(smtp_client_cert)) + + smtp_config = get_smtp_config(provider) + smtp_host = smtp_config.host + smtp_port = smtp_config.port + + self.smtp_config = LeapSMTPConfig( + userid, + smtp_client_cert, smtp_host, smtp_port) + + def account_email(self): + return self.userid + + def close(self): + pass + + @property + def is_closed(self): + return self._is_closed + + def remove_from_cache(self): + key = SessionCache.session_key(self.provider, self.userid) + SessionCache.remove_session(key) + + def sync(self): + return self.soledad.sync() + + +class Config(object): + pass + + +def _start_in_single_user_mode(leap_session, config, resource, + services_factory): + start_site(config, resource) + reactor.callLater( + 0, start_user_agent_in_single_user_mode, + resource, services_factory, + leap_session.config.leap_home, leap_session) + + +@defer.inlineCallbacks +def start_user_agent_in_single_user_mode(root_resource, + services_factory, + leap_home, leap_session): + log.info('Pixelated bootstrap done, loading services for user %s' + % leap_session.user_auth.uuid) + _services = services.Services(leap_session) + yield _services.setup() + + if leap_session.fresh_account: + yield add_welcome_mail(leap_session.mail_store) + + services_factory.add_session(leap_session.user_auth.uuid, _services) + + root_resource.initialize(provider=leap_session.provider) + log.info('Done, the Pixelated User Agent is ready to be used') -- cgit v1.2.3