Skip to content

Commit 5be4f8e

Browse files
authored
Kludge mode fix (WILL SGA/DO SGA) (jquast#130)
First Linemode fix, that kludge was not correctly detected. WILL SGA and DO SGA do not really specify direction. It is a server's responsibility (WILL SGA) to suppress a GA. But RFC wording is unclear about this, and so it can be implemented [either way and often is! 50/50](https://muds.modem.xyz/statistics.html#telnet-option-negotiation). I believe either inetutils-telnetd or bsd4.4-derived telnetd sends both WILL and DO SGA and one of them has a long explanatory comment about it, that if i recall some popular telnet made a mistake with SGA, and so the workaround is to do the same as they do -- to support both directions to mean the same.
1 parent c351848 commit 5be4f8e

8 files changed

Lines changed: 97 additions & 7 deletions

File tree

docs/history.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
History
22
=======
3+
3.0.3 (unreleased)
4+
* new: :func:`~telnetlib3.accessories.make_logger` accepts a ``filemode``
5+
parameter (``"a"`` append or ``"w"`` overwrite) so callers can control
6+
whether log files are truncated on each run. The default remains
7+
``"a"`` (append), matching historical behaviour.
8+
* new: ``--logfile-mode {append,rewrite}`` CLI flag controls whether the
9+
log file is appended to (default) or overwritten on each connection.
10+
* new: ``--typescript-mode {append,rewrite}`` CLI flag controls whether the
11+
typescript file is appended to (default) or overwritten on each connection.
12+
* bugfix: kludge mode not detected when server negotiates WILL ECHO + DO SGA
13+
(requesting client WILL SGA) instead of WILL ECHO + WILL SGA.
14+
:attr:`~telnetlib3.stream_writer.TelnetWriter.mode` and
15+
``_server_will_sga()`` now check SGA in either direction
16+
(``remote_option`` or ``local_option``), so clients correctly switch from
17+
line mode to character-at-a-time mode with these servers.
18+
319
3.0.2
420
* bugfix: :meth:`~telnetlib3.stream_writer.TelnetWriter.request_charset` raised :exc:`TypeError`,
521
:ghissue:`128`. Offer callbacks (no-arg, returning a list of items to propose) are now

telnetlib3/accessories.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,11 @@ def hexdump(data: bytes, prefix: str = "") -> str:
130130

131131

132132
def make_logger(
133-
name: str, loglevel: str = "info", logfile: Optional[str] = None, logfmt: str = _DEFAULT_LOGFMT
133+
name: str,
134+
loglevel: str = "info",
135+
logfile: Optional[str] = None,
136+
logfmt: str = _DEFAULT_LOGFMT,
137+
filemode: str = "a",
134138
) -> logging.Logger:
135139
"""Create and return simple logger for given arguments."""
136140
lvl = getattr(logging, loglevel.upper(), None)
@@ -140,6 +144,7 @@ def make_logger(
140144
_cfg: Dict[str, Any] = {"format": logfmt}
141145
if logfile:
142146
_cfg["filename"] = logfile
147+
_cfg["filemode"] = filemode
143148
logging.basicConfig(**_cfg)
144149
for handler in logging.getLogger().handlers:
145150
if isinstance(handler, logging.StreamHandler) and not isinstance(

telnetlib3/client.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -645,7 +645,11 @@ async def run_client() -> None:
645645
config_msg = f"Client configuration: {accessories.repr_mapping(args)}"
646646

647647
log = accessories.make_logger(
648-
name=__name__, loglevel=args["loglevel"], logfile=args["logfile"], logfmt=args["logfmt"]
648+
name=__name__,
649+
loglevel=args["loglevel"],
650+
logfile=args["logfile"],
651+
logfmt=args["logfmt"],
652+
filemode="w" if args.get("logfile_mode") == "rewrite" else "a",
649653
)
650654
log.debug(config_msg)
651655

@@ -774,7 +778,11 @@ async def _typescript_shell(
774778
) -> None:
775779
ctx = writer_arg.ctx
776780
assert typescript_path is not None
777-
ts_file = open(typescript_path, "a", encoding="utf-8") # noqa: SIM115
781+
ts_file = open( # noqa: SIM115
782+
typescript_path,
783+
"w" if args.get("typescript_mode") == "rewrite" else "a",
784+
encoding="utf-8",
785+
)
778786
ctx.typescript_file = ts_file
779787
try:
780788
await _ts_inner(reader, writer_arg)
@@ -819,6 +827,13 @@ def _get_argument_parser() -> argparse.ArgumentParser:
819827
parser.add_argument("--loglevel", default="warn", help="log level")
820828
parser.add_argument("--logfmt", default=accessories._DEFAULT_LOGFMT, help="log format")
821829
parser.add_argument("--logfile", help="filepath")
830+
parser.add_argument(
831+
"--logfile-mode",
832+
default="append",
833+
choices=["append", "rewrite"],
834+
dest="logfile_mode",
835+
help="Log file write mode: append (default) or rewrite.",
836+
)
822837
parser.add_argument(
823838
"--shell", default="telnetlib3.telnet_client_shell", help="module.function_name"
824839
)
@@ -972,6 +987,13 @@ def _get_argument_parser() -> argparse.ArgumentParser:
972987
metavar="FILE",
973988
help="record session to FILE (like Unix script(1))",
974989
)
990+
parser.add_argument(
991+
"--typescript-mode",
992+
default="append",
993+
choices=["append", "rewrite"],
994+
dest="typescript_mode",
995+
help="Typescript write mode: append (default) or rewrite.",
996+
)
975997
return parser
976998

977999

@@ -1040,6 +1062,7 @@ def _transform_args(args: argparse.Namespace) -> Dict[str, Any]:
10401062
"port": args.port,
10411063
"loglevel": args.loglevel,
10421064
"logfile": args.logfile,
1065+
"logfile_mode": args.logfile_mode,
10431066
"logfmt": args.logfmt,
10441067
"encoding": args.encoding,
10451068
"tspeed": (args.speed, args.speed),
@@ -1068,6 +1091,7 @@ def _transform_args(args: argparse.Namespace) -> Dict[str, Any]:
10681091
),
10691092
"compression": args.compression,
10701093
"typescript": args.typescript,
1094+
"typescript_mode": args.typescript_mode,
10711095
}
10721096

