예제 #1
0
    async def open_connection(self, command: commands.OpenConnection) -> None:
        if not command.connection.address:
            self.log(f"Cannot open connection, no hostname given.")
            self.server_event(events.OpenConnectionCompleted(command, f"Cannot open connection, no hostname given."))
            return

        hook_data = server_hooks.ServerConnectionHookData(
            client=self.client,
            server=command.connection
        )
        await self.handle_hook(server_hooks.ServerConnectHook(hook_data))
        if command.connection.error:
            self.log(f"server connection to {human.format_address(command.connection.address)} killed before connect.")
            self.server_event(events.OpenConnectionCompleted(command, "Connection killed."))
            return

        async with self.max_conns[command.connection.address]:
            try:
                command.connection.timestamp_start = time.time()
                reader, writer = await asyncio.open_connection(*command.connection.address)
            except (IOError, asyncio.CancelledError) as e:
                err = str(e)
                if not err:  # str(CancelledError()) returns empty string.
                    err = "connection cancelled"
                self.log(f"error establishing server connection: {err}")
                command.connection.error = err
                self.server_event(events.OpenConnectionCompleted(command, err))
                if isinstance(e, asyncio.CancelledError):
                    # From https://docs.python.org/3/library/asyncio-exceptions.html#asyncio.CancelledError:
                    # > In almost all situations the exception must be re-raised.
                    # It is not really defined what almost means here, but we play safe.
                    raise
            else:
                command.connection.timestamp_tcp_setup = time.time()
                command.connection.state = ConnectionState.OPEN
                command.connection.peername = writer.get_extra_info('peername')
                command.connection.sockname = writer.get_extra_info('sockname')
                self.transports[command.connection].reader = reader
                self.transports[command.connection].writer = writer

                assert command.connection.peername
                if command.connection.address[0] != command.connection.peername[0]:
                    addr = f"{command.connection.address[0]} ({human.format_address(command.connection.peername)})"
                else:
                    addr = human.format_address(command.connection.address)
                self.log(f"server connect {addr}")
                connected_hook = asyncio_utils.create_task(
                    self.handle_hook(server_hooks.ServerConnectedHook(hook_data)),
                    name=f"handle_hook(server_connected) {addr}",
                    client=self.client.peername,
                )
                if not connected_hook:
                    return  # this should not be needed, see asyncio_utils.create_task

                self.server_event(events.OpenConnectionCompleted(command, None))

                # during connection opening, this function is the designated handler that can be cancelled.
                # once we have a connection, we do want the teardown here to happen in any case, so we
                # reassign the handler to .handle_connection and then clean up here once that is done.
                new_handler = asyncio_utils.create_task(
                    self.handle_connection(command.connection),
                    name=f"server connection handler for {addr}",
                    client=self.client.peername,
                )
                if not new_handler:
                    return  # this should not be needed, see asyncio_utils.create_task
                self.transports[command.connection].handler = new_handler
                await asyncio.wait([new_handler])

                self.log(f"server disconnect {addr}")
                command.connection.timestamp_end = time.time()
                await connected_hook  # wait here for this so that closed always comes after connected.
                await self.handle_hook(server_hooks.ServerDisconnectedHook(hook_data))
