telnetlib3 3.0.0__tar.gz → 3.0.2__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.
Files changed (119) hide show
  1. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/PKG-INFO +50 -6
  2. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/README.rst +49 -5
  3. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/conf.py +5 -3
  4. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/history.rst +14 -0
  5. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/rfcs.rst +8 -1
  6. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/pyproject.toml +1 -1
  7. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/_base.py +9 -0
  8. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/accessories.py +1 -1
  9. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/client.py +54 -17
  10. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/client_base.py +91 -1
  11. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/fingerprinting.py +4 -0
  12. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/server.py +87 -5
  13. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/server_base.py +32 -0
  14. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/stream_writer.py +111 -5
  15. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/telopt.py +3 -1
  16. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_charset.py +69 -5
  17. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_client_unit.py +3 -3
  18. telnetlib3-3.0.2/telnetlib3/tests/test_mccp.py +683 -0
  19. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_stream_writer_full.py +17 -5
  20. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/.gitignore +0 -0
  21. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/CONTRIBUTING.rst +0 -0
  22. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/LICENSE.txt +0 -0
  23. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/Makefile +0 -0
  24. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/api/accessories.rst +0 -0
  25. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/api/client.rst +0 -0
  26. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/api/client_base.rst +0 -0
  27. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/api/client_shell.rst +0 -0
  28. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/api/color_filter.rst +0 -0
  29. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/api/fingerprinting.rst +0 -0
  30. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/api/guard_shells.rst +0 -0
  31. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/api/mud.rst +0 -0
  32. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/api/server.rst +0 -0
  33. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/api/server_base.rst +0 -0
  34. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/api/server_pty_shell.rst +0 -0
  35. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/api/server_shell.rst +0 -0
  36. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/api/session_context.rst +0 -0
  37. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/api/slc.rst +0 -0
  38. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/api/stream_reader.rst +0 -0
  39. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/api/stream_writer.rst +0 -0
  40. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/api/sync.rst +0 -0
  41. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/api/telnetlib.rst +0 -0
  42. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/api/telopt.rst +0 -0
  43. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/api.rst +0 -0
  44. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/contributing.rst +0 -0
  45. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/guidebook.rst +0 -0
  46. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/index.rst +0 -0
  47. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/intro.rst +0 -0
  48. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/make.bat +0 -0
  49. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/docs/sphinxext/github.py +0 -0
  50. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/requirements-analysis.txt +0 -0
  51. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/requirements-docs.txt +0 -0
  52. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/requirements-tests.txt +0 -0
  53. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/requirements.txt +0 -0
  54. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/.gitignore +0 -0
  55. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/__init__.py +0 -0
  56. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/_paths.py +0 -0
  57. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/_session_context.py +0 -0
  58. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/_types.py +0 -0
  59. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/client_shell.py +0 -0
  60. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/color_filter.py +0 -0
  61. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/encodings/__init__.py +0 -0
  62. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/encodings/atarist.py +0 -0
  63. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/encodings/atascii.py +0 -0
  64. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/encodings/petscii.py +0 -0
  65. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/fingerprinting_display.py +0 -0
  66. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/guard_shells.py +0 -0
  67. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/mud.py +0 -0
  68. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/py.typed +0 -0
  69. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/relay_server.py +0 -0
  70. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/server_fingerprinting.py +0 -0
  71. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/server_pty_shell.py +0 -0
  72. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/server_shell.py +0 -0
  73. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/slc.py +0 -0
  74. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/stream_reader.py +0 -0
  75. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/sync.py +0 -0
  76. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/telnetlib.py +0 -0
  77. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/accessories.py +0 -0
  78. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/conftest.py +0 -0
  79. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/pty_helper.py +0 -0
  80. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_accessories.py +0 -0
  81. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_accessories_extra.py +0 -0
  82. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_atascii_codec.py +0 -0
  83. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_benchmarks.py +0 -0
  84. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_client_shell.py +0 -0
  85. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_color_filter.py +0 -0
  86. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_core.py +0 -0
  87. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_encoding.py +0 -0
  88. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_environ.py +0 -0
  89. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_fingerprinting.py +0 -0
  90. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_guard_integration.py +0 -0
  91. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_linemode.py +0 -0
  92. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_mud.py +0 -0
  93. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_mud_negotiation.py +0 -0
  94. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_naws.py +0 -0
  95. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_petscii_codec.py +0 -0
  96. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_platform.py +0 -0
  97. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_pty_shell.py +0 -0
  98. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_reader.py +0 -0
  99. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_relay_server.py +0 -0
  100. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_server.py +0 -0
  101. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_server_api.py +0 -0
  102. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_server_cli.py +0 -0
  103. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_server_fingerprinting.py +0 -0
  104. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_server_mud.py +0 -0
  105. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_server_shell_unit.py +0 -0
  106. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_shell.py +0 -0
  107. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_slc.py +0 -0
  108. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_status_logger.py +0 -0
  109. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_stream_reader_extra.py +0 -0
  110. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_sync.py +0 -0
  111. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_telnetlib.py +0 -0
  112. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_timeout.py +0 -0
  113. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_tls.py +0 -0
  114. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_tspeed.py +0 -0
  115. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_ttype.py +0 -0
  116. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_uvloop_integration.py +0 -0
  117. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_writer.py +0 -0
  118. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/telnetlib3/tests/test_xdisploc.py +0 -0
  119. {telnetlib3-3.0.0 → telnetlib3-3.0.2}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: telnetlib3
