def start(self): # create a Session repr between ourselves and the Router. # pass in out ``MessageHandler`` which will process messages # before they are passed back to the client. self._session = Session( client=self, router=self.router, transport=self.transport, message_handler=self.message_handler, ) # establish the session message_obj = self.session.begin() # raise if Router aborts handshake or we cannot respond to a # Challenge. if message_obj.WAMP_CODE == Abort.WAMP_CODE: raise WampyError(message_obj.message) if message_obj.WAMP_CODE == Challenge.WAMP_CODE: if 'WAMPYSECRET' not in os.environ: raise WampyError("Wampy requires a client's secret to be " "in the environment as ``WAMPYSECRET``") raise WampyError("Failed to handle CHALLENGE") logger.debug('client %s has established a session with id "%s"', self.name, self.session.id)
def wrapper(*args, **kwargs): message = Call(procedure=name, args=args, kwargs=kwargs) response = self.client.make_rpc(message) wamp_code = response.WAMP_CODE if wamp_code == Error.WAMP_CODE: _, _, request_id, _, endpoint, exc_args, exc_kwargs = ( response.message) if endpoint == NOT_AUTHORISED: raise WampyError( "NOT_AUTHORISED: {} - {}".format( self.client.name, exc_args[0] ) ) raise WampyError( 'oops! wampy has failed, sorry: {}'.format( response.message ) ) if wamp_code != Result.WAMP_CODE: raise WampProtocolError( 'unexpected message code: "%s (%s) %s"', wamp_code, MESSAGE_TYPE_MAP[wamp_code], response[5] ) result = response.value logger.debug("RpcProxy got result: %s", result) return result
def start(self): # establish the underlying connection. this will raise on error. connection = self.transport.connect() # create a Session repr between ourselves and the Router. # pass in the live connection over a transport that the Session # doesn't need to care about - it only cares how to receive # messages over this. # pass in out ``MessageHandler`` which will process messages # before they are passed back to the client. self._session = Session( client=self, router=self.router, connection=connection, message_handler=self.message_handler, ) # establish the session message_obj = self.session.begin() # raise if Router aborts handshake or we cannot respond to a # Challenge. if message_obj.WAMP_CODE == Abort.WAMP_CODE: raise WelcomeAbortedError(message_obj.message) if message_obj.WAMP_CODE == Challenge.WAMP_CODE: if 'WAMPYSECRET' not in os.environ: raise WampyError("Wampy requires a client's secret to be " "in the environment as ``WAMPYSECRET``") raise WampyError("Failed to handle CHALLENGE") logger.debug('client %s has established a session with id "%s"', self.name, self.session.id)
def wait_for_subscriptions(container, number_of_subscriptions): if not container.started: raise WampyError( "Cannot look for registrations unless the service is running") for ext in container.extensions: if type(ext) == WampTopicProxy: break else: raise WampyError("no clients found subscribing to topics") session = ext.client.session success = False with eventlet.Timeout(TIMEOUT, False): while (len(session.subscription_map.keys()) < number_of_subscriptions): eventlet.sleep() success = True if not success: logger.error("%s has not subscribed to %s topics", ext.client.name, number_of_subscriptions) raise WampyError("Subscriptions Not Found") logger.info("found subscriptions: %s", session.subscription_map.keys())
def wait_for_registrations(container, number_of_registrations): if not container.started: raise WampyError( "Cannot look for registrations unless the service is running") for ext in container.extensions: if type(ext) == WampCalleeProxy: break else: raise WampyError("no clients found registering callees") session = ext.client.session success = False with eventlet.Timeout(TIMEOUT, False): while (len(session.registration_map.keys()) < number_of_registrations): eventlet.sleep() success = True if not success: logger.error("%s has not registered %s callees", ext.client.name, number_of_registrations) raise WampyError("Registrations Not Found: {}".format( session.registration_map.keys())) logger.info("found registrations: %s", session.registration_map.keys())
def start(self): """ Start Crossbar.io in a subprocess. """ if self.started is True: raise WampyError("Router already started") # will attempt to connect or start up the CrossBar crossbar_config_path = self.config_path cbdir = self.crossbar_directory # starts the process from the root of the test namespace cmd = [ 'crossbar', 'start', '--cbdir', cbdir, '--config', crossbar_config_path, ] self.proc = subprocess.Popen(cmd, preexec_fn=os.setsid) self._wait_until_ready() logger.info( "Crosbar.io is ready for connections on %s (IPV%s)", self.url, self.ipv ) self.started = True
def callee_callback(self, procedure_name, *args, **kwargs): for provider in self.providers: if provider.method_name == procedure_name: provider.handle_message(*args, **kwargs) break else: raise WampyError('no providers matching procedure_name')
def _connect(self): if self.ipv == 4: _socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: _socket.connect((self.host.encode(), self.port)) except socket_error as exc: if exc.errno == 61: logger.error('unable to connect to %s:%s (IPV%s)', self.host, self.port, self.ipv) raise elif self.ipv == 6: _socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) try: _socket.connect(("::", self.port)) except socket_error as exc: if exc.errno == 61: logger.error('unable to connect to %s:%s (IPV%s)', self.host, self.port, self.ipv) raise else: raise WampyError("unknown IPV: {}".format(self.ipv)) self.socket = _socket logger.debug("socket connected")
def __init__( self, config_path="./crossbar/config.json", crossbar_directory=None, ): with open(config_path) as data_file: config_data = json.load(data_file) self.config = config_data self.config_path = config_path config = self.config['workers'][0] self.realm = config['realms'][0] self.roles = self.realm['roles'] if len(config['transports']) > 1: raise WampyError( "Only a single websocket transport is supported by Wampy, " "sorry") self.transport = config['transports'][0] self.url = self.transport.get("url") if self.url is None: raise WampyError("The ``url`` value is required by Wampy. " "Please add to your configuration file. Thanks.") self.ipv = self.transport['endpoint'].get("version", None) if self.ipv is None: logger.warning( "defaulting to IPV 4 because neither was specified.") self.ipv = 4 self.parse_url() self.websocket_location = self.resource self.crossbar_directory = crossbar_directory try: self.certificate = self.transport['endpoint']['tls']['certificate'] except KeyError: self.certificate = None self.proc = None self.started = False
def __call__(self, *unsupported_args, **kwargs): if len(unsupported_args) != 0: raise WampyError( "wampy only supports publishing keyword arguments " "to a Topic.") topic = kwargs.pop("topic") if not kwargs: raise WampyError( "wampy requires at least one message to publish to a topic") if "options" not in kwargs: kwargs["options"] = {} message = Publish(topic=topic, **kwargs) logger.info('publishing message: "%s"', message) self.client.send_message(message)
def __init__(self, server_url, certificate_path, ipv=4): super(SecureWebSocket, self).__init__(server_url=server_url, ipv=ipv) # PROTOCOL_TLSv1_1 and PROTOCOL_TLSv1_2 are only available if Python is # linked with OpenSSL 1.0.1 or later. try: self.ssl_version = ssl.PROTOCOL_TLSv1_2 except AttributeError: raise WampyError("Your Python Environment does not support TLS") self.certificate = certificate_path
def __init__( self, url="ws://localhost:8080", config_path="./crossbar/config.json", crossbar_directory=None, ): """ A wrapper around a Crossbar Server. Wampy uses this when executing its test suite. Typically used in test cases, local dev and scripts rather than with production applications. For Production, just deploy and connect to as you would any other server. """ with open(config_path) as data_file: config_data = json.load(data_file) self.config = config_data self.config_path = config_path config = self.config['workers'][0] self.realm = config['realms'][0] self.roles = self.realm['roles'] if len(config['transports']) > 1: raise WampyError( "Only a single websocket transport is supported by Wampy, " "sorry" ) self.transport = config['transports'][0] self.url = url self.ipv = self.transport['endpoint'].get("version", None) if self.ipv is None: logger.warning( "defaulting to IPV 4 because neither was specified." ) self.ipv = 4 self.parse_url() self.websocket_location = self.resource self.crossbar_directory = crossbar_directory try: self.certificate = self.transport['endpoint']['tls']['certificate'] except KeyError: self.certificate = None self.proc = None self.started = False
def _say_hello(self): details = self.roles for role, features in details['roles'].items(): features.setdefault('features', {}) features['features'].setdefault('call_timeout', True) message = Hello(realm=self.realm, details=details) self.send_message(message) message_obj = self.recv_message() # raise if Router aborts handshake or we cannot respond to a # Challenge. if message_obj.WAMP_CODE == Abort.WAMP_CODE: raise WampyError(message_obj.message) if message_obj.WAMP_CODE == Challenge.WAMP_CODE: if 'WAMPYSECRET' not in os.environ: raise WampyError("Wampy requires a client's secret to be " "in the environment as ``WAMPYSECRET``") raise WampyError("Failed to handle CHALLENGE")
def register_router(self, router): super(SecureWebSocket, self).register_router(router) self.ipv = router.ipv # PROTOCOL_TLSv1_1 and PROTOCOL_TLSv1_2 are only available if Python is # linked with OpenSSL 1.0.1 or later. try: self.ssl_version = ssl.PROTOCOL_TLSv1_2 except AttributeError: raise WampyError("Your Python Environment does not support TLS") self.certificate = router.certificate
def _upgrade(self): handshake_headers = self._get_handshake_headers() handshake = '\r\n'.join(handshake_headers) + "\r\n\r\n" self.socket.send(handshake.encode()) try: with eventlet.Timeout(5): self.status, self.headers = self._read_handshake_response() except eventlet.Timeout: raise WampyError( 'No response after handshake "{}"'.format(handshake)) logger.debug("connection upgraded")
def get_async_adapter(): if async_name == GEVENT: from . gevent_ import Gevent _adapter = Gevent() return _adapter if async_name == EVENTLET: from . eventlet_ import Eventlet _adapter = Eventlet() return _adapter raise WampyError( 'only gevent and eventlet are supported, sorry. help out??' )
def wrapper(*args, **kwargs): # timeout is currently handled by wampy whilst # https://github.com/crossbario/crossbar/issues/299 # is addressed, but we pass in the value regardless, waiting # for the feature on CrossBar. # WAMP Call Message requires milliseconds... options = { 'timeout': int(self.client.call_timeout * 1000), } message = Call( procedure=name, options=options, args=args, kwargs=kwargs, ) response = self.client._make_rpc(message) wamp_code = response.WAMP_CODE if wamp_code == Error.WAMP_CODE: _, _, request_id, _, endpoint, exc_args, exc_kwargs = ( response.message) if endpoint == NOT_AUTHORISED: raise WampyError("NOT_AUTHORISED: {} - {}".format( self.client.name, exc_args[0])) raise WampyError('oops! wampy has failed, sorry: {}'.format( response.message)) if wamp_code != Result.WAMP_CODE: raise WampProtocolError( 'unexpected message code: "%s (%s) %s"', wamp_code, MESSAGE_TYPE_MAP[wamp_code], response[5]) result = response.value logger.debug("RpcProxy got result: %s", result) return result
def __init__(self, request_type, request_id, details=None, error="", args_list=None, kwargs_dict=None): """ Error reply sent by a Peer as an error response to different kinds of requests. :Parameters: :request_type: The WAMP message type code for the original request. :type request_type: int :request_id: The WAMP request ID of the original request (`Call`, `Subscribe`, ...) this error occurred for. :type request: int :args_list: Args to pass into an Application defined Exception :kwargs_list: Kwargs to pass into an Application defined Exception [ERROR, REQUEST.Type|int, REQUEST.Request|id, Details|dict, Error|uri, Arguments|list, ArgumentsKw|dict] """ super(Error, self).__init__() self.request_type = request_type self.request_id = request_id self.error = error self.args_list = args_list or [] self.kwargs_dict = kwargs_dict or {} # wampy is not implementing ``details`` which appears to be an # alternative to args and kwargs if details: raise WampyError("Not Implemented: must use ``args_list`` and '" "``kwargs_dict, not ``details``") self.details = {}
def __init__(self, bytes): super(ServerFrame, self).__init__(bytes) if not bytes: return try: self.payload_length_indicator = bytes[1] & 0b1111111 except Exception: raise IncompleteFrameError(required_bytes=1) # if this doesn't raise, all the above will receive a value self.ensure_complete_frame(bytes) # server must not mask the payload mask = bytes[1] >> 7 assert mask == 0 self.buffered_bytes = bytes self.len = 0 # Parse the first two bytes of header. self.fin = bytes[0] >> 7 if self.fin == 0: logger.exception("Multiple Frames Returned: %s", bytes) raise WampyError( 'Multiple framed responses not yet supported: {}'.format( bytes)) self.opcode = bytes[0] & 0b1111 if self.opcode != 9: # Wamp data frames contain a json-encoded payload. # The other kind of frame we handle (opcode 0x9) is a ping and it # has a non-json payload try: # decode required before loading JSON for python 2 only self.payload = json.loads(self.body.decode('utf-8')) except Exception: raise WebsocktProtocolError( 'Failed to load JSON object from: "%s"', self.body) else: self.payload = self.body
def try_connection(self): if self.ipv == 4: _socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: _socket.connect((self.host, self.port)) except socket_error: raise ConnectionError("Could not connect") elif self.ipv == 6: _socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) try: _socket.connect(("::", self.port)) except socket_error: raise ConnectionError("Could not connect") else: raise WampyError("unknown IPV: {}".format(self.ipv)) _socket.shutdown(socket.SHUT_RDWR) _socket.close()
def get_async_adapter(): class Gevent(Async): def __init__(self): self.message_queue = gevent.queue.Queue() def Timeout(self, timeout): return gevent.Timeout(timeout) def receive_message(self, timeout): try: message = self.message_queue.get(timeout=timeout) except gevent.queue.Empty: raise WampProtocolError( "no message returned (timed-out in {})".format(timeout) ) return message def spawn(self, *args, **kwargs): gthread = gevent.spawn(*args, **kwargs) return gthread def sleep(self, time): return gevent.sleep(time) class Eventlet(Async): def __init__(self): self.message_queue = eventlet.Queue() def Timeout(self, timeout): return eventlet.Timeout(timeout) def receive_message(self, timeout): try: message = self._wait_for_message(timeout) except eventlet.Timeout: raise WampProtocolError( "no message returned (timed-out in {})".format(timeout) ) return message def spawn(self, *args, **kwargs): gthread = eventlet.spawn(*args, **kwargs) return gthread def sleep(self, time): return eventlet.sleep(time) def _wait_for_message(self, timeout): q = self.message_queue with eventlet.Timeout(timeout): while q.qsize() == 0: eventlet.sleep() message = q.get() return message from wampy.config.defaults import async_name if async_name == GEVENT: _adapter = Gevent() return _adapter if async_name == EVENTLET: _adapter = Eventlet() return _adapter raise WampyError( 'only gevent and eventlet are supported, sorry. help out??' )
def __init__(self, **kwargs): if "topic" not in kwargs: raise WampyError("subscriber missing ``topic`` keyword argument") self.topic = kwargs['topic']
def __init__( self, router_url, message_handler, ipv, cert_path, call_timeout, realm, roles, ): """ A Session between a Client and a Router. The WAMP layer of the internal architecture. :Parameters: router_url : string The URL of the Router Peer. message_handler : instance An instance of ``wampy.message_handler.MessageHandler``, or a subclass of it. Handles incoming WAMP Messages. ipv : int The Internet Protocol version for the Transport to use """ self.url = router_url # decomposes the url, adding new Session instance variables for # them, so that the Session can decide on the Transport it needs # to use to connect to the Router self.parse_url() self.message_handler = message_handler self.ipv = ipv self.cert_path = cert_path self.call_timeout = call_timeout self.realm = realm self.roles = roles if self.scheme == "ws": self.transport = WebSocket( server_url=self.url, ipv=self.ipv, ) elif self.scheme == "wss": self.transport = SecureWebSocket( server_url=self.url, ipv=self.ipv, certificate_path=self.cert_path, ) else: raise WampyError( 'wampy only suppoers network protocol "ws" or "wss"') self.connection = self.transport.connect(upgrade=True) self.request_ids = {} self.subscription_map = {} self.registration_map = {} self.session_id = None # spawn a green thread to listen for incoming messages over # a connection and put them on a queue to be processed self._managed_thread = None # the MessageHandler is responsible for putting messages on # to this queue which are then returned to the Client. The # queue is shared between the green threads. self._message_queue = async_adapter.message_queue self._listen(self.connection)
def __init__( self, url=None, cert_path=None, realm=DEFAULT_REALM, roles=DEFAULT_ROLES, message_handler=None, name=None, router=None, ): """ A WAMP Client "Peer". WAMP is designed for application code to run within Clients, i.e. Peers. Peers have the roles Callee, Caller, Publisher, and Subscriber. Subclass this base class to implemente the Roles for your application. :Parameters: url : string The URL of the Router Peer. This must include protocol, host and port and an optional path, e.g. "ws://example.com:8080" or "wss://example.com:8080/ws". Note though that "ws" protocol defaults to port 8080, an "wss" to 443. cert_path : str If using ``wss`` protocol, a certificate might be required by the Router. If so, provide here. realm : str The routing namespace to construct the ``Session`` over. Defaults to ``realm1``. roles : dictionary Description of the Roles implemented by the ``Client``. Defaults to ``wampy.constants.DEFAULT_ROLES``. message_handler : instance An instance of ``wampy.message_handler.MessageHandler``, or a subclass of. This controls the conversation between the two Peers. name : string Optional name for your ``Client``. Useful for when testing your app or for logging. router : instance An alternative way to connect to a Router rather than ``url``. An instance of a Router Peer, e.g. ``wampy.peers.routers.Crossbar`` This is more configurable and powerful, but requires a copy of the Router's config file, making this only really useful in single host setups or testing. """ if url and router: raise WampyError( 'Both ``url`` and ``router`` decide how your client connects ' 'to the Router, and so only one can be defined on ' 'instantiation. Please choose one or the other.') # the endpoint of a WAMP Router self.url = url or CROSSBAR_DEFAULT # the ``realm`` is the administrive domain to route messages over. self.realm = realm # the ``roles`` define what Roles (features) the Client can act, # but also configure behaviour such as auth self.roles = roles # a Session is a transient conversation between two Peers - a Client # and a Router. Here we model the Peer we are going to connect to. self.router = router or Router(url=self.url, cert_path=cert_path) # wampy uses a decoupled "messge handler" to process incoming messages. # wampy also provides a very adequate default. self.message_handler = message_handler or MessageHandler() # this conversation is over a transport. WAMP messages are transmitted # as WebSocket messages by default (well, actually... that's because no # other transports are supported!) if self.router.scheme == "ws": self.transport = WebSocket() elif self.router.scheme == "wss": self.transport = SecureWebSocket() else: raise WampyError('Network protocl must be "ws" or "wss"') # the transport is responsible for the connection. self.transport.register_router(self.router) # generally ``name`` is used for debuggubg and logging only self.name = name or self.__class__.__name__ self._session = None
# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import logging import os from wampy.constants import GEVENT, EVENT_LOOP_BACKENDS from wampy.errors import WampyError logger = logging.getLogger(__name__) async_name = os.environ.get('WAMPY_ASYNC_NAME', GEVENT) logger.info('asycn name is "%s"', async_name) if async_name not in EVENT_LOOP_BACKENDS: logger.error('unsupported event loop for wampy! "%s"', async_name) raise WampyError( 'export your WAMPY_ASYNC_NAME os environ value to be one of "{}" ' 'or just remove and use the default gevent'.format( EVENT_LOOP_BACKENDS), )