예제 #2
0
class ConnectionHandler(metaclass=abc.ABCMeta):
    transports: typing.MutableMapping[Connection, ConnectionIO]
    timeout_watchdog: TimeoutWatchdog
    client: Client
    max_conns: typing.DefaultDict[Address, asyncio.Semaphore]
    layer: layer.Layer

    def __init__(self, context: Context) -> None:
        self.client = context.client
        self.transports = {}
        self.max_conns = collections.defaultdict(lambda: asyncio.Semaphore(5))

        # Ask for the first layer right away.
        # In a reverse proxy scenario, this is necessary as we would otherwise hang
        # on protocols that start with a server greeting.
        self.layer = layer.NextLayer(context, ask_on_start=True)
        self.timeout_watchdog = TimeoutWatchdog(self.on_timeout)

    async def handle_client(self) -> None:
        watch = asyncio_utils.create_task(
            self.timeout_watchdog.watch(),
            name="timeout watchdog",
            client=self.client.peername,
        )
        if not watch:
            return  # this should not be needed, see asyncio_utils.create_task

        self.log("client connect")
        await self.handle_hook(server_hooks.ClientConnectedHook(self.client))
        if self.client.error:
            self.log("client kill connection")
            writer = self.transports.pop(self.client).writer
            assert writer
            writer.close()
        else:
            handler = asyncio_utils.create_task(
                self.handle_connection(self.client),
                name=f"client connection handler",
                client=self.client.peername,
            )
            if not handler:
                return  # this should not be needed, see asyncio_utils.create_task
            self.transports[self.client].handler = handler
            self.server_event(events.Start())
            await asyncio.wait([handler])

        watch.cancel()

        self.log("client disconnect")
        self.client.timestamp_end = time.time()
        await self.handle_hook(server_hooks.ClientDisconnectedHook(self.client)
                               )

        if self.transports:
            self.log("closing transports...", "debug")
            for io in self.transports.values():
                if io.handler:
                    asyncio_utils.cancel_task(io.handler,
                                              "client disconnected")
            await asyncio.wait(
                [x.handler for x in self.transports.values() if x.handler])
            self.log("transports closed!", "debug")

    async def open_connection(self, command: commands.OpenConnection) -> None:
        if not command.connection.address:
            self.log(f"Cannot open connection, no hostname given.")
            self.server_event(
                events.OpenConnectionCompleted(
                    command, f"Cannot open connection, no hostname given."))
            return

        hook_data = server_hooks.ServerConnectionHookData(
            client=self.client, server=command.connection)
        await self.handle_hook(server_hooks.ServerConnectHook(hook_data))
        if err := command.connection.error:
            self.log(
                f"server connection to {human.format_address(command.connection.address)} killed before connect: {err}"
            )
            self.server_event(
                events.OpenConnectionCompleted(command,
                                               f"Connection killed: {err}"))
            return

        async with self.max_conns[command.connection.address]:
            try:
                command.connection.timestamp_start = time.time()
                reader, writer = await asyncio.open_connection(
                    *command.connection.address)
            except (IOError, asyncio.CancelledError) as e:
                err = str(e)
                if not err:  # str(CancelledError()) returns empty string.
                    err = "connection cancelled"
                self.log(f"error establishing server connection: {err}")
                command.connection.error = err
                self.server_event(events.OpenConnectionCompleted(command, err))
                if isinstance(e, asyncio.CancelledError):
                    # From https://docs.python.org/3/library/asyncio-exceptions.html#asyncio.CancelledError:
                    # > In almost all situations the exception must be re-raised.
                    # It is not really defined what almost means here, but we play safe.
                    raise
            else:
                command.connection.timestamp_tcp_setup = time.time()
                command.connection.state = ConnectionState.OPEN
                command.connection.peername = writer.get_extra_info('peername')
                command.connection.sockname = writer.get_extra_info('sockname')
                self.transports[command.connection].reader = reader
                self.transports[command.connection].writer = writer

                assert command.connection.peername
                if command.connection.address[
                        0] != command.connection.peername[0]:
                    addr = f"{command.connection.address[0]} ({human.format_address(command.connection.peername)})"
                else:
                    addr = human.format_address(command.connection.address)
                self.log(f"server connect {addr}")
                connected_hook = asyncio_utils.create_task(
                    self.handle_hook(
                        server_hooks.ServerConnectedHook(hook_data)),
                    name=f"handle_hook(server_connected) {addr}",
                    client=self.client.peername,
                )
                if not connected_hook:
                    return  # this should not be needed, see asyncio_utils.create_task

                self.server_event(events.OpenConnectionCompleted(
                    command, None))

                # during connection opening, this function is the designated handler that can be cancelled.
                # once we have a connection, we do want the teardown here to happen in any case, so we
                # reassign the handler to .handle_connection and then clean up here once that is done.
                new_handler = asyncio_utils.create_task(
                    self.handle_connection(command.connection),
                    name=f"server connection handler for {addr}",
                    client=self.client.peername,
                )
                if not new_handler:
                    return  # this should not be needed, see asyncio_utils.create_task
                self.transports[command.connection].handler = new_handler
                await asyncio.wait([new_handler])

                self.log(f"server disconnect {addr}")
                command.connection.timestamp_end = time.time()
                await connected_hook  # wait here for this so that closed always comes after connected.
                await self.handle_hook(
                    server_hooks.ServerDisconnectedHook(hook_data))