3
- Version: 3.0.0
3
+ Version: 3.0.2
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
@@ -83,8 +83,8 @@ The CLI utility ``telnetlib3-client`` is provided for connecting to servers and
83
83
  ``telnetlib3-server`` for hosting a server.
84
84
 
85
85
  Both tools accept the argument ``--shell=my_module.fn_shell`` describing a python module path to a
86
- function of signature ``async def shell(reader, writer)``. The server also provides ``--pty-exec``
87
- argument to host stand-alone programs.
86
+ function of signature ``async def shell(reader, writer)``. The server also provides a
87
+ ``--pty-exec`` argument allowing it to act as a telnet server for any CLI/TUI programs.
88
88
 
89
89
  ::
90
90
 
@@ -97,7 +97,7 @@ argument to host stand-alone programs.
97
97
  # automatic script communicates with a server
98
98
  telnetlib3-client --shell bin.client_wargame.shell 1984.ws 666
99
99
 
100
- # run a server bound with the default shell bound to 127.0.0.1 6023
100
+ # run a default shell server bound to 127.0.0.1 6023
101
101
  telnetlib3-server
102
102
 
103
103
  # or custom ip, port and shell
@@ -150,6 +150,48 @@ connected to a TCP socket without any telnet negotiation may require "raw" mode
150
150
 
151
151
  telnetlib3-client --raw-mode area52.tk 5200 --encoding=atascii
152
152
 
153
+ Go-Ahead (GA)
154
+ ~~~~~~~~~~~~~
155
+
156
+ When a client does not negotiate Suppress Go-Ahead (SGA), the server sends
157
+ ``IAC GA`` after output to signal that the client may transmit. This is
158
+ correct behavior for MUD clients like Mudlet that expect prompt detection
159
+ via GA.
160
+
161
+ If GA causes unwanted output for your use case, disable it::
162
+
163
+ telnetlib3-server --never-send-ga
164
+
165
+ For PTY shells, GA is sent after 500ms of output idle time to avoid
166
+ injecting GA in the middle of streaming output.
167
+
168
+ Compression (MCCP)
169
+ ~~~~~~~~~~~~~~~~~~
170
+
171
+ MCCP2 (server-to-client) and MCCP3 (client-to-server) zlib compression are
172
+ supported, widely used by MUD servers to reduce bandwidth::
173
+
174
+ # connect to a MUD that offers MCCP compression
175
+ telnetlib3-client dunemud.net 6789
176
+
177
+ # or with TLS (compression auto-disabled over TLS, CRIME/BREACH mitigation)
178
+ telnetlib3-client --ssl dunemud.net 6788
179
+
180
+ # actively request compression from a server
181
+ telnetlib3-client --compression dunemud.net 6789
182
+
183
+ # reject compression even if the server offers it
184
+ telnetlib3-client --no-compression dunemud.net 6789
185
+
186
+ # host a MUD server that advertises MCCP2/MCCP3
187
+ telnetlib3-server --compression --shell=my_mud.shell
188
+
189
+ By default (without ``--compression`` or ``--no-compression``), the client
190
+ passively accepts compression when offered by the server, and the server does
191
+ not advertise compression. Compression is automatically disabled over TLS
192
+ connections to avoid CRIME/BREACH attacks.
193
+
194
+
153
195
  Asyncio Protocol
154
196
  ----------------
155
197
 
@@ -177,8 +219,10 @@ To migrate code, change import statements:
177
219
  # NEW imports:
178
220
  import telnetlib3
179
221
 
