summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKali Kaneko (leap communications) <kali@leap.se>2016-12-27 02:08:19 +0100
committerKali Kaneko (leap communications) <kali@leap.se>2017-03-05 00:12:12 +0100
commit304f0cc995f6d861edca19ebf7c0ee8f8c6a2ea1 (patch)
tree1fa48d6badc62e81538bf73e47233569677ffa36
parentd908a262680fa50ea2dcef9a866d9f03af65ce6f (diff)
[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.
-rw-r--r--src/leap/bitmask/core/mail_services.py11
-rw-r--r--src/leap/bitmask/gui/app.py104
-rw-r--r--src/leap/bitmask/mail/outgoing/service.py4
-rw-r--r--src/leap/bitmask/pix.py220
4 files changed, 292 insertions, 47 deletions
diff --git a/src/leap/bitmask/core/mail_services.py b/src/leap/bitmask/core/mail_services.py
index 9271201..7d5d953 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 14025af..e686498 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 249cabe..bd578ed 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 0000000..519b7c1
--- /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 <http://www.gnu.org/licenses/>.
+"""
+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')