Skip to content

Commit ce3155f

Browse files
authored
bugfix servers using robot_check=True+encoding=False (#144)
* bugfix: servers using ``robot_check=True`` with ``encoding=False`` raised ``TypeError: buf expected bytes, got <class 'str'>``. ``telnetlib3-server`` now also accepts ``--encoding=False`` CLI argument. ``latin1`` encoding is used the default server shell and robot check.
1 parent 296cc45 commit ce3155f

11 files changed

Lines changed: 636 additions & 63 deletions

docs/history.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
History
22
=======
3+
4.0.4
4+
* bugfix: servers using ``robot_check=True`` with ``encoding=False`` raised ``TypeError: buf
5+
expected bytes, got <class 'str'>``. ``telnetlib3-server`` now also accepts ``--encoding=False``
6+
CLI argument. ``latin1`` encoding is used the default server shell and robot check.
7+
38
4.0.3
49
* bugfix: long-running servers leaked memory through :class:`~telnetlib3.server.Server`
510
``_protocols`` list and ``_new_client`` asyncio.Queue. Both are now bounded

telnetlib3/guard_shells.py

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import re
1919
import asyncio
2020
import logging
21-
from typing import Tuple, Union, Optional, Generator, cast
21+
from typing import Tuple, Union, Optional, Generator
2222
from contextlib import contextmanager
2323

2424
# local
@@ -64,6 +64,14 @@ def _latin1_reading(
6464
reader._decoder = None
6565

6666

67+
def _writer_write(writer: Union[TelnetWriter, TelnetWriterUnicode], data: str) -> None:
68+
"""Write string data to writer in the appropriate type (str or bytes)."""
69+
if writer.is_binary_writer:
70+
writer.write(data.encode("latin-1"))
71+
else:
72+
writer.write(data) # type: ignore[arg-type]
73+
74+
6775
class ConnectionCounter:
6876
"""Simple shared counter for limiting concurrent connections."""
6977

@@ -100,12 +108,13 @@ def count(self) -> int:
100108

101109
async def _read_line_inner(reader: Union[TelnetReader, TelnetReaderUnicode], max_len: int) -> str:
102110
"""Inner loop for _read_line, separated for wait_for compatibility."""
103-
_reader = cast(TelnetReaderUnicode, reader)
104111
buf = ""
105112
while len(buf) < max_len:
106-
char = await _reader.read(1)
113+
char = await reader.read(1)
107114
if not char:
108115
break
116+
if isinstance(char, bytes):
117+
char = char.decode("latin-1")
109118
if char in ("\r", "\n"):
110119
break
111120
buf += char
@@ -166,8 +175,7 @@ async def _get_cursor_position(
166175
:returns: (row, col) tuple or (None, None) on timeout/failure.
167176
"""
168177
# Send Device Status Report request
169-
_writer = cast(TelnetWriterUnicode, writer)
170-
_writer.write("\x1b[6n")
178+
_writer_write(writer, "\x1b[6n")
171179
await writer.drain()
172180

173181
# Read response: ESC [ row ; col R
@@ -189,21 +197,20 @@ async def _measure_width(
189197
190198
:returns: Width in columns, or None on failure.
191199
"""
192-
_writer = cast(TelnetWriterUnicode, writer)
193200
_, x1 = await _get_cursor_position(reader, writer, timeout)
194201
if x1 is None:
195202
return None
196203

197-
_writer.write(text)
198-
await _writer.drain()
204+
_writer_write(writer, text)
205+
await writer.drain()
199206

200207
_, x2 = await _get_cursor_position(reader, writer, timeout)
201208
if x2 is None:
202209
return None
203210

204211
# Clear the test character
205-
_writer.write(f"\x1b[{x1}G" + " " * (x2 - x1) + f"\x1b[{x1}G")
206-
await _writer.drain()
212+
_writer_write(writer, f"\x1b[{x1}G" + " " * (x2 - x1) + f"\x1b[{x1}G")
213+
await writer.drain()
207214

208215
return x2 - x1
209216

@@ -230,10 +237,9 @@ async def _ask_question(
230237
timeout: float = 10.0,
231238
) -> Optional[str]:
232239
"""Ask a question, echoing input and repeating prompt on blank input."""
233-
_writer = cast(TelnetWriterUnicode, writer)
234240
while True:
235-
_writer.write(prompt)
236-
await _writer.drain()
241+
_writer_write(writer, prompt)
242+
await writer.drain()
237243

238244
line = await _readline_with_echo(reader, writer, timeout)
239245
if line is None:
@@ -242,7 +248,7 @@ async def _ask_question(
242248
if line.strip():
243249
return line
244250
# Blank input - repeat prompt
245-
_writer.write("\r\n")
251+
_writer_write(writer, "\r\n")
246252

247253

248254
async def robot_shell(
@@ -254,7 +260,6 @@ async def robot_shell(
254260
255261
Asks philosophical questions, logs responses, and disconnects.
256262
"""
257-
writer = cast(TelnetWriterUnicode, writer)
258263
peername = writer.get_extra_info("peername")
259264
logger.info("robot_shell: connection from %s", peername)
260265

@@ -275,7 +280,7 @@ async def robot_shell(
275280
return
276281
answers.append(line2)
277282

278-
writer.write("\r\n")
283+
_writer_write(writer, "\r\n")
279284
await writer.drain()
280285
finally:
281286
if answers:
@@ -291,23 +296,22 @@ async def busy_shell(
291296
292297
Displays busy message, logs any input, and disconnects.
293298
"""
294-
writer = cast(TelnetWriterUnicode, writer)
295299
logger.info("busy_shell: connection from %s (limit reached)", writer.get_extra_info("peername"))
296300

297-
writer.write("Machine is busy, do not touch! ")
301+
_writer_write(writer, "Machine is busy, do not touch! ")
298302
await writer.drain()
299303

300304
with _latin1_reading(reader):
301305
line1 = await _read_line(reader, timeout=30.0)
302306
if line1 is not None:
303307
logger.info("busy_shell: input1=%r", line1)
304308

305-
writer.write("\r\nYou hear a distant explosion... ")
309+
_writer_write(writer, "\r\nYou hear a distant explosion... ")
306310
await writer.drain()
307311

308312
line2 = await _read_line(reader, timeout=30.0)
309313
if line2 is not None:
310314
logger.info("busy_shell: input2=%r", line2)
311315

312-
writer.write("\r\n")
316+
_writer_write(writer, "\r\n")
313317
await writer.drain()

telnetlib3/server.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -913,7 +913,7 @@ def begin_advanced_negotiation(self) -> None:
913913
self.writer.iac(DO, LINEMODE)
914914

915915
def _negotiate_echo(self) -> None:
916-
"""Skip ``WILL ECHO`` LINEMODE EDIT client handles local echo."""
916+
"""Skip ``WILL ECHO``: LINEMODE EDIT client handles local echo."""
917917
if self._echo_negotiated:
918918
return
919919
self._echo_negotiated = True
@@ -1297,7 +1297,12 @@ def parse_server_args(
12971297
default=_config.connect_maxwait,
12981298
help="timeout for pending negotiation",
12991299
)
1300-
parser.add_argument("--encoding", default=_config.encoding, help="encoding name")
1300+
parser.add_argument(
1301+
"--encoding",
1302+
default=_config.encoding,
1303+
type=lambda val: False if val.lower() == "false" else val,
1304+
help="encoding name, or 'false'/'False' to disable unicode",
1305+
)
13011306
parser.add_argument(
13021307
"--force-binary",
13031308
action="store_true",
@@ -1398,9 +1403,12 @@ def parse_server_args(
13981403
result["pty_raw"] = False
13991404

14001405
# Auto-enable force_binary for any non-ASCII encoding that uses high-bit bytes.
1401-
enc_key = result["encoding"].lower().replace("-", "_")
1402-
if enc_key not in ("us_ascii", "ascii"):
1406+
if result["encoding"] is False:
14031407
result["force_binary"] = True
1408+
else:
1409+
enc_key = result["encoding"].lower().replace("-", "_")
1410+
if enc_key not in ("us_ascii", "ascii"):
1411+
result["force_binary"] = True
14041412

14051413
# Build SSLContext from --ssl-certfile / --ssl-keyfile
14061414
ssl_certfile = result.pop("ssl_certfile", None)

0 commit comments

Comments
 (0)