180
- This library *also* provides an additional client (and server) API through a similar interface but
181
- offering more advanced negotiation features and options. See `sync API documentation`_ for more.
222
+ ``telnetlib3`` did not provide server support, while this library also provides
223
+ both client and server support through a similar Blocking API interface.
224
+
225
+ See `sync API documentation`_ for details.
182
226
 
183
227
  Quick Example
184
228
  =============
@@ -45,8 +45,8 @@ The CLI utility ``telnetlib3-client`` is provided for connecting to servers and
45
45
  ``telnetlib3-server`` for hosting a server.
46
46
 
47
47
  Both tools accept the argument ``--shell=my_module.fn_shell`` describing a python module path to a
48
- function of signature ``async def shell(reader, writer)``. The server also provides ``--pty-exec``
49
- argument to host stand-alone programs.
48
+ function of signature ``async def shell(reader, writer)``. The server also provides a
49
+ ``--pty-exec`` argument allowing it to act as a telnet server for any CLI/TUI programs.
50
50
 
51
51
  ::
52
52
 
@@ -59,7 +59,7 @@ argument to host stand-alone programs.
59
59
  # automatic script communicates with a server
60
60
  telnetlib3-client --shell bin.client_wargame.shell 1984.ws 666
61
61
 
62
- # run a server bound with the default shell bound to 127.0.0.1 6023
62
+ # run a default shell server bound to 127.0.0.1 6023
63
63
  telnetlib3-server
64
64
 
65
65
  # or custom ip, port and shell
@@ -112,6 +112,48 @@ connected to a TCP socket without any telnet negotiation may require "raw" mode
112
112
 
113
113
  telnetlib3-client --raw-mode area52.tk 5200 --encoding=atascii
114
114
 
115
+ Go-Ahead (GA)
116
+ ~~~~~~~~~~~~~
117
+
118
+ When a client does not negotiate Suppress Go-Ahead (SGA), the server sends
119
+ ``IAC GA`` after output to signal that the client may transmit. This is
120
+ correct behavior for MUD clients like Mudlet that expect prompt detection
121
+ via GA.
122
+
123
+ If GA causes unwanted output for your use case, disable it::
124
+
125
+ telnetlib3-server --never-send-ga
126
+
127
+ For PTY shells, GA is sent after 500ms of output idle time to avoid
128
+ injecting GA in the middle of streaming output.
129
+
130
+ Compression (MCCP)
131
+ ~~~~~~~~~~~~~~~~~~
132
+
133
+ MCCP2 (server-to-client) and MCCP3 (client-to-server) zlib compression are
134
+ supported, widely used by MUD servers to reduce bandwidth::
135
+
136
+ # connect to a MUD that offers MCCP compression
137
+ telnetlib3-client dunemud.net 6789
138
+
139
+ # or with TLS (compression auto-disabled over TLS, CRIME/BREACH mitigation)
140
+ telnetlib3-client --ssl dunemud.net 6788
141
+
142
+ # actively request compression from a server
143
+ telnetlib3-client --compression dunemud.net 6789
144
+
145
+ # reject compression even if the server offers it
146
+ telnetlib3-client --no-compression dunemud.net 6789
147
+
148
+ # host a MUD server that advertises MCCP2/MCCP3
149
+ telnetlib3-server --compression --shell=my_mud.shell
150
+
151
+ By default (without ``--compression`` or ``--no-compression``), the client
152
+ passively accepts compression when offered by the server, and the server does
153
+ not advertise compression. Compression is automatically disabled over TLS
154
+ connections to avoid CRIME/BREACH attacks.
155
+
156
+
115
157
  Asyncio Protocol
116
158
  ----------------
117
159
 
@@ -139,8 +181,10 @@ To migrate code, change import statements:
139
181
  # NEW imports:
140
182
  import telnetlib3
141
183
 
142
- This library *also* provides an additional client (and server) API through a similar interface but
143
- offering more advanced negotiation features and options. See `sync API documentation`_ for more.
184
+ ``telnetlib3`` did not provide server support, while this library also provides
185
+ both client and server support through a similar Blocking API interface.
186
+
187
+ See `sync API documentation`_ for details.
144
188
 
145
189
  Quick Example
146
190
  =============
@@ -22,7 +22,7 @@ sys.path.insert(
22
22
  0, os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))
23
23
  )
24
24
 
25
- suppress_warnings = ["image.nonlocal_uri"]
25
+ suppress_warnings = ["image.nonlocal_uri", "ref.class"]
26
26
 
