@@ -502,8 +502,34 @@ async def run_client() -> None:
502502 )
503503 log .debug (config_msg )
504504
505+ always_will : set [bytes ] = args ["always_will" ]
506+ always_do : set [bytes ] = args ["always_do" ]
507+
508+ # Wrap client factory to inject always_will/always_do before negotiation
509+ client_factory : Optional [Callable [..., client_base .BaseClient ]] = None
510+ if always_will or always_do :
511+
512+ def _client_factory (** kwargs : Any ) -> client_base .BaseClient :
513+ client : TelnetClient
514+ if sys .platform != "win32" and sys .stdin .isatty ():
515+ client = TelnetTerminalClient (** kwargs )
516+ else :
517+ client = TelnetClient (** kwargs )
518+ orig_connection_made = client .connection_made
519+
520+ def _patched_connection_made (transport : asyncio .BaseTransport ) -> None :
521+ orig_connection_made (transport )
522+ assert client .writer is not None
523+ client .writer .always_will = always_will
524+ client .writer .always_do = always_do
525+
526+ client .connection_made = _patched_connection_made # type: ignore[method-assign]
527+ return client
528+
529+ client_factory = _client_factory
530+
505531 # Build connection kwargs explicitly to avoid pylint false positive
506- connection_kwargs = {
532+ connection_kwargs : Dict [ str , Any ] = {
507533 "encoding" : args ["encoding" ],
508534 "tspeed" : args ["tspeed" ],
509535 "shell" : args ["shell" ],
@@ -514,6 +540,8 @@ async def run_client() -> None:
514540 "connect_timeout" : args ["connect_timeout" ],
515541 "send_environ" : args ["send_environ" ],
516542 }
543+ if client_factory is not None :
544+ connection_kwargs ["client_factory" ] = client_factory
517545
518546 # connect
519547 _ , writer = await open_connection (args ["host" ], args ["port" ], ** connection_kwargs )
@@ -565,9 +593,40 @@ def _get_argument_parser() -> argparse.ArgumentParser:
565593 default = "TERM,LANG,COLUMNS,LINES,COLORTERM" ,
566594 help = "comma-separated environment variables to send (NEW_ENVIRON)" ,
567595 )
596+ parser .add_argument (
597+ "--always-will" ,
598+ action = "append" ,
599+ default = [],
600+ metavar = "OPT" ,
601+ help = "always send WILL for this option (name like MXP or number, repeatable)" ,
602+ )
603+ parser .add_argument (
604+ "--always-do" ,
605+ action = "append" ,
606+ default = [],
607+ metavar = "OPT" ,
608+ help = "always send DO for this option (name like GMCP or number, repeatable)" ,
609+ )
568610 return parser
569611
570612
613+ def _parse_option_arg (value : str ) -> bytes :
614+ """
615+ Resolve a telnet option name or integer to option bytes.
616+
617+ :param value: Option name (e.g. ``"MXP"``) or decimal byte value (e.g. ``"91"``).
618+ :returns: Single-byte option value.
619+ :raises ValueError: When *value* is not a known name or valid integer.
620+ """
621+ # local
622+ from .telopt import option_from_name # pylint: disable=import-outside-toplevel
623+
624+ try :
625+ return option_from_name (value )
626+ except KeyError :
627+ return bytes ([int (value )])
628+
629+
571630def _transform_args (args : argparse .Namespace ) -> Dict [str , Any ]:
572631 return {
573632 "host" : args .host ,
@@ -584,12 +643,18 @@ def _transform_args(args: argparse.Namespace) -> Dict[str, Any]:
584643 "connect_minwait" : args .connect_minwait ,
585644 "connect_timeout" : args .connect_timeout ,
586645 "send_environ" : tuple (v .strip () for v in args .send_environ .split ("," ) if v .strip ()),
646+ "always_will" : {_parse_option_arg (v ) for v in args .always_will },
647+ "always_do" : {_parse_option_arg (v ) for v in args .always_do },
587648 }
588649
589650
590651def main () -> None :
591652 """Entry point for telnetlib3-client command."""
592- asyncio .run (run_client ())
653+ try :
654+ asyncio .run (run_client ())
655+ except OSError as err :
656+ print (f"Error: { err } " , file = sys .stderr )
657+ sys .exit (1 )
593658
594659
595660def _get_fingerprint_argument_parser () -> argparse .ArgumentParser :
@@ -634,13 +699,48 @@ def _get_fingerprint_argument_parser() -> argparse.ArgumentParser:
634699 parser .add_argument (
635700 "--ttype" , default = "VT100" , help = "terminal type sent in response to TTYPE requests"
636701 )
702+ parser .add_argument (
703+ "--scan-type" ,
704+ choices = ["quick" , "full" ],
705+ default = "quick" ,
706+ help = "probe depth: 'quick' probes core options only, " "'full' includes legacy options" ,
707+ )
637708 parser .add_argument (
638709 "--send-env" ,
639710 action = "append" ,
640711 metavar = "KEY=VALUE" ,
641712 default = [],
642713 help = "environment variable to send (repeatable)" ,
643714 )
715+ parser .add_argument (
716+ "--always-will" ,
717+ action = "append" ,
718+ default = [],
719+ metavar = "OPT" ,
720+ help = "always send WILL for this option (name like MXP or number, repeatable)" ,
721+ )
722+ parser .add_argument (
723+ "--always-do" ,
724+ action = "append" ,
725+ default = [],
726+ metavar = "OPT" ,
727+ help = "always send DO for this option (name like GMCP or number, repeatable)" ,
728+ )
729+ parser .add_argument (
730+ "--mssp-wait" ,
731+ default = 5.0 ,
732+ type = float ,
733+ help = "max seconds since connect to wait for MSSP data" ,
734+ )
735+ parser .add_argument (
736+ "--banner-quiet-time" ,
737+ default = 2.0 ,
738+ type = float ,
739+ help = "seconds of silence before considering banner complete" ,
740+ )
741+ parser .add_argument (
742+ "--banner-max-wait" , default = 8.0 , type = float , help = "max seconds to wait for banner data"
743+ )
644744 return parser
645745
646746
@@ -674,8 +774,16 @@ async def run_fingerprint_client() -> None:
674774 silent = args .silent ,
675775 set_name = args .set_name ,
676776 environ_encoding = args .stream_encoding ,
777+ scan_type = args .scan_type ,
778+ mssp_wait = args .mssp_wait ,
779+ banner_quiet_time = args .banner_quiet_time ,
780+ banner_max_wait = args .banner_max_wait ,
677781 )
678782
783+ # Parse --always-will/--always-do option names/numbers
784+ fp_always_will = {_parse_option_arg (v ) for v in args .always_will }
785+ fp_always_do = {_parse_option_arg (v ) for v in args .always_do }
786+
679787 # Parse --send-env KEY=VALUE pairs
680788 extra_env : Dict [str , str ] = {}
681789 for item in args .send_env :
@@ -705,6 +813,8 @@ def patched_connection_made(transport: asyncio.BaseTransport) -> None:
705813 orig_connection_made (transport )
706814 assert client .writer is not None
707815 client .writer .environ_encoding = environ_encoding
816+ client .writer .always_will = fp_always_will
817+ client .writer .always_do = fp_always_do
708818
709819 def patched_send_env (keys : Sequence [str ]) -> Dict [str , Any ]:
710820 result = orig_send_env (keys )
@@ -718,18 +828,22 @@ def patched_send_env(keys: Sequence[str]) -> Dict[str, Any]:
718828
719829 waiter_closed : asyncio .Future [None ] = asyncio .get_event_loop ().create_future ()
720830
721- _ , writer = await open_connection (
722- host = args .host ,
723- port = args .port ,
724- client_factory = fingerprint_client_factory ,
725- shell = shell ,
726- encoding = False ,
727- term = ttype ,
728- connect_minwait = 2.0 ,
729- connect_maxwait = 4.0 ,
730- connect_timeout = args .connect_timeout ,
731- waiter_closed = waiter_closed ,
732- )
831+ try :
832+ _ , writer = await open_connection (
833+ host = args .host ,
834+ port = args .port ,
835+ client_factory = fingerprint_client_factory ,
836+ shell = shell ,
837+ encoding = False ,
838+ term = ttype ,
839+ connect_minwait = 2.0 ,
840+ connect_maxwait = 4.0 ,
841+ connect_timeout = args .connect_timeout ,
842+ waiter_closed = waiter_closed ,
843+ )
844+ except OSError as err :
845+ log .error ("%s:%d: %s" , args .host , args .port , err )
846+ raise
733847
734848 assert writer .protocol is not None
735849 assert isinstance (writer .protocol , client_base .BaseClient )
@@ -740,7 +854,7 @@ def fingerprint_main() -> None:
740854 """Entry point for ``telnetlib3-fingerprint`` command."""
741855 try :
742856 asyncio .run (run_fingerprint_client ())
743- except ConnectionError as err :
857+ except OSError as err :
744858 print (f"Error: { err } " , file = sys .stderr )
745859 sys .exit (1 )
746860
0 commit comments