telnetlib3 4.0.4__tar.gz → 5.0.0__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-4.0.4 → telnetlib3-5.0.0}/PKG-INFO +1 -1
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/history.rst +36 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/rfcs.rst +6 -2
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/pyproject.toml +1 -1
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/_session_context.py +7 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/accessories.py +1 -1
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/client.py +99 -46
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/client_base.py +24 -4
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/client_shell.py +2 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/mud.py +19 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/server_base.py +16 -2
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/server_fingerprinting.py +22 -15
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/stream_writer.py +160 -35
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_client_shell.py +3 -2
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_client_unit.py +175 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_mccp.py +65 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_mud.py +26 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_mud_negotiation.py +20 -8
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_server_fingerprinting.py +45 -8
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_stream_writer_full.py +57 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_writer.py +41 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/.gitignore +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/CONTRIBUTING.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/LICENSE.txt +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/README.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/Makefile +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/api/accessories.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/api/client.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/api/client_base.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/api/client_shell.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/api/client_shell_win32.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/api/fingerprinting.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/api/guard_shells.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/api/mud.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/api/server.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/api/server_base.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/api/server_pty_shell.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/api/server_shell.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/api/session_context.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/api/slc.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/api/stream_reader.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/api/stream_writer.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/api/sync.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/api/telnetlib.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/api/telopt.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/api.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/conf.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/contributing.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/guidebook.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/index.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/intro.rst +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/make.bat +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/docs/sphinxext/github.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/requirements-analysis.txt +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/requirements-docs.txt +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/requirements-tests.txt +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/requirements.txt +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/.gitignore +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/__init__.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/_base.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/_paths.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/_types.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/client_shell_win32.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/encodings/__init__.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/encodings/atarist.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/encodings/atascii.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/encodings/big5bbs.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/encodings/petscii.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/fingerprinting.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/fingerprinting_display.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/guard_shells.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/py.typed +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/relay_server.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/server.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/server_pty_shell.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/server_shell.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/slc.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/stream_reader.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/sync.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/telnetlib.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/telopt.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/accessories.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/conftest.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/pty_helper.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_accessories.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_accessories_extra.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_atascii_codec.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_benchmarks.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_big5bbs_codec.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_charset.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_client_shell_win32.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_core.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_encoding.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_environ.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_fingerprinting.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_guard_integration.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_linemode.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_naws.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_petscii_codec.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_pty_shell.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_reader.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_relay_server.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_server.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_server_api.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_server_cli.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_server_mud.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_server_shell_unit.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_shell.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_slc.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_status_logger.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_stream_reader_extra.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_sync.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_telnetlib.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_telnetlib_import.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_timeout.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_tls.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_tspeed.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_ttype.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_uvloop_integration.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/telnetlib3/tests/test_xdisploc.py +0 -0
- {telnetlib3-4.0.4 → telnetlib3-5.0.0}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: telnetlib3
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.0
|
|
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,41 @@
|
|
|
1
1
|
History
|
|
2
2
|
=======
|
|
3
|
+
|
|
4
|
+
5.0.0
|
|
5
|
+
* changed: :meth:`~telnetlib3.stream_writer.TelnetWriter.handle_zmp` now receives ``command,
|
|
6
|
+
*args`` instead of one ``parts`` list; ``zmp_data`` moved to ``writer.ctx`` and is now a dict
|
|
7
|
+
keyed by command (was a list of messages). New
|
|
8
|
+
:meth:`~telnetlib3.stream_writer.TelnetWriter.send_zmp`.
|
|
9
|
+
* changed: MUD protocol subnegotiation data (``mssp_data``, ``atcp_data``, ``aardwolf_data``,
|
|
10
|
+
``mxp_data``, ``comport_data``) moved from :class:`~telnetlib3.stream_writer.TelnetWriter` to
|
|
11
|
+
:class:`~telnetlib3._session_context.TelnetSessionContext` (``writer.ctx.mssp_data``, etc.).
|
|
12
|
+
Deprecated writer properties delegate to ``ctx``; ``writer.zmp_data`` is removed without
|
|
13
|
+
deprecation.
|
|
14
|
+
* changed: client-side MUD protocol declines (GMCP, MSDP, MSSP, MSP, MXP, ZMP, AARDWOLF, ATCP)
|
|
15
|
+
remain the default; the decline log messages now name the enable mechanism (``always_will`` /
|
|
16
|
+
``always_do`` / ``passive_do``) used by downstream clients such as telix to accept them. Note
|
|
17
|
+
that MXP's negotiation direction is server-sends-``DO`` (the LPMud family convention, e.g.
|
|
18
|
+
Discworld), so a client accepting MXP replies ``WILL`` to ``IAC DO MXP``.
|
|
19
|
+
:meth:`~telnetlib3.stream_writer.TelnetWriter.remove_will_callback` for per-option callbacks
|
|
20
|
+
invoked after :meth:`~telnetlib3.stream_writer.TelnetWriter.handle_will` negotiation. Replaces
|
|
21
|
+
the previous
|
|
22
|
+
closure-wrapping pattern in :class:`~telnetlib3.client.TelnetClient` for GMCP, ZMP, and CHARSET
|
|
23
|
+
will-detection.
|
|
24
|
+
* enhancement: ``telnetlib3-fingerprint`` now accepts all MUD protocol offers (ATCP, AARDWOLF,
|
|
25
|
+
MSP, MXP, MSDP, MSSP) to collect subnegotiation data.
|
|
26
|
+
* enhancement: sub-negotiation payloads are bounded to 1,000KB.
|
|
27
|
+
* enhancement: ``--loglevel=trace`` receive dumps show the decompressed telnet stream (MCCP2,
|
|
28
|
+
MCCP3) instead of raw compressed bytes.
|
|
29
|
+
* bugfix: ``IAC SB IAC SE`` (sub-negotiation with no option byte) should not raise ``IndexError``
|
|
30
|
+
* new: :meth:`~telnetlib3.stream_writer.TelnetWriter.add_will_callback` and
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
4.0.6
|
|
34
|
+
* bugfix: default GMCP modules requested are now in lowercase instead of titlecase
|
|
35
|
+
|
|
36
|
+
4.0.5
|
|
37
|
+
* enhancement: ``telnetlib3-client`` client shell now drains stdout.
|
|
38
|
+
|
|
3
39
|
4.0.4
|
|
4
40
|
* bugfix: servers using ``robot_check=True`` with ``encoding=False`` raised ``TypeError: buf
|
|
5
41
|
expected bytes, got <class 'str'>``. ``telnetlib3-server`` now also accepts ``--encoding=False``
|
|
@@ -77,6 +77,9 @@ Dungeon) servers and clients.
|
|
|
77
77
|
* `GMCP`_ (Generic MUD Communication Protocol, option 201). JSON-based
|
|
78
78
|
bidirectional messaging for game data such as room info, character vitals,
|
|
79
79
|
and client metadata.
|
|
80
|
+
* `ZMP`_ (Zenith Mud Protocol, option 93). Bidirectional messaging of
|
|
81
|
+
NUL-delimited string lists (a command plus arguments), carrying data such
|
|
82
|
+
as room info and character vitals.
|
|
80
83
|
* `MSDP`_ (MUD Server Data Protocol, option 69). Structured key-value protocol
|
|
81
84
|
for game variables with support for nested tables and arrays.
|
|
82
85
|
* `MSSP`_ (MUD Server Status Protocol, option 70). Server metadata protocol
|
|
@@ -93,13 +96,14 @@ Dungeon) servers and clients.
|
|
|
93
96
|
.. _MSSP: https://tintin.mudhalla.net/protocols/mssp/
|
|
94
97
|
.. _MCCP2: https://tintin.mudhalla.net/protocols/mccp/
|
|
95
98
|
.. _MCCP3: https://tintin.mudhalla.net/protocols/mccp/
|
|
99
|
+
.. _ZMP: https://discworld.starturtle.net/external/protocols/zmp.html
|
|
96
100
|
|
|
97
101
|
MUDs Not Implemented
|
|
98
102
|
--------------------
|
|
99
103
|
|
|
100
104
|
Constants are also defined for the following MUD options, though their handlers
|
|
101
|
-
are not implemented: MCCP (85, legacy compression), MXP (91, markup),
|
|
102
|
-
|
|
105
|
+
are not implemented: MCCP (85, legacy compression), MXP (91, markup), MSP (90,
|
|
106
|
+
sound), and ATCP (200, Achaea-specific).
|
|
103
107
|
|
|
104
108
|
Additional Resources
|
|
105
109
|
--------------------
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "telnetlib3"
|
|
7
|
-
version = "
|
|
7
|
+
version = "5.0.0" # 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"
|
|
@@ -53,3 +53,10 @@ class TelnetSessionContext:
|
|
|
53
53
|
self.autoreply_wait_fn = autoreply_wait_fn
|
|
54
54
|
self.typescript_file = typescript_file
|
|
55
55
|
self.gmcp_data: dict[str, Any] = gmcp_data if gmcp_data is not None else {}
|
|
56
|
+
self.zmp_data: dict[str, list[str]] = {}
|
|
57
|
+
# MUD protocol data moved from TelnetWriter to ctx for consistency
|
|
58
|
+
self.mssp_data: Optional[dict[str, str | list[str]]] = None
|
|
59
|
+
self.atcp_data: list[tuple[str, str]] = []
|
|
60
|
+
self.aardwolf_data: list[dict[str, Any]] = []
|
|
61
|
+
self.mxp_data: list[bytes] = []
|
|
62
|
+
self.comport_data: Optional[dict[str, Any]] = None
|
|
@@ -42,7 +42,7 @@ PATIENCE_MESSAGES = [
|
|
|
42
42
|
|
|
43
43
|
def get_version() -> str:
|
|
44
44
|
"""Return the current version of telnetlib3."""
|
|
45
|
-
return "
|
|
45
|
+
return "5.0.0" # keep in sync with pyproject.toml !
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def encoding_from_lang(lang: str) -> Optional[str]:
|
|
@@ -12,7 +12,7 @@ import struct
|
|
|
12
12
|
import asyncio
|
|
13
13
|
import argparse
|
|
14
14
|
import functools
|
|
15
|
-
from typing import Any, Dict, List, Tuple, Union, Callable, Optional, Sequence
|
|
15
|
+
from typing import Any, Dict, List, Tuple, Union, Callable, Optional, Sequence, Collection
|
|
16
16
|
|
|
17
17
|
# local
|
|
18
18
|
from telnetlib3 import accessories, client_base
|
|
@@ -26,14 +26,14 @@ __all__ = ("TelnetClient", "TelnetTerminalClient", "open_connection")
|
|
|
26
26
|
#: Sub-modules are listed explicitly because not all servers treat
|
|
27
27
|
#: top-level subscriptions as wildcards.
|
|
28
28
|
_DEFAULT_GMCP_MODULES = [
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
29
|
+
"char 1",
|
|
30
|
+
"char.vitals 1",
|
|
31
|
+
"char.items 1",
|
|
32
|
+
"room 1",
|
|
33
|
+
"room.info 1",
|
|
34
|
+
"comm 1",
|
|
35
|
+
"comm.channel 1",
|
|
36
|
+
"group 1",
|
|
37
37
|
]
|
|
38
38
|
|
|
39
39
|
|
|
@@ -73,6 +73,8 @@ class TelnetClient(client_base.BaseClient):
|
|
|
73
73
|
waiter_closed: Optional[asyncio.Future[None]] = None,
|
|
74
74
|
_waiter_connected: Optional[asyncio.Future[None]] = None,
|
|
75
75
|
gmcp_modules: Optional[List[str]] = None,
|
|
76
|
+
zmp_check_handler: Optional[Callable[[str], bool]] = None,
|
|
77
|
+
zmp_supported_commands: Optional[Collection[str]] = None,
|
|
76
78
|
) -> None:
|
|
77
79
|
"""Initialize TelnetClient with terminal parameters."""
|
|
78
80
|
self._compression = compression
|
|
@@ -89,6 +91,11 @@ class TelnetClient(client_base.BaseClient):
|
|
|
89
91
|
)
|
|
90
92
|
self._gmcp_modules = gmcp_modules or list(_DEFAULT_GMCP_MODULES)
|
|
91
93
|
self._gmcp_hello_sent = False
|
|
94
|
+
self._zmp_check_handler = zmp_check_handler # None means refuse all
|
|
95
|
+
self._zmp_ident_sent = False
|
|
96
|
+
self._zmp_supported_commands = (
|
|
97
|
+
set(zmp_supported_commands) if zmp_supported_commands else set()
|
|
98
|
+
)
|
|
92
99
|
self._send_environ = set(send_environ or self.DEFAULT_SEND_ENVIRON)
|
|
93
100
|
self._extra.update(
|
|
94
101
|
{
|
|
@@ -144,61 +151,74 @@ class TelnetClient(client_base.BaseClient):
|
|
|
144
151
|
):
|
|
145
152
|
self.writer.set_ext_offer_callback(opt, offer_func)
|
|
146
153
|
|
|
147
|
-
|
|
148
|
-
# Store the original only on first connection to prevent chain growth on reconnect.
|
|
149
|
-
if not hasattr(self.writer, "_original_handle_will"):
|
|
150
|
-
self.writer._original_handle_will = self.writer.handle_will
|
|
151
|
-
else:
|
|
152
|
-
self.writer.handle_will = ( # type: ignore[method-assign]
|
|
153
|
-
self.writer._original_handle_will
|
|
154
|
-
)
|
|
155
|
-
original_handle_will = self.writer.handle_will
|
|
156
|
-
writer = self.writer
|
|
154
|
+
self.writer.add_will_callback(CHARSET, self.on_will_charset)
|
|
157
155
|
|
|
158
|
-
|
|
159
|
-
|
|
156
|
+
self.setup_gmcp()
|
|
157
|
+
self.setup_zmp()
|
|
160
158
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
opt == CHARSET
|
|
165
|
-
and writer.remote_option.enabled(CHARSET)
|
|
166
|
-
and writer.local_option.enabled(CHARSET)
|
|
167
|
-
):
|
|
168
|
-
self.log.debug("Both sides support CHARSET, ready for server to initiate REQUEST")
|
|
159
|
+
def on_will_charset(self, opt: bytes) -> None:
|
|
160
|
+
"""Log when both sides support CHARSET after WILL negotiation."""
|
|
161
|
+
from telnetlib3.telopt import CHARSET
|
|
169
162
|
|
|
170
|
-
self.writer.
|
|
163
|
+
if self.writer.remote_option.enabled(CHARSET) and self.writer.local_option.enabled(CHARSET):
|
|
164
|
+
self.log.debug("Both sides support CHARSET, ready for server to initiate REQUEST")
|
|
171
165
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def _setup_gmcp(self) -> None:
|
|
166
|
+
def setup_gmcp(self) -> None:
|
|
175
167
|
"""Wire GMCP callback and WILL-detection for Core.Hello handshake."""
|
|
176
168
|
from telnetlib3.telopt import GMCP
|
|
177
169
|
|
|
170
|
+
self.writer.passive_do.add(GMCP)
|
|
178
171
|
self.writer.set_ext_callback(GMCP, self.on_gmcp)
|
|
172
|
+
self.writer.add_will_callback(GMCP, self.on_will_gmcp)
|
|
179
173
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
original_handle_will_gmcp = self.writer.handle_will
|
|
174
|
+
def on_will_gmcp(self, opt: bytes) -> None:
|
|
175
|
+
"""Send Core.Hello after GMCP negotiation."""
|
|
176
|
+
from telnetlib3.telopt import GMCP
|
|
184
177
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
178
|
+
enabled = self.writer.remote_option.enabled(GMCP)
|
|
179
|
+
hello_sent = self._gmcp_hello_sent
|
|
180
|
+
self.log.debug("on_will_gmcp: remote_enabled=%s _gmcp_hello_sent=%s", enabled, hello_sent)
|
|
181
|
+
if enabled:
|
|
182
|
+
self.send_gmcp_hello()
|
|
189
183
|
|
|
190
|
-
|
|
184
|
+
def setup_zmp(self) -> None:
|
|
185
|
+
"""Wire ZMP callback and WILL-detection for zmp.ident handshake."""
|
|
186
|
+
from telnetlib3.telopt import ZMP
|
|
191
187
|
|
|
192
|
-
|
|
188
|
+
self.writer.passive_do.add(ZMP)
|
|
189
|
+
self.writer.set_ext_callback(ZMP, self.on_zmp)
|
|
190
|
+
self.writer.add_will_callback(ZMP, self.on_will_zmp)
|
|
191
|
+
|
|
192
|
+
def on_will_zmp(self, opt: bytes) -> None:
|
|
193
|
+
"""Send zmp.ident after ZMP negotiation."""
|
|
194
|
+
from telnetlib3.telopt import ZMP
|
|
195
|
+
|
|
196
|
+
if self.writer.remote_option.enabled(ZMP):
|
|
197
|
+
self.send_zmp_ident()
|
|
198
|
+
|
|
199
|
+
def send_gmcp_hello(self) -> None:
|
|
193
200
|
"""Send ``Core.Hello`` and ``Core.Supports.Set`` after GMCP negotiation."""
|
|
194
201
|
if self._gmcp_hello_sent:
|
|
195
202
|
return
|
|
196
203
|
self._gmcp_hello_sent = True
|
|
197
204
|
from telnetlib3.accessories import get_version
|
|
198
205
|
|
|
206
|
+
modules = [m.lower() for m in self._gmcp_modules]
|
|
199
207
|
self.writer.send_gmcp("Core.Hello", {"client": "telnetlib3", "version": get_version()})
|
|
200
|
-
self.writer.send_gmcp("Core.Supports.Set",
|
|
201
|
-
self.log.info("GMCP handshake: Core.Hello + Core.Supports.Set %s",
|
|
208
|
+
self.writer.send_gmcp("Core.Supports.Set", modules)
|
|
209
|
+
self.log.info("GMCP handshake: Core.Hello + Core.Supports.Set %s", modules)
|
|
210
|
+
|
|
211
|
+
def send_zmp_ident(self) -> None:
|
|
212
|
+
"""Send ``zmp.ident`` after ZMP negotiation."""
|
|
213
|
+
if self._zmp_ident_sent:
|
|
214
|
+
return
|
|
215
|
+
self._zmp_ident_sent = True
|
|
216
|
+
from telnetlib3.accessories import get_version
|
|
217
|
+
|
|
218
|
+
self.writer.send_zmp("zmp.ident", "telnetlib3", get_version())
|
|
219
|
+
self.log.info("ZMP handshake: zmp.ident telnetlib3 %s", get_version())
|
|
220
|
+
for cmd in sorted(self._zmp_supported_commands):
|
|
221
|
+
self.writer.send_zmp("zmp.support", cmd)
|
|
202
222
|
|
|
203
223
|
def on_gmcp(self, package: str, data: Any) -> None:
|
|
204
224
|
"""Store incoming GMCP data on ``writer.ctx``, merging dict updates."""
|
|
@@ -209,6 +229,39 @@ class TelnetClient(client_base.BaseClient):
|
|
|
209
229
|
gmcp[package] = data
|
|
210
230
|
self.log.debug("GMCP: %s %r", package, data)
|
|
211
231
|
|
|
232
|
+
def on_zmp(self, command: str, *args: str) -> None:
|
|
233
|
+
"""
|
|
234
|
+
Receive and dispatch a ZMP message.
|
|
235
|
+
|
|
236
|
+
Auto-responds to ``zmp.check`` and ``zmp.send-support``.
|
|
237
|
+
Stores latest value for each command in ``writer.ctx.zmp_data``.
|
|
238
|
+
"""
|
|
239
|
+
self.log.debug("ZMP: %s %r", command, args)
|
|
240
|
+
self.writer.ctx.zmp_data[command] = list(args)
|
|
241
|
+
if command == "zmp.check" and args:
|
|
242
|
+
cmd = args[0]
|
|
243
|
+
self._respond_zmp_support(cmd)
|
|
244
|
+
elif command == "zmp.send-support":
|
|
245
|
+
if args:
|
|
246
|
+
for cmd in args:
|
|
247
|
+
self._respond_zmp_support(cmd)
|
|
248
|
+
elif not self._zmp_ident_sent:
|
|
249
|
+
for cmd in sorted(self._zmp_supported_commands):
|
|
250
|
+
self._respond_zmp_support(cmd)
|
|
251
|
+
|
|
252
|
+
def _respond_zmp_support(self, cmd: str) -> None:
|
|
253
|
+
"""Send ``zmp.support`` or ``zmp.no-support`` for *cmd*."""
|
|
254
|
+
if self.zmp_check(cmd):
|
|
255
|
+
self.writer.send_zmp("zmp.support", cmd)
|
|
256
|
+
else:
|
|
257
|
+
self.writer.send_zmp("zmp.no-support", cmd)
|
|
258
|
+
|
|
259
|
+
def zmp_check(self, cmd: str) -> bool:
|
|
260
|
+
"""Return True if the client supports the given ZMP command."""
|
|
261
|
+
if self._zmp_check_handler is not None:
|
|
262
|
+
return self._zmp_check_handler(cmd)
|
|
263
|
+
return False
|
|
264
|
+
|
|
212
265
|
def send_ttype(self) -> str:
|
|
213
266
|
"""Callback for responding to TTYPE requests."""
|
|
214
267
|
result: str = self._extra["term"]
|
|
@@ -688,7 +741,7 @@ async def run_client() -> None:
|
|
|
688
741
|
client.writer.always_dont = always_dont
|
|
689
742
|
from .telopt import GMCP as _GMCP
|
|
690
743
|
|
|
691
|
-
client.writer.passive_do
|
|
744
|
+
client.writer.passive_do.add(_GMCP)
|
|
692
745
|
client.writer.environ_encoding = environ_encoding
|
|
693
746
|
client.writer._encoding_explicit = encoding_explicit
|
|
694
747
|
|
|
@@ -226,8 +226,6 @@ class BaseClient(TelnetProtocolBase, asyncio.streams.FlowControlMixin, asyncio.P
|
|
|
226
226
|
Buffer incoming data and schedule async processing to keep the event loop responsive. Apply
|
|
227
227
|
read-side backpressure using transport.pause_reading()/resume_reading().
|
|
228
228
|
"""
|
|
229
|
-
if self.log.isEnabledFor(TRACE):
|
|
230
|
-
self.log.log(TRACE, "recv %d bytes\n%s", len(data), hexdump(data, prefix="<< "))
|
|
231
229
|
self._last_received = datetime.datetime.now()
|
|
232
230
|
|
|
233
231
|
# Detect SyncTERM font switching sequences and auto-switch encoding.
|
|
@@ -354,6 +352,19 @@ class BaseClient(TelnetProtocolBase, asyncio.streams.FlowControlMixin, asyncio.P
|
|
|
354
352
|
|
|
355
353
|
# private methods
|
|
356
354
|
|
|
355
|
+
def _trace_recv(self, data: bytes, decompressed: bool = False) -> None:
|
|
356
|
+
"""
|
|
357
|
+
Log received bytes at TRACE level.
|
|
358
|
+
|
|
359
|
+
:param data: Bytes received from the transport.
|
|
360
|
+
:param decompressed: True when *data* is the MCCP2-decompressed telnet stream; the raw
|
|
361
|
+
compressed bytes are not logged.
|
|
362
|
+
"""
|
|
363
|
+
if not data or not self.log.isEnabledFor(TRACE):
|
|
364
|
+
return
|
|
365
|
+
label = " (decompressed)" if decompressed else ""
|
|
366
|
+
self.log.log(TRACE, "recv %d bytes%s\n%s", len(data), label, hexdump(data, prefix="<< "))
|
|
367
|
+
|
|
357
368
|
def _process_chunk(self, data: bytes) -> bool:
|
|
358
369
|
"""Process a chunk of received bytes; return True if any IAC/SB cmd observed."""
|
|
359
370
|
self._last_received = datetime.datetime.now()
|
|
@@ -380,14 +391,15 @@ class BaseClient(TelnetProtocolBase, asyncio.streams.FlowControlMixin, asyncio.P
|
|
|
380
391
|
if self._mccp2_decompressor.eof:
|
|
381
392
|
unused = self._mccp2_decompressor.unused_data
|
|
382
393
|
self._mccp2_end()
|
|
383
|
-
cmd = self._process_chunk_inner(data)
|
|
394
|
+
cmd = self._process_chunk_inner(data, decompressed=True)
|
|
384
395
|
if unused:
|
|
385
396
|
cmd = self._process_chunk(unused) or cmd
|
|
386
397
|
return cmd
|
|
398
|
+
return self._process_chunk_inner(data, decompressed=True)
|
|
387
399
|
|
|
388
400
|
return self._process_chunk_inner(data)
|
|
389
401
|
|
|
390
|
-
def _process_chunk_inner(self, data: bytes) -> bool:
|
|
402
|
+
def _process_chunk_inner(self, data: bytes, decompressed: bool = False) -> bool:
|
|
391
403
|
"""Inner chunk processing with IAC interpretation and mid-chunk MCCP2 detection."""
|
|
392
404
|
try:
|
|
393
405
|
mode = self.writer.mode
|
|
@@ -408,9 +420,17 @@ class BaseClient(TelnetProtocolBase, asyncio.streams.FlowControlMixin, asyncio.P
|
|
|
408
420
|
if self.writer._compressed_remainder is not None:
|
|
409
421
|
remainder = self.writer._compressed_remainder
|
|
410
422
|
self.writer._compressed_remainder = None
|
|
423
|
+
if not decompressed:
|
|
424
|
+
# The chunk activating MCCP2 mixes plain telnet and compressed
|
|
425
|
+
# bytes; trace only the plain prefix here, the compressed
|
|
426
|
+
# remainder is traced after decompression by the recursive
|
|
427
|
+
# _process_chunk() call.
|
|
428
|
+
self._trace_recv(data[: len(data) - len(remainder)])
|
|
411
429
|
self._mccp2_start()
|
|
412
430
|
if remainder:
|
|
413
431
|
cmd_received = self._process_chunk(remainder) or cmd_received
|
|
432
|
+
else:
|
|
433
|
+
self._trace_recv(data, decompressed=decompressed)
|
|
414
434
|
|
|
415
435
|
# MCCP3: start compressor when writer signals activation
|
|
416
436
|
if self.writer.mccp3_active and self._mccp3_compressor is None:
|
|
@@ -579,6 +579,8 @@ async def _raw_event_loop(
|
|
|
579
579
|
if raw_mode is None and want_repl():
|
|
580
580
|
state.reactivate_repl = True
|
|
581
581
|
stdout.write(out.encode())
|
|
582
|
+
if hasattr(stdout, "drain"):
|
|
583
|
+
await stdout.drain()
|
|
582
584
|
_ts_file = telnet_writer.ctx.typescript_file
|
|
583
585
|
if _ts_file is not None:
|
|
584
586
|
_ts_file.write(out)
|
|
@@ -40,6 +40,7 @@ __all__ = (
|
|
|
40
40
|
"mssp_encode",
|
|
41
41
|
"mssp_decode",
|
|
42
42
|
"MsdpParser",
|
|
43
|
+
"zmp_encode",
|
|
43
44
|
"zmp_decode",
|
|
44
45
|
"atcp_decode",
|
|
45
46
|
"aardwolf_decode",
|
|
@@ -276,6 +277,24 @@ def mssp_decode(buf: bytes, encoding: str = "utf-8") -> dict[str, str | list[str
|
|
|
276
277
|
return result
|
|
277
278
|
|
|
278
279
|
|
|
280
|
+
def zmp_encode(command: str, *args: str) -> bytes:
|
|
281
|
+
r"""
|
|
282
|
+
Encode a ZMP message.
|
|
283
|
+
|
|
284
|
+
:param command: ZMP command name (e.g., ``"zmp.ident"``).
|
|
285
|
+
:param args: Zero or more argument strings.
|
|
286
|
+
:returns: NUL-delimited payload bytes suitable for ZMP subnegotiation.
|
|
287
|
+
|
|
288
|
+
Example::
|
|
289
|
+
|
|
290
|
+
>>> zmp_encode("zmp.ident", "telnetlib3", "1.0")
|
|
291
|
+
b'zmp.ident\\x00telnetlib3\\x001.0\\x00'
|
|
292
|
+
"""
|
|
293
|
+
parts = [command.encode("utf-8")]
|
|
294
|
+
parts.extend(a.encode("utf-8") for a in args)
|
|
295
|
+
return b"\x00".join(parts) + b"\x00"
|
|
296
|
+
|
|
297
|
+
|
|
279
298
|
def zmp_decode(buf: bytes, encoding: str = "utf-8") -> list[str]:
|
|
280
299
|
"""
|
|
281
300
|
Decode ZMP payload to list of NUL-delimited strings.
|
|
@@ -188,14 +188,25 @@ class BaseServer(TelnetProtocolBase, asyncio.streams.FlowControlMixin, asyncio.P
|
|
|
188
188
|
loop = asyncio.get_event_loop()
|
|
189
189
|
loop.create_task(coro)
|
|
190
190
|
|
|
191
|
+
def _trace_recv(self, data: bytes, decompressed: bool = False) -> None:
|
|
192
|
+
"""
|
|
193
|
+
Log received bytes at TRACE level.
|
|
194
|
+
|
|
195
|
+
:param data: Bytes received from the transport.
|
|
196
|
+
:param decompressed: True when *data* is the MCCP3-decompressed telnet stream; the raw
|
|
197
|
+
compressed bytes are not logged.
|
|
198
|
+
"""
|
|
199
|
+
if not data or not logger.isEnabledFor(TRACE):
|
|
200
|
+
return
|
|
201
|
+
label = " (decompressed)" if decompressed else ""
|
|
202
|
+
logger.log(TRACE, "recv %d bytes%s\n%s", len(data), label, hexdump(data, prefix="<< "))
|
|
203
|
+
|
|
191
204
|
def data_received(self, data: bytes) -> None:
|
|
192
205
|
"""
|
|
193
206
|
Process bytes received by transport.
|
|
194
207
|
|
|
195
208
|
Feeds raw bytes through the writer's IAC interpreter, forwarding in-band data to the reader.
|
|
196
209
|
"""
|
|
197
|
-
if logger.isEnabledFor(TRACE):
|
|
198
|
-
logger.log(TRACE, "recv %d bytes\n%s", len(data), hexdump(data, prefix="<< "))
|
|
199
210
|
self._last_received = datetime.datetime.now()
|
|
200
211
|
self._rx_bytes += len(data)
|
|
201
212
|
|
|
@@ -207,12 +218,15 @@ class BaseServer(TelnetProtocolBase, asyncio.streams.FlowControlMixin, asyncio.P
|
|
|
207
218
|
logger.warning("MCCP3 decompression error, disabling")
|
|
208
219
|
self._mccp3_end()
|
|
209
220
|
return
|
|
221
|
+
self._trace_recv(data, decompressed=True)
|
|
210
222
|
if self._mccp3_decompressor.eof:
|
|
211
223
|
unused = self._mccp3_decompressor.unused_data
|
|
212
224
|
self._mccp3_end()
|
|
213
225
|
if unused:
|
|
214
226
|
self.data_received(unused)
|
|
215
227
|
return
|
|
228
|
+
else:
|
|
229
|
+
self._trace_recv(data)
|
|
216
230
|
|
|
217
231
|
if self.writer.slc_simulated:
|
|
218
232
|
slc_vals = {defn.val[0] for defn in self.writer.slctab.values() if defn.val != theNULL}
|
|
@@ -513,6 +513,13 @@ async def _fingerprint_session(
|
|
|
513
513
|
start_time = time.time()
|
|
514
514
|
cursor = _VirtualCursor(encoding=writer.environ_encoding)
|
|
515
515
|
|
|
516
|
+
# Accept all MUD protocol WILL requests so we can collect
|
|
517
|
+
# subnegotiation data (ATCP, AARDWOLF, MXP, MSP, etc.).
|
|
518
|
+
from telnetlib3.telopt import MSP, MXP, ATCP, MSDP, MSSP, AARDWOLF
|
|
519
|
+
|
|
520
|
+
for opt in (ATCP, AARDWOLF, MSP, MXP, MSDP, MSSP):
|
|
521
|
+
writer.passive_do.add(opt)
|
|
522
|
+
|
|
516
523
|
# 1. Let straggler negotiation settle -- read (and respond to DSR)
|
|
517
524
|
# instead of sleeping blind so early DSR requests get a CPR reply.
|
|
518
525
|
settle_data = await _read_banner_until_quiet(
|
|
@@ -629,8 +636,8 @@ async def _fingerprint_session(
|
|
|
629
636
|
"dsr_replies": cursor.dsr_replies,
|
|
630
637
|
}
|
|
631
638
|
)
|
|
632
|
-
if writer.mssp_data is not None:
|
|
633
|
-
session_data["mssp"] = writer.mssp_data
|
|
639
|
+
if writer.ctx.mssp_data is not None:
|
|
640
|
+
session_data["mssp"] = writer.ctx.mssp_data
|
|
634
641
|
session_data.update(_collect_mud_data(writer))
|
|
635
642
|
|
|
636
643
|
session_entry: dict[str, Any] = {
|
|
@@ -966,18 +973,18 @@ def _create_server_protocol_fingerprint(
|
|
|
966
973
|
|
|
967
974
|
|
|
968
975
|
def _collect_mud_data(writer: TelnetWriter) -> dict[str, Any]:
|
|
969
|
-
"""Collect MUD protocol data from *writer* into a dict."""
|
|
976
|
+
"""Collect MUD protocol data from *writer.ctx* into a dict."""
|
|
970
977
|
result: dict[str, Any] = {}
|
|
971
|
-
if writer.zmp_data:
|
|
972
|
-
result["zmp"] = writer.zmp_data
|
|
973
|
-
if writer.atcp_data:
|
|
974
|
-
result["atcp"] = [{"package": pkg, "value": val} for pkg, val in writer.atcp_data]
|
|
975
|
-
if writer.aardwolf_data:
|
|
976
|
-
result["aardwolf"] = writer.aardwolf_data
|
|
977
|
-
if writer.mxp_data:
|
|
978
|
-
result["mxp"] = [d.hex() if d else "activated" for d in writer.mxp_data]
|
|
979
|
-
if writer.comport_data:
|
|
980
|
-
result["comport"] = writer.comport_data
|
|
978
|
+
if writer.ctx.zmp_data:
|
|
979
|
+
result["zmp"] = writer.ctx.zmp_data
|
|
980
|
+
if writer.ctx.atcp_data:
|
|
981
|
+
result["atcp"] = [{"package": pkg, "value": val} for pkg, val in writer.ctx.atcp_data]
|
|
982
|
+
if writer.ctx.aardwolf_data:
|
|
983
|
+
result["aardwolf"] = writer.ctx.aardwolf_data
|
|
984
|
+
if writer.ctx.mxp_data:
|
|
985
|
+
result["mxp"] = [d.hex() if d else "activated" for d in writer.ctx.mxp_data]
|
|
986
|
+
if writer.ctx.comport_data:
|
|
987
|
+
result["comport"] = writer.ctx.comport_data
|
|
981
988
|
return result
|
|
982
989
|
|
|
983
990
|
|
|
@@ -1099,10 +1106,10 @@ def _format_banner(data: bytes, encoding: str = "utf-8") -> str:
|
|
|
1099
1106
|
|
|
1100
1107
|
async def _await_mssp_data(writer: TelnetWriter, deadline: float) -> None:
|
|
1101
1108
|
"""Wait for MSSP data until *deadline* if server acknowledged MSSP."""
|
|
1102
|
-
if not writer.remote_option.enabled(MSSP) or writer.mssp_data is not None:
|
|
1109
|
+
if not writer.remote_option.enabled(MSSP) or writer.ctx.mssp_data is not None:
|
|
1103
1110
|
return
|
|
1104
1111
|
remaining = deadline - time.time()
|
|
1105
|
-
while remaining > 0 and writer.mssp_data is None:
|
|
1112
|
+
while remaining > 0 and writer.ctx.mssp_data is None:
|
|
1106
1113
|
await asyncio.sleep(min(0.05, remaining))
|
|
1107
1114
|
remaining = deadline - time.time()
|
|
1108
1115
|
|