27
27
  autodoc_default_flags = [
28
28
  "members",
@@ -68,10 +68,12 @@ copyright = f"2013-{datetime.datetime.now().year} Jeff Quast"
68
68
  # built documents.
69
69
  #
70
70
  # The short X.Y version.
71
- version = "3.0"
71
+ import telnetlib3.accessories
72
+
73
+ version = release = telnetlib3.accessories.get_version()
72
74
 
73
75
  # The full version, including alpha/beta/rc tags.
74
- release = "3.0.0" # keep in sync with pyproject.toml and telnetlib3/accessories.py !!
76
+ #release = "3.0.1" # keep in sync with pyproject.toml and telnetlib3/accessories.py !!
75
77
 
76
78
  # The language for content auto-generated by Sphinx. Refer to documentation
77
79
  # for a list of supported languages.
@@ -1,5 +1,19 @@
1
1
  History
2
2
  =======
3
+ 3.0.2
4
+ * bugfix: :meth:`~telnetlib3.stream_writer.TelnetWriter.request_charset` raised :exc:`TypeError`,
5
+ :ghissue:`128`. Offer callbacks (no-arg, returning a list of items to propose) are now
6
+ separated from send callbacks (which respond to received requests) via new
7
+ :meth:`~telnetlib3.stream_writer.TelnetWriter.set_ext_offer_callback` method.
8
+
9
+ 3.0.1
10
+ * change: Unused client argument ``gmcp_log`` removed.
11
+ * new: MCCP2 and MCCP3. Both client and server ends passively support if requested, and request
12
+ support by --compression or deny support by --no-compression.
13
+ * new: :meth:`~telnetlib3.client.TelnetClient.on_request_charset` and
14
+ :meth:`~telnetlib3.client.TelnetClient.on_request_environ` offer callbacks
15
+ on the client, symmetric with the existing server-side callbacks.
16
+
3
17
  3.0.0
4
18
  * change: :attr:`~telnetlib3.client_base.BaseClient.connect_minwait` default
5
19
  now 0 (was 1.0 seconds in library API).
@@ -81,16 +81,23 @@ Dungeon) servers and clients.
81
81
  * `MSSP`_ (MUD Server Status Protocol, option 70). Server metadata protocol
82
82
  for MUD crawlers and directories, providing server name, player count,
83
83
  codebase, and other listing information.
84
+ * `MCCP2`_ (MUD Client Compression Protocol v2, option 86). Server-to-client
85
+ zlib compression, reducing bandwidth for output-heavy sessions. Activated
86
+ via ``IAC SB MCCP2 IAC SE``; all subsequent server output is compressed.
87
+ * `MCCP3`_ (MUD Client Compression Protocol v3, option 87). Client-to-server
88
+ zlib compression, the reverse direction of MCCP2.
84
89
 
85
90
  .. _GMCP: https://www.gammon.com.au/gmcp
86
91
  .. _MSDP: https://tintin.mudhalla.net/protocols/msdp/
87
92
  .. _MSSP: https://tintin.mudhalla.net/protocols/mssp/
93
+ .. _MCCP2: https://tintin.mudhalla.net/protocols/mccp/
94
+ .. _MCCP3: https://tintin.mudhalla.net/protocols/mccp/
88
95
 
89
96
  MUDs Not Implemented
90
97
  --------------------
91
98
 
92
99
  Constants are also defined for the following MUD options, though their handlers
93
- are not implemented: MCCP/MCCP2 (85/86, compression), MXP (91, markup), ZMP
100
+ are not implemented: MCCP (85, legacy compression), MXP (91, markup), ZMP
94
101
  (93, messaging), MSP (90, sound), and ATCP (200, Achaea-specific).
95
102
 
