mirror of https://github.com/GNS3/gns3-server
parent
798f0367b9
commit
e28079096e
@ -0,0 +1,148 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2013 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
JSON-RPC protocol over Websockets.
|
||||
"""
|
||||
|
||||
import zmq
|
||||
import uuid
|
||||
import tornado.websocket
|
||||
from tornado.escape import json_decode
|
||||
from ..jsonrpc import JSONRPCParseError, JSONRPCInvalidRequest, JSONRPCMethodNotFound
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
|
||||
"""
|
||||
STOMP protocol over Tornado Websockets with message
|
||||
routing to ZeroMQ dealer clients.
|
||||
|
||||
:param application: Tornado Application object
|
||||
:param request: Tornado Request object
|
||||
:param zmq_router: ZeroMQ router socket
|
||||
"""
|
||||
|
||||
clients = set()
|
||||
destinations = {}
|
||||
version = 2.0 # only JSON-RPC version 2.0 is supported
|
||||
|
||||
def __init__(self, application, request, zmq_router):
|
||||
tornado.websocket.WebSocketHandler.__init__(self, application, request)
|
||||
self._session_id = str(uuid.uuid4())
|
||||
self.zmq_router = zmq_router
|
||||
|
||||
@property
|
||||
def session_id(self):
|
||||
"""
|
||||
Session ID uniquely representing a Websocket client
|
||||
|
||||
:returns: the session id
|
||||
"""
|
||||
|
||||
return self._session_id
|
||||
|
||||
@classmethod
|
||||
def dispatch_message(cls, message):
|
||||
"""
|
||||
Sends a message to Websocket client
|
||||
|
||||
:param message: message from a module (received via ZeroMQ)
|
||||
"""
|
||||
|
||||
# Module name that is replying
|
||||
module = message[0].decode("utf-8")
|
||||
|
||||
# ZMQ responses are encoded in JSON
|
||||
# format is a JSON array: [session ID, JSON-RPC response]
|
||||
json_message = json_decode(message[1])
|
||||
session_id = json_message[0]
|
||||
jsonrpc_response = json_message[1]
|
||||
|
||||
log.debug("Received message from module {}: {}".format(module, json_message))
|
||||
|
||||
for client in cls.clients:
|
||||
if client.session_id == session_id:
|
||||
client.write_message(jsonrpc_response)
|
||||
|
||||
@classmethod
|
||||
def register_destination(cls, destination, module):
|
||||
"""
|
||||
Registers a destination handled by a module.
|
||||
Used to route requests to the right module.
|
||||
|
||||
:param destination: destination string
|
||||
:param module: module string
|
||||
"""
|
||||
|
||||
# Make sure the destination is not already registered
|
||||
# by another module for instance
|
||||
assert destination not in cls.destinations
|
||||
log.info("registering {} as a destination for {}".format(destination,
|
||||
module))
|
||||
cls.destinations[destination] = module
|
||||
|
||||
def open(self):
|
||||
"""
|
||||
Invoked when a new WebSocket is opened.
|
||||
"""
|
||||
|
||||
log.info("Websocket client {} connected".format(self.session_id))
|
||||
self.clients.add(self)
|
||||
|
||||
def on_message(self, message):
|
||||
"""
|
||||
Handles incoming messages.
|
||||
|
||||
:param message: message received over the Websocket
|
||||
"""
|
||||
|
||||
log.debug("Received Websocket message: {}".format(message))
|
||||
|
||||
try:
|
||||
request = json_decode(message)
|
||||
jsonrpc_version = request["jsonrpc"]
|
||||
method = request["method"]
|
||||
# warning: notifications cannot be sent by a client because check for an "id" here
|
||||
request_id = request["id"]
|
||||
except:
|
||||
return self.write_message(JSONRPCParseError()())
|
||||
|
||||
if jsonrpc_version != self.version:
|
||||
return self.write_message(JSONRPCInvalidRequest()())
|
||||
|
||||
if method not in self.destinations:
|
||||
return self.write_message(JSONRPCMethodNotFound(request_id)())
|
||||
|
||||
module = self.destinations[method]
|
||||
# ZMQ requests are encoded in JSON
|
||||
# format is a JSON array: [session ID, JSON-RPC request]
|
||||
zmq_request = [self.session_id, request]
|
||||
# Route to the correct module
|
||||
self.zmq_router.send_string(module, zmq.SNDMORE)
|
||||
# Send the encoded JSON request
|
||||
self.zmq_router.send_json(zmq_request)
|
||||
|
||||
def on_close(self):
|
||||
"""
|
||||
Invoked when the WebSocket is closed.
|
||||
"""
|
||||
|
||||
log.info("Websocket client {} disconnected".format(self.session_id))
|
||||
self.clients.remove(self)
|
@ -0,0 +1,182 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2013 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
JSON-RPC protocol implementation.
|
||||
http://www.jsonrpc.org/specification
|
||||
"""
|
||||
|
||||
import json
|
||||
import uuid
|
||||
|
||||
|
||||
class JSONRPCObject(object):
|
||||
"""
|
||||
Base object for JSON-RPC requests, responses,
|
||||
notifications and errors.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
return JSONRPCEncoder().default(self)
|
||||
|
||||
def __str__(self, *args, **kwargs):
|
||||
return json.dumps(self, cls=JSONRPCEncoder)
|
||||
|
||||
def __call__(self):
|
||||
return JSONRPCEncoder().default(self)
|
||||
|
||||
|
||||
class JSONRPCEncoder(json.JSONEncoder):
|
||||
"""
|
||||
Creates the JSON-RPC message.
|
||||
"""
|
||||
|
||||
def default(self, obj):
|
||||
"""
|
||||
Returns a Python dictionary corresponding to a JSON-RPC message.
|
||||
"""
|
||||
|
||||
if isinstance(obj, JSONRPCObject):
|
||||
message = {"jsonrpc": 2.0}
|
||||
for field in dir(obj):
|
||||
if not field.startswith('_'):
|
||||
value = getattr(obj, field)
|
||||
message[field] = value
|
||||
return message
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
class JSONRPCInvalidRequest(JSONRPCObject):
|
||||
"""
|
||||
Error response for an invalid request.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
JSONRPCObject.__init__(self)
|
||||
self.id = None
|
||||
self.error = {"code": -32600, "message": "Invalid Request"}
|
||||
|
||||
|
||||
class JSONRPCMethodNotFound(JSONRPCObject):
|
||||
"""
|
||||
Error response for an method not found.
|
||||
|
||||
:param request_id: JSON-RPC identifier
|
||||
"""
|
||||
|
||||
def __init__(self, request_id):
|
||||
JSONRPCObject.__init__(self)
|
||||
self.id = request_id
|
||||
self.error = {"code": -32601, "message": "Method not found"}
|
||||
|
||||
|
||||
class JSONRPCInvalidParams(JSONRPCObject):
|
||||
"""
|
||||
Error response for invalid parameters.
|
||||
|
||||
:param request_id: JSON-RPC identifier
|
||||
"""
|
||||
|
||||
def __init__(self, request_id):
|
||||
JSONRPCObject.__init__(self)
|
||||
self.id = request_id
|
||||
self.error = {"code": -32602, "message": "Invalid params"}
|
||||
|
||||
|
||||
class JSONRPCInternalError(JSONRPCObject):
|
||||
"""
|
||||
Error response for an internal error.
|
||||
|
||||
:param request_id: JSON-RPC identifier (optional)
|
||||
"""
|
||||
|
||||
def __init__(self, request_id=None):
|
||||
JSONRPCObject.__init__(self)
|
||||
self.id = request_id
|
||||
self.error = {"code": -32603, "message": "Internal error"}
|
||||
|
||||
|
||||
class JSONRPCParseError(JSONRPCObject):
|
||||
"""
|
||||
Error response for parsing error.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
JSONRPCObject.__init__(self)
|
||||
self.id = None
|
||||
self.error = {"code": -32700, "message": "Parse error"}
|
||||
|
||||
|
||||
class JSONRPCCustomError(JSONRPCObject):
|
||||
"""
|
||||
Error response for an custom error.
|
||||
|
||||
:param code: JSON-RPC error code
|
||||
:param message: JSON-RPC error message
|
||||
:param request_id: JSON-RPC identifier (optional)
|
||||
"""
|
||||
|
||||
def __init__(self, code, message, request_id=None):
|
||||
JSONRPCObject.__init__(self)
|
||||
self.id = request_id
|
||||
self.error = {"code": code, "message": message}
|
||||
|
||||
|
||||
class JSONRPCResponse(JSONRPCObject):
|
||||
"""
|
||||
JSON-RPC successful response.
|
||||
|
||||
:param result: JSON-RPC result
|
||||
:param request_id: JSON-RPC identifier
|
||||
"""
|
||||
|
||||
def __init__(self, result, request_id):
|
||||
JSONRPCObject.__init__(self)
|
||||
self.id = request_id
|
||||
self.result = result
|
||||
|
||||
|
||||
class JSONRPCRequest(JSONRPCObject):
|
||||
"""
|
||||
JSON-RPC request.
|
||||
|
||||
:param method: JSON-RPC destination method
|
||||
:param params: JSON-RPC params for the corresponding method (optional)
|
||||
:param request_id: JSON-RPC identifier (generated by default)
|
||||
"""
|
||||
|
||||
def __init__(self, method, params=None, request_id=str(uuid.uuid1())):
|
||||
JSONRPCObject.__init__(self)
|
||||
self.id = request_id
|
||||
self.method = method
|
||||
if params:
|
||||
self.params = params
|
||||
|
||||
|
||||
class JSONRPCNotification(JSONRPCObject):
|
||||
"""
|
||||
JSON-RPC notification.
|
||||
|
||||
:param method: JSON-RPC destination method
|
||||
:param params: JSON-RPC params for the corresponding method (optional)
|
||||
"""
|
||||
|
||||
def __init__(self, method, params=None):
|
||||
JSONRPCObject.__init__(self)
|
||||
self.method = method
|
||||
if params:
|
||||
self.params = params
|
@ -0,0 +1,91 @@
|
||||
import uuid
|
||||
from tornado.testing import AsyncTestCase
|
||||
from tornado.escape import json_encode, json_decode
|
||||
from ws4py.client.tornadoclient import TornadoWebSocketClient
|
||||
import gns3server.jsonrpc as jsonrpc
|
||||
|
||||
"""
|
||||
Tests for JSON-RPC protocol over Websockets
|
||||
"""
|
||||
|
||||
|
||||
class JSONRPC(AsyncTestCase):
|
||||
|
||||
URL = "ws://127.0.0.1:8000/"
|
||||
|
||||
def test_request(self):
|
||||
|
||||
params = {"echo": "test"}
|
||||
request = jsonrpc.JSONRPCRequest("dynamips.echo", params)
|
||||
AsyncWSRequest(self.URL, self.io_loop, self.stop, str(request))
|
||||
response = self.wait()
|
||||
json_response = json_decode(response)
|
||||
assert json_response["jsonrpc"] == 2.0
|
||||
assert json_response["id"] == request.id
|
||||
assert json_response["result"] == params
|
||||
|
||||
def test_request_with_invalid_method(self):
|
||||
|
||||
message = {"echo": "test"}
|
||||
request = jsonrpc.JSONRPCRequest("dynamips.non_existent", message)
|
||||
AsyncWSRequest(self.URL, self.io_loop, self.stop, str(request))
|
||||
response = self.wait()
|
||||
json_response = json_decode(response)
|
||||
assert json_response["error"].get("code") == -32601
|
||||
assert json_response["id"] == request.id
|
||||
|
||||
def test_request_with_invalid_version(self):
|
||||
|
||||
request = {"jsonrpc": "1.0", "method": "dynamips.echo", "id": 1}
|
||||
AsyncWSRequest(self.URL, self.io_loop, self.stop, json_encode(request))
|
||||
response = self.wait()
|
||||
json_response = json_decode(response)
|
||||
assert json_response["id"] == None
|
||||
assert json_response["error"].get("code") == -32600
|
||||
|
||||
def test_request_with_invalid_json(self):
|
||||
|
||||
request = "my non JSON request"
|
||||
AsyncWSRequest(self.URL, self.io_loop, self.stop, request)
|
||||
response = self.wait()
|
||||
json_response = json_decode(response)
|
||||
assert json_response["id"] == None
|
||||
assert json_response["error"].get("code") == -32700
|
||||
|
||||
def test_request_with_invalid_jsonrpc_field(self):
|
||||
|
||||
request = {"jsonrpc": "2.0", "method_bogus": "dynamips.echo", "id": 1}
|
||||
AsyncWSRequest(self.URL, self.io_loop, self.stop, json_encode(request))
|
||||
response = self.wait()
|
||||
json_response = json_decode(response)
|
||||
assert json_response["id"] == None
|
||||
assert json_response["error"].get("code") == -32700
|
||||
|
||||
def test_request_with_no_params(self):
|
||||
|
||||
request = jsonrpc.JSONRPCRequest("dynamips.echo")
|
||||
AsyncWSRequest(self.URL, self.io_loop, self.stop, str(request))
|
||||
response = self.wait()
|
||||
json_response = json_decode(response)
|
||||
assert json_response["id"] == request.id
|
||||
assert json_response["error"].get("code") == -32602
|
||||
|
||||
|
||||
class AsyncWSRequest(TornadoWebSocketClient):
|
||||
"""
|
||||
Very basic Websocket client for tests
|
||||
"""
|
||||
|
||||
def __init__(self, url, io_loop, callback, message):
|
||||
TornadoWebSocketClient.__init__(self, url, io_loop=io_loop)
|
||||
self._callback = callback
|
||||
self._message = message
|
||||
self.connect()
|
||||
|
||||
def opened(self):
|
||||
self.send(self._message, binary=False)
|
||||
|
||||
def received_message(self, message):
|
||||
self.close()
|
||||
if self._callback:
|
||||
self._callback(message.data)
|
Loading…
Reference in new issue