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))
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))