96
103
  Additional Resources
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "telnetlib3"
7
- version = "3.0.0"
7
+ version = "3.0.2" # 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"
@@ -45,6 +45,9 @@ def _process_data_chunk(
45
45
  or ``None`` when only IAC (255) is special.
46
46
  :param log_fn: Callable for logging exceptions (e.g. ``logger.warning``).
47
47
  :returns: ``True`` if any IAC/SB command was observed.
48
+
49
+ When MCCP2 is activated mid-chunk, the remaining compressed bytes are
50
+ stored in ``writer._compressed_remainder`` for the caller to consume.
48
51
  """
49
52
  cmd_received = False
50
53
  n = len(data)
@@ -84,6 +87,12 @@ def _process_data_chunk(
84
87
  out_start = i
85
88
  feeding_oob = bool(writer.is_oob)
86
89
 
90
+ if writer._mccp2_activated:
91
+ writer._mccp2_activated = False
92
+ writer.mccp2_active = True
93
+ writer._compressed_remainder = data[i:] if i < n else b""
94
+ return True
95
+
87
96
  return cmd_received
88
97
 
89
98
 
@@ -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.0" # keep in sync with pyproject.toml and docs/conf.py !!
45
+ return "3.0.2" # keep in sync with pyproject.toml !
46
46
 
47
47
 
48
48
  def encoding_from_lang(lang: str) -> Optional[str]:
@@ -23,6 +23,8 @@ from telnetlib3.stream_writer import TelnetWriter, TelnetWriterUnicode
23
23
  __all__ = ("TelnetClient", "TelnetTerminalClient", "open_connection")
24
24
 
25
25
  #: Default GMCP modules requested via ``Core.Supports.Set``.
26
+ #: Sub-modules are listed explicitly because not all servers treat
27
+ #: top-level subscriptions as wildcards.
26
28
  _DEFAULT_GMCP_MODULES = [
27
29
  "Char 1",
28
30
  "Char.Vitals 1",
@@ -31,6 +33,7 @@ _DEFAULT_GMCP_MODULES = [
31
33
  "Room.Info 1",
32
34
  "Comm 1",
33
35
  "Comm.Channel 1",
36
+ "Group 1",
34
37
  ]
35
38
 
36
39
 
@@ -65,13 +68,14 @@ class TelnetClient(client_base.BaseClient):
65
68
  force_binary: bool = False,
66
69
  connect_minwait: float = 0,
67
70
  connect_maxwait: float = 4.0,
71
+ compression: Optional[bool] = None,
68
72
  limit: Optional[int] = None,
69
73
  waiter_closed: Optional[asyncio.Future[None]] = None,
70
74
  _waiter_connected: Optional[asyncio.Future[None]] = None,
71
75
  gmcp_modules: Optional[List[str]] = None,
72
- gmcp_log: bool = False,
73
76
  ) -> None:
74
77
  """Initialize TelnetClient with terminal parameters."""
78
+ self._compression = compression
75
79
  super().__init__(
76
80
  shell=shell,
77
81
  encoding=encoding,
@@ -84,7 +88,6 @@ class TelnetClient(client_base.BaseClient):
84
88
  _waiter_connected=_waiter_connected,
85
89
  )
86
90
  self._gmcp_modules = gmcp_modules or list(_DEFAULT_GMCP_MODULES)
87
- self._gmcp_log = gmcp_log
88
91
  self._gmcp_hello_sent = False
89
92
  self._send_environ = set(send_environ or self.DEFAULT_SEND_ENVIRON)
90
93
  self._extra.update(
@@ -118,6 +121,9 @@ class TelnetClient(client_base.BaseClient):
118
121
 
119
122
  super().connection_made(transport)
120
123
 
124
+ # Set compression policy on writer
125
+ self.writer.compression = self._compression
126
+
121
127
  # Wire extended rfc callbacks for requests of
122
128
  # terminal attributes, environment values, etc.
123
129
  for opt, func in (
@@ -130,6 +136,14 @@ class TelnetClient(client_base.BaseClient):
130
136
  ):
131
137
  self.writer.set_ext_send_callback(opt, func)
132
138
 
139
+ # Offer callbacks define what to include in outgoing requests
140
+ # (e.g. what charsets to offer in SB CHARSET REQUEST).
141
+ for opt, offer_func in (
142
+ (CHARSET, self.on_request_charset),
143
+ (NEW_ENVIRON, self.on_request_environ),
144
+ ):
145
+ self.writer.set_ext_offer_callback(opt, offer_func)
146
+
133
147
  # Override the default handle_will method to detect when both sides support CHARSET
134
148
  # Store the original only on first connection to prevent chain growth on reconnect.
135
149
  if not hasattr(self.writer, "_original_handle_will"):
@@ -161,7 +175,7 @@ class TelnetClient(client_base.BaseClient):
161
175
  """Wire GMCP callback and WILL-detection for Core.Hello handshake."""
162
176
  from telnetlib3.telopt import GMCP
163
177
 
164
- self.writer.set_ext_callback(GMCP, self._on_gmcp)
178
+ self.writer.set_ext_callback(GMCP, self.on_gmcp)
165
179
 
166
180
  # Capture current handle_will (already includes CHARSET wrapper).
167
181
  # On reconnect, _original_handle_will was already restored in connection_made,
@@ -186,17 +200,14 @@ class TelnetClient(client_base.BaseClient):
186
200
  self.writer.send_gmcp("Core.Supports.Set", self._gmcp_modules)
187
201
  self.log.info("GMCP handshake: Core.Hello + Core.Supports.Set %s", self._gmcp_modules)
188
202
 
189
- def _on_gmcp(self, package: str, data: Any) -> None:
203
+ def on_gmcp(self, package: str, data: Any) -> None:
190
204
  """Store incoming GMCP data on ``writer.ctx``, merging dict updates."""
191
205
  gmcp = self.writer.ctx.gmcp_data
192
206
  if isinstance(data, dict) and isinstance(gmcp.get(package), dict):
193
207
  gmcp[package].update(data)
194
208
  else:
195
209
  gmcp[package] = data
196
- if self._gmcp_log:
197
- self.log.info("GMCP: %s %r", package, data)
198
- else:
199
- self.log.debug("GMCP: %s %r", package, data)
210
+ self.log.debug("GMCP: %s %r", package, data)
200
211
 
201
212
  def send_ttype(self) -> str:
202
213
  """Callback for responding to TTYPE requests."""
@@ -369,6 +380,28 @@ class TelnetClient(client_base.BaseClient):
369
380
  self.log.warning("No suitable encoding offered by server: %s", offered)
370
381
  return ""
371
382
 
383
+ def on_request_charset(self) -> List[str]:
384
+ """
385
+ Offer callback for client-initiated CHARSET REQUEST, :rfc:`2066`.
386
+
387
+ Called by :meth:`~.TelnetWriter.request_charset` to determine which
388
+ character sets the client offers to the server.
389
+
390
+ :returns: List of charset name strings to offer.
391
+ """
392
+ return ["UTF-8", "LATIN1", "US-ASCII"]
393
+
394
+ def on_request_environ(self) -> List[str]:
395
+ """
396
+ Offer callback for client-initiated NEW_ENVIRON SEND, :rfc:`1572`.
397
+
398
+ Called by :meth:`~.TelnetWriter.request_environ` to determine which
399
+ environment variable names the client requests from the server.
400
+
401
+ :returns: List of environment variable names to request.
402
+ """
403
+ return []
404
+
372
405
  def send_naws(self) -> Tuple[int, int]:
373
406
  """
374
407
  Callback for responding to NAWS requests.
@@ -472,6 +505,7 @@ async def open_connection(
472
505
  connect_minwait: float = 0,
473
506
  connect_maxwait: float = 3.0,
474
507
  connect_timeout: Optional[float] = None,
508
+ compression: Optional[bool] = None,
475
509
  waiter_closed: Optional[asyncio.Future[None]] = None,
476
510
  _waiter_connected: Optional[asyncio.Future[None]] = None,
477
511
  limit: Optional[int] = None,
@@ -531,6 +565,9 @@ async def open_connection(
531
565
  connection attempt may block indefinitely. When specified, a
532
566
  :exc:`ConnectionError` is raised if the connection is not established
533
567
  within the given time.
568
+ :param compression: MCCP compression policy. ``None`` (default) passively
569
+ accepts compression when offered by the server. ``True`` actively
570
+ requests MCCP2/MCCP3. ``False`` rejects all compression offers.
534
571
 
535
572
  :param force_binary: When ``True``, the encoding is used regardless
536
573
  of BINARY mode negotiation.
@@ -565,6 +602,7 @@ async def open_connection(
565
602
  shell=shell,
566
603
  connect_minwait=connect_minwait,
567
604
  connect_maxwait=connect_maxwait,
605
+ compression=compression,
568
606
  waiter_closed=waiter_closed,
569
607
  _waiter_connected=_waiter_connected,
570
608
  limit=limit,
@@ -618,12 +656,10 @@ async def run_client() -> None:
618
656
  # flags before negotiation starts.
619
657
  encoding_explicit = args["encoding"] not in ("utf8", "utf-8", False)
620
658
  gmcp_modules: Optional[List[str]] = args.get("gmcp_modules")
621
- gmcp_log: bool = args.get("gmcp_log", False)
622
659
 
623
660
  def _client_factory(**kwargs: Any) -> client_base.BaseClient:
624
661
  client: TelnetClient
625
662
  kwargs["gmcp_modules"] = gmcp_modules
626
- kwargs["gmcp_log"] = gmcp_log
627
663
  if sys.platform != "win32" and sys.stdin.isatty():
628
664
  client = TelnetTerminalClient(**kwargs)
629
665
  else:
@@ -897,6 +933,13 @@ def _get_argument_parser() -> argparse.ArgumentParser:
897
933
  "keys instead of encoding-specific control codes. Use for "
898
934
  "BBSes that expect ANSI cursor sequences.",
899
935
  )
936
+ parser.add_argument(
937
+ "--compression",
938
+ action=argparse.BooleanOptionalAction,
939
+ default=None,
940
+ help="MCCP compression: --compression to request, --no-compression to reject, "
941
+ "omit to passively accept (default)",
942
+ )
900
943
  parser.add_argument(
901
944
  "--ssl", action="store_true", default=False, help="connect using TLS (TELNETS)"
902
945
  )
@@ -923,12 +966,6 @@ def _get_argument_parser() -> argparse.ArgumentParser:
923
966
  '(e.g. "Char 1,Room 1,IRE.Rift 1"). '
924
967
  "When provided, replaces the built-in defaults.",
925
968
  )
926
- parser.add_argument(
927
- "--gmcp-log",
928
- action="store_true",
929
- default=False,
930
- help="log all incoming GMCP messages at INFO level " "(default: DEBUG only)",
931
- )
932
969
  parser.add_argument(
933
970
  "--typescript",
934
971
  default=None,
@@ -1029,7 +1066,7 @@ def _transform_args(args: argparse.Namespace) -> Dict[str, Any]:
1029
1066
  if args.gmcp_modules
1030
1067
  else None
1031
1068
  ),
1032
- "gmcp_log": args.gmcp_log,
1069
+ "compression": args.compression,
1033
1070
  "typescript": args.typescript,
1034
1071
  }
1035
1072
 
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  # std imports
6
+ import zlib
6
7
  import asyncio
7
8
  import logging
8
9
  import weakref
@@ -66,6 +67,12 @@ class BaseClient(TelnetProtocolBase, asyncio.streams.FlowControlMixin, asyncio.P
66
67
  self.writer: Optional[Union[TelnetWriter, TelnetWriterUnicode]] = None
67
68
  self._limit = limit
68
69
 
70
+ # MCCP2: server→client decompression
71
+ self._mccp2_decompressor: Optional[zlib._Decompress] = None
72
+ # MCCP3: client→server compression
73
+ self._mccp3_compressor: Optional[zlib._Compress] = None
74
+ self._mccp3_orig_write: Any = None
75
+
69
76
  # High-throughput receive pipeline
70
77
  self._rx_queue: collections.deque[bytes] = collections.deque()
71
78
  self._rx_bytes = 0
@@ -93,6 +100,11 @@ class BaseClient(TelnetProtocolBase, asyncio.streams.FlowControlMixin, asyncio.P
93
100
  return
94
101
  self._closing = True
95
102
 
103
+ # Clean up MCCP compressors/decompressors
104
+ self._mccp2_decompressor = None
105
+ self._mccp3_compressor = None
106
+ self._mccp3_orig_write = None
107
+
96
108
  # Drain any pending rx data before signalling EOF to prevent
97
109
  # _process_rx from calling feed_data() after feed_eof().
98
110
  self._rx_queue.clear()
@@ -343,6 +355,26 @@ class BaseClient(TelnetProtocolBase, asyncio.streams.FlowControlMixin, asyncio.P
343
355
  """Process a chunk of received bytes; return True if any IAC/SB cmd observed."""
344
356
  self._last_received = datetime.datetime.now()
345
357
 
358
+ # MCCP2: decompress server→client data when active
359
+ if self._mccp2_decompressor is not None:
360
+ try:
361
+ data = self._mccp2_decompressor.decompress(data)
362
+ except zlib.error:
363
+ self.log.warning("MCCP2 decompression error, disabling")
364
+ self._mccp2_end()
365
+ return False
366
+ if self._mccp2_decompressor.eof:
367
+ unused = self._mccp2_decompressor.unused_data
368
+ self._mccp2_end()
369
+ cmd = self._process_chunk_inner(data)
370
+ if unused:
371
+ cmd = self._process_chunk(unused) or cmd
372
+ return cmd
373
+
374
+ return self._process_chunk_inner(data)
375
+
376
+ def _process_chunk_inner(self, data: bytes) -> bool:
377
+ """Inner chunk processing with IAC interpretation and mid-chunk MCCP2 detection."""
346
378
  try:
347
379
  mode = self.writer.mode
348
380
  except Exception:
@@ -355,7 +387,22 @@ class BaseClient(TelnetProtocolBase, asyncio.streams.FlowControlMixin, asyncio.P
355
387
  else:
356
388
  slc_special = None
357
389
 
358
- return _process_data_chunk(data, self.writer, self.reader, slc_special, self.log.warning)
390
+ cmd_received = _process_data_chunk(
391
+ data, self.writer, self.reader, slc_special, self.log.warning
392
+ )
393
+
394
+ if self.writer._compressed_remainder is not None:
395
+ remainder = self.writer._compressed_remainder
396
+ self.writer._compressed_remainder = None
397
+ self._mccp2_start()
398
+ if remainder:
399
+ cmd_received = self._process_chunk(remainder) or cmd_received
400
+
401
+ # MCCP3: start compressor when writer signals activation
402
+ if self.writer.mccp3_active and self._mccp3_compressor is None:
403
+ self._mccp3_start()
404
+
405
+ return cmd_received
359
406
 
360
407
  async def _process_rx(self) -> None:
361
408
  """Async processor for receive queue that yields control and applies backpressure."""
@@ -395,6 +442,49 @@ class BaseClient(TelnetProtocolBase, asyncio.streams.FlowControlMixin, asyncio.P
395
442
  if any_cmd and not self._waiter_connected.done():
396
443
  self._check_negotiation_timer()
397
444
 
445
+ def _mccp2_start(self) -> None:
446
+ """Start MCCP2 decompression of server→client data."""
447
+ self._mccp2_decompressor = zlib.decompressobj()
448
+ self.log.debug("MCCP2 decompression started (server→client)")
449
+
450
+ def _mccp2_end(self) -> None:
451
+ """Stop MCCP2 decompression."""
452
+ self._mccp2_decompressor = None
453
+ self.writer.mccp2_active = False
454
+ self.log.debug("MCCP2 decompression ended (server→client)")
455
+
456
+ def _mccp3_start(self) -> None:
457
+ """Start MCCP3 compression of client→server data."""
458
+ self._mccp3_compressor = zlib.compressobj(
459
+ zlib.Z_BEST_COMPRESSION, zlib.DEFLATED, 12, 5, zlib.Z_DEFAULT_STRATEGY
460
+ )
461
+ # Wrap transport.write so all outbound bytes are compressed
462
+ transport = self.writer._transport
463
+ orig_write = transport.write
464
+
465
+ def compressed_write(data: bytes) -> None:
466
+ if self._mccp3_compressor is not None:
467
+ compressed = self._mccp3_compressor.compress(data)
468
+ compressed += self._mccp3_compressor.flush(zlib.Z_SYNC_FLUSH)
469
+ orig_write(compressed)
470
+ else:
471
+ orig_write(data)
472
+
473
+ transport.write = compressed_write # type: ignore[method-assign]
474
+ self._mccp3_orig_write = orig_write
475
+ self.log.debug("MCCP3 compression started (client→server)")
476
+
477
+ def _mccp3_end(self) -> None:
478
+ """Stop MCCP3 compression, flush Z_FINISH."""
479
+ if self._mccp3_compressor is not None:
480
+ if not self.writer.is_closing():
481
+ self._mccp3_orig_write(self._mccp3_compressor.flush(zlib.Z_FINISH))
482
+ self._mccp3_compressor = None
483
+ # Restore original transport.write
484
+ self.writer._transport.write = self._mccp3_orig_write # type: ignore[method-assign]
485
+ self.writer.mccp3_active = False
486
+ self.log.debug("MCCP3 compression ended (client→server)")
487
+
398
488
  def _check_negotiation_timer(self) -> None:
399
489
  self._check_later.cancel()
400
490
  self._tasks.remove(self._check_later)
@@ -83,6 +83,8 @@ from .telopt import (
83
83
  SUPDUPOUTPUT,
84
84
  VT3270REGIME,
85
85
  AUTHENTICATION,
86
+ MCCP2_COMPRESS,
87
+ MCCP3_COMPRESS,
86
88
  COM_PORT_OPTION,
87
89
  PRAGMA_HEARTBEAT,
88
90
  SUPPRESS_LOCAL_ECHO,
@@ -260,6 +262,8 @@ MUD_OPTIONS = [(COM_PORT_OPTION, "COM_PORT", "Serial port control (RFC 2217)")]
260
262
  # returning a hard error for anything else. GMCP-capable MUD clients
261
263
  # typically self-announce via IAC WILL GMCP, so probing is unnecessary.
262
264
  EXTENDED_OPTIONS = [
265
+ (MCCP2_COMPRESS, "MCCP2", "MUD Client Compression Protocol v2"),
266
+ (MCCP3_COMPRESS, "MCCP3", "MUD Client Compression Protocol v3"),
263
267
  (GMCP, "GMCP", "Generic MUD Communication Protocol"),
264
268
  (MSDP, "MSDP", "MUD Server Data Protocol"),
265
269
  (MSSP, "MSSP", "MUD Server Status Protocol"),