From 02207d8e781c3786a0273a300ac07bf2d6c94605 Mon Sep 17 00:00:00 2001 From: Thomas Herve Date: Wed, 19 Aug 2015 11:48:44 +0200 Subject: [PATCH] Support signed URLs in WebSocket Change-Id: Iab0b719f00fdcf4c84740d06a7d774cac410181c --- examples/websocket.html | 5 +- zaqar/api/handler.py | 38 ++++++++++ .../transport/websocket/v1_1/test_auth.py | 69 +++++++++++++++++++ zaqar/transport/websocket/driver.py | 3 +- zaqar/transport/websocket/factory.py | 3 +- zaqar/transport/websocket/protocol.py | 13 +++- 6 files changed, 126 insertions(+), 5 deletions(-) diff --git a/examples/websocket.html b/examples/websocket.html index 9f1e13991..498d52083 100644 --- a/examples/websocket.html +++ b/examples/websocket.html @@ -66,7 +66,9 @@ } else if (action == 'message_list') { var messages = data['body']['messages']; display_messages(messages); - } else if (action == 'queue_create' || action == 'queue_delete' || action == 'authenticate') { + } else if (action == 'queue_create' || action == 'queue_delete') { + list_queues(); + } else if (action == 'authenticate' && data["headers"]["status"] == 200) { list_queues(); } else if (action == 'message_post' || action == 'message_delete') { list_messages(); @@ -188,6 +190,7 @@ + diff --git a/zaqar/api/handler.py b/zaqar/api/handler.py index e957a24d0..0a6355fa3 100644 --- a/zaqar/api/handler.py +++ b/zaqar/api/handler.py @@ -18,6 +18,7 @@ from zaqar.api.v1_1 import request as schema_validator from zaqar.common.api import request from zaqar.common.api import response from zaqar.common import errors +from zaqar.common import urls class Handler(object): @@ -26,6 +27,15 @@ class Handler(object): The handler validates and process the requests """ + _actions_mapping = { + 'message_list': 'GET', + 'message_get': 'GET', + 'message_get_many': 'GET', + 'message_post': 'POST', + 'message_delete': 'DELETE', + 'message_delete_many': 'DELETE' + } + def __init__(self, storage, control, validate, defaults): self.v1_1_endpoints = endpoints.Endpoints(storage, control, validate, defaults) @@ -73,3 +83,31 @@ class Handler(object): def get_defaults(self): return self.v1_1_endpoints._defaults + + def verify_signature(self, key, payload): + action = payload.get('action') + method = self._actions_mapping.get(action) + + queue_name = payload.get('body', {}).get('queue_name') + path = '/v2/queues/%(queue_name)s/messages' % { + 'queue_name': queue_name} + + headers = payload.get('headers', {}) + project = headers.get('X-Project-ID') + expires = headers.get('URL-Expires') + methods = headers.get('URL-Methods') + signature = headers.get('URL-Signature') + + if not method or method not in methods: + return False + + try: + verified = urls.verify_signed_headers_data(key, path, + project=project, + methods=methods, + expires=expires, + signature=signature) + except ValueError: + return False + + return verified diff --git a/zaqar/tests/unit/transport/websocket/v1_1/test_auth.py b/zaqar/tests/unit/transport/websocket/v1_1/test_auth.py index b5eb54b0b..bf1c77fa5 100644 --- a/zaqar/tests/unit/transport/websocket/v1_1/test_auth.py +++ b/zaqar/tests/unit/transport/websocket/v1_1/test_auth.py @@ -19,6 +19,7 @@ import uuid from keystonemiddleware import auth_token import mock +from zaqar.common import urls from zaqar.tests.unit.transport.websocket import base from zaqar.tests.unit.transport.websocket import utils as test_utils @@ -30,6 +31,7 @@ class AuthTest(base.V1_1Base): def setUp(self): super(AuthTest, self).setUp() self.protocol = self.transport.factory() + self.protocol.factory._secret_key = 'secret' self.default_message_ttl = 3600 @@ -119,3 +121,70 @@ class AuthTest(base.V1_1Base): self.assertEqual(2, len(responses)) self.assertIn('cancelled', repr(handle)) self.assertNotIn('cancelled', repr(self.protocol._deauth_handle)) + + def test_signed_url(self): + send_mock = mock.Mock() + self.protocol.sendMessage = send_mock + + data = urls.create_signed_url('secret', '/v2/queues/myqueue/messages', + project=self.project_id, methods=['GET']) + + headers = self.headers.copy() + headers.update({ + 'URL-Signature': data['signature'], + 'URL-Expires': data['expires'], + 'URL-Methods': ['GET'] + }) + req = json.dumps({'action': 'message_list', + 'body': {'queue_name': 'myqueue'}, + 'headers': headers}) + self.protocol.onMessage(req, False) + + self.assertEqual(1, send_mock.call_count) + resp = json.loads(send_mock.call_args[0][0]) + self.assertEqual(200, resp['headers']['status']) + + def test_signed_url_wrong_queue(self): + send_mock = mock.Mock() + self.protocol.sendMessage = send_mock + + data = urls.create_signed_url('secret', '/v2/queues/myqueue/messages', + project=self.project_id, methods=['GET']) + + headers = self.headers.copy() + headers.update({ + 'URL-Signature': data['signature'], + 'URL-Expires': data['expires'], + 'URL-Methods': ['GET'] + }) + req = json.dumps({'action': 'message_list', + 'body': {'queue_name': 'otherqueue'}, + 'headers': headers}) + self.protocol.onMessage(req, False) + + self.assertEqual(1, send_mock.call_count) + resp = json.loads(send_mock.call_args[0][0]) + self.assertEqual(403, resp['headers']['status']) + + def test_signed_url_wrong_method(self): + send_mock = mock.Mock() + self.protocol.sendMessage = send_mock + + data = urls.create_signed_url('secret', '/v2/queues/myqueue/messages', + project=self.project_id, methods=['GET']) + + headers = self.headers.copy() + headers.update({ + 'URL-Signature': data['signature'], + 'URL-Expires': data['expires'], + 'URL-Methods': ['GET'] + }) + req = json.dumps({'action': 'message_delete', + 'body': {'queue_name': 'myqueue', + 'message_id': '123'}, + 'headers': headers}) + self.protocol.onMessage(req, False) + + self.assertEqual(1, send_mock.call_count) + resp = json.loads(send_mock.call_args[0][0]) + self.assertEqual(403, resp['headers']['status']) diff --git a/zaqar/transport/websocket/driver.py b/zaqar/transport/websocket/driver.py index 531732086..7a9d7780e 100644 --- a/zaqar/transport/websocket/driver.py +++ b/zaqar/transport/websocket/driver.py @@ -76,7 +76,8 @@ class Driver(base.DriverBase): handler=self._api, external_port=self._ws_conf.external_port, auth_strategy=self._auth_strategy, - loop=asyncio.get_event_loop()) + loop=asyncio.get_event_loop(), + secret_key=self._conf.signed_url.secret_key) def listen(self): """Self-host using 'bind' and 'port' from the WS config group.""" diff --git a/zaqar/transport/websocket/factory.py b/zaqar/transport/websocket/factory.py index 4cf5812a9..8232daddf 100644 --- a/zaqar/transport/websocket/factory.py +++ b/zaqar/transport/websocket/factory.py @@ -23,12 +23,13 @@ class ProtocolFactory(websocket.WebSocketServerFactory): protocol = protocol.MessagingProtocol def __init__(self, uri, debug, handler, external_port, auth_strategy, - loop): + loop, secret_key): websocket.WebSocketServerFactory.__init__( self, url=uri, debug=debug, externalPort=external_port) self._handler = handler self._auth_strategy = auth_strategy self._loop = loop + self._secret_key = secret_key def __call__(self): proto = self.protocol(self._handler, self._auth_strategy, self._loop) diff --git a/zaqar/transport/websocket/protocol.py b/zaqar/transport/websocket/protocol.py index 2335a951a..08cf2a787 100644 --- a/zaqar/transport/websocket/protocol.py +++ b/zaqar/transport/websocket/protocol.py @@ -73,8 +73,17 @@ class MessagingProtocol(websocket.WebSocketServerProtocol): if resp is None: if self._auth_strategy and not self._authentified: if self._auth_app or payload.get('action') != 'authenticate': - body = {'error': 'Not authentified.'} - resp = self._handler.create_response(403, body, req) + if 'URL-Signature' in payload.get('headers', {}): + if self._handler.verify_signature( + self.factory._secret_key, payload): + resp = self._handler.process_request(req) + else: + body = {'error': 'Not authentified.'} + resp = self._handler.create_response( + 403, body, req) + else: + body = {'error': 'Not authentified.'} + resp = self._handler.create_response(403, body, req) else: return self._authenticate(payload) elif payload.get('action') == 'authenticate':