"""Shared base utilities for server and client telnet protocol implementations.""" from __future__ import annotations # std imports import sys import types import logging import datetime import traceback from typing import Any, Type, Callable, Optional # Pre-allocated single-byte cache to avoid per-byte bytes() allocations _ONE_BYTE = [bytes([i]) for i in range(256)] def _log_exception( log_fn: Callable[..., Any], e_type: Optional[Type[BaseException]], e_value: Optional[BaseException], e_tb: Optional[types.TracebackType], ) -> None: """Log an exception's traceback and message via *log_fn*.""" rows_tbk = [line for line in "\n".join(traceback.format_tb(e_tb)).split("\n") if line] rows_exc = [line.rstrip() for line in traceback.format_exception_only(e_type, e_value)] for line in rows_tbk + rows_exc: log_fn(line) def _process_data_chunk( data: bytes, writer: Any, reader: Any, slc_special: frozenset[int] | None, log_fn: Callable[..., Any], ) -> bool: """ Scan *data* for IAC and SLC bytes, feed regular bytes to *reader*. :param data: Raw bytes received from the transport. :param writer: TelnetWriter instance for IAC interpretation. :param reader: TelnetReader instance for in-band data. :param slc_special: Frozenset of special byte values (IAC + SLC triggers), or ``None`` when only IAC (255) is special. :param log_fn: Callable for logging exceptions (e.g. ``logger.warning``). :returns: ``True`` if any IAC/SB command was observed. When MCCP2 is activated mid-chunk, the remaining compressed bytes are stored in ``writer._compressed_remainder`` for the caller to consume. """ cmd_received = False n = len(data) i = 0 out_start = 0 feeding_oob = bool(writer.is_oob) while i < n: if not feeding_oob: if slc_special is None: next_iac = data.find(255, i) if next_iac == -1: if n > out_start: reader.feed_data(data[out_start:]) return cmd_received i = next_iac else: while i < n and data[i] not in slc_special: i += 1 if i > out_start: reader.feed_data(data[out_start:i]) if i >= n: break try: recv_inband = writer.feed_byte(_ONE_BYTE[data[i]]) except ValueError as exc: logging.getLogger(__name__).debug("Invalid telnet byte: %s", exc) except BaseException: _log_exception(log_fn, *sys.exc_info()) else: if recv_inband: reader.feed_data(data[i : i + 1]) else: cmd_received = True i += 1 out_start = i feeding_oob = bool(writer.is_oob) if writer._mccp2_activated: writer._mccp2_activated = False writer.mccp2_active = True writer._compressed_remainder = data[i:] if i < n else b"" return True return cmd_received class TelnetProtocolBase: """Mixin providing properties and helpers shared by server and client protocols.""" _when_connected: Optional[datetime.datetime] = None _last_received: Optional[datetime.datetime] = None _transport: Any = None _extra: dict[str, Any] @property def duration(self) -> float: """Time elapsed since client connected, in seconds as float.""" assert self._when_connected is not None return (datetime.datetime.now() - self._when_connected).total_seconds() @property def idle(self) -> float: """Time elapsed since data last received, in seconds as float.""" assert self._last_received is not None return (datetime.datetime.now() - self._last_received).total_seconds() def __repr__(self) -> str: hostport = self.get_extra_info("peername", ["-", "closing"])[:2] return f"" def get_extra_info(self, name: str, default: Any = None) -> Any: """Get optional protocol or transport information.""" if self._transport: default = self._transport.get_extra_info(name, default) return self._extra.get(name, default) def _log_tls_info(self, logger: logging.Logger) -> None: """Log TLS connection details at debug level, if TLS is active.""" ssl_obj = self.get_extra_info("ssl_object") if ssl_obj is None: return version = getattr(ssl_obj, "version", lambda: None)() or "TLS" cipher_info = getattr(ssl_obj, "cipher", lambda: None)() if cipher_info: logger.debug("TLS handshake: %s cipher=%s", version, cipher_info[0]) else: logger.debug("TLS handshake: %s", version)