From 91e4481c450eb7eb928debc1cb7fa59bdb63dd7b Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 25 Jul 2017 11:40:11 -0400 Subject: [pkg] packaging and path changes - move all the pixelated python package under src/ - move the pixelated_www package under the leap namespace - allow to set globally the static folder - add hours and minutes to the timestamp in package version, to allow for several releases a day. --- service/src/pixelated/config/__init__.py | 0 service/src/pixelated/config/arguments.py | 78 +++++++ service/src/pixelated/config/credentials.py | 45 ++++ service/src/pixelated/config/leap.py | 114 ++++++++++ service/src/pixelated/config/leap_config.py | 42 ++++ service/src/pixelated/config/logger.py | 55 +++++ service/src/pixelated/config/services.py | 158 ++++++++++++++ service/src/pixelated/config/sessions.py | 311 ++++++++++++++++++++++++++++ service/src/pixelated/config/site.py | 47 +++++ 9 files changed, 850 insertions(+) create mode 100644 service/src/pixelated/config/__init__.py create mode 100644 service/src/pixelated/config/arguments.py create mode 100644 service/src/pixelated/config/credentials.py create mode 100644 service/src/pixelated/config/leap.py create mode 100644 service/src/pixelated/config/leap_config.py create mode 100644 service/src/pixelated/config/logger.py create mode 100644 service/src/pixelated/config/services.py create mode 100644 service/src/pixelated/config/sessions.py create mode 100644 service/src/pixelated/config/site.py (limited to 'service/src/pixelated/config') diff --git a/service/src/pixelated/config/__init__.py b/service/src/pixelated/config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/service/src/pixelated/config/arguments.py b/service/src/pixelated/config/arguments.py new file mode 100644 index 00000000..01152a34 --- /dev/null +++ b/service/src/pixelated/config/arguments.py @@ -0,0 +1,78 @@ +# +# Copyright (c) 2014 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see . + +import os +import argparse + + +def parse_user_agent_args(): + parser = argparse.ArgumentParser(description='Pixelated user agent.') + + parser_add_default_arguments(parser) + + parser.add_argument('--host', default='127.0.0.1', help='the host to run the user agent on') + parser.add_argument('--port', type=int, default=3333, help='the port to run the user agent on') + parser.add_argument('-sk', '--sslkey', metavar='', default=None, help='use specified file as web server\'s SSL key (when using the user-agent in server-mode)') + parser.add_argument('-sc', '--sslcert', metavar='', default=None, help='use specified file as web server\'s SSL certificate (when using the user-agent in server-mode)') + parser.add_argument('--multi-user', help='Run user agent in multi user mode', action='store_false', default=True, dest='single_user') + parser.add_argument('-p', '--provider', help='specify a provider for mutli-user mode', metavar='', default=None, dest='provider') + parser.add_argument('--banner', help='banner file to show on login screen') + parser.add_argument('--manhole', help='Run an interactive Python shell on port 8008', action='store_true', default=False, dest='manhole') + + args = parser.parse_args() + + return args + + +def parse_maintenance_args(): + parser = argparse.ArgumentParser(description='Pixelated maintenance') + parser_add_default_arguments(parser) + subparsers = parser.add_subparsers(help='commands', dest='command') + subparsers.add_parser('reset', help='reset account command') + mails_parser = subparsers.add_parser('load-mails', help='load mails into account') + mails_parser.add_argument('file', nargs='+', help='file(s) with mail data') + + markov_mails_parser = subparsers.add_parser('markov-generate', help='generate mails using markov chains') + markov_mails_parser.add_argument('--seed', default=None, help='Specify a seed to always generate the same output') + markov_mails_parser.add_argument('-l', '--limit', metavar='count', default='5', help='limit number of generated mails', dest='limit') + markov_mails_parser.add_argument('file', nargs='+', help='file(s) with mail data') + + subparsers.add_parser('dump-soledad', help='dump the soledad database') + subparsers.add_parser('sync', help='sync the soledad database') + subparsers.add_parser('repair', help='repair database if possible') + subparsers.add_parser('integrity-check', help='run integrity check on database') + + return parser.parse_args() + + +def parse_register_args(): + parser = argparse.ArgumentParser(description='Pixelated register') + parser.add_argument('provider', metavar='provider', action='store') + parser.add_argument('username', metavar='username', action='store') + parser.add_argument('-p', '--password', metavar='password', action='store', default=None, help='used just to register account automatically by scripts') + parser.add_argument('-lc', '--leap-provider-cert', metavar='', default=None, help='use specified file for LEAP provider cert authority certificate (url https:///ca.crt)') + parser.add_argument('-lf', '--leap-provider-cert-fingerprint', metavar='', default=None, help='use specified fingerprint to validate connection with LEAP provider', dest='leap_provider_cert_fingerprint') + parser.add_argument('--leap-home', help='The folder where the user agent stores its data. Defaults to ~/.leap', dest='leap_home', default=os.path.join(os.path.expanduser("~"), '.leap')) + parser.add_argument('--invite-code', help='invite code to register a user, if required', dest='invite_code', default=None) + return parser.parse_args() + + +def parser_add_default_arguments(parser): + parser.add_argument('--debug', action='store_true', help='DEBUG mode.') + parser.add_argument('-c', '--config', dest='credentials_file', metavar='', default=None, help='use specified file for credentials (for test purposes only)') + parser.add_argument('--leap-home', help='The folder where the user agent stores its data. Defaults to ~/.leap', dest='leap_home', default=os.path.join(os.path.expanduser("~"), '.leap')) + parser.add_argument('-lc', '--leap-provider-cert', metavar='', default=None, help='use specified file for LEAP provider cert authority certificate (url https:///ca.crt)') + parser.add_argument('-lf', '--leap-provider-cert-fingerprint', metavar='', default=None, help='use specified fingerprint to validate connection with LEAP provider', dest='leap_provider_cert_fingerprint') diff --git a/service/src/pixelated/config/credentials.py b/service/src/pixelated/config/credentials.py new file mode 100644 index 00000000..89901b3f --- /dev/null +++ b/service/src/pixelated/config/credentials.py @@ -0,0 +1,45 @@ +# +# Copyright (c) 2015 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see . + +import os +import getpass +import json +import sys +import ConfigParser + + +def read(credentials_file): + if credentials_file: + return read_from_file(credentials_file) + return prompt_for_credentials() + + +def prompt_for_credentials(): + provider = raw_input('Which provider do you want to connect to:\n') + username = raw_input('What\'s your username registered on the provider:\n') + password = getpass.getpass('Type your password:\n') + return provider, username, password + + +def read_from_file(credentials_file): + config_parser = ConfigParser.ConfigParser() + credentials_file_path = os.path.abspath(os.path.expanduser(credentials_file)) + config_parser.read(credentials_file_path) + provider, user, password = \ + config_parser.get('pixelated', 'leap_server_name'), \ + config_parser.get('pixelated', 'leap_username'), \ + config_parser.get('pixelated', 'leap_password') + return provider, user, password diff --git a/service/src/pixelated/config/leap.py b/service/src/pixelated/config/leap.py new file mode 100644 index 00000000..2b3a242a --- /dev/null +++ b/service/src/pixelated/config/leap.py @@ -0,0 +1,114 @@ +# +# Copyright (c) 2015 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see . + +from __future__ import absolute_import + +from leap.common.events import (server as events_server) +from pixelated.adapter.welcome_mail import add_welcome_mail +from pixelated.authentication import Authenticator +from pixelated.bitmask_libraries.certs import LeapCertificate +from pixelated.bitmask_libraries.provider import LeapProvider +from pixelated.config import credentials +from pixelated.config import leap_config +from pixelated.config.sessions import LeapSessionFactory +from twisted.internet import defer +from twisted.logger import Logger + +log = Logger() + + +def initialize_leap_provider(provider_hostname, provider_cert, provider_fingerprint, leap_home): + LeapCertificate.set_cert_and_fingerprint(provider_cert, + provider_fingerprint) + leap_config.set_leap_home(leap_home) + provider = LeapProvider(provider_hostname) + provider.setup_ca() + provider.download_settings() + return provider + + +@defer.inlineCallbacks +def initialize_leap_multi_user(provider_hostname, + leap_provider_cert, + leap_provider_cert_fingerprint, + credentials_file, + leap_home): + + config, provider = initialize_leap_provider(provider_hostname, leap_provider_cert, leap_provider_cert_fingerprint, leap_home) + + defer.returnValue((config, provider)) + + +@defer.inlineCallbacks +def create_leap_session(provider, username, password, auth=None): + leap_session = yield LeapSessionFactory(provider).create(username, password, auth) + defer.returnValue(leap_session) + + +@defer.inlineCallbacks +def initialize_leap_single_user(leap_provider_cert, + leap_provider_cert_fingerprint, + credentials_file, + leap_home): + + init_monkeypatches() + events_server.ensure_server() + + provider, username, password = credentials.read(credentials_file) + + provider = initialize_leap_provider(provider, leap_provider_cert, leap_provider_cert_fingerprint, leap_home) + + auth = yield Authenticator(provider).authenticate(username, password) + + leap_session = yield create_leap_session(provider, username, password, auth) + + defer.returnValue(leap_session) + + +def init_monkeypatches(): + import pixelated.extensions.requests_urllib3 + + +class BootstrapUserServices(object): + + def __init__(self, services_factory, provider): + self._services_factory = services_factory + self._provider = provider + + @defer.inlineCallbacks + def setup(self, user_auth, password, language='pt-BR'): + leap_session = None + try: + leap_session = yield create_leap_session(self._provider, user_auth.username, password, user_auth) + yield self._setup_user_services(leap_session) + yield self._add_welcome_email(leap_session, language) + except Exception as e: + log.warn('{0}: {1}. Closing session for user: {2}'.format(e.__class__.__name__, e, user_auth.username)) + if leap_session: + leap_session.close() + raise + + @defer.inlineCallbacks + def _setup_user_services(self, leap_session): + user_id = leap_session.user_auth.uuid + if not self._services_factory.has_session(user_id): + yield self._services_factory.create_services_from(leap_session) + self._services_factory.map_email(leap_session.user_auth.username, user_id) + + @defer.inlineCallbacks + def _add_welcome_email(self, leap_session, language): + if leap_session.fresh_account: + yield add_welcome_mail(leap_session.mail_store, language) diff --git a/service/src/pixelated/config/leap_config.py b/service/src/pixelated/config/leap_config.py new file mode 100644 index 00000000..7319d82b --- /dev/null +++ b/service/src/pixelated/config/leap_config.py @@ -0,0 +1,42 @@ +# +# Copyright (c) 2014 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see . + +import os +from distutils.spawn import find_executable + + +def discover_gpg_binary(): + path = find_executable('gpg') + if path is None: + raise Exception('Did not find a gpg executable!') + + if os.path.islink(path): + path = os.path.realpath(path) + + return path + + +SYSTEM_CA_BUNDLE = True +leap_home = os.path.expanduser('~/.leap/') +gpg_binary = discover_gpg_binary() + + +def set_leap_home(new_home): + leap_home = new_home + + +def set_gpg_binary(new_binary): + gpg_binary = binary diff --git a/service/src/pixelated/config/logger.py b/service/src/pixelated/config/logger.py new file mode 100644 index 00000000..bc4ab8d4 --- /dev/null +++ b/service/src/pixelated/config/logger.py @@ -0,0 +1,55 @@ +# +# Copyright (c) 2014 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see . + +import logging +import os +import sys +import time +from twisted.logger import globalLogBeginner, FileLogObserver + + +class PrivateKeyFilter(logging.Filter): + + def filter(self, record): + if '-----BEGIN PGP PRIVATE KEY BLOCK-----' in record.msg: + record.msg = '*** private key removed by %s.%s ***' % (type(self).__module__, type(self).__name__) + return True + + +def init(debug=False): + debug_enabled = debug or os.environ.get('DEBUG', False) + logging_level = logging.DEBUG if debug_enabled else logging.INFO + + logging.basicConfig(level=logging_level, + format='%(asctime)s [%(name)s] %(levelname)s %(message)s', + datefmt='%Y-%m-%d %H:%M:%S', + filemode='a') + + logging.getLogger('gnupg').setLevel(logging.WARN) + logging.getLogger('gnupg').addFilter(PrivateKeyFilter()) + + def formatter(event): + try: + event['log_time'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(event['log_time'])) + event['log_level'] = event['log_level'].name.upper() + event['log_format'] = str(event['log_format']) + '\n' if event.get('log_format') else '' + logstring = u'{log_time} [{log_namespace}] {log_level} ' + event['log_format'] + return logstring.format(**event) + except Exception as e: + return "Error while formatting log event: {!r}\nOriginal event: {!r}\n".format(e, event) + + observers = [FileLogObserver(sys.stdout, formatter)] + globalLogBeginner.beginLoggingTo(observers) diff --git a/service/src/pixelated/config/services.py b/service/src/pixelated/config/services.py new file mode 100644 index 00000000..48c1a528 --- /dev/null +++ b/service/src/pixelated/config/services.py @@ -0,0 +1,158 @@ +# +# Copyright (c) 2015 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see . + +import os + +from twisted.internet import defer, reactor +from twisted.logger import Logger + +from pixelated.adapter.mailstore.leap_attachment_store import LeapAttachmentStore +from pixelated.adapter.mailstore.searchable_mailstore import SearchableMailStore +from pixelated.adapter.services.mail_service import MailService +from pixelated.adapter.model.mail import InputMail +from pixelated.adapter.services.mail_sender import MailSender +from pixelated.adapter.search import SearchEngine +from pixelated.adapter.services.draft_service import DraftService +from pixelated.adapter.listeners.mailbox_indexer_listener import listen_all_mailboxes +from pixelated.adapter.search.index_storage_key import SearchIndexStorageKey +from pixelated.adapter.services.feedback_service import FeedbackService +from pixelated.config import leap_config + +logger = Logger() + + +class Services(object): + + def __init__(self, leap_session): + self._leap_home = leap_config.leap_home + self._pixelated_home = os.path.join(self._leap_home, 'pixelated') + self._leap_session = leap_session + + @defer.inlineCallbacks + def setup(self): + search_index_storage_key = self._setup_search_index_storage_key(self._leap_session.soledad) + yield self._setup_search_engine(self._leap_session.user_auth.uuid, search_index_storage_key) + + self._wrap_mail_store_with_indexing_mail_store(self._leap_session) + + yield listen_all_mailboxes(self._leap_session.account, self.search_engine, self._leap_session.mail_store) + + self.mail_service = self._setup_mail_service(self.search_engine) + + self.keymanager = self._leap_session.keymanager + self.draft_service = self._setup_draft_service(self._leap_session.mail_store) + self.feedback_service = self._setup_feedback_service() + yield self._index_all_mails() + + def close(self): + self._leap_session.close() + + def _wrap_mail_store_with_indexing_mail_store(self, leap_session): + leap_session.mail_store = SearchableMailStore(leap_session.mail_store, self.search_engine) + + @defer.inlineCallbacks + def _index_all_mails(self): + all_mails = yield self.mail_service.all_mails() + self.search_engine.index_mails(all_mails) + + @defer.inlineCallbacks + def _setup_search_engine(self, namespace, search_index_storage_key): + key_unicode = yield search_index_storage_key.get_or_create_key() + key = str(key_unicode) + logger.debug('The key len is: %s' % len(key)) + user_id = self._leap_session.user_auth.uuid + user_folder = os.path.join(self._pixelated_home, user_id) + search_engine = SearchEngine(key, user_home=user_folder) + self.search_engine = search_engine + + def _setup_mail_service(self, search_engine): + pixelated_mail_sender = MailSender(self._leap_session.smtp_config, self._leap_session.keymanager.keymanager) + + return MailService( + pixelated_mail_sender, + self._leap_session.mail_store, + search_engine, + self._leap_session.account_email(), + LeapAttachmentStore(self._leap_session.soledad)) + + def _setup_draft_service(self, mail_store): + return DraftService(mail_store) + + def _setup_search_index_storage_key(self, soledad): + return SearchIndexStorageKey(soledad) + + def _setup_feedback_service(self): + return FeedbackService(self._leap_session) + + +class ServicesFactory(object): + + def __init__(self, mode): + self._services_by_user = {} + self.mode = mode + self._map_email = {} + + def map_email(self, username, user_id): + self._map_email[username] = user_id + + def has_session(self, user_id): + return user_id in self._services_by_user + + def services(self, user_id): + return self._services_by_user[user_id] + + def destroy_session(self, user_id, using_email=False): + if using_email: + username = user_id.split('@')[0] + user_id = self._map_email.get(username, None) + + if user_id is not None and self.has_session(user_id): + _services = self._services_by_user[user_id] + _services.close() + del self._services_by_user[user_id] + + def add_session(self, user_id, services): + self._services_by_user[user_id] = services + + def online_sessions(self): + return len(self._services_by_user.keys()) + + @defer.inlineCallbacks + def create_services_from(self, leap_session): + _services = Services(leap_session) + yield _services.setup() + self._services_by_user[leap_session.user_auth.uuid] = _services + + +class SingleUserServicesFactory(object): + def __init__(self, mode): + self._services = None + self.mode = mode + + def add_session(self, user_id, services): + self._services = services + + def services(self, user_id): + return self._services + + def has_session(self, user_id): + return True + + def destroy_session(self, user_id, using_email=False): + reactor.stop() + + def online_sessions(self): + return 1 diff --git a/service/src/pixelated/config/sessions.py b/service/src/pixelated/config/sessions.py new file mode 100644 index 00000000..594b8e35 --- /dev/null +++ b/service/src/pixelated/config/sessions.py @@ -0,0 +1,311 @@ +# +# Copyright (c) 2015 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see . + +from __future__ import absolute_import + +import os +import errno +import requests + +from twisted.internet import defer, threads, reactor +from twisted.logger import Logger + +from leap.soledad.common.crypto import WrongMacError, UnknownMacMethodError +from leap.soledad.client import Soledad +from leap.bitmask.mail.incoming.service import IncomingMail +from leap.bitmask.mail.mail import Account +import leap.common.certs as leap_certs +from leap.common.events import ( + register, unregister, + catalog as events +) + +from pixelated.bitmask_libraries.keymanager import Keymanager +from pixelated.adapter.mailstore import LeapMailStore +from pixelated.config import leap_config +from pixelated.bitmask_libraries.smtp import LeapSMTPConfig + +logger = Logger() + + +class LeapSessionFactory(object): + def __init__(self, provider): + self._provider = provider + + @defer.inlineCallbacks + def create(self, username, password, auth): + key = SessionCache.session_key(self._provider, username) + session = SessionCache.lookup_session(key) + if not session: + session = yield self._create_new_session(username, password, auth) + yield session.first_required_sync() + SessionCache.remember_session(key, session) + defer.returnValue(session) + + @defer.inlineCallbacks + def _create_new_session(self, username, password, auth): + account_email = self._provider.address_for(username) + + self._create_database_dir(auth.uuid) + + api_cert = self._provider.provider_api_cert + + soledad = yield self.setup_soledad(auth.token, auth.uuid, password, api_cert) + + mail_store = LeapMailStore(soledad) + + keymanager = yield self.setup_keymanager(self._provider, soledad, account_email, auth.token, auth.uuid) + + smtp_client_cert = self._download_smtp_cert(auth) + smtp_host, smtp_port = self._provider.smtp_info() + smtp_config = LeapSMTPConfig(account_email, smtp_client_cert, smtp_host, smtp_port) + + leap_session = LeapSession(self._provider, auth, mail_store, soledad, keymanager, smtp_config) + + defer.returnValue(leap_session) + + @defer.inlineCallbacks + def setup_soledad(self, + user_token, + user_uuid, + password, + api_cert): + secrets = self._secrets_path(user_uuid) + local_db = self._local_db_path(user_uuid) + server_url = self._provider.discover_soledad_server(user_uuid) + try: + soledad = yield threads.deferToThread(Soledad, + user_uuid.encode('utf-8'), + passphrase=unicode(password, 'utf-8'), + secrets_path=secrets, + local_db_path=local_db, + server_url=server_url, + cert_file=api_cert, + shared_db=None, + auth_token=user_token) + defer.returnValue(soledad) + except (WrongMacError, UnknownMacMethodError), e: + raise SoledadWrongPassphraseException(e) + + @defer.inlineCallbacks + def setup_keymanager(self, provider, soledad, account_email, token, uuid): + keymanager = yield threads.deferToThread(Keymanager, + provider, + soledad, + account_email, + token, + uuid) + defer.returnValue(keymanager) + + def _download_smtp_cert(self, auth): + cert = SmtpClientCertificate(self._provider, auth, self._user_path(auth.uuid)) + return cert.cert_path() + + def _user_path(self, user_uuid): + return os.path.join(leap_config.leap_home, user_uuid) + + def _soledad_path(self, user_uuid): + return os.path.join(leap_config.leap_home, user_uuid, 'soledad') + + def _secrets_path(self, user_uuid): + return os.path.join(self._soledad_path(user_uuid), 'secrets') + + def _local_db_path(self, user_uuid): + return os.path.join(self._soledad_path(user_uuid), 'soledad.db') + + def _create_database_dir(self, user_uuid): + try: + os.makedirs(self._soledad_path(user_uuid)) + except OSError as exc: + if exc.errno == errno.EEXIST and os.path.isdir(self._soledad_path(user_uuid)): + pass + else: + raise + + +class LeapSession(object): + + def __init__(self, provider, user_auth, mail_store, soledad, keymanager, smtp_config): + self.smtp_config = smtp_config + self.provider = provider + self.user_auth = user_auth + self.mail_store = mail_store + self.soledad = soledad + self.keymanager = keymanager + self.fresh_account = False + self.incoming_mail_fetcher = None + self.account = None + self._has_been_initially_synced = False + self._is_closed = False + register(events.KEYMANAGER_FINISHED_KEY_GENERATION, self._set_fresh_account, uid=self.account_email(), replace=True) + + @defer.inlineCallbacks + def first_required_sync(self): + yield self.sync() + yield self.finish_bootstrap() + + @defer.inlineCallbacks + def finish_bootstrap(self): + yield self.keymanager.generate_openpgp_key() + yield self._create_account(self.soledad, self.user_auth.uuid) + self.incoming_mail_fetcher = yield self._create_incoming_mail_fetcher( + self.keymanager, + self.soledad, + self.account, + self.account_email()) + reactor.callFromThread(self.incoming_mail_fetcher.startService) + + def _create_account(self, soledad, user_id): + self.account = Account(soledad, user_id) + return self.account.deferred_initialization + + def _set_fresh_account(self, event, email_address): + logger.debug('Key for email %s has been generated' % email_address) + if email_address == self.account_email(): + self.fresh_account = True + + def account_email(self): + name = self.user_auth.username + return self.provider.address_for(name) + + def close(self): + self.stop_background_jobs() + unregister(events.KEYMANAGER_FINISHED_KEY_GENERATION, uid=self.account_email()) + self.soledad.close() + self._close_account() + self.remove_from_cache() + self._is_closed = True + + @property + def is_closed(self): + return self._is_closed + + def _close_account(self): + if self.account: + self.account.end_session() + + def remove_from_cache(self): + key = SessionCache.session_key(self.provider, self.user_auth.username) + SessionCache.remove_session(key) + + @defer.inlineCallbacks + def _create_incoming_mail_fetcher(self, keymanager, soledad, account, user_mail): + inbox = yield account.callWhenReady(lambda _: account.get_collection_by_mailbox('INBOX')) + defer.returnValue(IncomingMail(keymanager.keymanager, + soledad, + inbox, + user_mail)) + + def stop_background_jobs(self): + if self.incoming_mail_fetcher: + reactor.callFromThread(self.incoming_mail_fetcher.stopService) + self.incoming_mail_fetcher = None + + def sync(self): + try: + return self.soledad.sync() + except Exception as e: + logger.error(e) + raise + + +class SessionCache(object): + + sessions = {} + + @staticmethod + def lookup_session(key): + session = SessionCache.sessions.get(key, None) + if session is not None and session.is_closed: + SessionCache.remove_session(key) + return None + return session + + @staticmethod + def remember_session(key, session): + SessionCache.sessions[key] = session + + @staticmethod + def remove_session(key): + if key in SessionCache.sessions: + del SessionCache.sessions[key] + + @staticmethod + def session_key(provider, username): + return hash((provider, username)) + + +class SmtpClientCertificate(object): + def __init__(self, provider, auth, user_path): + self._provider = provider + self._auth = auth + self._user_path = user_path + + def cert_path(self): + if not self._is_cert_already_downloaded() or self._should_redownload(): + self._download_smtp_cert() + + return self._smtp_client_cert_path() + + def _is_cert_already_downloaded(self): + return os.path.exists(self._smtp_client_cert_path()) + + def _should_redownload(self): + return leap_certs.should_redownload(self._smtp_client_cert_path()) + + def _download_smtp_cert(self): + cert_path = self._smtp_client_cert_path() + + if not os.path.exists(os.path.dirname(cert_path)): + os.makedirs(os.path.dirname(cert_path)) + + self.download_to(cert_path) + + def _smtp_client_cert_path(self): + return os.path.join( + self._user_path, + "providers", + self._provider.domain, + "keys", "client", "smtp.pem") + + def download(self): + cert_url = '%s/%s/smtp_cert' % (self._provider.api_uri, self._provider.api_version) + headers = {} + headers["Authorization"] = 'Token token="{0}"'.format(self._auth.token) + params = {'address': self._auth.username} + response = requests.post( + cert_url, + params=params, + data=params, + verify=self._provider.provider_api_cert, + timeout=15, + headers=headers) + response.raise_for_status() + + client_cert = response.content + + return client_cert + + def download_to(self, target_file): + client_cert = self.download() + + with open(target_file, 'w') as f: + f.write(client_cert) + + +class SoledadWrongPassphraseException(Exception): + def __init__(self, *args, **kwargs): + super(SoledadWrongPassphraseException, self).__init__(*args, **kwargs) diff --git a/service/src/pixelated/config/site.py b/service/src/pixelated/config/site.py new file mode 100644 index 00000000..96554584 --- /dev/null +++ b/service/src/pixelated/config/site.py @@ -0,0 +1,47 @@ +# +# Copyright (c) 2015 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see . + +from twisted.web.server import Site, Request + + +class AddSecurityHeadersRequest(Request): + CSP_HEADER_VALUES = "default-src 'self'; style-src 'self' 'unsafe-inline'" + + def process(self): + self.setHeader('Content-Security-Policy', self.CSP_HEADER_VALUES) + self.setHeader('X-Content-Security-Policy', self.CSP_HEADER_VALUES) + self.setHeader('X-Webkit-CSP', self.CSP_HEADER_VALUES) + self.setHeader('X-Frame-Options', 'SAMEORIGIN') + self.setHeader('X-XSS-Protection', '1; mode=block') + self.setHeader('X-Content-Type-Options', 'nosniff') + + if self.isSecure(): + self.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains') + + Request.process(self) + + +class PixelatedSite(Site): + + requestFactory = AddSecurityHeadersRequest + + @classmethod + def enable_csp_requests(cls): + cls.requestFactory = AddSecurityHeadersRequest + + @classmethod + def disable_csp_requests(cls): + cls.requestFactory = Site.requestFactory -- cgit v1.2.3