Merge "Support signed URLs in WebSocket"
This commit is contained in:
commit
cac3bcfeb4
@ -66,7 +66,9 @@
|
|||||||
} else if (action == 'message_list') {
|
} else if (action == 'message_list') {
|
||||||
var messages = data['body']['messages'];
|
var messages = data['body']['messages'];
|
||||||
display_messages(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();
|
list_queues();
|
||||||
} else if (action == 'message_post' || action == 'message_delete') {
|
} else if (action == 'message_post' || action == 'message_delete') {
|
||||||
list_messages();
|
list_messages();
|
||||||
@ -188,6 +190,7 @@
|
|||||||
<button class='pure-button' onclick='delete_queue()'>Delete</button>
|
<button class='pure-button' onclick='delete_queue()'>Delete</button>
|
||||||
<button class='pure-button' onclick='list_messages()'>List messages</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='subscribe_queue()'>Subscribe</button>
|
||||||
|
<button class='pure-button' onclick='list_queues()'>Refresh</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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 request
|
||||||
from zaqar.common.api import response
|
from zaqar.common.api import response
|
||||||
from zaqar.common import errors
|
from zaqar.common import errors
|
||||||
|
from zaqar.common import urls
|
||||||
|
|
||||||
|
|
||||||
class Handler(object):
|
class Handler(object):
|
||||||
@ -26,6 +27,15 @@ class Handler(object):
|
|||||||
The handler validates and process the requests
|
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):
|
def __init__(self, storage, control, validate, defaults):
|
||||||
self.v1_1_endpoints = endpoints.Endpoints(storage, control,
|
self.v1_1_endpoints = endpoints.Endpoints(storage, control,
|
||||||
validate, defaults)
|
validate, defaults)
|
||||||
@ -73,3 +83,31 @@ class Handler(object):
|
|||||||
|
|
||||||
def get_defaults(self):
|
def get_defaults(self):
|
||||||
return self.v1_1_endpoints._defaults
|
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
|
from keystonemiddleware import auth_token
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
|
from zaqar.common import urls
|
||||||
from zaqar.tests.unit.transport.websocket import base
|
from zaqar.tests.unit.transport.websocket import base
|
||||||
from zaqar.tests.unit.transport.websocket import utils as test_utils
|
from zaqar.tests.unit.transport.websocket import utils as test_utils
|
||||||
|
|
||||||
@ -30,6 +31,7 @@ class AuthTest(base.V1_1Base):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(AuthTest, self).setUp()
|
super(AuthTest, self).setUp()
|
||||||
self.protocol = self.transport.factory()
|
self.protocol = self.transport.factory()
|
||||||
|
self.protocol.factory._secret_key = 'secret'
|
||||||
|
|
||||||
self.default_message_ttl = 3600
|
self.default_message_ttl = 3600
|
||||||
|
|
||||||
@ -119,3 +121,70 @@ class AuthTest(base.V1_1Base):
|
|||||||
self.assertEqual(2, len(responses))
|
self.assertEqual(2, len(responses))
|
||||||
self.assertIn('cancelled', repr(handle))
|
self.assertIn('cancelled', repr(handle))
|
||||||
self.assertNotIn('cancelled', repr(self.protocol._deauth_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,
|
handler=self._api,
|
||||||
external_port=self._ws_conf.external_port,
|
external_port=self._ws_conf.external_port,
|
||||||
auth_strategy=self._auth_strategy,
|
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):
|
def listen(self):
|
||||||
"""Self-host using 'bind' and 'port' from the WS config group."""
|
"""Self-host using 'bind' and 'port' from the WS config group."""
|
||||||
|
@ -23,12 +23,13 @@ class ProtocolFactory(websocket.WebSocketServerFactory):
|
|||||||
protocol = protocol.MessagingProtocol
|
protocol = protocol.MessagingProtocol
|
||||||
|
|
||||||
def __init__(self, uri, debug, handler, external_port, auth_strategy,
|
def __init__(self, uri, debug, handler, external_port, auth_strategy,
|
||||||
loop):
|
loop, secret_key):
|
||||||
websocket.WebSocketServerFactory.__init__(
|
websocket.WebSocketServerFactory.__init__(
|
||||||
self, url=uri, debug=debug, externalPort=external_port)
|
self, url=uri, debug=debug, externalPort=external_port)
|
||||||
self._handler = handler
|
self._handler = handler
|
||||||
self._auth_strategy = auth_strategy
|
self._auth_strategy = auth_strategy
|
||||||
self._loop = loop
|
self._loop = loop
|
||||||
|
self._secret_key = secret_key
|
||||||
|
|
||||||
def __call__(self):
|
def __call__(self):
|
||||||
proto = self.protocol(self._handler, self._auth_strategy, self._loop)
|
proto = self.protocol(self._handler, self._auth_strategy, self._loop)
|
||||||
|
@ -73,8 +73,17 @@ class MessagingProtocol(websocket.WebSocketServerProtocol):
|
|||||||
if resp is None:
|
if resp is None:
|
||||||
if self._auth_strategy and not self._authentified:
|
if self._auth_strategy and not self._authentified:
|
||||||
if self._auth_app or payload.get('action') != 'authenticate':
|
if self._auth_app or payload.get('action') != 'authenticate':
|
||||||
body = {'error': 'Not authentified.'}
|
if 'URL-Signature' in payload.get('headers', {}):
|
||||||
resp = self._handler.create_response(403, body, req)
|
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:
|
else:
|
||||||
return self._authenticate(payload)
|
return self._authenticate(payload)
|
||||||
elif payload.get('action') == 'authenticate':
|
elif payload.get('action') == 'authenticate':
|
||||||
|
Loading…
Reference in New Issue
Block a user