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':