telnetlib3 3.0.2__tar.gz → 3.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/PKG-INFO +1 -1
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/history.rst +10 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/pyproject.toml +1 -1
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/accessories.py +7 -2
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/client.py +26 -2
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/client_shell.py +156 -6
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/server.py +44 -1
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/server_shell.py +50 -2
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/slc.py +13 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/stream_writer.py +87 -12
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_client_shell.py +88 -6
- telnetlib3-3.0.3/telnetlib3/tests/test_linemode.py +459 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_server_shell_unit.py +1 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_shell.py +7 -1
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_stream_writer_full.py +8 -0
- telnetlib3-3.0.2/telnetlib3/tests/test_linemode.py +0 -106
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/.gitignore +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/CONTRIBUTING.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/LICENSE.txt +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/README.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/Makefile +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/accessories.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/client.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/client_base.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/client_shell.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/color_filter.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/fingerprinting.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/guard_shells.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/mud.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/server.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/server_base.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/server_pty_shell.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/server_shell.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/session_context.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/slc.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/stream_reader.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/stream_writer.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/sync.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/telnetlib.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api/telopt.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/api.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/conf.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/contributing.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/guidebook.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/index.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/intro.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/make.bat +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/rfcs.rst +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/docs/sphinxext/github.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/requirements-analysis.txt +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/requirements-docs.txt +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/requirements-tests.txt +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/requirements.txt +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/.gitignore +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/__init__.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/_base.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/_paths.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/_session_context.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/_types.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/client_base.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/color_filter.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/encodings/__init__.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/encodings/atarist.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/encodings/atascii.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/encodings/petscii.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/fingerprinting.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/fingerprinting_display.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/guard_shells.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/mud.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/py.typed +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/relay_server.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/server_base.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/server_fingerprinting.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/server_pty_shell.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/stream_reader.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/sync.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/telnetlib.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/telopt.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/accessories.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/conftest.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/pty_helper.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_accessories.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_accessories_extra.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_atascii_codec.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_benchmarks.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_charset.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_client_unit.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_color_filter.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_core.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_encoding.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_environ.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_fingerprinting.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_guard_integration.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_mccp.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_mud.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_mud_negotiation.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_naws.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_petscii_codec.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_platform.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_pty_shell.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_reader.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_relay_server.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_server.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_server_api.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_server_cli.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_server_fingerprinting.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_server_mud.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_slc.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_status_logger.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_stream_reader_extra.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_sync.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_telnetlib.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_timeout.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_tls.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_tspeed.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_ttype.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_uvloop_integration.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_writer.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/telnetlib3/tests/test_xdisploc.py +0 -0
- {telnetlib3-3.0.2 → telnetlib3-3.0.3}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: telnetlib3
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.3
|
|
4
4
|
Summary: Python Telnet server and client CLI and Protocol library
|
|
5
5
|
Project-URL: Homepage, https://github.com/jquast/telnetlib3
|
|
6
6
|
Project-URL: Documentation, https://telnetlib3.readthedocs.io
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
History
|
|
2
2
|
=======
|
|
3
|
+
3.0.3
|
|
4
|
+
* bugfix: server and client now correctly complete LINEMODE negotiation when prompted to.
|
|
5
|
+
* new: ``--logfile-mode {append,rewrite}`` and ``--typescript-mode`` CLI flags
|
|
6
|
+
and :func:`~telnetlib3.accessories.make_logger` ``filemode`` argument control whether the log
|
|
7
|
+
file is appended to (default) or overwritten on each connection.
|
|
8
|
+
* new: :class:`~telnetlib3.client_shell.LinemodeBuffer` used by ``telnetlib3-client``, a
|
|
9
|
+
client-side line buffer for LINEMODE EDIT mode with local erase-char, erase-line, erase-word
|
|
10
|
+
editing, forwardmask flushing, and TRAPSIG IAC command generation. The default 'telsh' server
|
|
11
|
+
was also updated to support linemode.
|
|
12
|
+
|
|
3
13
|
3.0.2
|
|
4
14
|
* bugfix: :meth:`~telnetlib3.stream_writer.TelnetWriter.request_charset` raised :exc:`TypeError`,
|
|
5
15
|
:ghissue:`128`. Offer callbacks (no-arg, returning a list of items to propose) are now
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "telnetlib3"
|
|
7
|
-
version = "3.0.
|
|
7
|
+
version = "3.0.3" # Keep in sync with telnetlib3/accessories.py::get_version !
|
|
8
8
|
description = " Python Telnet server and client CLI and Protocol library"
|
|
9
9
|
readme = "README.rst"
|
|
10
10
|
license = "ISC"
|
|
@@ -42,7 +42,7 @@ PATIENCE_MESSAGES = [
|
|
|
42
42
|
|
|
43
43
|
def get_version() -> str:
|
|
44
44
|
"""Return the current version of telnetlib3."""
|
|
45
|
-
return "3.0.
|
|
45
|
+
return "3.0.3" # keep in sync with pyproject.toml !
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def encoding_from_lang(lang: str) -> Optional[str]:
|
|
@@ -130,7 +130,11 @@ _DEFAULT_LOGFMT = " ".join(
|
|
|
130
130
|
|
|
131
131
|
|
|
132
132
|
def make_logger(
|
|
133
|
-
name: str,
|
|
133
|
+
name: str,
|
|
134
|
+
loglevel: str = "info",
|
|
135
|
+
logfile: Optional[str] = None,
|
|
136
|
+
logfmt: str = _DEFAULT_LOGFMT,
|
|
137
|
+
filemode: str = "a",
|
|
134
138
|
) -> logging.Logger:
|
|
135
139
|
"""Create and return simple logger for given arguments."""
|
|
136
140
|
lvl = getattr(logging, loglevel.upper(), None)
|
|
@@ -140,6 +144,7 @@ def make_logger(
|
|
|
140
144
|
_cfg: Dict[str, Any] = {"format": logfmt}
|
|
141
145
|
if logfile:
|
|
142
146
|
_cfg["filename"] = logfile
|
|
147
|
+
_cfg["filemode"] = filemode
|
|
143
148
|
logging.basicConfig(**_cfg)
|
|
144
149
|
for handler in logging.getLogger().handlers:
|
|
145
150
|
if isinstance(handler, logging.StreamHandler) and not isinstance(
|
|
@@ -645,7 +645,11 @@ async def run_client() -> None:
|
|
|
645
645
|
config_msg = f"Client configuration: {accessories.repr_mapping(args)}"
|
|
646
646
|
|
|
647
647
|
log = accessories.make_logger(
|
|
648
|
-
name=__name__,
|
|
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",
|
|
649
653
|
)
|
|
650
654
|
log.debug(config_msg)
|
|
651
655
|
|
|
@@ -774,7 +778,11 @@ async def run_client() -> None:
|
|
|
774
778
|
) -> None:
|
|
775
779
|
ctx = writer_arg.ctx
|
|
776
780
|
assert typescript_path is not None
|
|
777
|
-
ts_file = open(
|
|
781
|
+
ts_file = open( # noqa: SIM115
|
|
782
|
+
typescript_path,
|
|
783
|
+
"w" if args.get("typescript_mode") == "rewrite" else "a",
|
|
784
|
+
encoding="utf-8",
|
|
785
|
+
)
|
|
778
786
|
ctx.typescript_file = ts_file
|
|
779
787
|
try:
|
|
780
788
|
await _ts_inner(reader, writer_arg)
|
|
@@ -819,6 +827,13 @@ def _get_argument_parser() -> argparse.ArgumentParser:
|
|
|
819
827
|
parser.add_argument("--loglevel", default="warn", help="log level")
|
|
820
828
|
parser.add_argument("--logfmt", default=accessories._DEFAULT_LOGFMT, help="log format")
|
|
821
829
|
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
|
+
)
|
|
822
837
|
parser.add_argument(
|
|
823
838
|
"--shell", default="telnetlib3.telnet_client_shell", help="module.function_name"
|
|
824
839
|
)
|
|
@@ -972,6 +987,13 @@ def _get_argument_parser() -> argparse.ArgumentParser:
|
|
|
972
987
|
metavar="FILE",
|
|
973
988
|
help="record session to FILE (like Unix script(1))",
|
|
974
989
|
)
|
|
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
|
+
)
|
|
975
997
|
return parser
|
|
976
998
|
|
|
977
999
|
|
|
@@ -1040,6 +1062,7 @@ def _transform_args(args: argparse.Namespace) -> Dict[str, Any]:
|
|
|
1040
1062
|
"port": args.port,
|
|
1041
1063
|
"loglevel": args.loglevel,
|
|
1042
1064
|
"logfile": args.logfile,
|
|
1065
|
+
"logfile_mode": args.logfile_mode,
|
|
1043
1066
|
"logfmt": args.logfmt,
|
|
1044
1067
|
"encoding": args.encoding,
|
|
1045
1068
|
"tspeed": (args.speed, args.speed),
|
|
@@ -1068,6 +1091,7 @@ def _transform_args(args: argparse.Namespace) -> Dict[str, Any]:
|
|
|
1068
1091
|
),
|
|
1069
1092
|
"compression": args.compression,
|
|
1070
1093
|
"typescript": args.typescript,
|
|
1094
|
+
"typescript_mode": args.typescript_mode,
|
|
1071
1095
|
}
|
|
1072
1096
|
|
|
1073
1097
|
|
|
@@ -10,12 +10,14 @@ from typing import Any, Dict, Tuple, Union, Callable, Optional
|
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
|
|
12
12
|
# local
|
|
13
|
+
from . import slc as slc_module
|
|
13
14
|
from . import accessories
|
|
14
15
|
from ._session_context import TelnetSessionContext
|
|
15
16
|
|
|
16
17
|
log = logging.getLogger(__name__)
|
|
17
18
|
|
|
18
19
|
# local
|
|
20
|
+
from .telopt import LINEMODE # noqa: E402
|
|
19
21
|
from .accessories import TRACE # noqa: E402
|
|
20
22
|
from .stream_reader import TelnetReader, TelnetReaderUnicode # noqa: E402
|
|
21
23
|
from .stream_writer import TelnetWriter, TelnetWriterUnicode # noqa: E402
|
|
@@ -180,6 +182,93 @@ class _RawLoopState:
|
|
|
180
182
|
reactivate_repl: bool = False
|
|
181
183
|
|
|
182
184
|
|
|
185
|
+
class LinemodeBuffer:
|
|
186
|
+
"""
|
|
187
|
+
Client-side line buffer for LINEMODE EDIT mode (RFC 1184 §3.1).
|
|
188
|
+
|
|
189
|
+
Accumulates characters typed by the user, applying local SLC editing functions (erase-char,
|
|
190
|
+
erase-line, erase-word) and transmitting complete lines to the server. When TRAPSIG is enabled,
|
|
191
|
+
signal characters (^C etc.) are sent as IAC commands instead of buffered.
|
|
192
|
+
|
|
193
|
+
:param slctab: The writer's current SLC character table.
|
|
194
|
+
:param forwardmask: FORWARDMASK received from server, or None.
|
|
195
|
+
:param trapsig: When True, signal characters are sent as IAC commands.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
def __init__(
|
|
199
|
+
self,
|
|
200
|
+
slctab: Dict[bytes, slc_module.SLC],
|
|
201
|
+
forwardmask: Optional[slc_module.Forwardmask] = None,
|
|
202
|
+
trapsig: bool = False,
|
|
203
|
+
) -> None:
|
|
204
|
+
"""Initialize LinemodeBuffer."""
|
|
205
|
+
from .telopt import IP, AYT, BRK, EOF, IAC, SUSP, ABORT
|
|
206
|
+
|
|
207
|
+
self._buf: list[str] = []
|
|
208
|
+
self.slctab = slctab
|
|
209
|
+
self.forwardmask = forwardmask
|
|
210
|
+
self.trapsig = trapsig
|
|
211
|
+
self._trapsig_map: Dict[bytes, bytes] = {
|
|
212
|
+
slc_module.SLC_IP: IAC + IP,
|
|
213
|
+
slc_module.SLC_ABORT: IAC + ABORT,
|
|
214
|
+
slc_module.SLC_SUSP: IAC + SUSP,
|
|
215
|
+
slc_module.SLC_EOF: IAC + EOF,
|
|
216
|
+
slc_module.SLC_BRK: IAC + BRK,
|
|
217
|
+
slc_module.SLC_AYT: IAC + AYT,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
def _slc_val(self, func: bytes) -> Optional[int]:
|
|
221
|
+
"""Return the active byte value for SLC function, or None if unsupported."""
|
|
222
|
+
defn = self.slctab.get(func)
|
|
223
|
+
if defn is None or defn.nosupport:
|
|
224
|
+
return None
|
|
225
|
+
v = defn.val
|
|
226
|
+
return ord(v) if v and v != slc_module.theNULL else None
|
|
227
|
+
|
|
228
|
+
def feed(self, char: str) -> Tuple[str, Optional[bytes]]:
|
|
229
|
+
"""
|
|
230
|
+
Feed one character into the buffer.
|
|
231
|
+
|
|
232
|
+
:returns: ``(echo, data)`` where ``echo`` is text to display locally
|
|
233
|
+
(may be empty) and ``data`` is bytes to send to server, or None
|
|
234
|
+
if buffering.
|
|
235
|
+
"""
|
|
236
|
+
b = ord(char)
|
|
237
|
+
if self.trapsig:
|
|
238
|
+
for func, cmd in self._trapsig_map.items():
|
|
239
|
+
if b == self._slc_val(func):
|
|
240
|
+
return ("", cmd)
|
|
241
|
+
if b == self._slc_val(slc_module.SLC_EC):
|
|
242
|
+
if self._buf:
|
|
243
|
+
self._buf.pop()
|
|
244
|
+
return ("\b \b", None)
|
|
245
|
+
return ("", None)
|
|
246
|
+
if b == self._slc_val(slc_module.SLC_EL):
|
|
247
|
+
n = len(self._buf)
|
|
248
|
+
self._buf.clear()
|
|
249
|
+
return ("\b \b" * n, None)
|
|
250
|
+
if b == self._slc_val(slc_module.SLC_EW):
|
|
251
|
+
popped = 0
|
|
252
|
+
# skip trailing spaces (POSIX VWERASE behaviour)
|
|
253
|
+
while self._buf and self._buf[-1] == " ":
|
|
254
|
+
self._buf.pop()
|
|
255
|
+
popped += 1
|
|
256
|
+
while self._buf and self._buf[-1] != " ":
|
|
257
|
+
self._buf.pop()
|
|
258
|
+
popped += 1
|
|
259
|
+
return ("\b \b" * popped, None)
|
|
260
|
+
if char in ("\r", "\n"):
|
|
261
|
+
line = "".join(self._buf) + char
|
|
262
|
+
self._buf.clear()
|
|
263
|
+
return (char, line.encode())
|
|
264
|
+
if self.forwardmask is not None and b in self.forwardmask:
|
|
265
|
+
data = ("".join(self._buf) + char).encode()
|
|
266
|
+
self._buf.clear()
|
|
267
|
+
return (char, data)
|
|
268
|
+
self._buf.append(char)
|
|
269
|
+
return (char, None)
|
|
270
|
+
|
|
271
|
+
|
|
183
272
|
if sys.platform == "win32":
|
|
184
273
|
|
|
185
274
|
async def telnet_client_shell(
|
|
@@ -310,10 +399,11 @@ else:
|
|
|
310
399
|
)
|
|
311
400
|
|
|
312
401
|
def _server_will_sga(self) -> bool:
|
|
313
|
-
"""Whether
|
|
402
|
+
"""Whether SGA has been negotiated (either direction)."""
|
|
314
403
|
from .telopt import SGA
|
|
315
404
|
|
|
316
|
-
|
|
405
|
+
w = self.telnet_writer
|
|
406
|
+
return bool(w.client and (w.remote_option.enabled(SGA) or w.local_option.enabled(SGA)))
|
|
317
407
|
|
|
318
408
|
def check_auto_mode(
|
|
319
409
|
self, switched_to_raw: bool, last_will_echo: bool
|
|
@@ -330,6 +420,20 @@ else:
|
|
|
330
420
|
return None
|
|
331
421
|
wecho = self.telnet_writer.will_echo
|
|
332
422
|
wsga = self._server_will_sga()
|
|
423
|
+
# LINEMODE EDIT: kernel must handle line editing; keep/restore cooked mode.
|
|
424
|
+
# This takes priority over the SGA/ECHO raw-mode heuristics below.
|
|
425
|
+
if (
|
|
426
|
+
self.telnet_writer.local_option.enabled(LINEMODE)
|
|
427
|
+
and self.telnet_writer.linemode.edit
|
|
428
|
+
):
|
|
429
|
+
if switched_to_raw:
|
|
430
|
+
assert self._save_mode is not None
|
|
431
|
+
self.set_mode(self._save_mode)
|
|
432
|
+
self.telnet_writer.log.debug(
|
|
433
|
+
"auto: LINEMODE EDIT confirmed, restoring cooked mode"
|
|
434
|
+
)
|
|
435
|
+
return (False, wecho, False)
|
|
436
|
+
return None
|
|
333
437
|
# WILL ECHO alone = line mode with server echo (suppress local echo)
|
|
334
438
|
# WILL SGA (with or without ECHO) = raw/character-at-a-time
|
|
335
439
|
should_go_raw = not switched_to_raw and wsga
|
|
@@ -362,20 +466,35 @@ else:
|
|
|
362
466
|
|
|
363
467
|
Auto mode (``_raw_mode is None``): follows the server's negotiation.
|
|
364
468
|
|
|
365
|
-
================= ======== ==========
|
|
469
|
+
================= ======== ========== ========================================
|
|
366
470
|
Server negotiates ICANON ECHO Behavior
|
|
367
|
-
================= ======== ==========
|
|
471
|
+
================= ======== ========== ========================================
|
|
368
472
|
Nothing on on Line mode, local echo
|
|
473
|
+
LINEMODE EDIT **on** on Cooked mode, kernel handles EC/EL/echo
|
|
474
|
+
LINEMODE remote **off** **off** Raw, server echoes
|
|
369
475
|
WILL SGA only **off** on Character-at-a-time, local echo
|
|
370
476
|
WILL ECHO only on **off** Line mode, server echoes
|
|
371
477
|
WILL SGA + ECHO **off** **off** Full kludge mode (most common)
|
|
372
|
-
================= ======== ==========
|
|
478
|
+
================= ======== ========== ========================================
|
|
373
479
|
"""
|
|
374
480
|
raw_mode = _get_raw_mode(self.telnet_writer)
|
|
375
481
|
will_echo = self.telnet_writer.will_echo
|
|
376
482
|
will_sga = self._server_will_sga()
|
|
377
483
|
# Auto mode (None): follow server negotiation
|
|
378
484
|
if raw_mode is None:
|
|
485
|
+
if self.telnet_writer.local_option.enabled(LINEMODE):
|
|
486
|
+
linemode_mode = self.telnet_writer.linemode
|
|
487
|
+
if linemode_mode.edit:
|
|
488
|
+
# RFC 1184 / NetBSD reference: LINEMODE EDIT means ICANON on.
|
|
489
|
+
# The kernel line discipline handles EC (VERASE), EL (VKILL),
|
|
490
|
+
# EW (VWERASE), and echo. No software line editing needed.
|
|
491
|
+
self.telnet_writer.log.debug(
|
|
492
|
+
"auto: LINEMODE EDIT, cooked mode (kernel line editing)"
|
|
493
|
+
)
|
|
494
|
+
self.software_echo = False
|
|
495
|
+
return mode # keep ICANON on; kernel handles EC/EL/EW and echo
|
|
496
|
+
self.telnet_writer.log.debug("auto: LINEMODE remote, raw input server echo")
|
|
497
|
+
return self._make_raw(mode, suppress_echo=True)
|
|
379
498
|
if will_echo and will_sga:
|
|
380
499
|
self.telnet_writer.log.debug("auto: server echo + SGA, kludge mode")
|
|
381
500
|
return self._make_raw(mode)
|
|
@@ -531,6 +650,18 @@ else:
|
|
|
531
650
|
"""Return the autoreply engine from the writer's context, if set."""
|
|
532
651
|
return telnet_writer.ctx.autoreply_engine
|
|
533
652
|
|
|
653
|
+
def _get_linemode_buffer(writer: Union[TelnetWriter, TelnetWriterUnicode]) -> "LinemodeBuffer":
|
|
654
|
+
"""Return (or lazily create) the LinemodeBuffer attached to *writer*."""
|
|
655
|
+
buf: Optional[LinemodeBuffer] = getattr(writer, "_linemode_buf", None)
|
|
656
|
+
if buf is None:
|
|
657
|
+
buf = LinemodeBuffer(
|
|
658
|
+
slctab=writer.slctab,
|
|
659
|
+
forwardmask=writer.forwardmask,
|
|
660
|
+
trapsig=writer.linemode.trapsig,
|
|
661
|
+
)
|
|
662
|
+
writer._linemode_buf = buf
|
|
663
|
+
return buf
|
|
664
|
+
|
|
534
665
|
async def _raw_event_loop(
|
|
535
666
|
telnet_reader: Union[TelnetReader, TelnetReaderUnicode],
|
|
536
667
|
telnet_writer: Union[TelnetWriter, TelnetWriterUnicode],
|
|
@@ -589,7 +720,26 @@ else:
|
|
|
589
720
|
wait_for.remove(telnet_task)
|
|
590
721
|
handle_close("Connection closed.")
|
|
591
722
|
break
|
|
592
|
-
|
|
723
|
+
linemode_edit = (
|
|
724
|
+
telnet_writer.local_option.enabled(LINEMODE) and telnet_writer.linemode.edit
|
|
725
|
+
)
|
|
726
|
+
if linemode_edit and state.switched_to_raw:
|
|
727
|
+
# Raw PTY or non-TTY: kernel not doing line editing, use LinemodeBuffer
|
|
728
|
+
lmbuf = _get_linemode_buffer(telnet_writer)
|
|
729
|
+
for ch in inp.decode(errors="replace"):
|
|
730
|
+
echo, data = lmbuf.feed(ch)
|
|
731
|
+
if echo:
|
|
732
|
+
stdout.write(echo.encode())
|
|
733
|
+
if data:
|
|
734
|
+
telnet_writer._write(data)
|
|
735
|
+
new_timer, has_pending = None, False
|
|
736
|
+
elif linemode_edit:
|
|
737
|
+
# Cooked PTY: kernel already handled EC/EL/echo; forward line directly
|
|
738
|
+
new_timer, has_pending = _send_stdin(inp, telnet_writer, stdout, False)
|
|
739
|
+
else:
|
|
740
|
+
new_timer, has_pending = _send_stdin(
|
|
741
|
+
inp, telnet_writer, stdout, state.local_echo
|
|
742
|
+
)
|
|
593
743
|
if has_pending and esc_timer_task not in wait_for:
|
|
594
744
|
esc_timer_task = new_timer
|
|
595
745
|
if esc_timer_task is not None:
|
|
@@ -42,7 +42,14 @@ try:
|
|
|
42
42
|
except ImportError:
|
|
43
43
|
PTY_SUPPORT = False
|
|
44
44
|
|
|
45
|
-
__all__ = (
|
|
45
|
+
__all__ = (
|
|
46
|
+
"TelnetServer",
|
|
47
|
+
"LinemodeServer",
|
|
48
|
+
"Server",
|
|
49
|
+
"create_server",
|
|
50
|
+
"run_server",
|
|
51
|
+
"parse_server_args",
|
|
52
|
+
)
|
|
46
53
|
|
|
47
54
|
|
|
48
55
|
class CONFIG(NamedTuple):
|
|
@@ -806,6 +813,42 @@ class _TLSAutoDetectProtocol(asyncio.Protocol):
|
|
|
806
813
|
_ = exc
|
|
807
814
|
|
|
808
815
|
|
|
816
|
+
class LinemodeServer(TelnetServer):
|
|
817
|
+
"""
|
|
818
|
+
:class:`TelnetServer` subclass that negotiates LINEMODE EDIT.
|
|
819
|
+
|
|
820
|
+
In addition to the standard options negotiated by :class:`TelnetServer`,
|
|
821
|
+
this server sends ``DO LINEMODE`` during advanced negotiation, proposes
|
|
822
|
+
LINEMODE EDIT (local line editing by the client), and suppresses
|
|
823
|
+
``WILL ECHO`` so the client performs local echoing via its LINEMODE buffer.
|
|
824
|
+
|
|
825
|
+
Use with :func:`create_server` to enable RFC 1184 LINEMODE EDIT on a
|
|
826
|
+
:func:`~.telnet_server_shell` session or any custom shell.
|
|
827
|
+
"""
|
|
828
|
+
|
|
829
|
+
from . import slc as _slc_module
|
|
830
|
+
|
|
831
|
+
#: Propose LINEMODE EDIT (local line editing) instead of remote mode.
|
|
832
|
+
default_linemode = _slc_module.Linemode(_slc_module.LMODE_MODE_LOCAL)
|
|
833
|
+
|
|
834
|
+
def begin_advanced_negotiation(self) -> None:
|
|
835
|
+
"""Negotiate standard options plus ``DO LINEMODE``."""
|
|
836
|
+
from .telopt import DO, LINEMODE
|
|
837
|
+
|
|
838
|
+
super().begin_advanced_negotiation()
|
|
839
|
+
# Propagate the protocol-level default_linemode to the writer so that
|
|
840
|
+
# TelnetWriter.handle_will(LINEMODE) proposes the correct mode (LOCAL/EDIT)
|
|
841
|
+
# rather than the TelnetWriter class default (REMOTE).
|
|
842
|
+
self.writer.default_linemode = self.default_linemode
|
|
843
|
+
self.writer.iac(DO, LINEMODE)
|
|
844
|
+
|
|
845
|
+
def _negotiate_echo(self) -> None:
|
|
846
|
+
"""Skip ``WILL ECHO`` — LINEMODE EDIT client handles local echo."""
|
|
847
|
+
if self._echo_negotiated:
|
|
848
|
+
return
|
|
849
|
+
self._echo_negotiated = True
|
|
850
|
+
|
|
851
|
+
|
|
809
852
|
class Server:
|
|
810
853
|
"""
|
|
811
854
|
Telnet server that tracks connected clients.
|
|
@@ -137,7 +137,14 @@ class _LineEditor:
|
|
|
137
137
|
return char, None
|
|
138
138
|
|
|
139
139
|
|
|
140
|
-
__all__ = (
|
|
140
|
+
__all__ = (
|
|
141
|
+
"telnet_server_shell",
|
|
142
|
+
"readline_async",
|
|
143
|
+
"readline",
|
|
144
|
+
"get_linemode",
|
|
145
|
+
"get_slcdata",
|
|
146
|
+
"do_toggle",
|
|
147
|
+
)
|
|
141
148
|
|
|
142
149
|
|
|
143
150
|
async def telnet_server_shell(
|
|
@@ -179,7 +186,7 @@ async def telnet_server_shell(
|
|
|
179
186
|
writer.write("Goodbye." + CR + LF)
|
|
180
187
|
break
|
|
181
188
|
if command == "help":
|
|
182
|
-
writer.write("quit, writer, slc, toggle [option|all], reader, proto, dump")
|
|
189
|
+
writer.write("quit, writer, slc, linemode, toggle [option|all], reader, proto, dump")
|
|
183
190
|
elif command == "writer":
|
|
184
191
|
# show 'writer' status
|
|
185
192
|
writer.write(repr(writer))
|
|
@@ -194,6 +201,8 @@ async def telnet_server_shell(
|
|
|
194
201
|
elif command == "slc":
|
|
195
202
|
# show 'slc' support and data tables
|
|
196
203
|
writer.write(get_slcdata(writer))
|
|
204
|
+
elif command == "linemode":
|
|
205
|
+
writer.write(get_linemode(writer))
|
|
197
206
|
elif command.startswith("toggle"):
|
|
198
207
|
# toggle specified options
|
|
199
208
|
option = command[len("toggle ") :] or None
|
|
@@ -342,8 +351,25 @@ def get_slcdata(writer: Union[TelnetWriter, TelnetWriterUnicode]) -> str:
|
|
|
342
351
|
)
|
|
343
352
|
|
|
344
353
|
|
|
354
|
+
def get_linemode(writer: Union[TelnetWriter, TelnetWriterUnicode]) -> str:
|
|
355
|
+
"""Display current LINEMODE negotiation state."""
|
|
356
|
+
active = writer.remote_option.enabled(telopt.LINEMODE)
|
|
357
|
+
if not active:
|
|
358
|
+
return "LINEMODE not negotiated."
|
|
359
|
+
lm = writer.linemode
|
|
360
|
+
bits = (
|
|
361
|
+
f"EDIT={'on' if lm.edit else 'off'}"
|
|
362
|
+
f" TRAPSIG={'on' if lm.trapsig else 'off'}"
|
|
363
|
+
f" SOFT_TAB={'on' if lm.soft_tab else 'off'}"
|
|
364
|
+
f" LIT_ECHO={'on' if lm.lit_echo else 'off'}"
|
|
365
|
+
f" ACK={'on' if lm.ack else 'off'}"
|
|
366
|
+
)
|
|
367
|
+
return f"LINEMODE active. Mode: {writer.mode}\r\n{bits}"
|
|
368
|
+
|
|
369
|
+
|
|
345
370
|
def do_toggle(writer: Union[TelnetWriter, TelnetWriterUnicode], option: Optional[str]) -> str:
|
|
346
371
|
"""Display or toggle telnet session parameters."""
|
|
372
|
+
linemode_active = writer.remote_option.enabled(telopt.LINEMODE)
|
|
347
373
|
tbl_opt = {
|
|
348
374
|
"echo": writer.local_option.enabled(telopt.ECHO),
|
|
349
375
|
"goahead": not writer.local_option.enabled(telopt.SGA),
|
|
@@ -352,6 +378,9 @@ def do_toggle(writer: Union[TelnetWriter, TelnetWriterUnicode], option: Optional
|
|
|
352
378
|
"binary": writer.outbinary and writer.inbinary,
|
|
353
379
|
"xon-any": writer.xon_any,
|
|
354
380
|
"lflow": writer.lflow,
|
|
381
|
+
"linemode": linemode_active,
|
|
382
|
+
"linemode-edit": writer.linemode.edit if linemode_active else False,
|
|
383
|
+
"linemode-trapsig": writer.linemode.trapsig if linemode_active else False,
|
|
355
384
|
}
|
|
356
385
|
|
|
357
386
|
if not option:
|
|
@@ -390,6 +419,25 @@ def do_toggle(writer: Union[TelnetWriter, TelnetWriterUnicode], option: Optional
|
|
|
390
419
|
writer.send_lineflow_mode()
|
|
391
420
|
msgs.append(f"lineflow {'en' if writer.lflow else 'dis'}abled.")
|
|
392
421
|
|
|
422
|
+
if option in ("linemode",):
|
|
423
|
+
cmd = telopt.DONT if tbl_opt["linemode"] else telopt.DO
|
|
424
|
+
writer.iac(cmd, telopt.LINEMODE)
|
|
425
|
+
msgs.append(f"{telopt.name_command(cmd).lower()} linemode.")
|
|
426
|
+
|
|
427
|
+
if option in ("linemode-edit",):
|
|
428
|
+
if not tbl_opt["linemode"]:
|
|
429
|
+
msgs.append("linemode not active.")
|
|
430
|
+
else:
|
|
431
|
+
writer.request_linemode_change(edit=not tbl_opt["linemode-edit"])
|
|
432
|
+
msgs.append(f"linemode-edit {'dis' if tbl_opt['linemode-edit'] else 'en'}abled.")
|
|
433
|
+
|
|
434
|
+
if option in ("linemode-trapsig",):
|
|
435
|
+
if not tbl_opt["linemode"]:
|
|
436
|
+
msgs.append("linemode not active.")
|
|
437
|
+
else:
|
|
438
|
+
writer.request_linemode_change(trapsig=not tbl_opt["linemode-trapsig"])
|
|
439
|
+
msgs.append(f"linemode-trapsig {'dis' if tbl_opt['linemode-trapsig'] else 'en'}abled.")
|
|
440
|
+
|
|
393
441
|
if option not in tbl_opt and option != "all":
|
|
394
442
|
msgs.append("toggle: not an option.")
|
|
395
443
|
|
|
@@ -17,6 +17,7 @@ __all__ = (
|
|
|
17
17
|
"Linemode",
|
|
18
18
|
"LMODE_FORWARDMASK",
|
|
19
19
|
"LMODE_MODE",
|
|
20
|
+
"LMODE_MODE_EDIT",
|
|
20
21
|
"LMODE_MODE_REMOTE",
|
|
21
22
|
"LMODE_SLC",
|
|
22
23
|
"name_slc_command",
|
|
@@ -89,6 +90,9 @@ LMODE_MODE, LMODE_FORWARDMASK, LMODE_SLC = (bytes([const]) for const in range(1,
|
|
|
89
90
|
LMODE_MODE_REMOTE, LMODE_MODE_LOCAL, LMODE_MODE_TRAPSIG = (bytes([const]) for const in range(3))
|
|
90
91
|
LMODE_MODE_ACK, LMODE_MODE_SOFT_TAB, LMODE_MODE_LIT_ECHO = (bytes([4]), bytes([8]), bytes([16]))
|
|
91
92
|
|
|
93
|
+
#: RFC 1184's name for LMODE_MODE_LOCAL (EDIT bit)
|
|
94
|
+
LMODE_MODE_EDIT = LMODE_MODE_LOCAL
|
|
95
|
+
|
|
92
96
|
|
|
93
97
|
class SLC:
|
|
94
98
|
"""Defines the willingness to support a Special Linemode Character."""
|
|
@@ -315,6 +319,15 @@ class Linemode:
|
|
|
315
319
|
"""True if linemode is local."""
|
|
316
320
|
return bool(ord(self.mask) & ord(LMODE_MODE_LOCAL))
|
|
317
321
|
|
|
322
|
+
@property
|
|
323
|
+
def edit(self) -> bool:
|
|
324
|
+
"""
|
|
325
|
+
True if EDIT bit set (client performs local line editing).
|
|
326
|
+
|
|
327
|
+
RFC 1184 name.
|
|
328
|
+
"""
|
|
329
|
+
return self.local
|
|
330
|
+
|
|
318
331
|
@property
|
|
319
332
|
def remote(self) -> bool:
|
|
320
333
|
"""True if linemode is remote."""
|