Example #1
0
    def __init__(self, cfg, loop):
        self.loop = loop
        # List of bridges
        self.bridges = list()
        # Mapping of MUC-JID -> Nickname
        self.mucs = dict()
        # Mapping of MUC-JID -> Password
        self.muc_passwords = dict()

        try:
            # Get the optional XMPP address (host, port) if specified
            xmpp_address = tuple()
            if 'host' in cfg['xmpp']:
                xmpp_address = (cfg['xmpp']['host'], cfg['xmpp']['port'])
            # Parse the MUC definitions
            self.get_mucs(cfg)
            if len(self.mucs) == 0:
                logging.info("No MUCs defined.")
        except KeyError:
            raise InvalidConfigError

        # Parse the bridges from the config file
        need_incoming_webhooks = False
        for bridge_cfg in cfg['bridges']:
            bridge = SingleBridge(bridge_cfg, self)
            self.bridges.append(bridge)
            if bridge.has_incoming_webhooks():
                need_incoming_webhooks = True

        # Initialize XMPP client
        self.xmpp_client = XMPPBridgeBot(cfg['xmpp']['jid'],
                                         cfg['xmpp']['password'], self)
        self.xmpp_client.connect(address=xmpp_address)

        # Initialize HTTP server if needed
        if not need_incoming_webhooks:
            self.http_server = None
            logging.info("No incoming webhooks defined.")
        elif 'incoming_webhook_listener' in cfg:
            bind_address = cfg['incoming_webhook_listener']['bind_address']
            port = cfg['incoming_webhook_listener']['port']
            self.http_app = aiohttp.web.Application(loop=loop)
            self.http_app.router.add_route('POST', '/',
                                           self.handle_incoming_webhook)
            self.http_handler = self.http_app.make_handler()
            http_create_server = loop.create_server(self.http_handler,
                                                    bind_address, port)
            self.http_server = loop.run_until_complete(http_create_server)
            logging.info("Listening for incoming webhooks on "
                         "http://{}:{}/".format(bind_address, port))
        else:
            self.http_server = None
            logging.warn("No 'incoming_webhook_listener' in the config even "
                         "though incoming webhooks are defined. Ignoring all "
                         "incoming webhooks.")

        if not self.http_server:
            logging.info("Not listening for incoming webhooks.")
Example #2
0
    def __init__(self, cfg, loop):
        self.loop = loop
        self.incoming_normal_mappings = defaultdict(set)
        self.incoming_muc_mappings = defaultdict(set)
        self.outgoing_mappings = defaultdict(list)
        # Mapping of MUC-JID -> Nickname
        self.mucs = dict()
        # Mapping of MUC-JID -> Password
        self.muc_passwords = dict()

        try:
            # Get the optional XMPP address (host, port) if specified
            xmpp_address = tuple()
            if 'host' in cfg['xmpp']:
                xmpp_address = (cfg['xmpp']['host'], cfg['xmpp']['port'])

            self.get_mucs(cfg)
            self.get_outgoing_mappings(cfg)
            self.get_incoming_mappings(cfg)
        except KeyError:
            raise InvalidConfigError

        # Initialize XMPP client
        self.xmpp_client = XMPPBridgeBot(cfg['xmpp']['jid'],
                                         cfg['xmpp']['password'], self)
        self.xmpp_client.connect(address=xmpp_address)

        # Initialize HTTP server if needed
        incoming_webhooks_count = (len(self.incoming_muc_mappings) +
                                   len(self.incoming_normal_mappings))
        if incoming_webhooks_count == 0:
            self.http_server = None
            logging.info("No incoming webhooks defined.")
        elif 'incoming_webhook_listener' in cfg:
            bind_address = cfg['incoming_webhook_listener']['bind_address']
            port = cfg['incoming_webhook_listener']['port']
            self.http_app = aiohttp.web.Application(loop=loop)
            self.http_app.router.add_route('POST', '/', self.handle_incoming)
            self.http_handler = self.http_app.make_handler()
            http_create_server = loop.create_server(self.http_handler,
                                                    bind_address, port)
            self.http_server = loop.run_until_complete(http_create_server)
            logging.info("Listening for incoming webhooks on "
                         "http://{}:{}/".format(bind_address, port))
        else:
            self.http_server = None
            logging.warn("No 'incoming_webhook_listener' in the config even "
                         "though incoming webhooks are defined. Ignoring all "
                         "incoming webhooks.")

        if not self.http_server:
            logging.info("Not listening for incoming webhooks.")
