Support signed URLs in WebSocket
Change-Id: Iab0b719f00fdcf4c84740d06a7d774cac410181c
This commit is contained in:
parent
d55f1de3bc
commit
02207d8e78
@ -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 @@
|
||||
<button class='pure-button' onclick='delete_queue()'>Delete</button>
|
||||
<button class='pure-button' onclick='list_messages()'>List messages</button>
|
||||
<button class='pure-button' onclick='subscribe_queue()'>Subscribe</button>
|
||||
<button class='pure-button' onclick='list_queues()'>Refresh</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -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
|
||||
|
@ -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'])
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
|
@ -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':
|
||||
|
Loading…
Reference in New Issue
Block a user