Example #1
0
    def initialize(self, apiurl, auth_backend, deployer, tokens, io_loop=None):
        """Initialize the WebSocket server.

        Create a new WebSocket client and connect it to the Juju API.
        Set up the authentication system.
        Handle the queued messages.
        """
        if io_loop is None:
            io_loop = IOLoop.current()
        self._io_loop = io_loop
        self._summary = request_summary(self.request) + ' '
        logging.info(self._summary + 'client connected')
        self.connected = True
        self.juju_connected = False
        self._juju_message_queue = queue = deque()
        # Set up the authentication infrastructure.
        self.tokens = tokens
        write_message = wrap_write_message(self)
        self.user = User()
        self.auth = AuthMiddleware(
            self.user, auth_backend, tokens, write_message)
        # Set up the bundle deployment and change set infrastructure.
        self.deployment = DeployMiddleware(self.user, deployer, write_message)
        self.changeset = ChangeSetMiddleware(self.user, write_message)
        # XXX The handler is no longer path agnostic, and this can be fixed by
        # capturing the relevant path fragment on the regexp, and then
        # overriding the handler's open method to store the path in the
        # instance.
        path = self.request.path[len('/ws'):]
        if path:
            # If they provided a path in their request then we need to use
            # that api endpoint.
            apiurl = '{}{}'.format(apiurl, path)
        # Juju requires the Origin header to be included in the WebSocket
        # client handshake request. Propagate the client origin if present;
        # use the Juju API server as origin otherwise.
        headers = get_headers(self.request, apiurl)
        # Connect the WebSocket client to the Juju API server.
        self._juju_connected_future = websocket_connect(
            io_loop, apiurl, self.on_juju_message, headers=headers)
        try:
            self.juju_connection = yield self._juju_connected_future
        except Exception as err:
            logging.error(self._summary + 'unable to connect to the Juju API')
            logging.exception(err)
            self.connected = False
            return
        # At this point the Juju API is successfully connected.
        self.juju_connected = True
        logging.info(self._summary + 'Juju API connected')
        # Send all the messages that have been enqueued before the connection
        # to the Juju API server was established.
        while self.connected and self.juju_connected and len(queue):
            message = queue.popleft()
            encoded = message.encode('utf-8')
            logging.debug(self._summary + 'queue -> juju: {}'.format(encoded))
            self.juju_connection.write_message(message)
Example #2
0
    def initialize(
            self, apiurl, auth_backend, deployer, tokens, ws_source_template,
            ws_target_template, io_loop=None):
        """Initialize the WebSocket server.

        Create a new WebSocket client and connect it to the Juju API.
        Set up the authentication system.
        Handle the queued messages.
        """
        if io_loop is None:
            io_loop = IOLoop.current()
        self._io_loop = io_loop
        self._summary = request_summary(self.request) + ' '
        logging.info(self._summary + 'client connected')
        self.connected = True
        self.juju_connected = False
        self._juju_message_queue = queue = deque()
        # Set up the authentication infrastructure.
        self.tokens = tokens
        write_message = wrap_write_message(self)
        self.user = User()
        self.auth = AuthMiddleware(
            self.user, auth_backend, tokens, write_message)
        # Set up the bundle deployment and change set infrastructure.
        self.deployment = DeployMiddleware(self.user, deployer, write_message)
        self.changeset = ChangeSetMiddleware(self.user, write_message)
        apiurl = get_juju_api_url(
            self.request.path, ws_source_template, ws_target_template, apiurl)
        # Juju requires the Origin header to be included in the WebSocket
        # client handshake request. Propagate the client origin if present;
        # use the Juju API server as origin otherwise.
        headers = get_headers(self.request, apiurl)
        # Connect the WebSocket client to the Juju API server.
        self._juju_connected_future = websocket_connect(
            io_loop, apiurl, self.on_juju_message, headers=headers)
        try:
            self.juju_connection = yield self._juju_connected_future
        except Exception as err:
            logging.error(self._summary + 'unable to connect to the Juju API')
            logging.exception(err)
            self.connected = False
            return
        # At this point the Juju API is successfully connected.
        self.juju_connected = True
        logging.info(self._summary + 'Juju API connected: {}'.format(apiurl))
        # Send all the messages that have been enqueued before the connection
        # to the Juju API server was established.
        while self.connected and self.juju_connected and len(queue):
            message = queue.popleft()
            encoded = message.encode('utf-8')
            logging.debug(self._summary + 'queue -> juju: {}'.format(encoded))
            self.juju_connection.write_message(message)
