summaryrefslogtreecommitdiff
path: root/service/src/pixelated/resources
diff options
context:
space:
mode:
authorKali Kaneko <kali@leap.se>2017-07-25 11:40:11 -0400
committerKali Kaneko <kali@leap.se>2017-07-25 11:40:29 -0400
commit91e4481c450eb7eb928debc1cb7fa59bdb63dd7b (patch)
tree8fd7e6e77b6df669c33d96b7edad6db3cbe14dfe /service/src/pixelated/resources
parente4f755309d4cf5cfb6b0bcc62ed73d6070956ab5 (diff)
[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.
Diffstat (limited to 'service/src/pixelated/resources')
-rw-r--r--service/src/pixelated/resources/__init__.py159
-rw-r--r--service/src/pixelated/resources/account_recovery_resource.py87
-rw-r--r--service/src/pixelated/resources/attachments_resource.py110
-rw-r--r--service/src/pixelated/resources/auth.py117
-rw-r--r--service/src/pixelated/resources/backup_account_resource.py79
-rw-r--r--service/src/pixelated/resources/contacts_resource.py44
-rw-r--r--service/src/pixelated/resources/features_resource.py46
-rw-r--r--service/src/pixelated/resources/feedback_resource.py31
-rw-r--r--service/src/pixelated/resources/keys_resource.py46
-rw-r--r--service/src/pixelated/resources/login_resource.py173
-rw-r--r--service/src/pixelated/resources/logout_resource.py45
-rw-r--r--service/src/pixelated/resources/mail_resource.py92
-rw-r--r--service/src/pixelated/resources/mails_resource.py244
-rw-r--r--service/src/pixelated/resources/root_resource.py139
-rw-r--r--service/src/pixelated/resources/sandbox_resource.py37
-rw-r--r--service/src/pixelated/resources/session.py55
-rw-r--r--service/src/pixelated/resources/tags_resource.py38
-rw-r--r--service/src/pixelated/resources/user_settings_resource.py43
-rw-r--r--service/src/pixelated/resources/users.py30
19 files changed, 1615 insertions, 0 deletions
diff --git a/service/src/pixelated/resources/__init__.py b/service/src/pixelated/resources/__init__.py
new file mode 100644
index 00000000..58b56786
--- /dev/null
+++ b/service/src/pixelated/resources/__init__.py
@@ -0,0 +1,159 @@
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+import json
+import os
+
+from twisted.web.http import UNAUTHORIZED
+from twisted.web.resource import Resource
+from twisted.logger import Logger
+
+from pixelated.resources.session import IPixelatedSession
+
+from twisted.web.http import INTERNAL_SERVER_ERROR, SERVICE_UNAVAILABLE
+
+log = Logger()
+
+
+STATIC = None
+
+
+class SetEncoder(json.JSONEncoder):
+ def default(self, obj):
+ if isinstance(obj, set):
+ return list(obj)
+ return super(SetEncoder, self).default(obj)
+
+
+def respond_json(entity, request, status_code=200):
+ json_response = json.dumps(entity, cls=SetEncoder)
+ request.responseHeaders.setRawHeaders(b"content-type", [b"application/json"])
+ request.code = status_code
+ return json_response
+
+
+def respond_json_deferred(entity, request, status_code=200):
+ json_response = json.dumps(entity, cls=SetEncoder)
+ request.responseHeaders.setRawHeaders(b"content-type", [b"application/json"])
+ request.code = status_code
+ request.write(json_response)
+ request.finish()
+
+
+def handle_error_deferred(e, request):
+ log.error(e)
+ request.setResponseCode(INTERNAL_SERVER_ERROR)
+ request.write('Something went wrong!')
+ request.finish()
+
+
+def set_static_folder(static_folder):
+ global STATIC
+ STATIC = static_folder
+
+
+def get_protected_static_folder(static_folder=None):
+ static = static_folder or _get_static_folder()
+ return os.path.join(static, 'protected')
+
+
+def get_public_static_folder(static_folder=None):
+ static = static_folder or _get_static_folder()
+ return os.path.join(static, 'public')
+
+
+def _get_static_folder():
+ if not STATIC:
+ static_folder = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "..", "web-ui", "dist"))
+ if not os.path.exists(static_folder):
+ static_folder = os.path.join('/', 'usr', 'share', 'pixelated-user-agent')
+ else:
+ static_folder = STATIC
+ return static_folder
+
+
+class BaseResource(Resource):
+
+ def __init__(self, services_factory):
+ Resource.__init__(self)
+ self._services_factory = services_factory
+
+ def _get_user_id_from_request(self, request):
+ if self._services_factory.mode.is_single_user:
+ return None # it doesn't matter
+ session = self.get_session(request)
+ if session.is_logged_in():
+ return session.user_uuid
+ raise ValueError('Not logged in')
+
+ def is_logged_in(self, request):
+ session = self.get_session(request)
+ return session.is_logged_in() and self._services_factory.has_session(session.user_uuid)
+
+ def get_session(self, request):
+ return IPixelatedSession(request.getSession())
+
+ def is_admin(self, request):
+ services = self._services(request)
+ return services._leap_session.user_auth.is_admin()
+
+ def _services(self, request):
+ user_id = self._get_user_id_from_request(request)
+ return self._services_factory.services(user_id)
+
+ def _service(self, request, attribute):
+ return getattr(self._services(request), attribute)
+
+ def keymanager(self, request):
+ return self._service(request, 'keymanager')
+
+ def mail_service(self, request):
+ return self._service(request, 'mail_service')
+
+ def search_engine(self, request):
+ return self._service(request, 'search_engine')
+
+ def draft_service(self, request):
+ return self._service(request, 'draft_service')
+
+ def feedback_service(self, request):
+ return self._service(request, 'feedback_service')
+
+ def soledad(self, request):
+ return self._service(request, '_leap_session').soledad
+
+
+class UnAuthorizedResource(Resource):
+
+ def __init__(self):
+ Resource.__init__(self)
+
+ def render_GET(self, request):
+ request.setResponseCode(UNAUTHORIZED)
+ return "Unauthorized!"
+
+ def render_POST(self, request):
+ request.setResponseCode(UNAUTHORIZED)
+ return "Unauthorized!"
+
+
+class UnavailableResource(Resource):
+ def __init__(self):
+ Resource.__init__(self)
+
+ def render(self, request):
+ request.setResponseCode(SERVICE_UNAVAILABLE)
+ return "Service Unavailable"
diff --git a/service/src/pixelated/resources/account_recovery_resource.py b/service/src/pixelated/resources/account_recovery_resource.py
new file mode 100644
index 00000000..209a7693
--- /dev/null
+++ b/service/src/pixelated/resources/account_recovery_resource.py
@@ -0,0 +1,87 @@
+#
+# Copyright (c) 2017 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 <http://www.gnu.org/licenses/>.
+
+import os
+import json
+
+from twisted.python.filepath import FilePath
+from twisted.web.http import OK, INTERNAL_SERVER_ERROR
+from twisted.web.template import Element, XMLFile, renderElement
+from twisted.web.server import NOT_DONE_YET
+from twisted.internet import defer
+from twisted.logger import Logger
+
+from pixelated.resources import BaseResource
+from pixelated.resources import get_public_static_folder
+
+log = Logger()
+
+
+class InvalidPasswordError(Exception):
+ pass
+
+
+class AccountRecoveryPage(Element):
+ loader = XMLFile(FilePath(os.path.join(get_public_static_folder(), 'account_recovery.html')))
+
+ def __init__(self):
+ super(AccountRecoveryPage, self).__init__()
+
+
+class AccountRecoveryResource(BaseResource):
+ BASE_URL = 'account-recovery'
+ isLeaf = True
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def render_GET(self, request):
+ request.setResponseCode(OK)
+ return self._render_template(request)
+
+ def _render_template(self, request):
+ site = AccountRecoveryPage()
+ return renderElement(request, site)
+
+ def render_POST(self, request):
+ def success_response(response):
+ request.setResponseCode(OK)
+ request.finish()
+
+ def error_response(failure):
+ log.warn(failure)
+ request.setResponseCode(INTERNAL_SERVER_ERROR)
+ request.finish()
+
+ d = self._handle_post(request)
+ d.addCallbacks(success_response, error_response)
+ return NOT_DONE_YET
+
+ def _get_post_form(self, request):
+ return json.loads(request.content.getvalue())
+
+ def _validate_password(self, password, confirm_password):
+ return password == confirm_password and len(password) >= 8 and len(password) <= 9999
+
+ def _handle_post(self, request):
+ form = self._get_post_form(request)
+ password = form.get('password')
+ confirm_password = form.get('confirmPassword')
+
+ if not self._validate_password(password, confirm_password):
+ return defer.fail(InvalidPasswordError('The user entered an invalid password or confirmation'))
+
+ return defer.succeed('Done!')
diff --git a/service/src/pixelated/resources/attachments_resource.py b/service/src/pixelated/resources/attachments_resource.py
new file mode 100644
index 00000000..1081b4b8
--- /dev/null
+++ b/service/src/pixelated/resources/attachments_resource.py
@@ -0,0 +1,110 @@
+#
+# 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 <http://www.gnu.org/licenses/>.
+import cgi
+import io
+import re
+
+from twisted.internet import defer
+from twisted.protocols.basic import FileSender
+from twisted.python.log import msg
+from twisted.web import server
+from twisted.web.resource import Resource
+from twisted.web.server import NOT_DONE_YET
+from twisted.logger import Logger
+
+from pixelated.resources import respond_json_deferred, BaseResource
+
+
+logger = Logger()
+
+
+class AttachmentResource(Resource):
+ isLeaf = True
+
+ def __init__(self, mail_service, attachment_id):
+ Resource.__init__(self)
+ self.attachment_id = attachment_id
+ self.mail_service = mail_service
+
+ def render_GET(self, request):
+ def error_handler(failure):
+ msg(failure, 'attachment not found')
+ request.code = 404
+ request.finish()
+
+ encoding = request.args.get('encoding', [None])[0]
+ filename = request.args.get('filename', [self.attachment_id])[0]
+ content_type = request.args.get('content_type', ['application/octet-stream'])[0]
+ request.setHeader(b'Content-Type', content_type)
+ request.setHeader(b'Content-Disposition', bytes('attachment; filename="' + filename + '"'))
+
+ d = self._send_attachment(encoding, filename, request)
+ d.addErrback(error_handler)
+
+ return server.NOT_DONE_YET
+
+ @defer.inlineCallbacks
+ def _send_attachment(self, encoding, filename, request):
+ attachment = yield self.mail_service.attachment(self.attachment_id)
+
+ bytes_io = io.BytesIO(attachment['content'])
+
+ try:
+ request.code = 200
+ yield FileSender().beginFileTransfer(bytes_io, request)
+ finally:
+ bytes_io.close()
+ request.finish()
+
+ def _extract_mimetype(self, content_type):
+ match = re.compile('([A-Za-z-]+\/[A-Za-z-]+)').search(content_type)
+ return match.group(1)
+
+
+class AttachmentsResource(BaseResource):
+ BASE_URL = 'attachment'
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def getChild(self, attachment_id, request):
+ _mail_service = self.mail_service(request)
+ return AttachmentResource(_mail_service, attachment_id)
+
+ def render_POST(self, request):
+ _mail_service = self.mail_service(request)
+ fields = cgi.FieldStorage(fp=request.content, headers=(request.getAllHeaders()),
+ environ={'REQUEST_METHOD': 'POST'})
+ _file = fields['attachment']
+ deferred = _mail_service.save_attachment(_file.value, _file.type)
+
+ def send_location(attachment_id):
+ request.setHeader('Location', '/%s/%s' % (self.BASE_URL, attachment_id))
+ response_json = {"ident": attachment_id,
+ "content-type": _file.type,
+ "encoding": "base64", # hard coded for now -- not really used
+ "name": _file.filename,
+ "size": len(_file.value)}
+ respond_json_deferred(response_json, request, status_code=201)
+
+ def error_handler(error):
+ logger.error(error)
+ respond_json_deferred({"message": "Something went wrong. Attachment not saved."}, request, status_code=500)
+
+ deferred.addCallback(send_location)
+ deferred.addErrback(error_handler)
+
+ return NOT_DONE_YET
diff --git a/service/src/pixelated/resources/auth.py b/service/src/pixelated/resources/auth.py
new file mode 100644
index 00000000..adac985f
--- /dev/null
+++ b/service/src/pixelated/resources/auth.py
@@ -0,0 +1,117 @@
+#
+# Copyright (c) 2016 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 <http://www.gnu.org/licenses/>.
+
+import re
+
+from pixelated.resources import IPixelatedSession
+from twisted.cred import error
+from twisted.cred import portal, checkers
+from twisted.cred.checkers import ANONYMOUS
+from twisted.cred.credentials import ICredentials
+from twisted.internet import defer
+from twisted.logger import Logger
+from twisted.web import util
+from twisted.web._auth.wrapper import UnauthorizedResource
+from twisted.web.error import UnsupportedMethod
+from twisted.web.resource import IResource, ErrorPage
+from zope.interface import implements, implementer, Attribute
+
+
+log = Logger()
+
+
+class ISessionCredential(ICredentials):
+
+ request = Attribute('the current request')
+
+
+@implementer(ISessionCredential)
+class SessionCredential(object):
+ def __init__(self, request):
+ self.request = request
+
+
+@implementer(checkers.ICredentialsChecker)
+class SessionChecker(object):
+ credentialInterfaces = (ISessionCredential,)
+
+ def __init__(self, services_factory):
+ self._services_factory = services_factory
+
+ def requestAvatarId(self, credentials):
+ session = self.get_session(credentials.request)
+ if session.is_logged_in() and self._services_factory.has_session(session.user_uuid):
+ return defer.succeed(session.user_uuid)
+ return defer.succeed(ANONYMOUS)
+
+ def get_session(self, request):
+ return IPixelatedSession(request.getSession())
+
+
+class PixelatedRealm(object):
+ implements(portal.IRealm)
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if IResource in interfaces:
+ return IResource, avatarId, lambda: None
+ raise NotImplementedError()
+
+
+@implementer(IResource)
+class PixelatedAuthSessionWrapper(object):
+
+ isLeaf = False
+
+ def __init__(self, portal, root_resource, anonymous_resource, credentialFactories):
+ self._portal = portal
+ self._credentialFactories = credentialFactories
+ self._root_resource = root_resource
+ self._anonymous_resource = anonymous_resource
+
+ def render(self, request):
+ raise UnsupportedMethod(())
+
+ def getChildWithDefault(self, path, request):
+ request.postpath.insert(0, request.prepath.pop())
+ return self._authorizedResource(request)
+
+ def _authorizedResource(self, request):
+ creds = SessionCredential(request)
+ return util.DeferredResource(self._login(creds, request))
+
+ def _login(self, credentials, request):
+ pattern = re.compile("^/sandbox/")
+
+ def loginSucceeded(args):
+ interface, avatar, logout = args
+ if avatar == checkers.ANONYMOUS and not pattern.match(request.path):
+ return self._anonymous_resource
+ else:
+ return self._root_resource
+
+ def loginFailed(result):
+ if result.check(error.Unauthorized, error.LoginFailed):
+ return UnauthorizedResource(self._credentialFactories)
+ else:
+ log.err(
+ result,
+ "HTTPAuthSessionWrapper.getChildWithDefault encountered "
+ "unexpected error")
+ return ErrorPage(500, None, None)
+
+ d = self._portal.login(credentials, None, IResource)
+ d.addCallbacks(loginSucceeded, loginFailed)
+ return d
diff --git a/service/src/pixelated/resources/backup_account_resource.py b/service/src/pixelated/resources/backup_account_resource.py
new file mode 100644
index 00000000..94129122
--- /dev/null
+++ b/service/src/pixelated/resources/backup_account_resource.py
@@ -0,0 +1,79 @@
+#
+# Copyright (c) 2017 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 <http://www.gnu.org/licenses/>.
+
+import os
+import json
+
+from twisted.python.filepath import FilePath
+from twisted.web.http import OK, NO_CONTENT, INTERNAL_SERVER_ERROR
+from twisted.web.server import NOT_DONE_YET
+from twisted.web.template import Element, XMLFile, renderElement
+
+from pixelated.resources import BaseResource
+from pixelated.resources import get_protected_static_folder
+from pixelated.account_recovery import AccountRecovery
+from pixelated.support.language import parse_accept_language
+
+
+class BackupAccountPage(Element):
+ loader = XMLFile(FilePath(os.path.join(get_protected_static_folder(), 'backup_account.html')))
+
+ def __init__(self):
+ super(BackupAccountPage, self).__init__()
+
+
+class BackupAccountResource(BaseResource):
+ isLeaf = True
+
+ def __init__(self, services_factory, authenticator, leap_provider):
+ BaseResource.__init__(self, services_factory)
+ self._authenticator = authenticator
+ self._leap_provider = leap_provider
+
+ def render_GET(self, request):
+ request.setResponseCode(OK)
+ return self._render_template(request)
+
+ def _render_template(self, request):
+ site = BackupAccountPage()
+ return renderElement(request, site)
+
+ def render_POST(self, request):
+ account_recovery = AccountRecovery(
+ self._authenticator.bonafide_session,
+ self.soledad(request),
+ self._service(request, '_leap_session').smtp_config,
+ self._get_backup_email(request),
+ self._leap_provider.server_name,
+ language=self._get_language(request))
+
+ def update_response(response):
+ request.setResponseCode(NO_CONTENT)
+ request.finish()
+
+ def error_response(response):
+ request.setResponseCode(INTERNAL_SERVER_ERROR)
+ request.finish()
+
+ d = account_recovery.update_recovery_code()
+ d.addCallbacks(update_response, error_response)
+ return NOT_DONE_YET
+
+ def _get_backup_email(self, request):
+ return json.loads(request.content.getvalue()).get('backupEmail')
+
+ def _get_language(self, request):
+ return parse_accept_language(request.getAllHeaders())
diff --git a/service/src/pixelated/resources/contacts_resource.py b/service/src/pixelated/resources/contacts_resource.py
new file mode 100644
index 00000000..dc17d1ac
--- /dev/null
+++ b/service/src/pixelated/resources/contacts_resource.py
@@ -0,0 +1,44 @@
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+from pixelated.resources import respond_json_deferred, BaseResource
+from twisted.internet.threads import deferToThread
+from twisted.web import server
+from twisted.web.resource import Resource
+
+
+class ContactsResource(BaseResource):
+
+ isLeaf = True
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def render_GET(self, request):
+ _search_engine = self.search_engine(request)
+ query = request.args.get('q', [''])[-1]
+ d = deferToThread(lambda: _search_engine.contacts(query))
+ d.addCallback(lambda tags: respond_json_deferred(tags, request))
+
+ def handle_error(error):
+ print 'Something went wrong'
+ import traceback
+ traceback.print_exc()
+ print error
+
+ d.addErrback(handle_error)
+
+ return server.NOT_DONE_YET
diff --git a/service/src/pixelated/resources/features_resource.py b/service/src/pixelated/resources/features_resource.py
new file mode 100644
index 00000000..c1b61f12
--- /dev/null
+++ b/service/src/pixelated/resources/features_resource.py
@@ -0,0 +1,46 @@
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+from pixelated.resources import respond_json
+import os
+from twisted.web.resource import Resource
+
+from pixelated.resources.logout_resource import LogoutResource
+
+
+class FeaturesResource(Resource):
+ DISABLED_FEATURES = ['draftReply']
+ isLeaf = True
+
+ def __init__(self, multi_user=False):
+ Resource.__init__(self)
+ self._multi_user = multi_user
+
+ def render_GET(self, request):
+ disabled_features = self._disabled_features()
+ features = {'disabled_features': disabled_features}
+ self._add_multi_user_to(features)
+ return respond_json(features, request)
+
+ def _disabled_features(self):
+ disabled_features = [default_disabled_feature for default_disabled_feature in self.DISABLED_FEATURES]
+ if not os.environ.get('FEEDBACK_URL'):
+ disabled_features.append('feedback')
+ return disabled_features
+
+ def _add_multi_user_to(self, features):
+ if self._multi_user:
+ features.update({'multi_user': {'logout': LogoutResource.BASE_URL}})
diff --git a/service/src/pixelated/resources/feedback_resource.py b/service/src/pixelated/resources/feedback_resource.py
new file mode 100644
index 00000000..aeead401
--- /dev/null
+++ b/service/src/pixelated/resources/feedback_resource.py
@@ -0,0 +1,31 @@
+#
+# 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 <http://www.gnu.org/licenses/>.
+import json
+
+from pixelated.resources import respond_json, BaseResource
+
+
+class FeedbackResource(BaseResource):
+ isLeaf = True
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def render_POST(self, request):
+ _feedback_service = self.feedback_service(request)
+ feedback = json.loads(request.content.read()).get('feedback')
+ _feedback_service.open_ticket(feedback)
+ return respond_json({}, request)
diff --git a/service/src/pixelated/resources/keys_resource.py b/service/src/pixelated/resources/keys_resource.py
new file mode 100644
index 00000000..091c27d0
--- /dev/null
+++ b/service/src/pixelated/resources/keys_resource.py
@@ -0,0 +1,46 @@
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+from email.utils import parseaddr
+from pixelated.resources import respond_json_deferred, BaseResource
+from twisted.web import server
+
+
+class KeysResource(BaseResource):
+
+ isLeaf = True
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def render_GET(self, request):
+ _keymanager = self.keymanager(request)
+
+ def finish_request(key):
+ if key.private:
+ respond_json_deferred(None, request, status_code=401)
+ else:
+ respond_json_deferred(key.get_active_json(), request)
+
+ def key_not_found(_):
+ respond_json_deferred(None, request, status_code=404)
+
+ _, key_to_find = parseaddr(request.args.get('search')[0])
+ d = _keymanager.get_key(key_to_find)
+ d.addCallback(finish_request)
+ d.addErrback(key_not_found)
+
+ return server.NOT_DONE_YET
diff --git a/service/src/pixelated/resources/login_resource.py b/service/src/pixelated/resources/login_resource.py
new file mode 100644
index 00000000..5b0b70d0
--- /dev/null
+++ b/service/src/pixelated/resources/login_resource.py
@@ -0,0 +1,173 @@
+#
+# Copyright (c) 2016 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 <http://www.gnu.org/licenses/>.
+
+import os
+from xml.sax import SAXParseException
+
+from pixelated.authentication import Authenticator
+from pixelated.config.leap import BootstrapUserServices
+from pixelated.resources import BaseResource, UnAuthorizedResource, IPixelatedSession
+from pixelated.resources.account_recovery_resource import AccountRecoveryResource
+from pixelated.resources import get_public_static_folder, respond_json
+from pixelated.support.language import parse_accept_language
+
+from twisted.cred.error import UnauthorizedLogin
+from twisted.internet import defer
+from twisted.logger import Logger
+from twisted.python.filepath import FilePath
+from twisted.web import util
+from twisted.web.http import UNAUTHORIZED, OK
+from twisted.web.resource import NoResource
+from twisted.web.server import NOT_DONE_YET
+from twisted.web.static import File
+from twisted.web.template import Element, XMLFile, renderElement, renderer
+
+log = Logger()
+
+
+class DisclaimerElement(Element):
+ loader = XMLFile(FilePath(os.path.join(get_public_static_folder(), '_login_disclaimer_banner.html')))
+
+ def __init__(self, banner):
+ super(DisclaimerElement, self).__init__()
+ self._set_loader(banner)
+ self._banner_filename = banner or "_login_disclaimer_banner.html"
+
+ def _set_loader(self, banner):
+ if banner:
+ current_path = os.path.dirname(os.path.abspath(__file__))
+ banner_file_path = os.path.join(current_path, "..", "..", "..", banner)
+ self.loader = XMLFile(FilePath(banner_file_path))
+
+ def render(self, request):
+ try:
+ return super(DisclaimerElement, self).render(request)
+ except SAXParseException:
+ return ["Invalid XML template format for %s." % self._banner_filename]
+ except IOError:
+ return ["Disclaimer banner file %s could not be read or does not exit." % self._banner_filename]
+
+
+class LoginWebSite(Element):
+ loader = XMLFile(FilePath(os.path.join(get_public_static_folder(), 'login.html')))
+
+ def __init__(self, disclaimer_banner_file=None):
+ super(LoginWebSite, self).__init__()
+ self.disclaimer_banner_file = disclaimer_banner_file
+
+ @renderer
+ def disclaimer(self, request, tag):
+ return DisclaimerElement(self.disclaimer_banner_file).render(request)
+
+
+class LoginResource(BaseResource):
+ BASE_URL = 'login'
+
+ def __init__(self, services_factory, provider=None, disclaimer_banner=None, authenticator=None):
+ BaseResource.__init__(self, services_factory)
+ self._disclaimer_banner = disclaimer_banner
+ self._provider = provider
+ self._authenticator = authenticator
+ self._bootstrap_user_services = BootstrapUserServices(services_factory, provider)
+
+ static_folder = get_public_static_folder()
+ self.putChild('public', File(static_folder))
+ with open(os.path.join(static_folder, 'interstitial.html')) as f:
+ self.interstitial = f.read()
+
+ def getChild(self, path, request):
+ if path == '':
+ return self
+ if path == 'login':
+ return self
+ if path == 'status':
+ return LoginStatusResource(self._services_factory)
+ if path == AccountRecoveryResource.BASE_URL:
+ return AccountRecoveryResource(self._services_factory)
+ if not self.is_logged_in(request):
+ return UnAuthorizedResource()
+ return NoResource()
+
+ def render_GET(self, request):
+ request.setResponseCode(OK)
+ return self._render_template(request)
+
+ def _render_template(self, request):
+ site = LoginWebSite(disclaimer_banner_file=self._disclaimer_banner)
+ return renderElement(request, site)
+
+ def render_POST(self, request):
+ if self.is_logged_in(request):
+ return util.redirectTo("/", request)
+
+ def render_response(user_auth):
+ request.setResponseCode(OK)
+ request.write(self.interstitial)
+ request.finish()
+ self._complete_bootstrap(user_auth, request)
+
+ def render_error(error):
+ if error.type is UnauthorizedLogin:
+ log.info('Unauthorized login for %s. User typed wrong username/password combination.' % request.args['username'][0])
+ else:
+ log.error('Authentication error for %s' % request.args['username'][0])
+ log.error('%s' % error)
+ request.setResponseCode(UNAUTHORIZED)
+ content = util.redirectTo("/login?auth-error", request)
+ request.write(content)
+ request.finish()
+
+ d = self._handle_login(request)
+ d.addCallbacks(render_response, render_error)
+ return NOT_DONE_YET
+
+ @defer.inlineCallbacks
+ def _handle_login(self, request):
+ username = request.args['username'][0]
+ password = request.args['password'][0]
+ user_auth = yield self._authenticator.authenticate(username, password)
+ defer.returnValue(user_auth)
+
+ def _complete_bootstrap(self, user_auth, request):
+ def login_error(error, session):
+ log.error('Login error during %s services setup: %s \n %s' % (user_auth.username, error.getErrorMessage(), error.getTraceback()))
+ session.login_error()
+
+ def login_successful(_, session):
+ session.login_successful(user_auth.uuid)
+
+ language = parse_accept_language(request.getAllHeaders())
+ password = request.args['password'][0]
+ session = IPixelatedSession(request.getSession())
+ session.login_started()
+
+ d = self._bootstrap_user_services.setup(user_auth, password, language)
+ d.addCallback(login_successful, session)
+ d.addErrback(login_error, session)
+
+
+class LoginStatusResource(BaseResource):
+ isLeaf = True
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def render_GET(self, request):
+ session = IPixelatedSession(request.getSession())
+ status = 'completed' if self._services_factory.mode.is_single_user else str(session.check_login_status())
+
+ response = {'status': status}
+ return respond_json(response, request)
diff --git a/service/src/pixelated/resources/logout_resource.py b/service/src/pixelated/resources/logout_resource.py
new file mode 100644
index 00000000..a4fe584f
--- /dev/null
+++ b/service/src/pixelated/resources/logout_resource.py
@@ -0,0 +1,45 @@
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+from twisted.internet import defer
+from twisted.web import util
+from twisted.web.server import NOT_DONE_YET
+
+from pixelated.resources import BaseResource, handle_error_deferred
+from pixelated.resources.login_resource import LoginResource
+
+
+class LogoutResource(BaseResource):
+ BASE_URL = "logout"
+ isLeaf = True
+
+ @defer.inlineCallbacks
+ def _execute_logout(self, request):
+ http_session = self.get_session(request)
+ yield self._services_factory.destroy_session(http_session.user_uuid)
+ http_session.expire()
+
+ def render_POST(self, request):
+ def _redirect_to_login(_):
+ content = util.redirectTo("/%s" % LoginResource.BASE_URL, request)
+ request.write(content)
+ request.finish()
+
+ d = self._execute_logout(request)
+ d.addCallback(_redirect_to_login)
+ d.addErrback(handle_error_deferred, request)
+
+ return NOT_DONE_YET
diff --git a/service/src/pixelated/resources/mail_resource.py b/service/src/pixelated/resources/mail_resource.py
new file mode 100644
index 00000000..e1ba6087
--- /dev/null
+++ b/service/src/pixelated/resources/mail_resource.py
@@ -0,0 +1,92 @@
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+import json
+
+from twisted.python.log import err
+from twisted.web.resource import Resource
+from twisted.web.server import NOT_DONE_YET
+
+from pixelated.resources import respond_json_deferred, BaseResource, handle_error_deferred
+from pixelated.support import replier
+
+
+class MailTags(Resource):
+
+ isLeaf = True
+
+ def __init__(self, mail_id, mail_service):
+ Resource.__init__(self)
+ self._mail_service = mail_service
+ self._mail_id = mail_id
+
+ def render_POST(self, request):
+ new_tags = json.loads(request.content.read()).get('newtags')
+
+ d = self._mail_service.update_tags(self._mail_id, new_tags)
+ d.addCallback(lambda mail: respond_json_deferred(mail.as_dict(), request))
+
+ def handle403(failure):
+ failure.trap(ValueError)
+ return respond_json_deferred(failure.getErrorMessage(), request, 403)
+ d.addErrback(handle403)
+ return NOT_DONE_YET
+
+
+class Mail(Resource):
+
+ def __init__(self, mail_id, mail_service):
+ Resource.__init__(self)
+ self.putChild('tags', MailTags(mail_id, mail_service))
+ self._mail_id = mail_id
+ self._mail_service = mail_service
+
+ def render_GET(self, request):
+ def populate_reply(mail):
+ mail_dict = mail.as_dict()
+ current_user = self._mail_service.account_email
+ sender = mail.headers.get('Reply-to', mail.headers.get('From'))
+ to = mail.headers.get('To', [])
+ ccs = mail.headers.get('Cc', [])
+ mail_dict['replying'] = replier.generate_recipients(sender, to, ccs, current_user)
+ return mail_dict
+
+ d = self._mail_service.mail(self._mail_id)
+ d.addCallback(lambda mail: populate_reply(mail))
+ d.addCallback(lambda mail_dict: respond_json_deferred(mail_dict, request))
+ d.addErrback(handle_error_deferred, request)
+
+ return NOT_DONE_YET
+
+ def render_DELETE(self, request):
+ def response_failed(failure):
+ err(failure, 'something failed')
+ request.finish()
+
+ d = self._mail_service.delete_mail(self._mail_id)
+ d.addCallback(lambda _: respond_json_deferred(None, request))
+ d.addErrback(response_failed)
+ return NOT_DONE_YET
+
+
+class MailResource(BaseResource):
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def getChild(self, mail_id, request):
+ _mail_service = self.mail_service(request)
+ return Mail(mail_id, _mail_service)
diff --git a/service/src/pixelated/resources/mails_resource.py b/service/src/pixelated/resources/mails_resource.py
new file mode 100644
index 00000000..d911e0d2
--- /dev/null
+++ b/service/src/pixelated/resources/mails_resource.py
@@ -0,0 +1,244 @@
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+import time
+import json
+
+from twisted.internet import defer
+from twisted.logger import Logger
+from twisted.web.server import NOT_DONE_YET
+from twisted.web.resource import Resource
+from twisted.web import server
+
+from leap.common import events
+
+from pixelated.adapter.model.mail import InputMail
+from pixelated.resources import respond_json_deferred, BaseResource
+from pixelated.adapter.services.mail_sender import SMTPDownException
+from pixelated.support.functional import to_unicode
+
+
+log = Logger()
+
+
+class MailsUnreadResource(Resource):
+ isLeaf = True
+
+ def __init__(self, mail_service):
+ Resource.__init__(self)
+ self._mail_service = mail_service
+
+ def render_POST(self, request):
+ idents = json.load(request.content).get('idents')
+ deferreds = []
+ for ident in idents:
+ deferreds.append(self._mail_service.mark_as_unread(ident))
+
+ d = defer.gatherResults(deferreds, consumeErrors=True)
+ d.addCallback(lambda _: respond_json_deferred(None, request))
+ d.addErrback(lambda _: respond_json_deferred(None, request, status_code=500))
+
+ return NOT_DONE_YET
+
+
+class MailsReadResource(Resource):
+ isLeaf = True
+
+ def __init__(self, mail_service):
+ Resource.__init__(self)
+ self._mail_service = mail_service
+
+ def render_POST(self, request):
+ idents = json.load(request.content).get('idents')
+ deferreds = []
+ for ident in idents:
+ deferreds.append(self._mail_service.mark_as_read(ident))
+
+ d = defer.gatherResults(deferreds, consumeErrors=True)
+ d.addCallback(lambda _: respond_json_deferred(None, request))
+ d.addErrback(lambda _: respond_json_deferred(None, request, status_code=500))
+
+ return NOT_DONE_YET
+
+
+class MailsDeleteResource(Resource):
+ isLeaf = True
+
+ def __init__(self, mail_service):
+ Resource.__init__(self)
+ self._mail_service = mail_service
+
+ def render_POST(self, request):
+ def response_failed(failure):
+ log.error('something failed: %s' % failure.getErrorMessage())
+ request.finish()
+
+ idents = json.loads(request.content.read())['idents']
+ deferreds = []
+ for ident in idents:
+ deferreds.append(self._mail_service.delete_mail(ident))
+
+ d = defer.gatherResults(deferreds, consumeErrors=True)
+ d.addCallback(lambda _: respond_json_deferred(None, request))
+ d.addErrback(response_failed)
+ return NOT_DONE_YET
+
+
+class MailsRecoverResource(Resource):
+ isLeaf = True
+
+ def __init__(self, mail_service):
+ Resource.__init__(self)
+ self._mail_service = mail_service
+
+ def render_POST(self, request):
+ idents = json.loads(request.content.read())['idents']
+ deferreds = []
+ for ident in idents:
+ deferreds.append(self._mail_service.recover_mail(ident))
+ d = defer.gatherResults(deferreds, consumeErrors=True)
+ d.addCallback(lambda _: respond_json_deferred(None, request))
+ d.addErrback(lambda _: respond_json_deferred(None, request, status_code=500))
+ return NOT_DONE_YET
+
+
+class MailsArchiveResource(Resource):
+ isLeaf = True
+
+ def __init__(self, mail_service):
+ Resource.__init__(self)
+ self._mail_service = mail_service
+
+ def render_POST(self, request):
+ idents = json.loads(request.content.read())['idents']
+ deferreds = []
+ for ident in idents:
+ deferreds.append(self._mail_service.archive_mail(ident))
+ d = defer.gatherResults(deferreds, consumeErrors=True)
+ d.addCallback(lambda _: respond_json_deferred({'successMessage': 'your-message-was-archived'}, request))
+ d.addErrback(lambda _: respond_json_deferred(None, request, status_code=500))
+ return NOT_DONE_YET
+
+
+class MailsResource(BaseResource):
+
+ def _register_smtp_error_handler(self):
+
+ def on_error(event, content):
+ delivery_error_mail = InputMail.delivery_error_template(delivery_address=event.content)
+ self._mail_service.mailboxes.inbox.add(delivery_error_mail)
+
+ events.register(events.catalog.SMTP_SEND_MESSAGE_ERROR, callback=on_error)
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+ self._register_smtp_error_handler()
+
+ def getChild(self, action, request):
+ _mail_service = self.mail_service(request)
+
+ if action == 'delete':
+ return MailsDeleteResource(_mail_service)
+ if action == 'recover':
+ return MailsRecoverResource(_mail_service)
+ if action == 'archive':
+ return MailsArchiveResource(_mail_service)
+ if action == 'read':
+ return MailsReadResource(_mail_service)
+ if action == 'unread':
+ return MailsUnreadResource(_mail_service)
+
+ def _build_mails_response(self, (mails, total)):
+ return {
+ "stats": {
+ "total": total,
+ },
+ "mails": [mail.as_dict() for mail in mails]
+ }
+
+ def render_GET(self, request):
+
+ _mail_service = self.mail_service(request)
+ query, window_size, page = request.args.get('q')[0], request.args.get('w')[0], request.args.get('p')[0]
+ unicode_query = to_unicode(query)
+ d = _mail_service.mails(unicode_query, window_size, page)
+
+ d.addCallback(self._build_mails_response)
+ d.addCallback(lambda res: respond_json_deferred(res, request))
+
+ def error_handler(error):
+ print error
+
+ d.addErrback(error_handler)
+
+ return NOT_DONE_YET
+
+ def render_POST(self, request):
+ def onError(error):
+ if isinstance(error.value, SMTPDownException):
+ respond_json_deferred({'message': str(error.value)}, request, status_code=503)
+ else:
+ log.error('error occurred while sending: %s' % error.getErrorMessage())
+ respond_json_deferred({'message': 'an error occurred while sending'}, request, status_code=422)
+
+ deferred = self._handle_post(request)
+ deferred.addErrback(onError)
+
+ return server.NOT_DONE_YET
+
+ def render_PUT(self, request):
+ def onError(error):
+ log.error('error saving draft: %s' % error.getErrorMessage())
+ respond_json_deferred("", request, status_code=422)
+
+ deferred = self._handle_put(request)
+ deferred.addErrback(onError)
+
+ return server.NOT_DONE_YET
+
+ @defer.inlineCallbacks
+ def _fetch_attachment_contents(self, content_dict, _mail_service):
+ attachments = content_dict.get('attachments', []) if content_dict else []
+ for attachment in attachments:
+ retrieved_attachment = yield _mail_service.attachment(attachment['ident'])
+ attachment['raw'] = retrieved_attachment['content']
+ content_dict['attachments'] = attachments
+ defer.returnValue(content_dict)
+
+ @defer.inlineCallbacks
+ def _handle_post(self, request):
+ _mail_service = self.mail_service(request)
+ content_dict = json.loads(request.content.read())
+ with_attachment_content = yield self._fetch_attachment_contents(content_dict, _mail_service)
+
+ sent_mail = yield _mail_service.send_mail(with_attachment_content)
+ respond_json_deferred(sent_mail.as_dict(), request, status_code=201)
+
+ @defer.inlineCallbacks
+ def _handle_put(self, request):
+ _draft_service = self.draft_service(request)
+ _mail_service = self.mail_service(request)
+ content_dict = json.loads(request.content.read())
+ with_attachment_content = yield self._fetch_attachment_contents(content_dict, _mail_service)
+
+ _mail = InputMail.from_dict(with_attachment_content, from_address=_mail_service.account_email)
+ draft_id = content_dict.get('ident')
+ pixelated_mail = yield _draft_service.process_draft(draft_id, _mail)
+
+ if not pixelated_mail:
+ respond_json_deferred("", request, status_code=422)
+ else:
+ respond_json_deferred({'ident': pixelated_mail.ident}, request)
diff --git a/service/src/pixelated/resources/root_resource.py b/service/src/pixelated/resources/root_resource.py
new file mode 100644
index 00000000..b014a590
--- /dev/null
+++ b/service/src/pixelated/resources/root_resource.py
@@ -0,0 +1,139 @@
+#
+# Copyright (c) 2016 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 <http://www.gnu.org/licenses/>.
+import hashlib
+import json
+import os
+from string import Template
+from pixelated.resources.users import UsersResource
+
+from pixelated.resources import BaseResource, UnAuthorizedResource, UnavailableResource
+from pixelated.resources import get_public_static_folder, get_protected_static_folder
+from pixelated.resources.attachments_resource import AttachmentsResource
+from pixelated.resources.sandbox_resource import SandboxResource
+from pixelated.resources.account_recovery_resource import AccountRecoveryResource
+from pixelated.resources.backup_account_resource import BackupAccountResource
+from pixelated.resources.contacts_resource import ContactsResource
+from pixelated.resources.features_resource import FeaturesResource
+from pixelated.resources.feedback_resource import FeedbackResource
+from pixelated.resources.login_resource import LoginResource, LoginStatusResource
+from pixelated.resources.logout_resource import LogoutResource
+from pixelated.resources.user_settings_resource import UserSettingsResource
+from pixelated.resources.mail_resource import MailResource
+from pixelated.resources.mails_resource import MailsResource
+from pixelated.resources.tags_resource import TagsResource
+from pixelated.resources.keys_resource import KeysResource
+from twisted.web.resource import NoResource
+from twisted.web.static import File
+
+from twisted.logger import Logger
+
+log = Logger()
+
+
+CSRF_TOKEN_LENGTH = 32
+
+MODE_STARTUP = 1
+MODE_RUNNING = 2
+
+
+class RootResource(BaseResource):
+ def __init__(self, services_factory, static_folder=None):
+ BaseResource.__init__(self, services_factory)
+ self._public_static_folder = get_public_static_folder(static_folder)
+ self._protected_static_folder = get_protected_static_folder(static_folder)
+ self._html_template = open(os.path.join(self._protected_static_folder, 'index.html')).read()
+ self._services_factory = services_factory
+ self._child_resources = ChildResourcesMap()
+ with open(os.path.join(self._public_static_folder, 'interstitial.html')) as f:
+ self.interstitial = f.read()
+ self._startup_mode()
+
+ def _startup_mode(self):
+ self.putChild('public', File(self._public_static_folder))
+ self.putChild('status', LoginStatusResource(self._services_factory))
+ self._mode = MODE_STARTUP
+
+ def getChild(self, path, request):
+ if path == '':
+ return self
+ if self._mode == MODE_STARTUP:
+ return UnavailableResource()
+ if self._is_xsrf_valid(request):
+ return self._child_resources.get(path)
+ return UnAuthorizedResource()
+
+ def _is_xsrf_valid(self, request):
+ get_request = (request.method == 'GET')
+ if get_request:
+ return True
+
+ xsrf_token = request.getCookie('XSRF-TOKEN')
+
+ ajax_request = (request.getHeader('x-requested-with') == 'XMLHttpRequest')
+ if ajax_request:
+ xsrf_header = request.getHeader('x-xsrf-token')
+ return xsrf_header and xsrf_header == xsrf_token
+
+ csrf_input = request.args.get('csrftoken', [None])[0] or json.loads(request.content.read()).get('csrftoken', [None])[0]
+ return csrf_input and csrf_input == xsrf_token
+
+ def initialize(self, provider=None, disclaimer_banner=None, authenticator=None):
+ self._child_resources.add('assets', File(self._protected_static_folder))
+ self._child_resources.add(AccountRecoveryResource.BASE_URL, AccountRecoveryResource(self._services_factory))
+ self._child_resources.add('backup-account', BackupAccountResource(self._services_factory, authenticator, provider))
+ self._child_resources.add('sandbox', SandboxResource(self._protected_static_folder))
+ self._child_resources.add('keys', KeysResource(self._services_factory))
+ self._child_resources.add(AttachmentsResource.BASE_URL, AttachmentsResource(self._services_factory))
+ self._child_resources.add('contacts', ContactsResource(self._services_factory))
+ self._child_resources.add('features', FeaturesResource(provider))
+ self._child_resources.add('tags', TagsResource(self._services_factory))
+ self._child_resources.add('mails', MailsResource(self._services_factory))
+ self._child_resources.add('mail', MailResource(self._services_factory))
+ self._child_resources.add('feedback', FeedbackResource(self._services_factory))
+ self._child_resources.add('user-settings', UserSettingsResource(self._services_factory))
+ self._child_resources.add('users', UsersResource(self._services_factory))
+ self._child_resources.add(LoginResource.BASE_URL,
+ LoginResource(self._services_factory, provider, disclaimer_banner=disclaimer_banner, authenticator=authenticator))
+ self._child_resources.add(LogoutResource.BASE_URL, LogoutResource(self._services_factory))
+
+ self._mode = MODE_RUNNING
+
+ def _is_starting(self):
+ return self._mode == MODE_STARTUP
+
+ def _add_csrf_cookie(self, request):
+ csrf_token = hashlib.sha256(os.urandom(CSRF_TOKEN_LENGTH)).hexdigest()
+ request.addCookie('XSRF-TOKEN', csrf_token)
+
+ def render_GET(self, request):
+ self._add_csrf_cookie(request)
+ if self._is_starting():
+ return self.interstitial
+ else:
+ account_email = self.mail_service(request).account_email
+ response = Template(self._html_template).safe_substitute(account_email=account_email)
+ return str(response)
+
+
+class ChildResourcesMap(object):
+ def __init__(self):
+ self._registry = {}
+
+ def add(self, path, resource):
+ self._registry[path] = resource
+
+ def get(self, path):
+ return self._registry.get(path) or NoResource()
diff --git a/service/src/pixelated/resources/sandbox_resource.py b/service/src/pixelated/resources/sandbox_resource.py
new file mode 100644
index 00000000..35f99774
--- /dev/null
+++ b/service/src/pixelated/resources/sandbox_resource.py
@@ -0,0 +1,37 @@
+#
+# Copyright (c) 2016 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 <http://www.gnu.org/licenses/>.
+
+from twisted.web.static import File
+
+
+class SandboxResource(File):
+ CSP_HEADER_VALUES = "sandbox allow-popups allow-scripts;" \
+ "default-src 'self';" \
+ "style-src *;" \
+ "script-src *;" \
+ "font-src *;" \
+ "img-src *;" \
+ "object-src 'none';" \
+ "connect-src 'none';"
+
+ def render_GET(self, request):
+ request.setHeader('Content-Security-Policy', self.CSP_HEADER_VALUES)
+ request.setHeader('X-Content-Security-Policy', self.CSP_HEADER_VALUES)
+ request.setHeader('X-Webkit-CSP', self.CSP_HEADER_VALUES)
+ request.setHeader('Access-Control-Allow-Origin', '*')
+ request.setHeader('Access-Control-Allow-Methods', 'GET')
+
+ return super(SandboxResource, self).render_GET(request)
diff --git a/service/src/pixelated/resources/session.py b/service/src/pixelated/resources/session.py
new file mode 100644
index 00000000..5dfa52e6
--- /dev/null
+++ b/service/src/pixelated/resources/session.py
@@ -0,0 +1,55 @@
+#
+# Copyright (c) 2016 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 <http://www.gnu.org/licenses/>.
+
+from zope.interface import Interface, Attribute, implements
+from twisted.python.components import registerAdapter
+from twisted.web.server import Session
+
+
+class IPixelatedSession(Interface):
+ user_uuid = Attribute('The uuid of the currently logged in user')
+ login_status = Attribute('The status during user login')
+
+
+class PixelatedSession(object):
+ implements(IPixelatedSession)
+
+ def __init__(self, session):
+ self.user_uuid = None
+ self.login_status = None
+
+ def is_logged_in(self):
+ return self.user_uuid is not None
+
+ def expire(self):
+ self.user_uuid = None
+ self.login_status = None
+
+ def login_started(self):
+ self.login_status = 'started'
+
+ def login_successful(self, user_uuid):
+ self.user_uuid = user_uuid
+ self.login_status = 'completed'
+
+ def login_error(self):
+ self.login_status = 'error'
+
+ def check_login_status(self):
+ return self.login_status
+
+
+registerAdapter(PixelatedSession, Session, IPixelatedSession)
diff --git a/service/src/pixelated/resources/tags_resource.py b/service/src/pixelated/resources/tags_resource.py
new file mode 100644
index 00000000..4cea4ca7
--- /dev/null
+++ b/service/src/pixelated/resources/tags_resource.py
@@ -0,0 +1,38 @@
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+from pixelated.resources import respond_json_deferred, BaseResource, handle_error_deferred
+from twisted.internet.threads import deferToThread
+from twisted.web.server import NOT_DONE_YET
+
+
+class TagsResource(BaseResource):
+
+ isLeaf = True
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def render_GET(self, request):
+ _search_engine = self.search_engine(request)
+ query = request.args.get('q', [''])[0]
+ skip_default_tags = request.args.get('skipDefaultTags', [False])[0]
+
+ d = deferToThread(lambda: _search_engine.tags(query=query, skip_default_tags=skip_default_tags))
+ d.addCallback(lambda tags: respond_json_deferred(tags, request))
+ d.addErrback(handle_error_deferred, request)
+
+ return NOT_DONE_YET
diff --git a/service/src/pixelated/resources/user_settings_resource.py b/service/src/pixelated/resources/user_settings_resource.py
new file mode 100644
index 00000000..04b434bd
--- /dev/null
+++ b/service/src/pixelated/resources/user_settings_resource.py
@@ -0,0 +1,43 @@
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+from pixelated.resources import respond_json_deferred, BaseResource
+from twisted.web import server
+
+FINGERPRINT_NOT_FOUND = 'Fingerprint not found'
+
+
+class UserSettingsResource(BaseResource):
+ isLeaf = True
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def render_GET(self, request):
+ _account_email = self.mail_service(request).account_email
+
+ def finish_request(key):
+ _fingerprint = key.fingerprint
+ respond_json_deferred({'account_email': _account_email, 'fingerprint': _fingerprint}, request)
+
+ def key_not_found(_):
+ respond_json_deferred({'account_email': _account_email, 'fingerprint': FINGERPRINT_NOT_FOUND}, request)
+
+ d = self.keymanager(request).get_key(_account_email)
+ d.addCallback(finish_request)
+ d.addErrback(key_not_found)
+
+ return server.NOT_DONE_YET
diff --git a/service/src/pixelated/resources/users.py b/service/src/pixelated/resources/users.py
new file mode 100644
index 00000000..a3e6118e
--- /dev/null
+++ b/service/src/pixelated/resources/users.py
@@ -0,0 +1,30 @@
+#
+# Copyright (c) 2016 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 <http://www.gnu.org/licenses/>.
+
+from pixelated.resources import respond_json_deferred, BaseResource, respond_json, UnAuthorizedResource
+from twisted.web import server
+
+
+class UsersResource(BaseResource):
+ isLeaf = True
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def render_GET(self, request):
+ if self.is_admin(request):
+ return respond_json({"count": self._services_factory.online_sessions()}, request)
+ return UnAuthorizedResource().render_GET(request)