eba55100a8c45ad652ff9704da807f083a3e14cb
[pixelated-user-agent.git] / service / pixelated / config / sessions.py
1 #
2 # Copyright (c) 2015 ThoughtWorks, Inc.
3 #
4 # Pixelated is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # Pixelated is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
16
17 from __future__ import absolute_import
18
19 import os
20 import errno
21 import traceback
22 import requests
23 import sys
24
25 from twisted.internet import defer, threads, reactor
26 from twisted.logger import Logger
27
28 from leap.soledad.common.crypto import WrongMacError, UnknownMacMethodError
29 from leap.soledad.client import Soledad
30 from leap.bitmask.mail.incoming.service import IncomingMail
31 from leap.bitmask.mail.mail import Account
32 import leap.common.certs as leap_certs
33 from leap.common.events import (
34     register, unregister,
35     catalog as events
36 )
37
38 from pixelated.bitmask_libraries.keymanager import Keymanager, UploadKeyError
39 from pixelated.adapter.mailstore import LeapMailStore
40 from pixelated.config import leap_config
41 from pixelated.bitmask_libraries.certs import LeapCertificate
42 from pixelated.bitmask_libraries.smtp import LeapSMTPConfig
43
44 logger = Logger()
45
46
47 class LeapSessionFactory(object):
48     def __init__(self, provider):
49         self._provider = provider
50
51     @defer.inlineCallbacks
52     def create(self, username, password, auth):
53         key = SessionCache.session_key(self._provider, username)
54         session = SessionCache.lookup_session(key)
55         if not session:
56             session = yield self._create_new_session(username, password, auth)
57             yield session.first_required_sync()
58             SessionCache.remember_session(key, session)
59         defer.returnValue(session)
60
61     @defer.inlineCallbacks
62     def _create_new_session(self, username, password, auth):
63         account_email = self._provider.address_for(username)
64
65         self._create_database_dir(auth.uuid)
66
67         api_cert = self._provider.provider_api_cert
68
69         soledad = yield self.setup_soledad(auth.token, auth.uuid, password, api_cert)
70
71         mail_store = LeapMailStore(soledad)
72
73         keymanager = yield self.setup_keymanager(self._provider, soledad, account_email, auth.token, auth.uuid)
74
75         smtp_client_cert = self._download_smtp_cert(auth)
76         smtp_host, smtp_port = self._provider.smtp_info()
77         smtp_config = LeapSMTPConfig(account_email, smtp_client_cert, smtp_host, smtp_port)
78
79         leap_session = LeapSession(self._provider, auth, mail_store, soledad, keymanager, smtp_config)
80
81         defer.returnValue(leap_session)
82
83     @defer.inlineCallbacks
84     def setup_soledad(self,
85                       user_token,
86                       user_uuid,
87                       password,
88                       api_cert):
89         secrets = self._secrets_path(user_uuid)
90         local_db = self._local_db_path(user_uuid)
91         server_url = self._provider.discover_soledad_server(user_uuid)
92         try:
93             soledad = yield threads.deferToThread(Soledad,
94                                                   user_uuid.encode('utf-8'),
95                                                   passphrase=unicode(password, 'utf-8'),
96                                                   secrets_path=secrets,
97                                                   local_db_path=local_db,
98                                                   server_url=server_url,
99                                                   cert_file=api_cert,
100                                                   shared_db=None,
101                                                   auth_token=user_token,
102                                                   defer_encryption=False)
103             defer.returnValue(soledad)
104         except (WrongMacError, UnknownMacMethodError), e:
105             raise SoledadWrongPassphraseException(e)
106
107     @defer.inlineCallbacks
108     def setup_keymanager(self, provider, soledad, account_email, token, uuid):
109         keymanager = yield threads.deferToThread(Keymanager,
110                                                  provider,
111                                                  soledad,
112                                                  account_email,
113                                                  token,
114                                                  uuid)
115         defer.returnValue(keymanager)
116
117     def _download_smtp_cert(self, auth):
118         cert = SmtpClientCertificate(self._provider, auth, self._user_path(auth.uuid))
119         return cert.cert_path()
120
121     def _create_dir(self, path):
122         try:
123             os.makedirs(path)
124         except OSError as exc:
125             if exc.errno == errno.EEXIST and os.path.isdir(path):
126                 pass
127             else:
128                 raise
129
130     def _user_path(self, user_uuid):
131         return os.path.join(leap_config.leap_home, user_uuid)
132
133     def _soledad_path(self, user_uuid):
134         return os.path.join(leap_config.leap_home, user_uuid, 'soledad')
135
136     def _secrets_path(self, user_uuid):
137         return os.path.join(self._soledad_path(user_uuid), 'secrets')
138
139     def _local_db_path(self, user_uuid):
140         return os.path.join(self._soledad_path(user_uuid), 'soledad.db')
141
142     def _create_database_dir(self, user_uuid):
143         try:
144             os.makedirs(self._soledad_path(user_uuid))
145         except OSError as exc:
146             if exc.errno == errno.EEXIST and os.path.isdir(self._soledad_path(user_uuid)):
147                 pass
148             else:
149                 raise
150
151
152 class LeapSession(object):
153
154     def __init__(self, provider, user_auth, mail_store, soledad, keymanager, smtp_config):
155         self.smtp_config = smtp_config
156         self.provider = provider
157         self.user_auth = user_auth
158         self.mail_store = mail_store
159         self.soledad = soledad
160         self.keymanager = keymanager
161         self.fresh_account = False
162         self.incoming_mail_fetcher = None
163         self.account = None
164         self._has_been_initially_synced = False
165         self._is_closed = False
166         register(events.KEYMANAGER_FINISHED_KEY_GENERATION, self._set_fresh_account, uid=self.account_email())
167
168     @defer.inlineCallbacks
169     def first_required_sync(self):
170         yield self.sync()
171         yield self.finish_bootstrap()
172
173     @defer.inlineCallbacks
174     def finish_bootstrap(self):
175         try:
176             yield self.keymanager.generate_openpgp_key()
177         except UploadKeyError as e:
178             logger.warn('{0}: {1}. Closing session for user: {2}'.format(e.__class__.__name__, e, self.account_email()))
179             self.close()
180             raise
181
182         yield self._create_account(self.soledad, self.user_auth.uuid)
183         self.incoming_mail_fetcher = yield self._create_incoming_mail_fetcher(
184             self.keymanager,
185             self.soledad,
186             self.account,
187             self.account_email())
188         reactor.callFromThread(self.incoming_mail_fetcher.startService)
189
190     def _create_account(self, soledad, user_id):
191         self.account = Account(soledad, user_id)
192         return self.account.deferred_initialization
193
194     def _set_fresh_account(self, event, email_address):
195         logger.debug('Key for email %s has been generated' % email_address)
196         if email_address == self.account_email():
197             self.fresh_account = True
198
199     def account_email(self):
200         name = self.user_auth.username
201         return self.provider.address_for(name)
202
203     def close(self):
204         self.stop_background_jobs()
205         unregister(events.KEYMANAGER_FINISHED_KEY_GENERATION, uid=self.account_email())
206         self.soledad.close()
207         self._close_account()
208         self.remove_from_cache()
209         self._is_closed = True
210
211     @property
212     def is_closed(self):
213         return self._is_closed
214
215     def _close_account(self):
216         if self.account:
217             self.account.end_session()
218
219     def remove_from_cache(self):
220         key = SessionCache.session_key(self.provider, self.user_auth.username)
221         SessionCache.remove_session(key)
222
223     @defer.inlineCallbacks
224     def _create_incoming_mail_fetcher(self, keymanager, soledad, account, user_mail):
225         inbox = yield account.callWhenReady(lambda _: account.get_collection_by_mailbox('INBOX'))
226         defer.returnValue(IncomingMail(keymanager.keymanager,
227                           soledad,
228                           inbox,
229                           user_mail))
230
231     def stop_background_jobs(self):
232         if self.incoming_mail_fetcher:
233             reactor.callFromThread(self.incoming_mail_fetcher.stopService)
234             self.incoming_mail_fetcher = None
235
236     def sync(self):
237         try:
238             return self.soledad.sync()
239         except:
240             traceback.print_exc(file=sys.stderr)
241             raise
242
243
244 class SessionCache(object):
245
246     sessions = {}
247
248     @staticmethod
249     def lookup_session(key):
250         session = SessionCache.sessions.get(key, None)
251         if session is not None and session.is_closed:
252             SessionCache.remove_session(key)
253             return None
254         return session
255
256     @staticmethod
257     def remember_session(key, session):
258         SessionCache.sessions[key] = session
259
260     @staticmethod
261     def remove_session(key):
262         if key in SessionCache.sessions:
263             del SessionCache.sessions[key]
264
265     @staticmethod
266     def session_key(provider, username):
267         return hash((provider, username))
268
269
270 class SmtpClientCertificate(object):
271     def __init__(self, provider, auth, user_path):
272         self._provider = provider
273         self._auth = auth
274         self._user_path = user_path
275
276     def cert_path(self):
277         if not self._is_cert_already_downloaded() or self._should_redownload():
278             self._download_smtp_cert()
279
280         return self._smtp_client_cert_path()
281
282     def _is_cert_already_downloaded(self):
283         return os.path.exists(self._smtp_client_cert_path())
284
285     def _should_redownload(self):
286         return leap_certs.should_redownload(self._smtp_client_cert_path())
287
288     def _download_smtp_cert(self):
289         cert_path = self._smtp_client_cert_path()
290
291         if not os.path.exists(os.path.dirname(cert_path)):
292             os.makedirs(os.path.dirname(cert_path))
293
294         self.download_to(cert_path)
295
296     def _smtp_client_cert_path(self):
297         return os.path.join(
298             self._user_path,
299             "providers",
300             self._provider.domain,
301             "keys", "client", "smtp.pem")
302
303     def download(self):
304         cert_url = '%s/%s/smtp_cert' % (self._provider.api_uri, self._provider.api_version)
305         headers = {}
306         headers["Authorization"] = 'Token token="{0}"'.format(self._auth.token)
307         params = {'address': self._auth.username}
308         response = requests.post(
309             cert_url,
310             params=params,
311             data=params,
312             verify=self._provider.provider_api_cert,
313             timeout=15,
314             headers=headers)
315         response.raise_for_status()
316
317         client_cert = response.content
318
319         return client_cert
320
321     def download_to(self, target_file):
322         client_cert = self.download()
323
324         with open(target_file, 'w') as f:
325             f.write(client_cert)
326
327
328 class SoledadWrongPassphraseException(Exception):
329     def __init__(self, *args, **kwargs):
330         super(SoledadWrongPassphraseException, self).__init__(*args, **kwargs)