10731097

telnetlib3/client_shell.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,10 +310,14 @@ def _make_raw(
310310
)
311311

312312
def _server_will_sga(self) -> bool:
313-
"""Whether server has negotiated WILL SGA."""
313+
"""Whether SGA has been negotiated (either direction)."""
314314
from .telopt import SGA
315315

316-
return bool(self.telnet_writer.client and self.telnet_writer.remote_option.enabled(SGA))
316+
w = self.telnet_writer
317+
return bool(
318+
w.client
319+
and (w.remote_option.enabled(SGA) or w.local_option.enabled(SGA))
320+
)
317321

318322
def check_auto_mode(
319323
self, switched_to_raw: bool, last_will_echo: bool

telnetlib3/stream_writer.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -911,7 +911,9 @@ def mode(self) -> str:
911911
if self.local_option.enabled(ECHO) and self.local_option.enabled(SGA):
912912
return "kludge"
913913
return "local"
914-
if self.remote_option.enabled(ECHO) and self.remote_option.enabled(SGA):
914+
if self.remote_option.enabled(ECHO) and (
915+
self.remote_option.enabled(SGA) or self.local_option.enabled(SGA)
916+
):
915917
return "kludge"
916918
return "local"
917919

telnetlib3/tests/test_client_shell.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ def enabled(self, key: bytes) -> bool:
4141

4242

4343
def _make_writer(
44-
will_echo: bool = False, raw_mode: "bool | None" = False, will_sga: bool = False
44+
will_echo: bool = False,
45+
raw_mode: "bool | None" = False,
46+
will_sga: bool = False,
47+
local_sga: bool = False,
4548
) -> object:
4649
"""Build a minimal mock writer with the attributes Terminal needs."""
4750
from telnetlib3.telopt import SGA
@@ -52,6 +55,7 @@ def _make_writer(
5255
will_echo=will_echo,
5356
client=True,
5457
remote_option=_MockOption({SGA: will_sga}),
58+
local_option=_MockOption({SGA: local_sga}),
5559
log=types.SimpleNamespace(debug=lambda *a, **kw: None),
5660
ctx=ctx,
5761
)
@@ -190,6 +194,32 @@ def test_make_raw_toggle_echo_flag() -> None:
190194
assert not restored.lflag & termios.ICANON
191195

192196

197+
def test_server_will_sga_local_option() -> None:
198+
"""DO SGA from server (client WILL SGA) is detected as SGA active."""
199+
term = _make_term(_make_writer(local_sga=True, raw_mode=None))
200+
assert term._server_will_sga() is True
201+
202+
203+
def test_determine_mode_local_sga_goes_raw() -> None:
204+
"""WILL ECHO + DO SGA (local_option) triggers kludge raw mode."""
205+
term = _make_term(_make_writer(will_echo=True, raw_mode=None, local_sga=True))
206+
mode = _cooked_mode()
207+
result = term.determine_mode(mode)
208+
assert result is not mode
209+
assert not result.lflag & termios.ICANON
210+
assert not result.lflag & termios.ECHO
211+
212+
213+
def test_determine_mode_local_sga_without_echo() -> None:
214+
"""DO SGA alone (no WILL ECHO) triggers character-at-a-time with software echo."""
215+
term = _make_term(_make_writer(raw_mode=None, local_sga=True))
216+
mode = _cooked_mode()
217+
result = term.determine_mode(mode)
218+
assert result is not mode
219+
assert not result.lflag & termios.ICANON
220+
assert term.software_echo is True
221+
222+
193223
@pytest.mark.parametrize(
194224
"encoding,key,expected",
195225
[

telnetlib3/tests/test_server_shell_unit.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ async def test_terminal_determine_mode(monkeypatch):
137137
will_echo=False,
138138
client=True,
139139
remote_option=_mock_opt,
140+
local_option=_mock_opt,
140141
log=types.SimpleNamespace(debug=lambda *a, **k: None),
141142
ctx=TelnetSessionContext(),
142143
)

telnetlib3/tests/test_stream_writer_full.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,14 @@ def test_mode_client_kludge_and_server_kludge_and_remote_local():
820820
assert wc.mode == "remote"
821821

822822

823+
def test_mode_client_kludge_local_sga():
824+
"""Client mode is kludge when server WILL ECHO + client WILL SGA (DO SGA from server)."""
825+
wc, _, _ = new_writer(server=False, client=True)
826+
wc.remote_option[ECHO] = True
827+
wc.local_option[SGA] = True
828+
assert wc.mode == "kludge"
829+
830+
823831
def test_handle_send_server_and_client_charset_returns():
824832
ws, ts, ps = new_writer(server=True)
825833
assert ws.handle_send_server_charset() == ["UTF-8"]

0 commit comments

Comments
 (0)