Example #3
0
class WebSocketHandler(_WebSocketBaseHandler):
    """WebSocket handler supporting secure WebSockets.

    This handler acts as a proxy between the browser connection and the
    Juju API server. It also handles API authentication and requests for
    bundles deployment (using the juju-deployer deployment format).

    Relevant attributes:

      - connected: True if the current browser is connected, False otherwise;
      - juju_connected: True if the Juju API is connected, False otherwise;
      - juju_connection: the WebSocket client connection to the Juju API.

    Callbacks:

      - on_message(message): called when a message arrives from the browser;
      - on_juju_message(message): called when a message arrives from Juju;
      - on_close(): called when the browser closes the connection;
      - on_juju_close(): called when juju closes the connection.

    Methods:
      - write_message(message): send a message to the browser;
      - close(): terminate the browser connection.
    """

    @gen.coroutine
    def initialize(self, apiurl, auth_backend, deployer, tokens, io_loop=None):
        """Initialize the WebSocket server.

        Create a new WebSocket client and connect it to the Juju API.
        Set up the authentication system.
        Handle the queued messages.
        """
        if io_loop is None:
            io_loop = IOLoop.current()
        self._io_loop = io_loop
        self._summary = request_summary(self.request) + ' '
        logging.info(self._summary + 'client connected')
        self.connected = True
        self.juju_connected = False
        self._juju_message_queue = queue = deque()
        # Set up the authentication infrastructure.
        self.tokens = tokens
        write_message = wrap_write_message(self)
        self.user = User()
        self.auth = AuthMiddleware(
            self.user, auth_backend, tokens, write_message)
        # Set up the bundle deployment and change set infrastructure.
        self.deployment = DeployMiddleware(self.user, deployer, write_message)
        self.changeset = ChangeSetMiddleware(self.user, write_message)
        # XXX The handler is no longer path agnostic, and this can be fixed by
        # capturing the relevant path fragment on the regexp, and then
        # overriding the handler's open method to store the path in the
        # instance.
        path = self.request.path[len('/ws'):]
        if path:
            # If they provided a path in their request then we need to use
            # that api endpoint.
            apiurl = '{}{}'.format(apiurl, path)
        # Juju requires the Origin header to be included in the WebSocket
        # client handshake request. Propagate the client origin if present;
        # use the Juju API server as origin otherwise.
        headers = get_headers(self.request, apiurl)
        # Connect the WebSocket client to the Juju API server.
        self._juju_connected_future = websocket_connect(
            io_loop, apiurl, self.on_juju_message, headers=headers)
        try:
            self.juju_connection = yield self._juju_connected_future
        except Exception as err:
            logging.error(self._summary + 'unable to connect to the Juju API')
            logging.exception(err)
            self.connected = False
            return
        # At this point the Juju API is successfully connected.
        self.juju_connected = True
        logging.info(self._summary + 'Juju API connected')
        # Send all the messages that have been enqueued before the connection
        # to the Juju API server was established.
        while self.connected and self.juju_connected and len(queue):
            message = queue.popleft()
            encoded = message.encode('utf-8')
            logging.debug(self._summary + 'queue -> juju: {}'.format(encoded))
            self.juju_connection.write_message(message)

    def on_message(self, message):
        """Hook called when a new message is received from the browser.

        If the message is a change set request, return the resulting changes.
        If the message is a deployment request, start the deployment process.
        Otherwise the message is propagated to the Juju API server.
        Messages sent before the client connection to the Juju API server is
        established are queued for later delivery.
        """
        data = json_decode_dict(message)
        encoded = None
        if data is not None:
            # Handle change set requests.
            if self.changeset.requested(data):
                return self.changeset.process_request(data)
            # Handle deployment requests.
            if self.deployment.requested(data):
                return self.deployment.process_request(data)
            # Handle authentication requests.
            if not self.user.is_authenticated:
                new_data = self.auth.process_request(data)
                if new_data is None:
                    # The None marker indicates that a response was sent.
                    return
                elif new_data != data:
                    encoded = escape.json_encode(new_data)
                    message = encoded.decode('utf8')
            # Handle authentication token requests.
            if self.tokens.token_requested(data):
                return self.tokens.process_token_request(
                    data, self.user, wrap_write_message(self))
        # Propagate messages to the Juju API server.
        if encoded is None:
            encoded = message.encode('utf-8')
        if self.juju_connected:
            logging.debug(self._summary + 'client -> juju: {}'.format(encoded))
            return self.juju_connection.write_message(message)
        logging.debug(self._summary + 'client -> queue: {}'.format(encoded))
        self._juju_message_queue.append(message)

    def on_juju_message(self, message):
        """Hook called when a new message is received from the Juju API server.

        The message is propagated to the browser.
        """
        if message is None:
            # The Juju API closed the connection.
            return self.on_juju_close()
        data = json_decode_dict(message)
        if (data is not None) and self.auth.in_progress():
            encoded = escape.json_encode(
                self.auth.process_response(data))
            message = encoded.decode('utf8')
        else:
            encoded = message.encode('utf-8')
        logging.debug(self._summary + 'juju -> client: {}'.format(encoded))
        self.write_message(message)

    def on_close(self):
        """Hook called when the WebSocket connection is terminated."""
        logging.info(self._summary + 'client connection closed')
        self.connected = False
        # At this point the WebSocket client connection to the Juju API server
        # might not yet be established. For this reason the connection is
        # terminated adding a callback to the corresponding future.
        callback = lambda _: self.juju_connection.close()
        self._io_loop.add_future(self._juju_connected_future, callback)

    def on_juju_close(self):
        """Hook called when the WebSocket connection to Juju is terminated."""
        logging.info(self._summary + 'Juju API connection closed')
        self.juju_connected = False
        self.juju_connection = None
        # Usually the Juju API connection is terminated as a consequence of a
        # browser disconnection. A server disconnection is unexpected and
        # unlikely to happen. In the future Juju will support HA and we will
        # need to react accordingly to server disconnections, but for the time
        # being we just disconnect the browser and log an error.
        if self.connected:
            logging.error(self._summary + 'Juju API unexpectedly disconnected')
            self.close()