Example #3
0
class XMPPWebhookBridge:
    """This is the central component: It initializes and connects the
    :class:`XMPPBridgeBot` with the webhook handling part. The webhook part
    consists of an HTTP server listening for incoming webhooks (POST requests),
    and an HTTP client part for sending outgoing webhooks (POST requests).
    """

    def __init__(self, cfg, loop):
        self.loop = loop
        # List of bridges
        self.bridges = list()
        # Mapping of MUC-JID -> Nickname
        self.mucs = dict()
        # Mapping of MUC-JID -> Password
        self.muc_passwords = dict()
        # Prometheus access token
        self.prometheus_token = ""

        try:
            # Get the optional XMPP address (host, port) if specified
            xmpp_address = tuple()
            if 'host' in cfg['xmpp']:
                xmpp_address = (cfg['xmpp']['host'], cfg['xmpp']['port'])
            # Parse the MUC definitions
            self.get_mucs(cfg)
            if len(self.mucs) == 0:
                logging.info("No MUCs defined.")
        except KeyError:
            raise InvalidConfigError

        # Parse the bridges from the config file
        need_incoming_webhooks = False
        for bridge_cfg in cfg['bridges']:
            bridge = SingleBridge(bridge_cfg, self)
            self.bridges.append(bridge)
            if bridge.has_incoming_webhooks():
                need_incoming_webhooks = True

        # Get optional prometheus token from config file
        if 'prometheus_token' in cfg:
            self.prometheus_token = cfg['prometheus_token']

        # Initialize XMPP client
        self.xmpp_client = XMPPBridgeBot(cfg['xmpp']['jid'],
                                         cfg['xmpp']['password'],
                                         self)
        self.xmpp_client.connect(address=xmpp_address)

        # Initialize HTTP server if needed
        if not need_incoming_webhooks:
            self.http_server = None
            logging.info("No incoming webhooks defined.")
        elif 'incoming_webhook_listener' in cfg:
            bind_address = cfg['incoming_webhook_listener']['bind_address']
            port = cfg['incoming_webhook_listener']['port']
            self.http_app = aiohttp.web.Application(loop=loop)
            self.http_app.router.add_route('POST',
                                           '/',
                                           self.handle_incoming_webhook)
            self.register_prometheus_endpoint()
            self.http_handler = self.http_app.make_handler()
            http_create_server = loop.create_server(
                self.http_handler,
                bind_address,
                port)
            self.http_server = loop.run_until_complete(http_create_server)
            logging.info("Listening for incoming webhooks on "
                         "http://{}:{}/".format(bind_address, port))
        else:
            self.http_server = None
            logging.warn("No 'incoming_webhook_listener' in the config even "
                         "though incoming webhooks are defined. Ignoring all "
                         "incoming webhooks.")

        if not self.http_server:
            logging.info("Not listening for incoming webhooks.")

    def register_prometheus_endpoint(self):
        self.http_app.router.add_route('GET',
                                       '/metrics',
                                       self.handle_prometheus_request)
        logging.info("Serving prometheus metrics under /metrics")

    def process(self):
        self.loop.run_forever()

    async def send_outgoing_webhook(self, outgoing_webhook, msg):
        """This coroutine handles outgoing webhooks: It relays the messages
        received from XMPP and triggers external webhooks.
        """
        from_jid = msg['from']
        username = str(from_jid)
        if 'override_username' in outgoing_webhook:
            username = self.format_jid_string(
                outgoing_webhook['override_username'],
                from_jid,
                msg['type'] == 'groupchat')

        message = msg['body']
        if 'message_template' in outgoing_webhook:
            message = outgoing_webhook['message_template'].format(
                msg=message)

        payload = {
            'text': message,
            'username': username
        }

        if 'override_channel' in outgoing_webhook:
            payload['channel'] = outgoing_webhook['override_channel']

        if 'avatar_url' in outgoing_webhook:
            icon_url = self.format_jid_string(
                outgoing_webhook['avatar_url'],
                from_jid,
                msg['type'] == 'groupchat')
            payload['icon_url'] = icon_url

        # Attachment formatting is useful for integrating with RocketChat.
        if ('use_attachment_formatting' in outgoing_webhook and
                outgoing_webhook['use_attachment_formatting']):
            payload_attachment = {
                'title': "From: {}".format(username),
                'text': message
            }
            if 'attachment_link' in outgoing_webhook:
                payload_attachment['title_link'] = \
                    outgoing_webhook['attachment_link']
            payload = {
                'attachments': [payload_attachment]
            }

        logging.debug("<-- Sending outgoing webhook. (from '{}')".format(
            from_jid))
        request = await outgoing_webhook['session'].post(
            outgoing_webhook['url'],
            data=json.dumps(payload),
            headers={'content-type': 'application/json'})
        await request.release()
        return

    async def handle_incoming_webhook(self, request):
        """This coroutine handles incoming webhooks: It receives incoming
        webhooks and relays the messages to XMPP."""
        if request.content_type == 'application/json':
            payload = await request.json()
        else:
            # TODO: Handle other content types
            payload = await request.post()

        # Disgard empty messages
        if payload['text'] == "":
            return aiohttp.web.Response(status=412)

        token = payload['token']
        logging.debug("--> Handling incoming request from token "
                      "'{}'...".format(token))
        username = payload['user_name']
        msg = payload['text']

        for bridge in self.bridges:
            bridge.handle_incoming_webhook(token, username, msg)

        Metrics.increment_messages_sent()
        return aiohttp.web.Response(status=200)

    async def handle_prometheus_request(self, request: aiohttp.web.Request):
        params = request.rel_url.query
        token = params.get("token", "")
        if token != self.prometheus_token:
            return aiohttp.web.Response(status=401)

        Metrics.check_connection_active(self.xmpp_client)
        return aiohttp.web.Response(status=200, body=pc.generate_latest(pc.REGISTRY), content_type="text/plain")

    def get_mucs(self, cfg):
        """Reads the MUC definitions from the config file."""
        if 'mucs' not in cfg['xmpp']:
            return

        for muc in cfg['xmpp']['mucs']:
            jid = muc['jid']
            nickname = muc['nickname']
            self.mucs[jid] = nickname
            if 'password' in muc:
                self.muc_passwords[jid] = muc['password']

    def format_jid_string(self, string, jid, is_groupchat=False):
        """Formats the given string by replacing all placeholders.

        The placeholders are replaced with corresponding values from the JID.
        """
        formatted_string = ""
        if is_groupchat:
            formatted_string = string.format(
                bare_jid=jid.bare,
                full_jid=jid.full,
                local_jid=jid.local,
                nick=jid.resource,
                jid=jid.full)
        else:
            formatted_string = string.format(
                bare_jid=jid.bare,
                full_jid=jid.full,
                local_jid=jid.local,
                nick=jid.local,
                jid=jid.bare)

        return formatted_string

    def close(self):
        """Closes all open connections, servers and handlers. This is used
        when exiting the bridge.
        """
        if self.http_server:
            logging.info("Closing HTTP server...")
            self.http_server.close()
            self.loop.run_until_complete(self.http_server.wait_closed())
            self.loop.run_until_complete(self.http_handler.
                                         finish_connections(1.0))
            self.loop.run_until_complete(self.http_app.finish())
            logging.info("Closed HTTP server..")

        logging.info("Closing HTTP client sessions...")
        for bridge in self.bridges:
            for webhook in bridge.outgoing_webhooks:
                webhook['session'].close()
        logging.info("Closed HTTP client sessions..")
        logging.info("Disconnecting from XMPP...")
        self.xmpp_client.disconnect()
        logging.info("Disconnected from XMPP.")
