""" Telnet client — interactive telnet session with the same interface as ShellSession. """ import asyncio import threading from core.logger import log class TelnetSession: """Interactive telnet session — same interface as ShellSession from ssh_client.py.""" def __init__(self, server: dict, cols: int = 80, rows: int = 24): self.server = server self.cols = cols self.rows = rows self._loop: asyncio.AbstractEventLoop | None = None self._thread: threading.Thread | None = None self._reader = None self._writer = None self._running = False # Callbacks — set by the owner self.on_data = None # on_data(data: bytes) self.on_disconnect = None # on_disconnect() @property def connected(self) -> bool: return self._running and self._writer is not None def connect(self): """Start telnet connection in a background thread running an asyncio event loop.""" self._running = True self._thread = threading.Thread(target=self._run_loop, daemon=True) self._thread.start() def _run_loop(self): """Entry point for the background thread — creates event loop and runs connection.""" self._loop = asyncio.new_event_loop() asyncio.set_event_loop(self._loop) try: self._loop.run_until_complete(self._async_connect()) except Exception as e: log.debug(f"TelnetSession loop error: {e}") finally: self._running = False try: self._loop.close() except Exception: pass self._loop = None if self.on_disconnect: self.on_disconnect() async def _async_connect(self): """Async telnet connection: open, login, then read loop.""" try: import telnetlib3 except ImportError: log.error("telnetlib3 not installed. Run: pip install telnetlib3") raise ImportError("telnetlib3 is required for telnet connections") hostname = self.server["ip"] port = self.server.get("port", 23) user = self.server.get("user", "") password = self.server.get("password", "") log.info(f"TelnetSession connecting to {self.server.get('alias', '?')} port {port}") reader, writer = await telnetlib3.open_connection( host=hostname, port=port, cols=self.cols, rows=self.rows, connect_minwait=0.5, ) self._reader = reader self._writer = writer # Login sequence — wait for prompts and send credentials await self._login_sequence(reader, writer, user, password) # Main read loop await self._read_loop(reader) async def _login_sequence(self, reader, writer, user: str, password: str): """Wait for login/password prompts and send credentials.""" buf = "" timeout = 10.0 # seconds to wait for login prompt while self._running: try: data = await asyncio.wait_for(reader.read(4096), timeout=timeout) except asyncio.TimeoutError: log.debug("TelnetSession login sequence timed out waiting for prompt") break except Exception: break if not data: break if self.on_data: self.on_data(data.encode("utf-8", errors="replace") if isinstance(data, str) else data) buf += data if isinstance(data, str) else data.decode("utf-8", errors="replace") buf_lower = buf.lower() if "login:" in buf_lower or "username:" in buf_lower: writer.write(user + "\r\n") buf = "" continue if "password:" in buf_lower: writer.write(password + "\r\n") buf = "" break # Login done, proceed to read loop # If we see a shell prompt, login may not be required if buf_lower.rstrip().endswith(("$", "#", ">")): break log.debug("TelnetSession login sequence complete") async def _read_loop(self, reader): """Read data from telnet and forward to on_data callback.""" try: while self._running: try: data = await asyncio.wait_for(reader.read(65536), timeout=0.5) except asyncio.TimeoutError: continue except Exception: break if not data: break raw = data.encode("utf-8", errors="replace") if isinstance(data, str) else data if self.on_data: self.on_data(raw) except Exception as e: log.debug(f"TelnetSession read loop error: {e}") def send(self, data: bytes): """Send data to the telnet session.""" if not self._running or self._writer is None or self._loop is None: return text = data.decode("utf-8", errors="replace") try: self._loop.call_soon_threadsafe(self._writer.write, text) except RuntimeError: self._running = False if self.on_disconnect: self.on_disconnect() def resize(self, cols: int, rows: int): """Resize terminal — NAWS negotiation if supported, otherwise no-op.""" self.cols = cols self.rows = rows # telnetlib3 handles NAWS during initial negotiation; # runtime resize requires protocol-level support which # is not reliably available, so this is a best-effort no-op. log.debug(f"TelnetSession resize requested: {cols}x{rows} (no-op)") def disconnect(self): """Close telnet session and stop background thread.""" self._running = False if self._writer is not None: try: self._writer.close() except Exception as e: log.debug(f"TelnetSession writer close: {e}") self._writer = None self._reader = None if self._loop is not None: try: self._loop.call_soon_threadsafe(self._loop.stop) except RuntimeError: pass