Example #4
0
 def check_connection_active(cls, xmpp_client: XMPPBridgeBot):
     if xmpp_client.is_connected():
         cls.connection_active.set(1)
     else:
         cls.connection_active.set(0)
Example #5
0
class XMPPWebhookBridge:
    """This is the central component: It initializes and connects the
    :class:`XMPPBridgeBot` with the webhook handling part. The webhook part
    consists of an HTTP server listening for incoming webhooks (POST requests),
    and an HTTP client part for sending outgoing webhooks (POST requests).
    """
    def __init__(self, cfg, loop):
        self.loop = loop
        self.incoming_normal_mappings = defaultdict(set)
        self.incoming_muc_mappings = defaultdict(set)
        self.outgoing_mappings = defaultdict(list)
        # Mapping of MUC-JID -> Nickname
        self.mucs = dict()
        # Mapping of MUC-JID -> Password
        self.muc_passwords = dict()

        try:
            # Get the optional XMPP address (host, port) if specified
            xmpp_address = tuple()
            if 'host' in cfg['xmpp']:
                xmpp_address = (cfg['xmpp']['host'], cfg['xmpp']['port'])

            self.get_mucs(cfg)
            self.get_outgoing_mappings(cfg)
            self.get_incoming_mappings(cfg)
        except KeyError:
            raise InvalidConfigError

        # Initialize XMPP client
        self.xmpp_client = XMPPBridgeBot(cfg['xmpp']['jid'],
                                         cfg['xmpp']['password'], self)
        self.xmpp_client.connect(address=xmpp_address)

        # Initialize HTTP server if needed
        incoming_webhooks_count = (len(self.incoming_muc_mappings) +
                                   len(self.incoming_normal_mappings))
        if incoming_webhooks_count == 0:
            self.http_server = None
            logging.info("No incoming webhooks defined.")
        elif 'incoming_webhook_listener' in cfg:
            bind_address = cfg['incoming_webhook_listener']['bind_address']
            port = cfg['incoming_webhook_listener']['port']
            self.http_app = aiohttp.web.Application(loop=loop)
            self.http_app.router.add_route('POST', '/', self.handle_incoming)
            self.http_handler = self.http_app.make_handler()
            http_create_server = loop.create_server(self.http_handler,
                                                    bind_address, port)
            self.http_server = loop.run_until_complete(http_create_server)
            logging.info("Listening for incoming webhooks on "
                         "http://{}:{}/".format(bind_address, port))
        else:
            self.http_server = None
            logging.warn("No 'incoming_webhook_listener' in the config even "
                         "though incoming webhooks are defined. Ignoring all "
                         "incoming webhooks.")

        if not self.http_server:
            logging.info("Not listening for incoming webhooks.")

    def process(self):
        self.loop.run_forever()

    async def handle_outgoing(self, outgoing_webhook, msg):
        """This coroutine handles outgoing webhooks: It relays the messages
        received from XMPP and triggers external webhooks.
        """
        from_jid = msg['from']
        username = str(from_jid)
        if 'override_username' in outgoing_webhook:
            if msg['type'] == 'groupchat':
                username = outgoing_webhook['override_username'].format(
                    bare_jid=from_jid.bare,
                    full_jid=from_jid.full,
                    local_jid=from_jid.local,
                    nick=from_jid.resource,
                    jid=from_jid.full)
            else:
                username = outgoing_webhook['override_username'].format(
                    bare_jid=from_jid.bare,
                    full_jid=from_jid.full,
                    local_jid=from_jid.local,
                    nick=from_jid.local,
                    jid=from_jid.bare)

        message = msg['body']
        if 'message_template' in outgoing_webhook:
            message = outgoing_webhook['message_template'].format(msg=message)

        payload = {'text': message, 'username': username}

        if 'override_channel' in outgoing_webhook:
            payload['channel'] = outgoing_webhook['override_channel']

        # Attachment formatting is useful for integrating with RocketChat.
        if ('use_attachment_formatting' in outgoing_webhook
                and outgoing_webhook['use_attachment_formatting']):
            payload_attachment = {
                'title': "From: {}".format(username),
                'text': message
            }
            if 'attachment_link' in outgoing_webhook:
                payload_attachment['title_link'] = \
                                            outgoing_webhook['attachment_link']
            payload = {'attachments': [payload_attachment]}

        logging.debug(
            "<-- Sending outgoing webhook. (from '{}')".format(from_jid))
        request = await outgoing_webhook['session'].post(
            outgoing_webhook['url'],
            data=json.dumps(payload),
            headers={'content-type': 'application/json'})
        await request.release()
        return

    async def handle_incoming(self, request):
        """This coroutine handles incoming webhooks: It receives incoming
        webhooks and relays the messages to XMPP."""
        if request.content_type == 'application/json':
            payload = await request.json()
            # print(payload)
        else:
            # TODO: Handle other content types
            payload = await request.post()

        # Disgard empty messages
        if payload['text'] == "":
            return aiohttp.web.Response()

        token = payload['token']
        logging.debug("--> Handling incoming request from token "
                      "'{}'...".format(token))
        msg = payload['text']
        for xmpp_normal_jid in self.incoming_normal_mappings[token]:
            logging.debug("<-- Sending a normal chat message to XMPP.")
            self.xmpp_client.send_message(mto=xmpp_normal_jid,
                                          mbody=msg,
                                          mtype='chat',
                                          mnick=payload['user_name'])
        for xmpp_muc_jid in self.incoming_muc_mappings[token]:
            logging.debug("<-- Sending a MUC chat message to XMPP.")
            self.xmpp_client.send_message(mto=xmpp_muc_jid,
                                          mbody=msg,
                                          mtype='groupchat',
                                          mnick=payload['user_name'])
        return aiohttp.web.Response()

    def get_mucs(self, cfg):
        """Reads the MUC definitions from the config file."""
        for muc in cfg['xmpp']['mucs']:
            jid = muc['jid']
            nickname = muc['nickname']
            self.mucs[jid] = nickname
            if 'password' in muc:
                self.muc_passwords[jid] = muc['password']

    def get_outgoing_mappings(self, cfg):
        """Reads the outgoing webhook definitions from the config file.

        This also sets up the HTTP client session for each webhook."""
        bridges = cfg['bridges']
        for bridge in bridges:
            if 'outgoing_webhooks' not in bridge:
                # No outgoing webhooks in this bridge.
                continue

            outgoing_webhooks = bridge['outgoing_webhooks']
            xmpp_endpoints = bridge['xmpp_endpoints']

            # Check whether all normal messages to this bridge should be
            # relayed.
            relay_all_normal = False
            for xmpp_endpoint in xmpp_endpoints:
                if ('relay_all_normal' in xmpp_endpoint
                        and xmpp_endpoint['relay_all_normal'] is True):
                    relay_all_normal = True
                    break

            for outgoing_webhook in outgoing_webhooks:
                if 'url' not in outgoing_webhook:
                    raise InvalidConfigError("Error in config file: "
                                             "'url' is missing from an "
                                             "outgoing webhook definition.")

                # Set up SSL context for certificate pinning.
                if 'cafile' in outgoing_webhook:
                    cafile = os.path.abspath(outgoing_webhook['cafile'])
                    sslcontext = ssl.create_default_context(cafile=cafile)
                    conn = aiohttp.TCPConnector(ssl_context=sslcontext)
                    session = aiohttp.ClientSession(loop=self.loop,
                                                    connector=conn)
                else:
                    session = aiohttp.ClientSession(loop=self.loop)
                # TODO: Handle ConnectionRefusedError.
                outgoing_webhook['session'] = session

                if relay_all_normal:
                    self.outgoing_mappings['all_normal'].append(
                        outgoing_webhook)

                for xmpp_endpoint in xmpp_endpoints:
                    # Determine whether the JID corresponds to a MUC or a
                    # normal chat:
                    if 'muc' in xmpp_endpoint:
                        if xmpp_endpoint['muc'] not in self.mucs:
                            raise InvalidConfigError(
                                "Error in config file: XMPP MUC '{}' was not "
                                "defined in the xmpp.mucs section.".format(
                                    xmpp_endpoint['muc']))

                        self.outgoing_mappings[xmpp_endpoint['muc']].append(
                            outgoing_webhook)
                    elif 'normal' in xmpp_endpoint:
                        if relay_all_normal:
                            # Don't add normal JIDs when all normal messages
                            # are relayed anyways.
                            continue
                        self.outgoing_mappings[xmpp_endpoint['normal']].append(
                            outgoing_webhook)

    def get_incoming_mappings(self, cfg):
        """Reads the incoming webhook definitions from the config file."""
        bridges = cfg['bridges']
        for bridge in bridges:
            if 'incoming_webhooks' not in bridge:
                # No incoming webhooks in this bridge.
                continue

            incoming_webhooks = bridge['incoming_webhooks']
            xmpp_endpoints = bridge['xmpp_endpoints']
            for incoming_webhook in incoming_webhooks:
                if 'token' not in incoming_webhook:
                    raise InvalidConfigError("Invalid config file: "
                                             "'token' missing from outgoing "
                                             "webhook definition.")
                token = incoming_webhook['token']
                for xmpp_endpoint in xmpp_endpoints:
                    if 'muc' in xmpp_endpoint:
                        self.incoming_muc_mappings[token].add(
                            xmpp_endpoint['muc'])
                    elif 'normal' in xmpp_endpoint:
                        self.incoming_normal_mappings[token].add(
                            xmpp_endpoint['normal'])

    def close(self):
        """Closes all open connections, servers and handlers. This is used
        when exiting the bridge.
        """
        if self.http_server:
            logging.info("Closing HTTP server...")
            self.http_server.close()
            self.loop.run_until_complete(self.http_server.wait_closed())
            self.loop.run_until_complete(
                self.http_handler.finish_connections(1.0))
            self.loop.run_until_complete(self.http_app.finish())
            logging.info("Closed HTTP server..")

        logging.info("Closing HTTP client sessions...")
        for outgoing_mapping in self.outgoing_mappings.values():
            for webhook in outgoing_mapping:
                webhook['session'].close()
        logging.info("Closed HTTP client sessions..")
        logging.info("Disconnecting from XMPP...")
        self.xmpp_client.disconnect()
        logging.info("Disconnected from XMPP.")