├── docs ├── intro.rst ├── contributing.rst ├── api │ ├── slc.rst │ ├── client.rst │ ├── server.rst │ ├── telopt.rst │ ├── telnetlib.rst │ ├── accessories.rst │ ├── client_base.rst │ ├── server_base.rst │ ├── client_shell.rst │ ├── server_shell.rst │ ├── stream_writer.rst │ └── stream_reader.rst ├── api.rst ├── index.rst ├── example_linemode.py ├── history.rst ├── sphinxext │ └── github.py ├── rfcs.rst ├── Makefile ├── make.bat ├── conf.py └── example_readline.py ├── telnetlib3 ├── example │ └── __init__.py ├── .gitignore ├── tests │ ├── accessories.py │ ├── test_uvloop_integration.py │ ├── test_accessories.py │ ├── test_tspeed.py │ ├── test_accessories_extra.py │ ├── test_xdisploc.py │ ├── test_timeout.py │ ├── test_relay_server.py │ ├── test_linemode.py │ ├── test_naws.py │ ├── test_stream_reader_extra.py │ ├── test_stream_reader_more.py │ ├── test_environ.py │ ├── test_server_shell_unit.py │ ├── test_ttype.py │ ├── test_encoding.py │ └── test_shell.py ├── __init__.py ├── accessories.py ├── relay_server.py ├── telopt.py ├── server_shell.py └── client_shell.py ├── requirements-docs.txt ├── requirements-analysis.txt ├── requirements-tests.txt ├── .pre-commit-config.yaml ├── requirements.txt ├── .git-blame-ignore-revs ├── MANIFEST.in ├── .gitignore ├── .readthedocs.yaml ├── .github └── workflows │ └── tests.yaml ├── tox.ini ├── setup.py ├── CONTRIBUTING.rst ├── LICENSE.txt └── README.rst /docs/intro.rst: -------------------------------------------------------------------------------- 1 | ../README.rst -------------------------------------------------------------------------------- /telnetlib3/example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | ../CONTRIBUTING.rst -------------------------------------------------------------------------------- /telnetlib3/.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | *.pyc 3 | -------------------------------------------------------------------------------- /requirements-docs.txt: -------------------------------------------------------------------------------- 1 | Sphinx 2 | sphinx-rtd-theme 3 | sphinx-paramlinks 4 | -------------------------------------------------------------------------------- /docs/api/slc.rst: -------------------------------------------------------------------------------- 1 | slc 2 | --- 3 | 4 | .. automodule:: telnetlib3.slc 5 | :members: 6 | -------------------------------------------------------------------------------- /requirements-analysis.txt: -------------------------------------------------------------------------------- 1 | restructuredtext_lint 2 | pygments 3 | doc8 4 | pylint 5 | black 6 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :glob: 7 | 8 | api/* 9 | -------------------------------------------------------------------------------- /docs/api/client.rst: -------------------------------------------------------------------------------- 1 | client 2 | ------ 3 | 4 | .. automodule:: telnetlib3.client 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/server.rst: -------------------------------------------------------------------------------- 1 | server 2 | ------ 3 | 4 | .. automodule:: telnetlib3.server 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/telopt.rst: -------------------------------------------------------------------------------- 1 | telopt 2 | ------ 3 | 4 | .. automodule:: telnetlib3.telopt 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/telnetlib.rst: -------------------------------------------------------------------------------- 1 | telnetlib 2 | --------- 3 | 4 | .. automodule:: telnetlib3.telnetlib 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/accessories.rst: -------------------------------------------------------------------------------- 1 | accessories 2 | ----------- 3 | 4 | .. automodule:: telnetlib3.accessories 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/client_base.rst: -------------------------------------------------------------------------------- 1 | client_base 2 | ----------- 3 | 4 | .. automodule:: telnetlib3.client_base 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/server_base.rst: -------------------------------------------------------------------------------- 1 | server_base 2 | ----------- 3 | 4 | .. automodule:: telnetlib3.server_base 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/client_shell.rst: -------------------------------------------------------------------------------- 1 | client_shell 2 | ------------ 3 | 4 | .. automodule:: telnetlib3.client_shell 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/server_shell.rst: -------------------------------------------------------------------------------- 1 | server_shell 2 | ------------ 3 | 4 | .. automodule:: telnetlib3.server_shell 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/stream_writer.rst: -------------------------------------------------------------------------------- 1 | stream_writer 2 | ------------- 3 | 4 | .. automodule:: telnetlib3.stream_writer 5 | :members: 6 | -------------------------------------------------------------------------------- /requirements-tests.txt: -------------------------------------------------------------------------------- 1 | tox 2 | pytest 3 | pytest-asyncio 4 | pytest-xdist 5 | pytest-cov 6 | pytest-timeout 7 | pexpect 8 | uvloop 9 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 23.3.0 4 | hooks: 5 | - id: black 6 | language_version: python3.13 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # this package has no requirements! 2 | # Except for testing, install from: 3 | # requirements-analysis.txt 4 | # requirements-docs.txt 5 | # requirements-tests.txt 6 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # black reformat, use command to ignore in git blame: 2 | # 3 | # git config blame.ignoreRevsFile .git-blame-ignore-revs 4 | # 5 | 5ab38df575ef812b62211fc70a0eb8904f6b0f9d 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | suggested MANIFEST.in rules: 2 | include *.rst 3 | include *.yaml 4 | include .coveragerc 5 | include tox.ini 6 | recursive-include docs *.bat 7 | recursive-include docs *.py 8 | recursive-include docs *.rst 9 | recursive-include docs Makefile 10 | recursive-include telnetlib3 *.py 11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.xml 2 | .coverage 3 | ._coverage.* 4 | .coverage.* 5 | .cache 6 | .tox 7 | *.egg-info 8 | *.egg 9 | *.pyc 10 | results*.xml 11 | build 12 | .build 13 | dist 14 | docs/_build 15 | htmlcov 16 | .coveralls.yml 17 | .DS_Store 18 | .*.sw? 19 | .python-version 20 | .idea 21 | .venv/ 22 | .vscode/ 23 | .pytest_cache/ 24 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | telnetlib3 3 | ========== 4 | 5 | Python 3 asyncio Telnet server and client Protocol library. 6 | 7 | Contents: 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :glob: 12 | 13 | intro 14 | api 15 | rfcs 16 | contributing 17 | history 18 | 19 | ======= 20 | Indexes 21 | ======= 22 | 23 | * :ref:`genindex` 24 | * :ref:`modindex` 25 | -------------------------------------------------------------------------------- /telnetlib3/tests/accessories.py: -------------------------------------------------------------------------------- 1 | """Test accessories for telnetlib3 project.""" 2 | 3 | # std imports 4 | import logging 5 | 6 | # 3rd-party 7 | import pytest 8 | from pytest_asyncio.plugin import unused_tcp_port 9 | 10 | 11 | @pytest.fixture(scope="module", params=["127.0.0.1"]) 12 | def bind_host(request): 13 | """Localhost bind address.""" 14 | return request.param 15 | 16 | 17 | __all__ = ( 18 | "bind_host", 19 | "unused_tcp_port", 20 | ) 21 | -------------------------------------------------------------------------------- /docs/example_linemode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | A very simple linemode server shell. 4 | """ 5 | # std 6 | import asyncio 7 | import sys 8 | import pkg_resources 9 | 10 | # local 11 | import telnetlib3 12 | 13 | 14 | async def shell(reader, writer): 15 | from telnetlib3 import WONT, ECHO 16 | 17 | writer.iac(WONT, ECHO) 18 | 19 | while True: 20 | writer.write("> ") 21 | 22 | recv = await reader.readline() 23 | 24 | # eof 25 | if not recv: 26 | return 27 | 28 | writer.write("\r\n") 29 | 30 | if recv.rstrip() == "bye": 31 | writer.write("goodbye.\r\n") 32 | await writer.drain() 33 | writer.close() 34 | 35 | writer.write("".join(reversed(recv)) + "\r\n") 36 | 37 | 38 | if __name__ == "__main__": 39 | kwargs = telnetlib3.parse_server_args() 40 | kwargs["shell"] = shell 41 | telnetlib3.run_server(**kwargs) 42 | # sys.argv.append('--shell={ 43 | sys.exit( 44 | pkg_resources.load_entry_point( 45 | "telnetlib3", "console_scripts", "telnetlib3-server" 46 | )() 47 | ) 48 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-24.04 10 | tools: 11 | python: "3.12" 12 | # You can also specify other tool versions: 13 | # nodejs: "20" 14 | # rust: "1.70" 15 | # golang: "1.20" 16 | 17 | # Build documentation in the "docs/" directory with Sphinx 18 | sphinx: 19 | configuration: docs/conf.py 20 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 21 | # builder: "dirhtml" 22 | # Fail on all warnings to avoid broken references 23 | fail_on_warning: true 24 | 25 | # Optionally build your docs in additional formats such as PDF and ePub 26 | # formats: 27 | # - pdf 28 | # - epub 29 | 30 | # Optional but recommended, declare the Python requirements required 31 | # to build your documentation 32 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 33 | python: 34 | install: 35 | - requirements: requirements-docs.txt 36 | -------------------------------------------------------------------------------- /telnetlib3/__init__.py: -------------------------------------------------------------------------------- 1 | """telnetlib3: an asyncio Telnet Protocol implemented in python.""" 2 | 3 | # pylint: disable=wildcard-import,undefined-variable 4 | from .server_base import * # noqa 5 | from .server_shell import * # noqa 6 | from .server import * # noqa 7 | from .stream_writer import * # noqa 8 | from .stream_reader import * # noqa 9 | from .client_base import * # noqa 10 | from .client_shell import * # noqa 11 | from .client import * # noqa 12 | from .telopt import * # noqa 13 | from .telnetlib import * # noqa 14 | from .slc import * # noqa 15 | from .accessories import get_version as __get_version 16 | 17 | __all__ = ( 18 | server_base.__all__ 19 | + server_shell.__all__ 20 | + server.__all__ 21 | + client_base.__all__ 22 | + client_shell.__all__ 23 | + client.__all__ 24 | + stream_writer.__all__ 25 | + stream_reader.__all__ 26 | + telopt.__all__ 27 | + slc.__all__ 28 | + telnetlib.__all__ 29 | ) # noqa 30 | 31 | __author__ = "Jeff Quast" 32 | __url__ = "https://github.com/jquast/telnetlib3/" 33 | __copyright__ = "Copyright 2013" 34 | __credits__ = ["Jim Storch", "Wijnand Modderman-Lenstra"] 35 | __license__ = "ISC" 36 | __version__ = __get_version() 37 | -------------------------------------------------------------------------------- /telnetlib3/tests/test_uvloop_integration.py: -------------------------------------------------------------------------------- 1 | """Minimal uvloop integration test for telnetlib3.""" 2 | 3 | import asyncio 4 | import pytest 5 | import uvloop 6 | import telnetlib3 7 | from telnetlib3.tests.accessories import unused_tcp_port, bind_host 8 | 9 | 10 | @pytest.fixture(scope="module") 11 | def event_loop_policy(): 12 | return uvloop.EventLoopPolicy() 13 | 14 | 15 | async def minimal_shell(reader, writer): 16 | """Minimal shell that just sends OK and closes.""" 17 | writer.write("OK\r\n") 18 | await writer.drain() 19 | writer.close() 20 | 21 | 22 | @pytest.mark.asyncio 23 | async def test_uvloop_telnet_integration(bind_host, unused_tcp_port): 24 | """Test basic telnet client-server connection with uvloop.""" 25 | # Verify we're running with uvloop 26 | loop = asyncio.get_running_loop() 27 | assert "uvloop" in str(type(loop)).lower(), f"Expected uvloop, got {type(loop)}" 28 | 29 | # Create server 30 | server = await telnetlib3.create_server( 31 | host=bind_host, port=unused_tcp_port, shell=minimal_shell 32 | ) 33 | 34 | # Connect client 35 | reader, writer = await telnetlib3.open_connection( 36 | host=bind_host, port=unused_tcp_port 37 | ) 38 | 39 | # Read response and verify connection works 40 | data = await reader.read(1024) 41 | response = data if isinstance(data, str) else data.decode("utf-8", errors="ignore") 42 | assert "OK" in response 43 | 44 | # Clean up 45 | writer.close() 46 | await writer.wait_closed() 47 | server.close() 48 | await server.wait_closed() 49 | -------------------------------------------------------------------------------- /docs/api/stream_reader.rst: -------------------------------------------------------------------------------- 1 | stream_reader 2 | ------------- 3 | 4 | Closing a connection 5 | ~~~~~~~~~~~~~~~~~~~~ 6 | 7 | - Application code should not call ``reader.close()``. To gracefully close a connection, call ``writer.close()`` and, if needed, ``await writer.wait_closed()``. The protocol will signal end-of-input to the reader. 8 | - The protocol layer calls ``reader.feed_eof()`` when the underlying transport indicates EOF (for example in ``connection_lost()``). This marks the reader as EOF and wakes any pending read coroutines. 9 | - After ``feed_eof()``, subsequent ``read()`` calls will drain any buffered bytes and then return ``b""``; ``readline()``/iteration will stop at EOF. Use ``reader.at_eof()`` to test EOF state. 10 | 11 | Example (application code): 12 | :: 13 | 14 | async def app(reader, writer): 15 | # ... use reader/readline/readuntil ... 16 | writer.close() 17 | await writer.wait_closed() 18 | # reader will eventually see EOF; reads return b"" once buffer drains 19 | 20 | Example (protocol integration): 21 | :: 22 | 23 | class MyProtocol(asyncio.Protocol): 24 | def __init__(self, reader): 25 | self.reader = reader 26 | def connection_lost(self, exc): 27 | if exc: 28 | self.reader.set_exception(exc) 29 | self.reader.feed_eof() 30 | 31 | Deprecation notes: 32 | - ``TelnetReader.close()`` is deprecated; use ``feed_eof()`` (protocol) and ``writer.close()``/``wait_closed()`` (application). 33 | - ``TelnetReader.connection_closed`` property is deprecated; use ``reader.at_eof()``. 34 | 35 | .. automodule:: telnetlib3.stream_reader 36 | :members: 37 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Run telnetlib3 tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | release: 7 | schedule: 8 | # Every Thursday at 1 AM 9 | - cron: '0 1 * * 4' 10 | 11 | 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.runs-on }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 19 | runs-on: [ubuntu-latest] 20 | include: 21 | - python-version: "3.7" 22 | runs-on: ubuntu-20.04 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: Set up Python ${{ matrix.python-version }} 27 | uses: actions/setup-python@v4 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | pip install -r requirements-tests.txt 34 | - name: Run tests 35 | run: | 36 | tox 37 | - name: Upload to Codecov 38 | uses: codecov/codecov-action@v5 39 | with: 40 | verbose: true 41 | name: ${{ matrix.label || matrix.python-version }} 42 | token: ${{ secrets.CODECOV_TOKEN }} 43 | fail_ci_if_error: true 44 | os: linux 45 | env_vars: TOXENV 46 | 47 | # Work around for https://github.com/codecov/codecov-action/issues/1277 48 | - name: Upload to Codecov Workaround 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: coverage.${{ matrix.python-version }}.xml 52 | path: coverage.xml 53 | retention-days: 1 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /telnetlib3/tests/test_accessories.py: -------------------------------------------------------------------------------- 1 | from telnetlib3.accessories import ( 2 | encoding_from_lang, 3 | name_unicode, 4 | eightbits, 5 | ) 6 | 7 | 8 | def test_name_unicode(): 9 | """Test mapping of ascii table to name_unicode result.""" 10 | given_expected = { 11 | chr(0): r"^@", 12 | chr(1): r"^A", 13 | chr(26): r"^Z", 14 | chr(29): r"^]", 15 | chr(31): r"^_", 16 | chr(32): r" ", 17 | chr(126): r"~", 18 | chr(127): r"^?", 19 | chr(128): r"\x80", 20 | chr(254): r"\xfe", 21 | chr(255): r"\xff", 22 | } 23 | for given, expected in sorted(given_expected.items()): 24 | # exercise, 25 | result = name_unicode(given) 26 | 27 | # verify, 28 | assert result == expected 29 | 30 | 31 | def test_eightbits(): 32 | """Test mapping of bit values to binary appearance string.""" 33 | given_expected = { 34 | 0: "0b00000000", 35 | 127: "0b01111111", 36 | 128: "0b10000000", 37 | 255: "0b11111111", 38 | } 39 | for given, expected in sorted(given_expected.items()): 40 | # exercise, 41 | result = eightbits(given) 42 | 43 | # verify 44 | assert result == expected 45 | 46 | 47 | def test_encoding_from_lang(): 48 | """Test inference of encoding from LANG value.""" 49 | given_expected = { 50 | "en_US.UTF-8@misc": "UTF-8", 51 | "en_US.UTF-8": "UTF-8", 52 | "abc.def": "def", 53 | ".def@ghi": "def", 54 | "def@": "def", 55 | "UTF-8": "UTF-8", 56 | } 57 | for given, expected in sorted(given_expected.items()): 58 | # exercise, 59 | result = encoding_from_lang(given) 60 | 61 | # verify, 62 | assert result == expected 63 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{38,39,310,311,312,313}, docs 3 | skip_missing_interpreters = true 4 | 5 | [testenv] 6 | whitelist_externals = cp 7 | deps = -rrequirements-tests.txt 8 | install_command = pip install --upgrade {packages} 9 | passenv = PYTHONASYNCIODEBUG 10 | usedevelop = True 11 | commands = {envbindir}/pytest {posargs} telnetlib3/tests 12 | 13 | # report coverage to coveralls 14 | [testenv:coveralls] 15 | passenv = COVERALLS_REPO_TOKEN 16 | deps = coveralls 17 | commands = coveralls --verbose --rcfile={toxinidir}/.coveragerc 18 | 19 | [testenv:sa] 20 | # perform static analysis and style enforcement 21 | # Disabled: needs to be brought up-to-date 22 | basepython = python3.13 23 | deps = -rrequirements-tests.txt 24 | -rrequirements-analysis.txt 25 | commands = python -m compileall -fq {toxinidir}/telnetlib3 26 | {envbindir}/rst-lint README.rst 27 | {envbindir}/doc8 --ignore-path docs/_build --ignore D000,D001 docs 28 | {envbindir}/black . 29 | 30 | [testenv:docs] 31 | # build html documentation, roughly matches .readthedocs.yaml settings 32 | whitelist_externals = echo 33 | basepython = python3.12 34 | deps = -rrequirements-docs.txt 35 | commands = {envbindir}/sphinx-build -E -a -v -n \ 36 | -d {toxinidir}/docs/_build/doctrees \ 37 | {posargs:-b html} docs \ 38 | {toxinidir}/docs/_build/html 39 | 40 | [pytest] 41 | norecursedirs = .git .tox 42 | asyncio_mode = auto 43 | log_level = debug 44 | log_format = %(levelname)8s %(filename)s:%(lineno)s %(message)s 45 | # set this to display all log output, even when tests succeed 46 | #log_cli = 1 47 | addopts = 48 | --strict 49 | --verbose 50 | --verbose 51 | --color=yes 52 | --cov 53 | --cov-append 54 | --cov-report=xml 55 | --disable-pytest-warnings 56 | --ignore=setup.py 57 | --ignore=.tox 58 | --durations=10 59 | --timeout=15 60 | --junit-xml=.tox/results.{envname}.xml 61 | 62 | [coverage:run] 63 | branch = True 64 | parallel = True 65 | source = telnetlib3 66 | omit = telnetlib3/tests/* 67 | data_file = .coverage.${TOX_ENV_NAME} 68 | 69 | [coverage:report] 70 | precision = 1 71 | exclude_lines = pragma: no cover 72 | omit = telnetlib3/tests/* 73 | 74 | [coverage:paths] 75 | source = telnetlib3/ 76 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Setuptools distribution file.""" 3 | import os 4 | from setuptools import setup 5 | 6 | 7 | def _get_here(fname): 8 | return os.path.join(os.path.dirname(__file__), fname) 9 | 10 | 11 | def _get_long_description(fname, encoding="utf8"): 12 | return open(fname, "r", encoding=encoding).read() 13 | 14 | 15 | setup( 16 | name="telnetlib3", 17 | version="2.0.8", # keep in sync with docs/conf.py and telnetlib3/accessories.py !! 18 | url="http://telnetlib3.rtfd.org/", 19 | license="ISC", 20 | author="Jeff Quast", 21 | description="Python 3 asyncio Telnet server and client Protocol library", 22 | long_description=_get_long_description(fname=_get_here("README.rst")), 23 | # requires python 3.7 and greater beginning with 2.0.0 release 24 | python_requires=">=3.7", 25 | packages=["telnetlib3"], 26 | package_data={ 27 | "": ["README.rst", "requirements.txt"], 28 | }, 29 | entry_points={ 30 | "console_scripts": [ 31 | "telnetlib3-server = telnetlib3.server:main", 32 | "telnetlib3-client = telnetlib3.client:main", 33 | ] 34 | }, 35 | author_email="contact@jeffquast.com", 36 | platforms="any", 37 | zip_safe=True, 38 | keywords=", ".join( 39 | ( 40 | "telnet", 41 | "server", 42 | "client", 43 | "bbs", 44 | "mud", 45 | "utf8", 46 | "cp437", 47 | "api", 48 | "library", 49 | "asyncio", 50 | "talker", 51 | ) 52 | ), 53 | classifiers=[ 54 | "License :: OSI Approved :: ISC License (ISCL)", 55 | "Programming Language :: Python :: 3.7", 56 | "Programming Language :: Python :: 3.8", 57 | "Programming Language :: Python :: 3.9", 58 | "Programming Language :: Python :: 3.10", 59 | "Programming Language :: Python :: 3.11", 60 | "Programming Language :: Python :: 3.12", 61 | "Programming Language :: Python :: 3.13", 62 | "Intended Audience :: Developers", 63 | "Development Status :: 4 - Beta", 64 | "Topic :: System :: Networking", 65 | "Topic :: Terminals :: Telnet", 66 | "Topic :: System :: Shells", 67 | "Topic :: Internet", 68 | ], 69 | ) 70 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | We welcome contributions via GitHub pull requests: 5 | 6 | - `Fork a Repo `_ 7 | - `Creating a pull request 8 | `_ 9 | 10 | Developing 11 | ---------- 12 | 13 | Prepare a developer environment. Then, from the telnetlib3 code folder:: 14 | 15 | pip install --editable . 16 | 17 | Any changes made in this project folder are then made available to the python 18 | interpreter as the 'telnetlib3' module irregardless of the current working 19 | directory. 20 | 21 | Running Tests 22 | ------------- 23 | 24 | `Py.test ` is the test runner. Install and run tox 25 | 26 | :: 27 | 28 | pip install --upgrade tox 29 | tox 30 | 31 | A convenience target, 'develop' is provided, which adds `-vv` and `--looponfail` 32 | arguments, where the tests automatically re-trigger on any file change:: 33 | 34 | tox -e develop 35 | 36 | Code Formatting 37 | --------------- 38 | 39 | To make code formatting easy on developers, and to simplify the conversation 40 | around pull request reviews, this project has adopted the `black `_ 41 | code formatter. This formatter must be run against any new code written for this 42 | project. The advantage is, you no longer have to think about how your code is 43 | styled; it's all handled for you! 44 | 45 | To make this even easier on you, you can set up most editors to auto-run 46 | ``black`` for you. We have also set up a `pre-commit `_ 47 | hook to run automatically on every commit, with just a small bit of extra setup: 48 | 49 | :: 50 | 51 | pip install pre-commit 52 | pre-commit install --install-hooks 53 | 54 | Now, before each git commit is accepted, this hook will run to ensure the code 55 | has been properly formatted by ``black``. 56 | 57 | 58 | Style and Static Analysis 59 | ------------------------- 60 | 61 | All standards enforced by the underlying tools are adhered to by this project, 62 | with the declarative exception of those found in `landscape.yml 63 | `_, or inline 64 | using ``pylint: disable=`` directives. 65 | 66 | Perform static analysis using tox target *sa*:: 67 | 68 | tox -esa 69 | -------------------------------------------------------------------------------- /telnetlib3/tests/test_tspeed.py: -------------------------------------------------------------------------------- 1 | """Test TSPEED, rfc-1079_.""" 2 | 3 | # std imports 4 | import asyncio 5 | 6 | # local imports 7 | import telnetlib3 8 | import telnetlib3.stream_writer 9 | from telnetlib3.tests.accessories import unused_tcp_port, bind_host 10 | 11 | # 3rd party 12 | import pytest 13 | 14 | 15 | async def test_telnet_server_on_tspeed(bind_host, unused_tcp_port): 16 | """Test Server's callback method on_tspeed().""" 17 | # given 18 | from telnetlib3.telopt import IAC, WILL, SB, SE, IS, TSPEED 19 | 20 | _waiter = asyncio.Future() 21 | 22 | class ServerTestTspeed(telnetlib3.TelnetServer): 23 | def on_tspeed(self, rx, tx): 24 | super().on_tspeed(rx, tx) 25 | _waiter.set_result(self) 26 | 27 | await telnetlib3.create_server( 28 | protocol_factory=ServerTestTspeed, host=bind_host, port=unused_tcp_port 29 | ) 30 | 31 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 32 | 33 | # exercise, 34 | writer.write(IAC + WILL + TSPEED) 35 | writer.write(IAC + SB + TSPEED + IS + b"123,456" + IAC + SE) 36 | 37 | # verify, 38 | srv_instance = await asyncio.wait_for(_waiter, 0.5) 39 | assert srv_instance.get_extra_info("tspeed") == "123,456" 40 | 41 | 42 | async def test_telnet_client_send_tspeed(bind_host, unused_tcp_port): 43 | """Test Client's callback method send_tspeed().""" 44 | # given 45 | _waiter = asyncio.Future() 46 | given_rx, given_tx = 1337, 1919 47 | 48 | class ServerTestTspeed(telnetlib3.TelnetServer): 49 | def on_tspeed(self, rx, tx): 50 | super().on_tspeed(rx, tx) 51 | _waiter.set_result((rx, tx)) 52 | 53 | def begin_advanced_negotiation(self): 54 | from telnetlib3.telopt import DO, TSPEED 55 | 56 | super().begin_advanced_negotiation() 57 | self.writer.iac(DO, TSPEED) 58 | 59 | await telnetlib3.create_server( 60 | protocol_factory=ServerTestTspeed, host=bind_host, port=unused_tcp_port 61 | ) 62 | 63 | reader, writer = await telnetlib3.open_connection( 64 | host=bind_host, 65 | port=unused_tcp_port, 66 | tspeed=(given_rx, given_tx), 67 | connect_minwait=0.05, 68 | ) 69 | 70 | recv_rx, recv_tx = await asyncio.wait_for(_waiter, 0.5) 71 | assert recv_rx == given_rx 72 | assert recv_tx == given_tx 73 | -------------------------------------------------------------------------------- /telnetlib3/tests/test_accessories_extra.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import shlex 4 | from collections import OrderedDict 5 | 6 | import pytest 7 | 8 | from telnetlib3.accessories import ( 9 | make_logger, 10 | repr_mapping, 11 | function_lookup, 12 | make_reader_task, 13 | ) 14 | 15 | 16 | def test_make_logger_no_file(): 17 | logger = make_logger("acc_no_file", loglevel="info") 18 | assert logger.name == "acc_no_file" 19 | # ensure level applied 20 | assert logger.level == logging.INFO 21 | assert logger.isEnabledFor(logging.INFO) 22 | 23 | 24 | def test_make_logger_with_file(tmp_path): 25 | log_path = tmp_path / "acc.log" 26 | logger = make_logger("acc_with_file", loglevel="warning", logfile=str(log_path)) 27 | assert logger.name == "acc_with_file" 28 | assert logger.level == logging.WARNING 29 | assert logger.isEnabledFor(logging.WARNING) 30 | # emit (do not assert file contents to avoid coupling with global logging config) 31 | logger.warning("file logging branch executed") 32 | 33 | 34 | def test_repr_mapping_quotes_roundtrip(): 35 | mapping = OrderedDict( 36 | [ 37 | ("a", "simple"), 38 | ("b", "needs space"), 39 | ("c", "quote'"), 40 | ("d", 42), 41 | ] 42 | ) 43 | result = repr_mapping(mapping) 44 | expected = " ".join(f"{k}={shlex.quote(str(v))}" for k, v in mapping.items()) 45 | assert result == expected 46 | 47 | 48 | def test_function_lookup_success_and_not_callable(): 49 | fn = function_lookup("telnetlib3.accessories.get_version") 50 | assert callable(fn) 51 | # call to ensure the returned object is usable 52 | assert isinstance(fn(), str) 53 | 54 | with pytest.raises(AssertionError): 55 | function_lookup("telnetlib3.accessories.__all__") 56 | 57 | 58 | class _DummyReader: 59 | def __init__(self, payload): 60 | self.payload = payload 61 | self.calls = [] 62 | 63 | async def read(self, size): 64 | self.calls.append(size) 65 | return self.payload 66 | 67 | 68 | @pytest.mark.asyncio 69 | async def test_make_reader_task_awaits_and_uses_default_size(): 70 | reader = _DummyReader("abc") 71 | task = make_reader_task(reader) 72 | result = await asyncio.wait_for(task, timeout=0.5) 73 | assert result == "abc" 74 | assert reader.calls and reader.calls[0] == 2**12 75 | -------------------------------------------------------------------------------- /telnetlib3/tests/test_xdisploc.py: -------------------------------------------------------------------------------- 1 | """Test XDISPLOC, rfc-1096_.""" 2 | 3 | # std imports 4 | import asyncio 5 | 6 | # local imports 7 | import telnetlib3 8 | import telnetlib3.stream_writer 9 | from telnetlib3.tests.accessories import unused_tcp_port, bind_host 10 | 11 | # 3rd party 12 | import pytest 13 | 14 | 15 | async def test_telnet_server_on_xdisploc(bind_host, unused_tcp_port): 16 | """Test Server's callback method on_xdisploc().""" 17 | # given 18 | from telnetlib3.telopt import IAC, WILL, SB, SE, IS, XDISPLOC 19 | 20 | _waiter = asyncio.Future() 21 | given_xdisploc = "alpha:0" 22 | 23 | class ServerTestXdisploc(telnetlib3.TelnetServer): 24 | def on_xdisploc(self, xdisploc): 25 | super().on_xdisploc(xdisploc) 26 | _waiter.set_result(self) 27 | 28 | await telnetlib3.create_server( 29 | protocol_factory=ServerTestXdisploc, host=bind_host, port=unused_tcp_port 30 | ) 31 | 32 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 33 | 34 | # exercise, 35 | writer.write(IAC + WILL + XDISPLOC) 36 | writer.write(IAC + SB + XDISPLOC + IS + given_xdisploc.encode("ascii") + IAC + SE) 37 | 38 | # verify, 39 | srv_instance = await asyncio.wait_for(_waiter, 0.5) 40 | assert srv_instance.get_extra_info("xdisploc") == "alpha:0" 41 | 42 | 43 | async def test_telnet_client_send_xdisploc(bind_host, unused_tcp_port): 44 | """Test Client's callback method send_xdisploc().""" 45 | # given 46 | _waiter = asyncio.Future() 47 | given_xdisploc = "alpha" 48 | 49 | class ServerTestXdisploc(telnetlib3.TelnetServer): 50 | def on_xdisploc(self, xdisploc): 51 | super().on_xdisploc(xdisploc) 52 | _waiter.set_result(xdisploc) 53 | 54 | def begin_advanced_negotiation(self): 55 | from telnetlib3.telopt import DO, XDISPLOC 56 | 57 | super().begin_advanced_negotiation() 58 | self.writer.iac(DO, XDISPLOC) 59 | 60 | await telnetlib3.create_server( 61 | protocol_factory=ServerTestXdisploc, host=bind_host, port=unused_tcp_port 62 | ) 63 | 64 | reader, writer = await telnetlib3.open_connection( 65 | host=bind_host, 66 | port=unused_tcp_port, 67 | xdisploc=given_xdisploc, 68 | connect_minwait=0.05, 69 | ) 70 | 71 | recv_xdisploc = await asyncio.wait_for(_waiter, 0.5) 72 | assert recv_xdisploc == given_xdisploc 73 | -------------------------------------------------------------------------------- /telnetlib3/accessories.py: -------------------------------------------------------------------------------- 1 | """Accessory functions.""" 2 | 3 | # std imports 4 | import importlib 5 | import logging 6 | import asyncio 7 | import shlex 8 | 9 | __all__ = ( 10 | "encoding_from_lang", 11 | "name_unicode", 12 | "eightbits", 13 | "make_logger", 14 | "repr_mapping", 15 | "function_lookup", 16 | "make_reader_task", 17 | ) 18 | 19 | 20 | def get_version(): 21 | return "2.0.8" # keep in sync with setup.py and docs/conf.py !! 22 | 23 | 24 | def encoding_from_lang(lang): 25 | """ 26 | Parse encoding from LANG environment value. 27 | 28 | Example:: 29 | 30 | >>> encoding_from_lang('en_US.UTF-8@misc') 31 | 'UTF-8' 32 | """ 33 | encoding = lang 34 | if "." in lang: 35 | _, encoding = lang.split(".", 1) 36 | if "@" in encoding: 37 | encoding, _ = encoding.split("@", 1) 38 | return encoding 39 | 40 | 41 | def name_unicode(ucs): 42 | """Return 7-bit ascii printable of any string.""" 43 | # more or less the same as curses.ascii.unctrl -- but curses 44 | # module is conditionally excluded from many python distributions! 45 | bits = ord(ucs) 46 | if 32 <= bits <= 126: 47 | # ascii printable as one cell, as-is 48 | rep = chr(bits) 49 | elif bits == 127: 50 | rep = "^?" 51 | elif bits < 32: 52 | rep = "^" + chr(((bits & 0x7F) | 0x20) + 0x20) 53 | else: 54 | rep = r"\x{:02x}".format(bits) 55 | return rep 56 | 57 | 58 | def eightbits(number): 59 | """ 60 | Binary representation of ``number`` padded to 8 bits. 61 | 62 | Example:: 63 | 64 | >>> eightbits(ord('a')) 65 | '0b01100001' 66 | """ 67 | # useful only so far in context of a forwardmask or any bitmask. 68 | prefix, value = bin(number).split("b") 69 | return "0b%0.8i" % (int(value),) 70 | 71 | 72 | _DEFAULT_LOGFMT = " ".join( 73 | ("%(asctime)s", "%(levelname)s", "%(filename)s:%(lineno)d", "%(message)s") 74 | ) 75 | 76 | 77 | def make_logger(name, loglevel="info", logfile=None, logfmt=_DEFAULT_LOGFMT): 78 | """Create and return simple logger for given arguments.""" 79 | lvl = getattr(logging, loglevel.upper()) 80 | 81 | _cfg = {"format": logfmt} 82 | if logfile: 83 | _cfg["filename"] = logfile 84 | logging.basicConfig(**_cfg) 85 | logging.getLogger().setLevel(lvl) 86 | logging.getLogger(name).setLevel(lvl) 87 | return logging.getLogger(name) 88 | 89 | 90 | def repr_mapping(mapping): 91 | """Return printable string, 'key=value [key=value ...]' for mapping.""" 92 | return " ".join( 93 | f"{key}={shlex.quote(str(value))}" for key, value in mapping.items() 94 | ) 95 | 96 | 97 | def function_lookup(pymod_path): 98 | """Return callable function target from standard module.function path.""" 99 | module_name, func_name = pymod_path.rsplit(".", 1) 100 | module = importlib.import_module(module_name) 101 | shell_function = getattr(module, func_name) 102 | assert callable(shell_function), shell_function 103 | return shell_function 104 | 105 | 106 | def make_reader_task(reader, size=2**12): 107 | """Return asyncio task wrapping coroutine of reader.read(size).""" 108 | return asyncio.ensure_future(reader.read(size)) 109 | -------------------------------------------------------------------------------- /telnetlib3/relay_server.py: -------------------------------------------------------------------------------- 1 | CR, LF, NUL = "\r\n\x00" 2 | 3 | import logging 4 | import asyncio 5 | from .server_shell import readline 6 | from .client import open_connection 7 | from .accessories import make_reader_task 8 | 9 | 10 | async def relay_shell(client_reader, client_writer): 11 | """ 12 | An example 'telnet relay shell', appropriate for use with 13 | telnetlib3.create_server, run command:: 14 | 15 | telnetlib3 --shell telnetlib3.relay_server.relay_shell 16 | 17 | This relay service is very basic, it still needs to somehow forward the TERM 18 | type and environment variable of value COLORTERM 19 | """ 20 | log = logging.getLogger("relay_server") 21 | 22 | password_prompt = readline(client_reader, client_writer) 23 | password_prompt.send(None) 24 | 25 | client_writer.write("Telnet Relay shell ready." + CR + LF + CR + LF) 26 | 27 | client_passcode = "867-5309" 28 | num_tries = 3 29 | next_host, next_port = "1984.ws", 23 30 | passcode = None 31 | for _ in range(num_tries): 32 | client_writer.write("Passcode: ") 33 | while passcode is None: 34 | inp = await client_reader.read(1) 35 | if not inp: 36 | log.info("EOF from client") 37 | return 38 | passcode = password_prompt.send(inp) 39 | await asyncio.sleep(1) 40 | client_writer.write(CR + LF) 41 | if passcode == client_passcode: 42 | log.info("passcode accepted") 43 | break 44 | passcode = None 45 | 46 | # wrong passcode after 3 tires 47 | if passcode is None: 48 | log.info("passcode failed after %s tries", num_tries) 49 | client_writer.close() 50 | return 51 | 52 | # connect to another telnet server (next_host, next_port) 53 | client_writer.write("Connecting to {}:{} ... ".format(next_host, next_port)) 54 | server_reader, server_writer = await open_connection( 55 | next_host, 56 | next_port, 57 | cols=client_writer.get_extra_info("cols"), 58 | rows=client_writer.get_extra_info("rows"), 59 | ) 60 | client_writer.write("connected!" + CR + LF) 61 | 62 | done = [] 63 | client_stdin = make_reader_task(client_reader) 64 | server_stdout = make_reader_task(server_reader) 65 | wait_for = {client_stdin, server_stdout} 66 | while wait_for: 67 | done, remaining = await asyncio.wait( 68 | wait_for, return_when=asyncio.FIRST_COMPLETED 69 | ) 70 | while done: 71 | task = done.pop() 72 | wait_for.remove(task) 73 | if task == client_stdin: 74 | inp = task.result() 75 | if inp: 76 | server_writer.write(inp) 77 | client_stdin = make_reader_task(client_reader) 78 | wait_for.add(client_stdin) 79 | else: 80 | log.info("EOF from client") 81 | server_writer.close() 82 | elif task == server_stdout: 83 | out = task.result() 84 | if out: 85 | client_writer.write(out) 86 | server_stdout = make_reader_task(server_reader) 87 | wait_for.add(server_stdout) 88 | else: 89 | log.info("EOF from server") 90 | client_writer.close() 91 | log.info("No more tasks: relay server complete") 92 | -------------------------------------------------------------------------------- /telnetlib3/tests/test_timeout.py: -------------------------------------------------------------------------------- 1 | """Test the server's shell(reader, writer) callback.""" 2 | 3 | # std imports 4 | import asyncio 5 | import time 6 | 7 | # local imports 8 | import telnetlib3 9 | import telnetlib3.stream_writer 10 | from telnetlib3.tests.accessories import unused_tcp_port, bind_host 11 | 12 | # 3rd party 13 | import pytest 14 | 15 | 16 | async def test_telnet_server_default_timeout(bind_host, unused_tcp_port): 17 | """Test callback on_timeout() as coroutine of create_server().""" 18 | from telnetlib3.telopt import IAC, WONT, TTYPE 19 | 20 | # given, 21 | _waiter = asyncio.Future() 22 | given_timeout = 19.29 23 | 24 | await telnetlib3.create_server( 25 | _waiter_connected=_waiter, 26 | host=bind_host, 27 | port=unused_tcp_port, 28 | timeout=given_timeout, 29 | ) 30 | 31 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 32 | 33 | writer.write(IAC + WONT + TTYPE) 34 | 35 | server = await asyncio.wait_for(_waiter, 0.5) 36 | assert server.get_extra_info("timeout") == given_timeout 37 | 38 | # exercise, calling set_timeout remains the default given_value. 39 | server.set_timeout() 40 | assert server.get_extra_info("timeout") == given_timeout 41 | 42 | 43 | async def test_telnet_server_set_timeout(bind_host, unused_tcp_port): 44 | """Test callback on_timeout() as coroutine of create_server().""" 45 | from telnetlib3.telopt import IAC, WONT, TTYPE 46 | 47 | # given, 48 | _waiter = asyncio.Future() 49 | 50 | # exercise, 51 | await telnetlib3.create_server( 52 | _waiter_connected=_waiter, host=bind_host, port=unused_tcp_port 53 | ) 54 | 55 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 56 | 57 | writer.write(IAC + WONT + TTYPE) 58 | 59 | server = await asyncio.wait_for(_waiter, 0.5) 60 | for value in (19.29, 0): 61 | server.set_timeout(value) 62 | assert server.get_extra_info("timeout") == value 63 | 64 | # calling with no arguments does nothing, only resets 65 | # the timer. value remains the last-most value from 66 | # previous loop 67 | server.set_timeout() 68 | assert server.get_extra_info("timeout") == 0 69 | 70 | 71 | async def test_telnet_server_waitfor_timeout(bind_host, unused_tcp_port): 72 | """Test callback on_timeout() as coroutine of create_server().""" 73 | from telnetlib3.telopt import IAC, DO, WONT, TTYPE 74 | 75 | # given, 76 | expected_output = IAC + DO + TTYPE + b"\r\nTimeout.\r\n" 77 | 78 | await telnetlib3.create_server(host=bind_host, port=unused_tcp_port, timeout=0.050) 79 | 80 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 81 | 82 | writer.write(IAC + WONT + TTYPE) 83 | 84 | stime = time.time() 85 | output = await asyncio.wait_for(reader.read(), 0.5) 86 | elapsed = time.time() - stime 87 | assert 0.050 <= round(elapsed, 3) <= 0.100 88 | assert output == expected_output 89 | 90 | 91 | async def test_telnet_server_binary_mode(bind_host, unused_tcp_port): 92 | """Test callback on_timeout() in BINARY mode when encoding=False is used""" 93 | from telnetlib3.telopt import IAC, WONT, DO, TTYPE, BINARY 94 | 95 | # given 96 | expected_output = IAC + DO + TTYPE + b"\r\nTimeout.\r\n" 97 | 98 | await telnetlib3.create_server( 99 | host=bind_host, port=unused_tcp_port, timeout=0.150, encoding=False 100 | ) 101 | 102 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 103 | 104 | writer.write(IAC + WONT + TTYPE) 105 | 106 | stime = time.time() 107 | output = await asyncio.wait_for(reader.read(), 0.5) 108 | elapsed = time.time() - stime 109 | assert 0.050 <= round(elapsed, 3) <= 0.200 110 | assert output == expected_output 111 | -------------------------------------------------------------------------------- /telnetlib3/tests/test_relay_server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import types 3 | 4 | import pytest 5 | 6 | from telnetlib3.relay_server import relay_shell 7 | 8 | 9 | class FakeWriter: 10 | def __init__(self): 11 | self.buffer = [] 12 | self.closed = False 13 | self._closing = False 14 | self._extra = {"cols": 80, "rows": 24} 15 | 16 | def write(self, data): 17 | # Collect text written by the shell 18 | self.buffer.append(data) 19 | 20 | def echo(self, data): 21 | # Readline may call echo; we do not need to simulate terminal behavior 22 | self.buffer.append(data) 23 | 24 | def get_extra_info(self, key, default=None): 25 | return self._extra.get(key, default) 26 | 27 | def is_closing(self): 28 | return self._closing 29 | 30 | def close(self): 31 | self._closing = True 32 | self.closed = True 33 | 34 | 35 | class SeqReader: 36 | """ 37 | Async reader that returns provided sequence 1 byte at a time. 38 | When the sequence is exhausted, returns '' to indicate EOF. 39 | """ 40 | 41 | def __init__(self, sequence): 42 | # sequence must be str 43 | self.data = sequence 44 | self.pos = 0 45 | 46 | async def read(self, n): 47 | # Only 1-byte reads are requested by relay_shell 48 | if self.pos >= len(self.data): 49 | return "" 50 | ch = self.data[self.pos] 51 | self.pos += 1 52 | return ch 53 | 54 | 55 | class PayloadReader: 56 | """ 57 | Reader that yields a list of payloads on subsequent read() calls, then ''. 58 | """ 59 | 60 | def __init__(self, payloads): 61 | self.payloads = list(payloads) 62 | 63 | async def read(self, n): 64 | if not self.payloads: 65 | return "" 66 | return self.payloads.pop(0) 67 | 68 | 69 | class DummyServerWriter: 70 | def __init__(self): 71 | self.writes = [] 72 | self.closed = False 73 | 74 | def write(self, data): 75 | self.writes.append(data) 76 | 77 | def close(self): 78 | self.closed = True 79 | 80 | 81 | @pytest.mark.asyncio 82 | async def test_relay_shell_wrong_passcode_closes(monkeypatch): 83 | """ 84 | Relay shell should prompt for passcode 3 times and close on failure. 85 | """ 86 | # Prepare fake client I/O 87 | client_reader = SeqReader("bad1\rbad2\rbad3\r") 88 | client_writer = FakeWriter() 89 | 90 | # Avoid 1-second sleeps in loop 91 | async def _no_sleep(_): 92 | return None 93 | 94 | monkeypatch.setattr(asyncio, "sleep", _no_sleep) 95 | 96 | await relay_shell(client_reader, client_writer) 97 | 98 | out = "".join(client_writer.buffer) 99 | # Greeting and prompts 100 | assert "Telnet Relay shell ready." in out 101 | assert out.count("Passcode: ") == 3 102 | # Connection should not be attempted on wrong pass 103 | assert "Connecting to" not in out 104 | # Writer is closed 105 | assert client_writer.closed is True 106 | 107 | 108 | @pytest.mark.asyncio 109 | async def test_relay_shell_success_relays_and_closes(monkeypatch): 110 | """ 111 | Relay shell should connect on correct passcode and relay server output. 112 | """ 113 | # Client enters correct passcode then EOF from client 114 | client_reader = PayloadReader( 115 | # readline() is fed 1 char at a time 116 | list("867-5309\r") 117 | + [""] # then EOF from client stdin after connection 118 | ) 119 | client_writer = FakeWriter() 120 | 121 | # Avoid 1-second sleeps in loop 122 | async def _no_sleep(_): 123 | return None 124 | 125 | monkeypatch.setattr(asyncio, "sleep", _no_sleep) 126 | 127 | # Mock open_connection to a dummy server that sends "hello" then EOF 128 | server_reader = PayloadReader(["hello", ""]) 129 | server_writer = DummyServerWriter() 130 | 131 | async def _fake_open_connection(host, port, cols=None, rows=None): 132 | # Basic sanity on forwarded cols/rows 133 | assert cols == 80 and rows == 24 134 | return server_reader, server_writer 135 | 136 | monkeypatch.setattr( 137 | "telnetlib3.relay_server.open_connection", _fake_open_connection 138 | ) 139 | 140 | await relay_shell(client_reader, client_writer) 141 | 142 | out = "".join(client_writer.buffer) 143 | # Greeting, connect, connected, and relayed output 144 | assert "Telnet Relay shell ready." in out 145 | assert "Connecting to 1984.ws:23" in out 146 | assert "connected!" in out 147 | assert "hello" in out 148 | 149 | # Both sides closed 150 | assert client_writer.closed is True 151 | assert server_writer.closed is True 152 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | telnetlib3 is (c) 2013 Jeff Quast . 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted, provided that the above copyright notice 5 | and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 11 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 12 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 13 | THIS SOFTWARE. 14 | 15 | SLC functions were transcribed from NetBSD: 16 | 17 | Copyright (c) 1989, 1993 18 | The Regents of the University of California. All rights reserved. 19 | 20 | Redistribution and use in source and binary forms, with or without 21 | modification, are permitted provided that the following conditions 22 | are met: 23 | 1. Redistributions of source code must retain the above copyright 24 | notice, this list of conditions and the following disclaimer. 25 | 2. Redistributions in binary form must reproduce the above copyright 26 | notice, this list of conditions and the following disclaimer in the 27 | documentation and/or other materials provided with the distribution. 28 | 3. Neither the name of the University nor the names of its contributors 29 | may be used to endorse or promote products derived from this software 30 | without specific prior written permission. 31 | 32 | THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND 33 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 34 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 35 | ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE 36 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 37 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 38 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 39 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 40 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 41 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 42 | SUCH DAMAGE. 43 | 44 | telnetlib.py and telnetlib3/tests/test_telnetlib.py derived from Python 3.12: 45 | 46 | PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 47 | -------------------------------------------- 48 | 49 | 1. This LICENSE AGREEMENT is between the Python Software Foundation 50 | ("PSF"), and the Individual or Organization ("Licensee") accessing and 51 | otherwise using this software ("Python") in source or binary form and 52 | its associated documentation. 53 | 54 | 2. Subject to the terms and conditions of this License Agreement, PSF hereby 55 | grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, 56 | analyze, test, perform and/or display publicly, prepare derivative works, 57 | distribute, and otherwise use Python alone or in any derivative version, 58 | provided, however, that PSF's License Agreement and PSF's notice of copyright, 59 | i.e., "Copyright (c) 2001 Python Software Foundation; All Rights Reserved" 60 | are retained in Python alone or in any derivative version prepared by Licensee. 61 | 62 | 3. In the event Licensee prepares a derivative work that is based on 63 | or incorporates Python or any part thereof, and wants to make 64 | the derivative work available to others as provided herein, then 65 | Licensee hereby agrees to include in any such work a brief summary of 66 | the changes made to Python. 67 | 68 | 4. PSF is making Python available to Licensee on an "AS IS" 69 | basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR 70 | IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND 71 | DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS 72 | FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT 73 | INFRINGE ANY THIRD PARTY RIGHTS. 74 | 75 | 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 76 | FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS 77 | A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, 78 | OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 79 | 80 | 6. This License Agreement will automatically terminate upon a material 81 | breach of its terms and conditions. 82 | 83 | 7. Nothing in this License Agreement shall be deemed to create any 84 | relationship of agency, partnership, or joint venture between PSF and 85 | Licensee. This License Agreement does not grant permission to use PSF 86 | trademarks or trade name in a trademark sense to endorse or promote 87 | products or services of Licensee, or any third party. 88 | 89 | 8. By copying, installing or otherwise using Python, Licensee 90 | agrees to be bound by the terms and conditions of this License 91 | Agreement. 92 | -------------------------------------------------------------------------------- /telnetlib3/tests/test_linemode.py: -------------------------------------------------------------------------------- 1 | """Test LINEMODE, rfc-1184_.""" 2 | 3 | # std imports 4 | import asyncio 5 | 6 | # local imports 7 | import telnetlib3 8 | import telnetlib3.stream_writer 9 | from telnetlib3.tests.accessories import unused_tcp_port, bind_host 10 | 11 | # 3rd party 12 | import pytest 13 | 14 | 15 | async def test_server_demands_remote_linemode_client_agrees(bind_host, unused_tcp_port): 16 | from telnetlib3.telopt import IAC, DO, WILL, LINEMODE, SB, SE 17 | from telnetlib3.slc import LMODE_MODE, LMODE_MODE_ACK 18 | 19 | _waiter = asyncio.Future() 20 | 21 | class ServerTestLinemode(telnetlib3.BaseServer): 22 | def begin_negotiation(self): 23 | super().begin_negotiation() 24 | self.writer.iac(DO, LINEMODE) 25 | asyncio.get_event_loop().call_later(0.1, self.connection_lost, None) 26 | 27 | await telnetlib3.create_server( 28 | protocol_factory=ServerTestLinemode, 29 | host=bind_host, 30 | port=unused_tcp_port, 31 | _waiter_connected=_waiter, 32 | ) 33 | 34 | client_reader, client_writer = await asyncio.open_connection( 35 | host=bind_host, port=unused_tcp_port 36 | ) 37 | 38 | expect_mode = telnetlib3.stream_writer.TelnetWriter.default_linemode.mask 39 | expect_stage1 = IAC + DO + LINEMODE 40 | expect_stage2 = IAC + SB + LINEMODE + LMODE_MODE + expect_mode + IAC + SE 41 | 42 | reply_mode = bytes([ord(expect_mode) | ord(LMODE_MODE_ACK)]) 43 | reply_stage1 = IAC + WILL + LINEMODE 44 | reply_stage2 = IAC + SB + LINEMODE + LMODE_MODE + reply_mode + IAC + SE 45 | 46 | result = await client_reader.readexactly(len(expect_stage1)) 47 | assert result == expect_stage1 48 | client_writer.write(reply_stage1) 49 | 50 | result = await client_reader.readexactly(len(expect_stage2)) 51 | assert result == expect_stage2 52 | client_writer.write(reply_stage2) 53 | 54 | srv_instance = await asyncio.wait_for(_waiter, 0.1) 55 | assert not any(srv_instance.writer.pending_option.values()) 56 | 57 | result = await client_reader.read() 58 | assert result == b"" 59 | 60 | assert srv_instance.writer.mode == "remote" 61 | assert srv_instance.writer.linemode.remote is True 62 | assert srv_instance.writer.linemode.local is False 63 | assert srv_instance.writer.linemode.trapsig is False 64 | assert srv_instance.writer.linemode.ack is True 65 | assert srv_instance.writer.linemode.soft_tab is False 66 | assert srv_instance.writer.linemode.lit_echo is True 67 | assert srv_instance.writer.remote_option.enabled(LINEMODE) 68 | 69 | 70 | async def test_server_demands_remote_linemode_client_demands_local( 71 | bind_host, unused_tcp_port 72 | ): 73 | from telnetlib3.telopt import IAC, DO, WILL, LINEMODE, SB, SE 74 | from telnetlib3.slc import LMODE_MODE, LMODE_MODE_LOCAL, LMODE_MODE_ACK 75 | 76 | _waiter = asyncio.Future() 77 | 78 | class ServerTestLinemode(telnetlib3.BaseServer): 79 | def begin_negotiation(self): 80 | super().begin_negotiation() 81 | self.writer.iac(DO, LINEMODE) 82 | asyncio.get_event_loop().call_later(0.1, self.connection_lost, None) 83 | 84 | await telnetlib3.create_server( 85 | protocol_factory=ServerTestLinemode, 86 | host=bind_host, 87 | port=unused_tcp_port, 88 | _waiter_connected=_waiter, 89 | ) 90 | 91 | client_reader, client_writer = await asyncio.open_connection( 92 | host=bind_host, port=unused_tcp_port 93 | ) 94 | 95 | expect_mode = telnetlib3.stream_writer.TelnetWriter.default_linemode.mask 96 | expect_stage1 = IAC + DO + LINEMODE 97 | expect_stage2 = IAC + SB + LINEMODE + LMODE_MODE + expect_mode + IAC + SE 98 | 99 | # No, we demand local mode -- using ACK will finalize such request 100 | reply_mode = bytes([ord(LMODE_MODE_LOCAL) | ord(LMODE_MODE_ACK)]) 101 | reply_stage1 = IAC + WILL + LINEMODE 102 | reply_stage2 = IAC + SB + LINEMODE + LMODE_MODE + reply_mode + IAC + SE 103 | 104 | result = await client_reader.readexactly(len(expect_stage1)) 105 | assert result == expect_stage1 106 | client_writer.write(reply_stage1) 107 | 108 | result = await client_reader.readexactly(len(expect_stage2)) 109 | assert result == expect_stage2 110 | client_writer.write(reply_stage2) 111 | 112 | srv_instance = await asyncio.wait_for(_waiter, 0.1) 113 | assert not any(srv_instance.writer.pending_option.values()) 114 | 115 | result = await client_reader.read() 116 | assert result == b"" 117 | 118 | assert srv_instance.writer.mode == "local" 119 | assert srv_instance.writer.linemode.remote is False 120 | assert srv_instance.writer.linemode.local is True 121 | assert srv_instance.writer.linemode.trapsig is False 122 | assert srv_instance.writer.linemode.ack is True 123 | assert srv_instance.writer.linemode.soft_tab is False 124 | assert srv_instance.writer.linemode.lit_echo is False 125 | assert srv_instance.writer.remote_option.enabled(LINEMODE) 126 | -------------------------------------------------------------------------------- /telnetlib3/tests/test_naws.py: -------------------------------------------------------------------------------- 1 | """Negotiate About Window Size, *NAWS*. rfc-1073_.""" 2 | 3 | # std imports 4 | import platform 5 | import asyncio 6 | import pexpect 7 | import struct 8 | 9 | # local imports 10 | import telnetlib3 11 | from telnetlib3.tests.accessories import unused_tcp_port, bind_host 12 | 13 | # 3rd party 14 | import pytest 15 | 16 | 17 | async def test_telnet_server_on_naws(bind_host, unused_tcp_port): 18 | """Test Server's Negotiate about window size (NAWS).""" 19 | # given 20 | from telnetlib3.telopt import IAC, WILL, SB, SE, NAWS 21 | 22 | _waiter = asyncio.Future() 23 | given_cols, given_rows = 40, 20 24 | 25 | class ServerTestNaws(telnetlib3.TelnetServer): 26 | def on_naws(self, width, height): 27 | super().on_naws(width, height) 28 | _waiter.set_result(self) 29 | 30 | await telnetlib3.create_server( 31 | protocol_factory=ServerTestNaws, 32 | host=bind_host, 33 | port=unused_tcp_port, 34 | connect_maxwait=0.05, 35 | ) 36 | 37 | reader, writer = await asyncio.open_connection( 38 | host=bind_host, 39 | port=unused_tcp_port, 40 | ) 41 | 42 | # exercise, 43 | writer.write(IAC + WILL + NAWS) 44 | writer.write( 45 | IAC + SB + NAWS + struct.pack("!HH", given_cols, given_rows) + IAC + SE 46 | ) 47 | 48 | srv_instance = await asyncio.wait_for(_waiter, 0.5) 49 | assert srv_instance.get_extra_info("cols") == given_cols 50 | assert srv_instance.get_extra_info("rows") == given_rows 51 | 52 | 53 | async def test_telnet_client_send_naws(bind_host, unused_tcp_port): 54 | """Test Client's NAWS of callback method send_naws().""" 55 | # given a server 56 | _waiter = asyncio.Future() 57 | given_cols, given_rows = 40, 20 58 | 59 | class ServerTestNaws(telnetlib3.TelnetServer): 60 | def on_naws(self, width, height): 61 | super().on_naws(width, height) 62 | _waiter.set_result((height, width)) 63 | 64 | await telnetlib3.create_server( 65 | protocol_factory=ServerTestNaws, 66 | host=bind_host, 67 | port=unused_tcp_port, 68 | connect_maxwait=0.05, 69 | ) 70 | 71 | reader, writer = await telnetlib3.open_connection( 72 | host=bind_host, 73 | port=unused_tcp_port, 74 | cols=given_cols, 75 | rows=given_rows, 76 | connect_minwait=0.05, 77 | ) 78 | 79 | recv_cols, recv_rows = await asyncio.wait_for(_waiter, 0.5) 80 | assert recv_cols == given_cols 81 | assert recv_rows == given_rows 82 | 83 | 84 | @pytest.mark.skipif( 85 | tuple(map(int, platform.python_version_tuple())) > (3, 10), 86 | reason="those shabby pexpect maintainers still use @asyncio.coroutine", 87 | ) 88 | async def test_telnet_client_send_tty_naws(bind_host, unused_tcp_port): 89 | """Test Client's NAWS of callback method send_naws().""" 90 | # given a client, 91 | _waiter = asyncio.Future() 92 | given_cols, given_rows = 40, 20 93 | prog, args = "telnetlib3-client", [ 94 | bind_host, 95 | str(unused_tcp_port), 96 | "--loglevel=warning", 97 | "--connect-minwait=0.005", 98 | "--connect-maxwait=0.010", 99 | ] 100 | 101 | # a server, 102 | class ServerTestNaws(telnetlib3.TelnetServer): 103 | def on_naws(self, width, height): 104 | super().on_naws(width, height) 105 | _waiter.set_result((height, width)) 106 | asyncio.get_event_loop().call_soon(self.connection_lost, None) 107 | 108 | await telnetlib3.create_server( 109 | protocol_factory=ServerTestNaws, 110 | host=bind_host, 111 | port=unused_tcp_port, 112 | connect_maxwait=0.05, 113 | ) 114 | 115 | proc = pexpect.spawn(prog, args, dimensions=(given_rows, given_cols)) 116 | await proc.expect(pexpect.EOF, async_=True, timeout=5) 117 | assert proc.match == pexpect.EOF 118 | 119 | recv_cols, recv_rows = await asyncio.wait_for(_waiter, 0.5) 120 | assert recv_cols == given_cols 121 | assert recv_rows == given_rows 122 | 123 | 124 | async def test_telnet_client_send_naws_65534(bind_host, unused_tcp_port): 125 | """Test Client's NAWS boundary values.""" 126 | # given a server 127 | _waiter = asyncio.Future() 128 | given_cols, given_rows = 9999999, -999999 129 | expect_cols, expect_rows = 65535, 0 130 | 131 | class ServerTestNaws(telnetlib3.TelnetServer): 132 | def on_naws(self, width, height): 133 | super().on_naws(width, height) 134 | _waiter.set_result((height, width)) 135 | 136 | await telnetlib3.create_server( 137 | protocol_factory=ServerTestNaws, 138 | host=bind_host, 139 | port=unused_tcp_port, 140 | connect_maxwait=0.05, 141 | ) 142 | 143 | reader, writer = await telnetlib3.open_connection( 144 | host=bind_host, 145 | port=unused_tcp_port, 146 | cols=given_cols, 147 | rows=given_rows, 148 | connect_minwait=0.05, 149 | ) 150 | 151 | recv_cols, recv_rows = await asyncio.wait_for(_waiter, 0.5) 152 | assert recv_cols == expect_cols 153 | assert recv_rows == expect_rows 154 | -------------------------------------------------------------------------------- /telnetlib3/tests/test_stream_reader_extra.py: -------------------------------------------------------------------------------- 1 | # std imports 2 | import asyncio 3 | import re 4 | 5 | # 3rd party 6 | import pytest 7 | 8 | # local 9 | from telnetlib3.stream_reader import TelnetReader, TelnetReaderUnicode 10 | 11 | 12 | class MockTransport: 13 | def __init__(self): 14 | self.paused = False 15 | self.resumed = False 16 | self._closing = False 17 | self.writes = [] 18 | 19 | def pause_reading(self): 20 | self.paused = True 21 | 22 | def resume_reading(self): 23 | self.resumed = True 24 | 25 | def is_closing(self): 26 | return self._closing 27 | 28 | def get_extra_info(self, name, default=None): 29 | return default 30 | 31 | def write(self, data): 32 | self.writes.append(bytes(data)) 33 | 34 | 35 | @pytest.mark.asyncio 36 | async def test_readuntil_success_consumes_and_returns(): 37 | r = TelnetReader(limit=64) 38 | r.feed_data(b"abc\nrest") 39 | out = await r.readuntil(b"\n") 40 | assert out == b"abc\n" 41 | # buffer consumed up to and including separator 42 | assert bytes(r._buffer) == b"rest" 43 | 44 | 45 | @pytest.mark.asyncio 46 | async def test_readuntil_eof_incomplete_raises_and_clears(): 47 | r = TelnetReader(limit=64) 48 | r.feed_data(b"partial") 49 | r.feed_eof() 50 | with pytest.raises(asyncio.IncompleteReadError) as exc: 51 | await r.readuntil(b"\n") 52 | assert exc.value.partial == b"partial" 53 | # buffer cleared on EOF path 54 | assert r._buffer == bytearray() 55 | 56 | 57 | @pytest.mark.asyncio 58 | async def test_readuntil_limit_overrun_leaves_buffer(): 59 | r = TelnetReader(limit=5) 60 | # 7 bytes, no separator, should exceed limit 61 | r.feed_data(b"abcdefg") 62 | with pytest.raises(asyncio.LimitOverrunError): 63 | await r.readuntil(b"\n") 64 | # buffer left intact on limit overrun 65 | assert bytes(r._buffer) == b"abcdefg" 66 | 67 | 68 | @pytest.mark.asyncio 69 | async def test_readuntil_pattern_success_and_eof_incomplete(): 70 | r = TelnetReader(limit=64) 71 | pat = re.compile(b"XYZ") 72 | r.feed_data(b"aaXYZbb") 73 | out = await r.readuntil_pattern(pat) 74 | assert out == b"aaXYZ" 75 | assert bytes(r._buffer) == b"bb" 76 | 77 | # EOF incomplete for pattern 78 | r2 = TelnetReader(limit=64) 79 | r2.feed_data(b"aaaa") 80 | r2.feed_eof() 81 | with pytest.raises(asyncio.IncompleteReadError) as exc: 82 | await r2.readuntil_pattern(pat) 83 | assert exc.value.partial == b"aaaa" 84 | assert r2._buffer == bytearray() 85 | 86 | 87 | @pytest.mark.asyncio 88 | async def test_read_negative_reads_until_eof_in_blocks(): 89 | r = TelnetReader(limit=4) 90 | r.feed_data(b"12345678") 91 | r.feed_eof() 92 | out = await r.read(-1) 93 | assert out == b"12345678" 94 | assert r.at_eof() is True 95 | 96 | 97 | @pytest.mark.asyncio 98 | async def test_pause_and_resume_transport_based_on_buffer_limit(): 99 | r = TelnetReader(limit=4) 100 | t = MockTransport() 101 | r.set_transport(t) 102 | # exceed 2*limit (8) to pause 103 | r.feed_data(b"123456789") 104 | assert t.paused is True 105 | 106 | # consume enough to drop buffer length <= limit and trigger resume 107 | got = await r.read(5) 108 | assert got == b"12345" 109 | assert t.resumed is True 110 | 111 | 112 | @pytest.mark.asyncio 113 | async def test_anext_iterates_lines_and_stops_on_eof(): 114 | r = TelnetReader() 115 | r.feed_data(b"Line1\nLine2\n") 116 | # first line 117 | one = await r.__anext__() 118 | assert one == b"Line1\n" 119 | # second line 120 | two = await r.__anext__() 121 | assert two == b"Line2\n" 122 | # signal EOF then StopAsyncIteration on next 123 | r.feed_eof() 124 | with pytest.raises(StopAsyncIteration): 125 | await r.__anext__() 126 | 127 | 128 | @pytest.mark.asyncio 129 | async def test_exception_propagates_to_read_calls(): 130 | r = TelnetReader() 131 | r.set_exception(RuntimeError("boom")) 132 | with pytest.raises(RuntimeError, match="boom"): 133 | await r.read(1) 134 | 135 | 136 | def test_deprecated_close_and_connection_closed_warns(): 137 | r = TelnetReader() 138 | # property warns 139 | with pytest.warns(DeprecationWarning): 140 | _ = r.connection_closed 141 | # close warns and sets eof 142 | with pytest.warns(DeprecationWarning): 143 | r.close() 144 | assert r._eof is True 145 | 146 | 147 | @pytest.mark.asyncio 148 | async def test_readexactly_negative_and_eof_partial(): 149 | r = TelnetReader() 150 | with pytest.raises(ValueError): 151 | await r.readexactly(-5) 152 | 153 | r2 = TelnetReader() 154 | r2.feed_data(b"abc") 155 | r2.feed_eof() 156 | with pytest.raises(asyncio.IncompleteReadError) as exc: 157 | await r2.readexactly(5) 158 | assert exc.value.partial == b"abc" 159 | 160 | 161 | @pytest.mark.asyncio 162 | async def test_unicode_reader_read_zero_and_read_consumes(): 163 | def enc(incoming): 164 | return "ascii" 165 | 166 | ur = TelnetReaderUnicode(fn_encoding=enc) 167 | # read(0) yields empty string 168 | out0 = await ur.read(0) 169 | assert out0 == "" 170 | 171 | ur.feed_data(b"abc") 172 | out2 = await ur.read(2) 173 | assert out2 == "ab" 174 | # remaining one char 175 | out1 = await ur.read(10) 176 | assert out1 == "c" 177 | 178 | 179 | @pytest.mark.asyncio 180 | async def test_unicode_readexactly_reads_characters_not_bytes(): 181 | def enc(incoming): 182 | return "utf-8" 183 | 184 | ur = TelnetReaderUnicode(fn_encoding=enc) 185 | s = "☭ab" # first is multibyte in utf-8 186 | ur.feed_data(s.encode("utf-8")) 187 | out = await ur.readexactly(2) 188 | assert out == "☭a" 189 | # next call should return remaining 'b' 190 | out2 = await ur.readexactly(1) 191 | assert out2 == "b" 192 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | History 2 | ======= 3 | 2.0.8 4 | * bugfix: object has no attribute '_extra' :ghissue:`100` 5 | 6 | 2.0.7 7 | * bugfix: respond WILL CHARSET with DO CHARSET 8 | 9 | 2.0.6 10 | * bugfix: corrected CHARSET protocol client/server role behavior :ghissue:`59` 11 | * bugfix: allow ``--force-binary`` and ``--encoding`` to be combined to prevent 12 | long ``encoding failed after 4.00s`` delays in ``telnetlib3-server`` with 13 | non-compliant clients, :ghissue:`74`. 14 | * bugfix: reduce ``telnetlib3-client`` connection delay, session begins as 15 | soon as TTYPE and either NEW_ENVIRON or CHARSET negotiation is completed. 16 | * bugfix: remove `'NoneType' object has no attribute 'is_closing'` message 17 | on some types of closed connections 18 | * bugfix: further improve ``telnetlib3-client`` performance, capable of 19 | 11.2 Mbit/s or more. 20 | * bugfix: more gracefully handle unsupported SB STATUS codes. 21 | * feature: ``telnetlib3-client`` now negotiates terminal resize events. 22 | 23 | 2.0.5 24 | * feature: legacy `telnetlib.py` from Python 3.11 now redistributed, 25 | note change to project `LICENSE.txt` file. 26 | * feature: Add `TelnetReader.readuntil_pattern` :ghissue:`92` by 27 | :ghuser:`agicy` 28 | * feature: Add `TelnetWriter.wait_closed` async method in response to 29 | :ghissue:`82`. 30 | * bugfix: README Examples do not work :ghissue:`81` 31 | * bugfix: `TypeError: buf expected bytes, got ` on client timeout 32 | in `TelnetServer`, :ghissue:`87` 33 | * bugfix: Performance issues with client protocol under heavy load, 34 | demonstrating server `telnet://1984.ws` now documented in README. 35 | * bugfix: annoying `socket.send() raised exception` repeating warning, 36 | :ghissue:`89`. 37 | * bugfix: legacy use of get_event_loop, :ghissue:`85`. 38 | * document: about encoding and force_binary in response to :ghissue:`90` 39 | * feature: add tests to source distribution, :ghissue:`37` 40 | * test coverage increased by ~20% 41 | 42 | 2.0.4 43 | * change: stop using setuptools library to get current software version 44 | 45 | 2.0.3 46 | * bugfix: NameError: when debug=True is used with asyncio.run, :ghissue:`75` 47 | 48 | 2.0.2 49 | * bugfix: NameError: name 'sleep' is not defined in stream_writer.py 50 | 51 | 2.0.1 52 | * bugfix: "write after close" is disregarded, caused many errors logged in socket.send() 53 | * bugfix: in accessories.repr_mapping() about using shlex.quote on non-str, 54 | `TypeError: expected string or bytes-like object, got 'int'` 55 | * bugfix: about fn_encoding using repr() on TelnetReaderUnicode 56 | * bugfix: TelnetReader.is_closing() raises AttributeError 57 | * deprecation: `TelnetReader.close` and `TelnetReader.connection_closed` emit 58 | warning, use `at_eof()` and `feed_eof()` instead. 59 | * deprecation: the ``loop`` argument are is no longer accepted by TelnetReader. 60 | * enhancement: Add Generic Mud Communication Protocol support :ghissue:`63` by 61 | :ghuser:`gtaylor`! 62 | * change: TelnetReader and TelnetWriter no longer derive from 63 | `asyncio.StreamReader` and `asyncio.StreamWriter`, this fixes some TypeError 64 | in signatures and runtime 65 | 66 | 2.0.0 67 | * change: Support Python 3.9, 3.10, 3.11. Drop Python 3.6 and earlier, All code 68 | and examples have been updated to the new-style PEP-492 syntax. 69 | * change: the ``loop``, ``event_loop``, and ``log`` arguments are no longer accepted to 70 | any class initializers. 71 | * note: This release has a known memory leak when using the ``_waiter_connected`` and 72 | ``_waiter_closed`` arguments to Client or Shell class initializers, please do 73 | not use them, A replacement "wait_for_negotiation" awaitable is planned for a 74 | future release. 75 | * enhancement: Add COM-PORT-OPTION subnegotiation support :ghissue:`57` by 76 | :ghuser:`albireox` 77 | 78 | 1.0.4 79 | * bugfix: NoneType error on EOF/Timeout, introduced in previous 80 | version 1.0.3, :ghissue:`51` by :ghuser:`zofy`. 81 | 82 | 1.0.3 83 | * bugfix: circular reference between transport and protocol, :ghissue:`43` by 84 | :ghuser:`fried`. 85 | 86 | 1.0.2 87 | * add --speed argument to telnet client :ghissue:`35` by :ghuser:`hughpyle`. 88 | 89 | 1.0.1 90 | * add python3.7 support, drop python 3.4 and earlier, :ghissue:`33` by 91 | :ghuser:`AndrewNelis`. 92 | 93 | 1.0.0 94 | * First general release for standard API: Instead of encouraging twisted-like 95 | override of protocol methods, we provide a "shell" callback interface, 96 | receiving argument pairs (reader, writer). 97 | 98 | 0.5.0 99 | * bugfix: linemode MODE is now acknowledged. 100 | * bugfix: default stream handler sends 80 x 24 in cols x rows, not 24 x 80. 101 | * bugfix: waiter_closed future on client defaulted to wrong type. 102 | * bugfix: telnet shell (TelSh) no longer paints over final exception line. 103 | 104 | 0.4.0 105 | * bugfix: cannot connect to IPv6 address as client. 106 | * change: TelnetClient.CONNECT_DEFERED class attribute renamed DEFERRED. 107 | Default value changed to 50ms from 100ms. 108 | * change: TelnetClient.waiter renamed to TelnetClient.waiter_closed. 109 | * enhancement: TelnetClient.waiter_connected future added. 110 | 111 | 0.3.0 112 | * bugfix: cannot bind to IPv6 address :ghissue:`5`. 113 | * enhancement: Futures waiter_connected, and waiter_closed added to server. 114 | * change: TelSh.feed_slc merged into TelSh.feed_byte as slc_function keyword. 115 | * change: TelnetServer.CONNECT_DEFERED class attribute renamed DEFERRED. 116 | Default value changed to 50ms from 100ms. 117 | * enhancement: Default TelnetServer.PROMPT_IMMEDIATELY = False ensures prompt 118 | is not displayed until negotiation is considered final. It is no longer 119 | "aggressive". 120 | * enhancement: TelnetServer.pause_writing and resume_writing callback wired. 121 | * enhancement: TelSh.pause_writing and resume_writing methods added. 122 | 123 | 0.2.4 124 | * bugfix: pip installation issue :ghissue:`8`. 125 | 126 | 0.2 127 | * enhancement: various example programs were included in this release. 128 | 129 | 0.1 130 | * Initial release. 131 | -------------------------------------------------------------------------------- /telnetlib3/tests/test_stream_reader_more.py: -------------------------------------------------------------------------------- 1 | # std imports 2 | import asyncio 3 | import re 4 | 5 | # 3rd party 6 | import pytest 7 | 8 | # local 9 | from telnetlib3.stream_reader import TelnetReader, TelnetReaderUnicode 10 | 11 | 12 | class PauseNIErrorTransport: 13 | """Transport that raises NotImplementedError on pause_reading().""" 14 | 15 | def __init__(self): 16 | self.paused = False 17 | self.resumed = False 18 | self._closing = False 19 | 20 | def pause_reading(self): 21 | raise NotImplementedError 22 | 23 | def resume_reading(self): 24 | self.resumed = True 25 | 26 | def is_closing(self): 27 | return self._closing 28 | 29 | def get_extra_info(self, name, default=None): 30 | return default 31 | 32 | 33 | class ResumeTransport: 34 | def __init__(self): 35 | self.paused = False 36 | self.resumed = False 37 | self._closing = False 38 | 39 | def pause_reading(self): 40 | self.paused = True 41 | 42 | def resume_reading(self): 43 | self.resumed = True 44 | 45 | def is_closing(self): 46 | return self._closing 47 | 48 | def get_extra_info(self, name, default=None): 49 | return default 50 | 51 | 52 | def test_repr_shows_key_fields(): 53 | r = TelnetReader(limit=1234) 54 | # populate buffer and state bits 55 | r.feed_data(b"abc") 56 | r.feed_eof() 57 | # set exception, transport and paused 58 | r.set_exception(RuntimeError("boom")) 59 | r.set_transport(ResumeTransport()) 60 | r._paused = True 61 | 62 | rep = repr(r) 63 | # sanity: contains these tokens 64 | assert "TelnetReader" in rep 65 | assert "3 bytes" in rep 66 | assert "eof" in rep 67 | assert "limit=1234" in rep 68 | assert "exception=" in rep 69 | assert "transport=" in rep 70 | assert "paused" in rep 71 | assert "encoding=False" in rep 72 | 73 | 74 | def test_set_exception_and_wakeup_waiter(): 75 | r = TelnetReader() 76 | loop = asyncio.get_event_loop() 77 | fut = loop.create_future() 78 | r._waiter = fut 79 | err = RuntimeError("oops") 80 | r.set_exception(err) 81 | assert r.exception() is err 82 | assert fut.done() 83 | with pytest.raises(RuntimeError): 84 | fut.result() 85 | 86 | # also verify _wakeup_waiter sets result when not cancelled 87 | fut2 = loop.create_future() 88 | r._waiter = fut2 89 | r._wakeup_waiter() 90 | assert fut2.done() 91 | assert fut2.result() is None 92 | 93 | 94 | @pytest.mark.asyncio 95 | async def test_wait_for_data_resumes_when_paused_and_data_arrives(): 96 | r = TelnetReader(limit=4) 97 | t = ResumeTransport() 98 | r.set_transport(t) 99 | # capture paused state by pushing > 2*limit 100 | r.feed_data(b"123456789") 101 | assert t.paused is True or len(r._buffer) > 2 * r._limit 102 | # mark manually paused and ensure resume happens in _wait_for_data 103 | r._paused = True 104 | 105 | async def feeder(): 106 | await asyncio.sleep(0.01) 107 | r.feed_data(b"x") 108 | 109 | feeder_task = asyncio.create_task(feeder()) 110 | # this will set a waiter, see paused=True, and resume_reading() 111 | await asyncio.wait_for(r._wait_for_data("read"), 0.5) 112 | await feeder_task 113 | assert t.resumed is True 114 | 115 | 116 | @pytest.mark.asyncio 117 | async def test_concurrent_reads_raise_runtimeerror(): 118 | r = TelnetReader() 119 | 120 | async def first(): 121 | # will block until data or eof 122 | return await r.read(1) 123 | 124 | async def second(): 125 | # should raise RuntimeError because first is already waiting 126 | with pytest.raises(RuntimeError, match="already waiting"): 127 | await r.read(1) 128 | 129 | t1 = asyncio.create_task(first()) 130 | await asyncio.sleep(0) # allow t1 to start and set _waiter 131 | t2 = asyncio.create_task(second()) 132 | # wake first so it can complete 133 | await asyncio.sleep(0.01) 134 | r.feed_data(b"A") 135 | res = await asyncio.wait_for(t1, 0.5) 136 | assert res == b"A" 137 | await t2 # assertion inside 138 | 139 | 140 | def test_feed_data_notimplemented_pause_drops_transport(): 141 | r = TelnetReader(limit=1) 142 | t = PauseNIErrorTransport() 143 | r.set_transport(t) 144 | # force > 2*limit -> pause_reading raises NotImplementedError and 145 | # implementation should set _transport to None 146 | r.feed_data(b"ABCD") 147 | assert r._transport is None 148 | 149 | 150 | @pytest.mark.asyncio 151 | async def test_read_zero_returns_empty_bytes(): 152 | r = TelnetReader() 153 | out = await r.read(0) 154 | assert out == b"" 155 | 156 | 157 | @pytest.mark.asyncio 158 | async def test_read_until_wait_path_then_data_arrives(): 159 | r = TelnetReader() 160 | 161 | # start waiting 162 | async def waiter(): 163 | return await r.read(3) 164 | 165 | task = asyncio.create_task(waiter()) 166 | await asyncio.sleep(0.01) 167 | r.feed_data(b"xyz") 168 | out = await asyncio.wait_for(task, 0.5) 169 | assert out == b"xyz" 170 | 171 | 172 | @pytest.mark.asyncio 173 | async def test_readexactly_exact_and_split_paths(): 174 | r = TelnetReader() 175 | r.feed_data(b"abcd") 176 | got = await r.readexactly(4) # exact path 177 | assert got == b"abcd" 178 | # split path (buffer > n or needs to wait) 179 | r2 = TelnetReader() 180 | r2.feed_data(b"abcde") 181 | got2 = await r2.readexactly(3) 182 | assert got2 == b"abc" 183 | assert bytes(r2._buffer) == b"de" 184 | 185 | 186 | def test_readuntil_separator_empty_raises(): 187 | r = TelnetReader() 188 | with pytest.raises(ValueError): 189 | # empty separator not allowed 190 | asyncio.get_event_loop().run_until_complete(r.readuntil(b"")) 191 | 192 | 193 | def test_readuntil_pattern_invalid_types(): 194 | r = TelnetReader() 195 | with pytest.raises(ValueError, match="pattern should be a re\\.Pattern"): 196 | asyncio.get_event_loop().run_until_complete(r.readuntil_pattern(None)) 197 | 198 | # pattern compiled 199 | -------------------------------------------------------------------------------- /docs/sphinxext/github.py: -------------------------------------------------------------------------------- 1 | """Define text roles for GitHub 2 | 3 | * ghissue - Issue 4 | * ghpull - Pull Request 5 | * ghuser - User 6 | 7 | Adapted from bitbucket example here: 8 | https://bitbucket.org/birkenfeld/sphinx-contrib/src/tip/bitbucket/sphinxcontrib/bitbucket.py 9 | 10 | Authors 11 | ------- 12 | 13 | * Doug Hellmann 14 | * Min RK 15 | """ 16 | 17 | # 18 | # Original Copyright (c) 2010 Doug Hellmann. All rights reserved. 19 | # 20 | 21 | from docutils import nodes, utils 22 | from docutils.parsers.rst.roles import set_classes 23 | 24 | 25 | def make_link_node(rawtext, app, type, slug, options): 26 | """Create a link to a github resource. 27 | 28 | :param rawtext: Text being replaced with link node. 29 | :param app: Sphinx application context 30 | :param type: Link type (issues, changeset, etc.) 31 | :param slug: ID of the thing to link to 32 | :param options: Options dictionary passed to role func. 33 | """ 34 | 35 | try: 36 | base = app.config.github_project_url 37 | if not base: 38 | raise AttributeError 39 | if not base.endswith("/"): 40 | base += "/" 41 | except AttributeError as err: 42 | raise ValueError( 43 | "github_project_url configuration value is not set (%s)" % str(err) 44 | ) 45 | 46 | ref = base + type + "/" + slug + "/" 47 | set_classes(options) 48 | prefix = "#" 49 | if type == "pull": 50 | prefix = "PR " + prefix 51 | node = nodes.reference( 52 | rawtext, prefix + utils.unescape(slug), refuri=ref, **options 53 | ) 54 | return node 55 | 56 | 57 | def ghissue_role(name, rawtext, text, lineno, inliner, options={}, content=[]): 58 | """Link to a GitHub issue. 59 | 60 | Returns 2 part tuple containing list of nodes to insert into the 61 | document and a list of system messages. Both are allowed to be 62 | empty. 63 | 64 | :param name: The role name used in the document. 65 | :param rawtext: The entire markup snippet, with role. 66 | :param text: The text marked with the role. 67 | :param lineno: The line number where rawtext appears in the input. 68 | :param inliner: The inliner instance that called us. 69 | :param options: Directive options for customization. 70 | :param content: The directive content for customization. 71 | """ 72 | 73 | try: 74 | issue_num = int(text) 75 | if issue_num <= 0: 76 | raise ValueError 77 | except ValueError: 78 | msg = inliner.reporter.error( 79 | "GitHub issue number must be a number greater than or equal to 1; " 80 | '"%s" is invalid.' % text, 81 | line=lineno, 82 | ) 83 | prb = inliner.problematic(rawtext, rawtext, msg) 84 | return [prb], [msg] 85 | app = inliner.document.settings.env.app 86 | # app.info('issue %r' % text) 87 | if "pull" in name.lower(): 88 | category = "pull" 89 | elif "issue" in name.lower(): 90 | category = "issues" 91 | else: 92 | msg = inliner.reporter.error( 93 | 'GitHub roles include "ghpull" and "ghissue", ' '"%s" is invalid.' % name, 94 | line=lineno, 95 | ) 96 | prb = inliner.problematic(rawtext, rawtext, msg) 97 | return [prb], [msg] 98 | node = make_link_node(rawtext, app, category, str(issue_num), options) 99 | return [node], [] 100 | 101 | 102 | def ghuser_role(name, rawtext, text, lineno, inliner, options={}, content=[]): 103 | """Link to a GitHub user. 104 | 105 | Returns 2 part tuple containing list of nodes to insert into the 106 | document and a list of system messages. Both are allowed to be 107 | empty. 108 | 109 | :param name: The role name used in the document. 110 | :param rawtext: The entire markup snippet, with role. 111 | :param text: The text marked with the role. 112 | :param lineno: The line number where rawtext appears in the input. 113 | :param inliner: The inliner instance that called us. 114 | :param options: Directive options for customization. 115 | :param content: The directive content for customization. 116 | """ 117 | app = inliner.document.settings.env.app 118 | # app.info('user link %r' % text) 119 | ref = "https://github.com/" + text 120 | node = nodes.reference(rawtext, text, refuri=ref, **options) 121 | return [node], [] 122 | 123 | 124 | def ghcommit_role(name, rawtext, text, lineno, inliner, options={}, content=[]): 125 | """Link to a GitHub commit. 126 | 127 | Returns 2 part tuple containing list of nodes to insert into the 128 | document and a list of system messages. Both are allowed to be 129 | empty. 130 | 131 | :param name: The role name used in the document. 132 | :param rawtext: The entire markup snippet, with role. 133 | :param text: The text marked with the role. 134 | :param lineno: The line number where rawtext appears in the input. 135 | :param inliner: The inliner instance that called us. 136 | :param options: Directive options for customization. 137 | :param content: The directive content for customization. 138 | """ 139 | app = inliner.document.settings.env.app 140 | # app.info('user link %r' % text) 141 | try: 142 | base = app.config.github_project_url 143 | if not base: 144 | raise AttributeError 145 | if not base.endswith("/"): 146 | base += "/" 147 | except AttributeError as err: 148 | raise ValueError( 149 | "github_project_url configuration value is not set (%s)" % str(err) 150 | ) 151 | 152 | ref = base + text 153 | node = nodes.reference(rawtext, text[:6], refuri=ref, **options) 154 | return [node], [] 155 | 156 | 157 | def setup(app): 158 | """Install the plugin. 159 | 160 | :param app: Sphinx application context. 161 | """ 162 | # app.info('Initializing GitHub plugin') 163 | app.add_role("ghissue", ghissue_role) 164 | app.add_role("ghpull", ghissue_role) 165 | app.add_role("ghuser", ghuser_role) 166 | app.add_role("ghcommit", ghcommit_role) 167 | app.add_config_value("github_project_url", None, "env") 168 | return 169 | -------------------------------------------------------------------------------- /telnetlib3/tests/test_environ.py: -------------------------------------------------------------------------------- 1 | """Test NEW_ENVIRON, rfc-1572_.""" 2 | 3 | # std imports 4 | import asyncio 5 | 6 | # local imports 7 | import telnetlib3 8 | import telnetlib3.stream_writer 9 | from telnetlib3.tests.accessories import unused_tcp_port, bind_host 10 | 11 | # 3rd party 12 | import pytest 13 | 14 | 15 | async def test_telnet_server_on_environ(bind_host, unused_tcp_port): 16 | """Test Server's callback method on_environ().""" 17 | # given 18 | from telnetlib3.telopt import IAC, WILL, SB, SE, IS, NEW_ENVIRON 19 | 20 | _waiter = asyncio.Future() 21 | 22 | class ServerTestEnviron(telnetlib3.TelnetServer): 23 | def on_environ(self, mapping): 24 | super().on_environ(mapping) 25 | _waiter.set_result(self) 26 | 27 | await telnetlib3.create_server( 28 | protocol_factory=ServerTestEnviron, host=bind_host, port=unused_tcp_port 29 | ) 30 | 31 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 32 | 33 | # exercise, 34 | writer.write(IAC + WILL + NEW_ENVIRON) 35 | writer.write( 36 | IAC 37 | + SB 38 | + NEW_ENVIRON 39 | + IS 40 | + telnetlib3.stream_writer._encode_env_buf( 41 | { 42 | # note how the default implementation .upper() cases 43 | # all environment keys. 44 | "aLpHa": "oMeGa", 45 | "beta": "b", 46 | "gamma": "".join(chr(n) for n in range(0, 128)), 47 | } 48 | ) 49 | + IAC 50 | + SE 51 | ) 52 | 53 | srv_instance = await asyncio.wait_for(_waiter, 0.5) 54 | assert srv_instance.get_extra_info("ALPHA") == "oMeGa" 55 | assert srv_instance.get_extra_info("BETA") == "b" 56 | assert srv_instance.get_extra_info("GAMMA") == ( 57 | "".join(chr(n) for n in range(0, 128)) 58 | ) 59 | 60 | 61 | async def test_telnet_client_send_environ(bind_host, unused_tcp_port): 62 | """Test Client's callback method send_environ() for specific requests.""" 63 | # given 64 | _waiter = asyncio.Future() 65 | given_cols = 19 66 | given_rows = 84 67 | given_encoding = "cp437" 68 | given_term = "vt220" 69 | 70 | class ServerTestEnviron(telnetlib3.TelnetServer): 71 | def on_environ(self, mapping): 72 | super().on_environ(mapping) 73 | _waiter.set_result(mapping) 74 | 75 | await telnetlib3.create_server( 76 | protocol_factory=ServerTestEnviron, host=bind_host, port=unused_tcp_port 77 | ) 78 | 79 | reader, writer = await telnetlib3.open_connection( 80 | host=bind_host, 81 | port=unused_tcp_port, 82 | cols=given_cols, 83 | rows=given_rows, 84 | encoding=given_encoding, 85 | term=given_term, 86 | connect_minwait=0.05, 87 | ) 88 | 89 | mapping = await asyncio.wait_for(_waiter, 0.5) 90 | assert mapping == { 91 | "COLUMNS": str(given_cols), 92 | "LANG": "en_US." + given_encoding, 93 | "LINES": str(given_rows), 94 | "TERM": "vt220", 95 | } 96 | 97 | 98 | async def test_telnet_client_send_var_uservar_environ(bind_host, unused_tcp_port): 99 | """Test Client's callback method send_environ() for VAR/USERVAR request.""" 100 | # given 101 | _waiter = asyncio.Future() 102 | given_cols = 19 103 | given_rows = 84 104 | given_encoding = "cp437" 105 | given_term = "vt220" 106 | 107 | class ServerTestEnviron(telnetlib3.TelnetServer): 108 | def on_environ(self, mapping): 109 | super().on_environ(mapping) 110 | _waiter.set_result(mapping) 111 | 112 | def on_request_environ(self): 113 | from telnetlib3.telopt import VAR, USERVAR 114 | 115 | return [VAR, USERVAR] 116 | 117 | await telnetlib3.create_server( 118 | protocol_factory=ServerTestEnviron, 119 | host=bind_host, 120 | port=unused_tcp_port, 121 | ) 122 | 123 | reader, writer = await telnetlib3.open_connection( 124 | host=bind_host, 125 | port=unused_tcp_port, 126 | cols=given_cols, 127 | rows=given_rows, 128 | encoding=given_encoding, 129 | term=given_term, 130 | connect_minwait=0.05, 131 | connect_maxwait=0.05, 132 | ) 133 | 134 | mapping = await asyncio.wait_for(_waiter, 0.5) 135 | # although nothing was demanded by server, 136 | assert mapping == {} 137 | 138 | # the client still volunteered these basic variables, 139 | mapping == { 140 | "COLUMNS": str(given_cols), 141 | "LANG": "en_US." + given_encoding, 142 | "LINES": str(given_rows), 143 | "TERM": "vt220", 144 | } 145 | for key, val in mapping.items(): 146 | assert writer.get_extra_info(key) == val 147 | 148 | 149 | async def test_telnet_server_reject_environ(bind_host, unused_tcp_port): 150 | """Test Client's callback method send_environ() for specific requests.""" 151 | from telnetlib3.telopt import SB, NEW_ENVIRON 152 | 153 | # given 154 | given_cols = 19 155 | given_rows = 84 156 | given_encoding = "cp437" 157 | given_term = "vt220" 158 | 159 | class ServerTestEnviron(telnetlib3.TelnetServer): 160 | def on_request_environ(self): 161 | return None 162 | 163 | await telnetlib3.create_server( 164 | protocol_factory=ServerTestEnviron, 165 | host=bind_host, 166 | port=unused_tcp_port, 167 | ) 168 | 169 | reader, writer = await telnetlib3.open_connection( 170 | host=bind_host, 171 | port=unused_tcp_port, 172 | cols=given_cols, 173 | rows=given_rows, 174 | encoding=given_encoding, 175 | term=given_term, 176 | connect_minwait=0.05, 177 | connect_maxwait=0.05, 178 | ) 179 | 180 | # this causes the client to expect the server to have demanded environment 181 | # values, since it did, of course demand DO NEW_ENVIRON! However, our API 182 | # choice here has chosen not to -- the client then indicates this as a 183 | # failed sub-negotiation (SB + NEW_ENVIRON). 184 | _failed = {key: val for key, val in writer.pending_option.items() if val} 185 | assert _failed == {SB + NEW_ENVIRON: True} 186 | -------------------------------------------------------------------------------- /telnetlib3/tests/test_server_shell_unit.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import types 3 | import sys 4 | 5 | import pytest 6 | 7 | from telnetlib3 import server_shell as ss 8 | from telnetlib3 import slc as slc_mod 9 | from telnetlib3 import client_shell as cs 10 | 11 | 12 | class DummyWriter: 13 | def __init__(self, slctab=None): 14 | self.echos = [] 15 | self.slctab = slctab or slc_mod.generate_slctab() 16 | # minimal attributes for do_toggle (unused here) 17 | self.local_option = types.SimpleNamespace(enabled=lambda opt: False) 18 | self.outbinary = False 19 | self.inbinary = False 20 | self.xon_any = False 21 | self.lflow = True 22 | 23 | def echo(self, data): 24 | self.echos.append(data) 25 | 26 | 27 | def _run_readline(sequence): 28 | """ 29 | Drive ss.readline coroutine with given sequence and return list of commands produced. 30 | """ 31 | w = DummyWriter() 32 | gen = ss.readline(None, w) 33 | # prime the coroutine 34 | gen.send(None) 35 | cmds = [] 36 | for ch in sequence: 37 | out = gen.send(ch) 38 | if out is not None: 39 | cmds.append(out) 40 | return cmds, w.echos 41 | 42 | 43 | def test_readline_basic_and_crlf_and_backspace(): 44 | # simple command, CR terminator 45 | cmds, echos = _run_readline("foo\r") 46 | assert cmds == ["foo"] 47 | assert "".join(echos).endswith("foo") # echoed chars 48 | 49 | # CRLF pair: the LF after CR should be consumed and not yield an empty command 50 | cmds, echos = _run_readline("bar\r\n") 51 | assert cmds == ["bar"] 52 | 53 | # LF as terminator alone 54 | cmds, _ = _run_readline("baz\n") 55 | assert cmds == ["baz"] 56 | 57 | # CR NUL should be treated like CRLF (LF/NUL consumed after CR) 58 | cmds, _ = _run_readline("zip\r\x00zap\r\n") 59 | assert cmds == ["zip", "zap"] 60 | 61 | # backspace handling (^H and DEL): 'help' after correction 62 | cmds, echos = _run_readline("\bhel\blp\r") 63 | assert cmds == ["help"] 64 | # ensure backspace echoing placed sequence '\b \b' 65 | assert "\b \b" in "".join(echos) 66 | 67 | 68 | def test_character_dump_yields_patterns_and_summary(): 69 | it = ss.character_dump(1) # enter loop 70 | first = next(it) 71 | second = next(it) 72 | assert first.startswith("/" * 80) 73 | assert second.startswith("\\" * 80) 74 | 75 | # when kb_limit is 0, no loop, only the summary line is yielded 76 | summary = list(ss.character_dump(0))[-1] 77 | assert summary.endswith("wrote 0 bytes") 78 | 79 | 80 | def test_get_slcdata_contains_expected_sections(): 81 | writer = DummyWriter(slctab=slc_mod.generate_slctab()) 82 | out = ss.get_slcdata(writer) 83 | assert "Special Line Characters:" in out 84 | # a known supported mapping should appear (like SLC_EC) 85 | assert "SLC_EC" in out 86 | # and known unset entries should be listed 87 | assert "Unset by client:" in out and "SLC_BRK" in out 88 | # and some not-supported entries section is present 89 | assert "Not supported by server:" in out 90 | 91 | 92 | @pytest.mark.asyncio 93 | async def test_terminal_determine_mode_no_echo_returns_same(monkeypatch): 94 | # Build a dummy telnet_writer with will_echo False 95 | class TW: 96 | will_echo = False 97 | log = types.SimpleNamespace(debug=lambda *a, **k: None) 98 | 99 | # pytest captures stdin; provide a fake with fileno() for Terminal.__init__ 100 | monkeypatch.setattr(sys, "stdin", types.SimpleNamespace(fileno=lambda: 0)) 101 | 102 | term = cs.Terminal(TW()) 103 | ModeDef = cs.Terminal.ModeDef 104 | 105 | # construct a plausible mode tuple (values aren't important here) 106 | base_mode = ModeDef( 107 | iflag=0xFFFF, 108 | oflag=0xFFFF, 109 | cflag=0xFFFF, 110 | lflag=0xFFFF, 111 | ispeed=38400, 112 | ospeed=38400, 113 | cc=[0] * 32, 114 | ) 115 | 116 | result = term.determine_mode(base_mode) 117 | # must be the exact same object when will_echo is False 118 | assert result is base_mode 119 | 120 | 121 | @pytest.mark.asyncio 122 | async def test_terminal_determine_mode_will_echo_adjusts_flags(monkeypatch): 123 | # Build a dummy telnet_writer with will_echo True 124 | class TW: 125 | will_echo = True 126 | log = types.SimpleNamespace(debug=lambda *a, **k: None) 127 | 128 | # pytest captures stdin; provide a fake with fileno() for Terminal.__init__ 129 | monkeypatch.setattr(sys, "stdin", types.SimpleNamespace(fileno=lambda: 0)) 130 | 131 | term = cs.Terminal(TW()) 132 | ModeDef = cs.Terminal.ModeDef 133 | t = cs.termios 134 | 135 | # Start with flags that should be cleared by determine_mode 136 | iflag = 0 137 | for flag in (t.BRKINT, t.ICRNL, t.INPCK, t.ISTRIP, t.IXON): 138 | iflag |= flag 139 | 140 | # oflag clears OPOST and ONLCR 141 | oflag = t.OPOST | getattr(t, "ONLCR", 0) 142 | 143 | # cflag: set PARENB and a size other than CS8 to ensure it flips 144 | cflag = t.PARENB | getattr(t, "CS7", 0) | getattr(t, "CREAD", 0) 145 | 146 | # lflag: will clear ICANON | IEXTEN | ISIG | ECHO 147 | lflag = t.ICANON | t.IEXTEN | t.ISIG | t.ECHO 148 | 149 | # cc array with different VMIN/VTIME values that should be overridden 150 | cc = [0] * 32 151 | cc[t.VMIN] = 0 152 | cc[t.VTIME] = 1 153 | 154 | base_mode = ModeDef( 155 | iflag=iflag, 156 | oflag=oflag, 157 | cflag=cflag, 158 | lflag=lflag, 159 | ispeed=38400, 160 | ospeed=38400, 161 | cc=list(cc), 162 | ) 163 | 164 | new_mode = term.determine_mode(base_mode) 165 | 166 | # verify input flags cleared 167 | for flag in (t.BRKINT, t.ICRNL, t.INPCK, t.ISTRIP, t.IXON): 168 | assert not (new_mode.iflag & flag) 169 | 170 | # verify output flags cleared 171 | assert not (new_mode.oflag & t.OPOST) 172 | if hasattr(t, "ONLCR"): 173 | assert not (new_mode.oflag & t.ONLCR) 174 | 175 | # verify cflag: PARENB cleared, CS8 set, CSIZE cleared except CS8 176 | assert not (new_mode.cflag & t.PARENB) 177 | assert new_mode.cflag & t.CS8 178 | # CSIZE mask bits should be exactly CS8 now 179 | assert (new_mode.cflag & t.CSIZE) == t.CS8 180 | # CREAD (if present) should remain unchanged 181 | if hasattr(t, "CREAD") and (cflag & t.CREAD): 182 | assert new_mode.cflag & t.CREAD 183 | 184 | # verify lflag cleared for ICANON, IEXTEN, ISIG, ECHO 185 | for flag in (t.ICANON, t.IEXTEN, t.ISIG, t.ECHO): 186 | assert not (new_mode.lflag & flag) 187 | 188 | # cc changes 189 | assert new_mode.cc[t.VMIN] == 1 190 | assert new_mode.cc[t.VTIME] == 0 191 | -------------------------------------------------------------------------------- /telnetlib3/telopt.py: -------------------------------------------------------------------------------- 1 | # Exported from the telnetlib module, which is marked for deprecation in version 2 | # 3.11 and removal in 3.13 3 | LINEMODE = b'"' 4 | NAWS = b"\x1f" 5 | NEW_ENVIRON = b"'" 6 | BINARY = b"\x00" 7 | SGA = b"\x03" 8 | ECHO = b"\x01" 9 | STATUS = b"\x05" 10 | TTYPE = b"\x18" 11 | TSPEED = b" " 12 | LFLOW = b"!" 13 | XDISPLOC = b"#" 14 | IAC = b"\xff" 15 | DONT = b"\xfe" 16 | DO = b"\xfd" 17 | WONT = b"\xfc" 18 | WILL = b"\xfb" 19 | SE = b"\xf0" 20 | NOP = b"\xf1" 21 | TM = b"\x06" 22 | DM = b"\xf2" 23 | BRK = b"\xf3" 24 | IP = b"\xf4" 25 | AO = b"\xf5" 26 | AYT = b"\xf6" 27 | EC = b"\xf7" 28 | EL = b"\xf8" 29 | EOR = b"\x19" 30 | GA = b"\xf9" 31 | SB = b"\xfa" 32 | LOGOUT = b"\x12" 33 | CHARSET = b"*" 34 | SNDLOC = b"\x17" 35 | theNULL = b"\x00" 36 | ENCRYPT = b"&" 37 | AUTHENTICATION = b"%" 38 | TN3270E = b"(" 39 | XAUTH = b")" 40 | RSP = b"+" 41 | COM_PORT_OPTION = b"," 42 | SUPPRESS_LOCAL_ECHO = b"-" 43 | TLS = b"." 44 | KERMIT = b"/" 45 | SEND_URL = b"0" 46 | FORWARD_X = b"1" 47 | PRAGMA_LOGON = b"\x8a" 48 | SSPI_LOGON = b"\x8b" 49 | PRAGMA_HEARTBEAT = b"\x8c" 50 | EXOPL = b"\xff" 51 | X3PAD = b"\x1e" 52 | VT3270REGIME = b"\x1d" 53 | TTYLOC = b"\x1c" 54 | SUPDUPOUTPUT = b"\x16" 55 | SUPDUP = b"\x15" 56 | DET = b"\x14" 57 | BM = b"\x13" 58 | XASCII = b"\x11" 59 | RCP = b"\x02" 60 | NAMS = b"\x04" 61 | RCTE = b"\x07" 62 | NAOL = b"\x08" 63 | NAOP = b"\t" 64 | NAOCRD = b"\n" 65 | NAOHTS = b"\x0b" 66 | NAOHTD = b"\x0c" 67 | NAOFFD = b"\r" 68 | NAOVTS = b"\x0e" 69 | NAOVTD = b"\x0f" 70 | NAOLFD = b"\x10" 71 | 72 | __all__ = ( 73 | "ABORT", 74 | "ACCEPTED", 75 | "AO", 76 | "AUTHENTICATION", 77 | "AYT", 78 | "BINARY", 79 | "BM", 80 | "BRK", 81 | "CHARSET", 82 | "CMD_EOR", 83 | "COM_PORT_OPTION", 84 | "DET", 85 | "DM", 86 | "DO", 87 | "DONT", 88 | "EC", 89 | "ECHO", 90 | "EL", 91 | "ENCRYPT", 92 | "EOF", 93 | "EOR", 94 | "ESC", 95 | "EXOPL", 96 | "FORWARD_X", 97 | "GA", 98 | "IAC", 99 | "INFO", 100 | "IP", 101 | "IS", 102 | "KERMIT", 103 | "LFLOW", 104 | "LFLOW_OFF", 105 | "LFLOW_ON", 106 | "LFLOW_RESTART_ANY", 107 | "LFLOW_RESTART_XON", 108 | "LINEMODE", 109 | "LOGOUT", 110 | "MCCP2_COMPRESS", 111 | "MCCP_COMPRESS", 112 | "GMCP", 113 | "NAMS", 114 | "NAOCRD", 115 | "NAOFFD", 116 | "NAOHTD", 117 | "NAOHTS", 118 | "NAOL", 119 | "NAOLFD", 120 | "NAOP", 121 | "NAOVTD", 122 | "NAOVTS", 123 | "NAWS", 124 | "NEW_ENVIRON", 125 | "NOP", 126 | "PRAGMA_HEARTBEAT", 127 | "PRAGMA_LOGON", 128 | "RCP", 129 | "RCTE", 130 | "REJECTED", 131 | "REQUEST", 132 | "RSP", 133 | "SB", 134 | "SE", 135 | "SEND", 136 | "SEND_URL", 137 | "SGA", 138 | "SNDLOC", 139 | "SSPI_LOGON", 140 | "STATUS", 141 | "SUPDUP", 142 | "SUPDUPOUTPUT", 143 | "SUPPRESS_LOCAL_ECHO", 144 | "SUSP", 145 | "TLS", 146 | "TM", 147 | "TN3270E", 148 | "TSPEED", 149 | "TTABLE_ACK", 150 | "TTABLE_IS", 151 | "TTABLE_NAK", 152 | "TTABLE_REJECTED", 153 | "TTYLOC", 154 | "TTYPE", 155 | "USERVAR", 156 | "VALUE", 157 | "VAR", 158 | "VT3270REGIME", 159 | "WILL", 160 | "WONT", 161 | "X3PAD", 162 | "XASCII", 163 | "XAUTH", 164 | "XDISPLOC", 165 | "theNULL", 166 | "name_command", 167 | "name_commands", 168 | ) 169 | 170 | (EOF, SUSP, ABORT, CMD_EOR) = (bytes([const]) for const in range(236, 240)) 171 | (IS, SEND, INFO) = (bytes([const]) for const in range(3)) 172 | (VAR, VALUE, ESC, USERVAR) = (bytes([const]) for const in range(4)) 173 | (LFLOW_OFF, LFLOW_ON, LFLOW_RESTART_ANY, LFLOW_RESTART_XON) = ( 174 | bytes([const]) for const in range(4) 175 | ) 176 | (REQUEST, ACCEPTED, REJECTED, TTABLE_IS, TTABLE_REJECTED, TTABLE_ACK, TTABLE_NAK) = ( 177 | bytes([const]) for const in range(1, 8) 178 | ) 179 | (MCCP_COMPRESS, MCCP2_COMPRESS) = (bytes([85]), bytes([86])) 180 | GMCP = bytes([201]) 181 | 182 | #: List of globals that may match an iac command option bytes 183 | _DEBUG_OPTS = dict( 184 | [ 185 | (value, key) 186 | for key, value in globals().items() 187 | if key 188 | in ( 189 | "LINEMODE", 190 | "LMODE_FORWARDMASK", 191 | "NAWS", 192 | "NEW_ENVIRON", 193 | "ENCRYPT", 194 | "AUTHENTICATION", 195 | "BINARY", 196 | "SGA", 197 | "ECHO", 198 | "STATUS", 199 | "TTYPE", 200 | "TSPEED", 201 | "LFLOW", 202 | "XDISPLOC", 203 | "IAC", 204 | "DONT", 205 | "DO", 206 | "WONT", 207 | "WILL", 208 | "SE", 209 | "NOP", 210 | "DM", 211 | "TM", 212 | "BRK", 213 | "IP", 214 | "ABORT", 215 | "AO", 216 | "AYT", 217 | "EC", 218 | "EL", 219 | "EOR", 220 | "GA", 221 | "SB", 222 | "EOF", 223 | "SUSP", 224 | "ABORT", 225 | "CMD_EOR", 226 | "LOGOUT", 227 | "CHARSET", 228 | "SNDLOC", 229 | "MCCP_COMPRESS", 230 | "MCCP2_COMPRESS", 231 | "GMCP", 232 | "ENCRYPT", 233 | "AUTHENTICATION", 234 | "TN3270E", 235 | "XAUTH", 236 | "RSP", 237 | "COM_PORT_OPTION", 238 | "SUPPRESS_LOCAL_ECHO", 239 | "TLS", 240 | "KERMIT", 241 | "SEND_URL", 242 | "FORWARD_X", 243 | "PRAGMA_LOGON", 244 | "SSPI_LOGON", 245 | "PRAGMA_HEARTBEAT", 246 | "EXOPL", 247 | "X3PAD", 248 | "VT3270REGIME", 249 | "TTYLOC", 250 | "SUPDUPOUTPUT", 251 | "SUPDUP", 252 | "DET", 253 | "BM", 254 | "XASCII", 255 | "RCP", 256 | "NAMS", 257 | "RCTE", 258 | "NAOL", 259 | "NAOP", 260 | "NAOCRD", 261 | "NAOHTS", 262 | "NAOHTD", 263 | "NAOFFD", 264 | "NAOVTS", 265 | "NAOVTD", 266 | "NAOLFD", 267 | ) 268 | ] 269 | ) 270 | 271 | 272 | def name_command(byte): 273 | """Return string description for (maybe) telnet command byte.""" 274 | return _DEBUG_OPTS.get(byte, repr(byte)) 275 | 276 | 277 | def name_commands(cmds, sep=" "): 278 | """Return string description for array of (maybe) telnet command bytes.""" 279 | return sep.join([name_command(bytes([byte])) for byte in cmds]) 280 | -------------------------------------------------------------------------------- /docs/rfcs.rst: -------------------------------------------------------------------------------- 1 | RFCs 2 | ==== 3 | 4 | Implemented 5 | ----------- 6 | 7 | * :rfc:`727`, "Telnet Logout Option," Apr 1977. 8 | * :rfc:`779`, "Telnet Send-Location Option", Apr 1981. 9 | * :rfc:`854`, "Telnet Protocol Specification", May 1983. 10 | * :rfc:`855`, "Telnet Option Specifications", May 1983. 11 | * :rfc:`856`, "Telnet Binary Transmission", May 1983. 12 | * :rfc:`857`, "Telnet Echo Option", May 1983. 13 | * :rfc:`858`, "Telnet Suppress Go Ahead Option", May 1983. 14 | * :rfc:`859`, "Telnet Status Option", May 1983. 15 | * :rfc:`860`, "Telnet Timing mark Option", May 1983. 16 | * :rfc:`885`, "Telnet End of Record Option", Dec 1983. 17 | * :rfc:`1073`, "Telnet Window Size Option", Oct 1988. 18 | * :rfc:`1079`, "Telnet Terminal Speed Option", Dec 1988. 19 | * :rfc:`1091`, "Telnet Terminal-Type Option", Feb 1989. 20 | * :rfc:`1096`, "Telnet X Display Location Option", Mar 1989. 21 | * :rfc:`1123`, "Requirements for Internet Hosts", Oct 1989. 22 | * :rfc:`1184`, "Telnet Linemode Option (extended options)", Oct 1990. 23 | * :rfc:`1372`, "Telnet Remote Flow Control Option", Oct 1992. 24 | * :rfc:`1408`, "Telnet Environment Option", Jan 1993. 25 | * :rfc:`1571`, "Telnet Environment Option Interoperability Issues", Jan 1994. 26 | * :rfc:`1572`, "Telnet Environment Option", Jan 1994. 27 | * :rfc:`2066`, "Telnet Charset Option", Jan 1997. 28 | 29 | Not Implemented 30 | --------------- 31 | 32 | * :rfc:`861`, "Telnet Extended Options List", May 1983. describes a method of 33 | negotiating options after all possible 255 option bytes are exhausted by 34 | future implementations. This never happened (about 100 remain), it was 35 | perhaps, ambitious in thinking more protocols would incorporate Telnet (such 36 | as FTP did). 37 | * :rfc:`927`, "TACACS_ User Identification Telnet Option", describes a method 38 | of identifying terminal clients by a 32-bit UUID, providing a form of 39 | 'rlogin'. This system, published in 1984, was designed for MILNET_ by BBN_, 40 | and the actual TACACS_ implementation is undocumented, though partially 41 | re-imagined by Cisco in :rfc:`1492`. Essentially, the user's credentials are 42 | forwarded to a TACACS_ daemon to verify that the client does in fact have 43 | access. The UUID is a form of an early Kerberos_ token. 44 | * :rfc:`933`, "Output Marking Telnet Option", describes a method of sending 45 | banners, such as displayed on login, with an associated ID to be stored by 46 | the client. The server may then indicate at which time during the session 47 | the banner is relevant. This was implemented by Mitre_ for DOD installations 48 | that might, for example, display various levels of "TOP SECRET" messages 49 | each time a record is opened -- preferably on the top, bottom, left or right 50 | of the screen. 51 | * :rfc:`946`, "Telnet Terminal Location Number Option", only known to be 52 | implemented at Carnegie Mellon University in the mid-1980's, this was a 53 | mechanism to identify a Terminal by ID, which would then be read and 54 | forwarded by gatewaying hosts. So that user traveling from host A -> B -> C 55 | appears as though his "from" address is host A in the system "who" and 56 | "finger" services. There exists more appropriate solutions, such as the 57 | "Report Terminal ID" sequences ``CSI + c`` and ``CSI + 0c`` for vt102, and 58 | ``ESC + z`` (vt52), which sends a terminal ID in-band as ASCII. 59 | * :rfc:`1041`, "Telnet 3270 Regime Option", Jan 1988 60 | * :rfc:`1043`, "Telnet Data Entry Terminal Option", Feb 1988 61 | * :rfc:`1097`, "Telnet Subliminal-Message Option", Apr 1989 62 | * :rfc:`1143`, "The Q Method of Implementing .. Option Negotiation", Feb 1990 63 | * :rfc:`1205`, "5250 Telnet Interface", Feb 1991 64 | * :rfc:`1411`, "Telnet Authentication: Kerberos_ Version 4", Jan 1993 65 | * :rfc:`1412`, "Telnet Authentication: SPX" 66 | * :rfc:`1416`, "Telnet Authentication Option" 67 | * :rfc:`2217`, "Telnet Com Port Control Option", Oct 1997 68 | 69 | Additional Resources 70 | -------------------- 71 | 72 | These RFCs predate, or are superseded by, :rfc:`854`, but may be relevant for 73 | study of the telnet protocol. 74 | 75 | * :rfc:`97` A First Cut at a Proposed Telnet Protocol 76 | * :rfc:`137` Telnet Protocol. 77 | * :rfc:`139` Discussion of Telnet Protocol. 78 | * :rfc:`318` Telnet Protocol. 79 | * :rfc:`328` Suggested Telnet Protocol Changes. 80 | * :rfc:`340` Proposed Telnet Changes. 81 | * :rfc:`393` Comments on TELNET Protocol Changes. 82 | * :rfc:`435` Telnet Issues. 83 | * :rfc:`513` Comments on the new Telnet Specifications. 84 | * :rfc:`529` A Note on Protocol Synch Sequences. 85 | * :rfc:`559` Comments on the new Telnet Protocol and its Implementation. 86 | * :rfc:`563` Comments on the RCTE Telnet Option. 87 | * :rfc:`593` Telnet and FTP Implementation Schedule Change. 88 | * :rfc:`595` Some Thoughts in Defense of the Telnet Go-Ahead. 89 | * :rfc:`596` Second Thoughts on Telnet Go-Ahead. 90 | * :rfc:`652` Telnet Output Carriage-Return Disposition Option. 91 | * :rfc:`653` Telnet Output Horizontal Tabstops Option. 92 | * :rfc:`654` Telnet Output Horizontal Tab Disposition Option. 93 | * :rfc:`655` Telnet Output Formfeed Disposition Option. 94 | * :rfc:`656` Telnet Output Vertical Tabstops Option. 95 | * :rfc:`657` Telnet Output Vertical Tab Disposition Option. 96 | * :rfc:`658` Telnet Output Linefeed Disposition. 97 | * :rfc:`659` Announcing Additional Telnet Options. 98 | * :rfc:`698` Telnet Extended ASCII Option. 99 | * :rfc:`701` August, 1974, Survey of New-Protocol Telnet Servers. 100 | * :rfc:`702` September, 1974, Survey of New-Protocol Telnet Servers. 101 | * :rfc:`703` July, 1975, Survey of New-Protocol Telnet Servers. 102 | * :rfc:`718` Comments on RCTE from the TENEX Implementation Experience. 103 | * :rfc:`719` Discussion on RCTE. 104 | * :rfc:`726` Remote Controlled Transmission and Echoing Telnet Option. 105 | * :rfc:`728` A Minor Pitfall in the Telnet Protocol. 106 | * :rfc:`732` Telnet Data Entry Terminal Option (Obsoletes: :rfc:`731`) 107 | * :rfc:`734` SUPDUP Protocol. 108 | * :rfc:`735` Revised Telnet Byte Macro Option (Obsoletes: :rfc:`729`, 109 | :rfc:`736`) 110 | * :rfc:`749` Telnet SUPDUP-Output Option. 111 | * :rfc:`818` The Remote User Telnet Service. 112 | 113 | The following further describe the telnet protocol and various extensions of 114 | related interest: 115 | 116 | * "Telnet Protocol," MIL-STD-1782_, U.S. Department of Defense, May 1984. 117 | * "Mud Terminal Type Standard," http://tintin.sourceforge.net/mtts/ 118 | * "Mud Client Protocol, Version 2.1," http://www.moo.mud.org/mcp/mcp2.html 119 | * "Telnet Protocol in C-Kermit 8.0 and Kermit 95 2.0," http://www.columbia.edu/kermit/telnet80.html 120 | * "Telnet Negotiation Concepts," http://lpc.psyc.eu/doc/concepts/negotiation 121 | * "Telnet RFCs," http://www.omnifarious.org/~hopper/telnet-rfc.html" 122 | * "Telnet Options", http://www.iana.org/assignments/telnet-options/telnet-options.xml 123 | 124 | .. _MIL-STD-1782: http://www.everyspec.com/MIL-STD/MIL-STD-1700-1799/MIL-STD-1782_6678/ 125 | .. _Mitre: https://mitre.org 126 | .. _MILNET: https://en.wikipedia.org/wiki/MILNET 127 | .. _BBN: https://en.wikipedia.org/wiki/BBN_Technologies 128 | .. _Kerberos: https://en.wikipedia.org/wiki/Kerberos_%28protocol%29 129 | .. _TACACS: https://en.wikipedia.org/wiki/TACACS 130 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = .build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/telnetlib3.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/telnetlib3.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/telnetlib3" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/telnetlib3" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=.build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\telnetlib3.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\telnetlib3.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # std imports 2 | import sys 3 | import os 4 | import json 5 | 6 | # 3rd-party 7 | import sphinx_rtd_theme 8 | import sphinx.environment 9 | from docutils.utils import get_source_line 10 | 11 | # This file is execfile()d with the current directory set to its 12 | # containing dir. 13 | 14 | # If extensions (or modules to document with autodoc) are in another directory, 15 | # add these directories to sys.path here. If the directory is relative to the 16 | # documentation root, use os.path.abspath to make it absolute, like shown here. 17 | sys.path.insert(0, os.path.abspath("sphinxext")) # for github.py 18 | github_project_url = "https://github.com/jquast/telnetlib3" 19 | 20 | # this path insert is needed for readthedocs.org (only) 21 | sys.path.insert( 22 | 0, os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)) 23 | ) 24 | 25 | suppress_warnings = ["image.nonlocal_uri"] 26 | 27 | autodoc_default_flags = [ 28 | "members", 29 | "undoc-members", 30 | "inherited-members", 31 | "show-inheritance", 32 | ] 33 | 34 | # -- General configuration ---------------------------------------------------- 35 | 36 | # If your documentation needs a minimal Sphinx version, state it here. 37 | # needs_sphinx = '1.0' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 41 | extensions = [ 42 | "sphinx.ext.autodoc", 43 | "sphinx.ext.intersphinx", 44 | "sphinx.ext.viewcode", 45 | "github", 46 | ] 47 | 48 | # Add any paths that contain templates here, relative to this directory. 49 | templates_path = ["_templates"] 50 | 51 | # The suffix of source filenames. 52 | source_suffix = ".rst" 53 | 54 | # The encoding of source files. 55 | # source_encoding = 'utf-8-sig' 56 | 57 | # The master toctree document. 58 | master_doc = "index" 59 | 60 | # General information about the project. 61 | project = "telnetlib3" 62 | copyright = "2013 Jeff Quast" 63 | 64 | # The version info for the project you're documenting, acts as replacement for 65 | # |version| and |release|, also used in various other places throughout the 66 | # built documents. 67 | # 68 | # The short X.Y version. 69 | version = "2.0" 70 | 71 | # The full version, including alpha/beta/rc tags. 72 | release = "2.0.8" # keep in sync with setup.py and telnetlib3/accessories.py !! 73 | 74 | # The language for content auto-generated by Sphinx. Refer to documentation 75 | # for a list of supported languages. 76 | # language = None 77 | 78 | # There are two options for replacing |today|: either, you set today to some 79 | # non-false value, then it is used: 80 | # today = '' 81 | # Else, today_fmt is used as the format for a strftime call. 82 | # today_fmt = '%B %d, %Y' 83 | 84 | # List of patterns, relative to source directory, that match files and 85 | # directories to ignore when looking for source files. 86 | exclude_patterns = ["_build"] 87 | 88 | # The reST default role (used for this markup: `text`) to use for all 89 | # documents. 90 | # default_role = None 91 | 92 | # If true, '()' will be appended to :func: etc. cross-reference text. 93 | add_function_parentheses = True 94 | 95 | # If true, the current module name will be prepended to all description 96 | # unit titles (such as .. function::). 97 | add_module_names = False 98 | 99 | # If true, sectionauthor and moduleauthor directives will be shown in the 100 | # output. They are ignored by default. 101 | # show_authors = False 102 | 103 | # The name of the Pygments (syntax highlighting) style to use. 104 | pygments_style = "sphinx" 105 | 106 | # A list of ignored prefixes for module index sorting. 107 | # modindex_common_prefix = [] 108 | 109 | 110 | # -- Options for HTML output -------------------------------------------------- 111 | 112 | # The theme to use for HTML and HTML Help pages. See the documentation for 113 | # a list of builtin themes. 114 | html_theme = "sphinx_rtd_theme" 115 | 116 | # Theme options are theme-specific and customize the look and feel of a theme 117 | # further. For a list of options available for each theme, see the 118 | # documentation. 119 | # html_theme_options = {} 120 | 121 | # Add any paths that contain custom themes here, relative to this directory. 122 | # html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 123 | 124 | # The name for this set of Sphinx documents. If None, it defaults to 125 | # " v documentation". 126 | # html_title = None 127 | 128 | # A shorter title for the navigation bar. Default is the same as html_title. 129 | # html_short_title = None 130 | 131 | # The name of an image file (relative to this directory) to place at the top 132 | # of the sidebar. 133 | # html_logo = None 134 | 135 | # The name of an image file (within the static path) to use as favicon of the 136 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 137 | # pixels large. 138 | # html_favicon = None 139 | 140 | # Add any paths that contain custom static files (such as style sheets) here, 141 | # relative to this directory. They are copied after the builtin static files, 142 | # so a file named "default.css" will overwrite the builtin "default.css". 143 | # html_static_path = ['_static'] 144 | 145 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 146 | # using the given strftime format. 147 | # html_last_updated_fmt = '%b %d, %Y' 148 | 149 | # If true, SmartyPants will be used to convert quotes and dashes to 150 | # typographically correct entities. 151 | # html_use_smartypants = True 152 | 153 | # Custom sidebar templates, maps document names to template names. 154 | # html_sidebars = {} 155 | 156 | # Additional templates that should be rendered to pages, maps page names to 157 | # template names. 158 | # html_additional_pages = {} 159 | 160 | # If false, no module index is generated. 161 | # html_domain_indices = True 162 | 163 | # If false, no index is generated. 164 | # html_use_index = True 165 | 166 | # If true, the index is split into individual pages for each letter. 167 | html_split_index = True 168 | 169 | # If true, links to the reST sources are added to the pages. 170 | html_show_sourcelink = True 171 | 172 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 173 | html_show_sphinx = False 174 | 175 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 176 | html_show_copyright = True 177 | 178 | # If true, an OpenSearch description file will be output, and all pages will 179 | # contain a tag referring to it. The value of this option must be the 180 | # base URL from which the finished HTML is served. 181 | # html_use_opensearch = '' 182 | 183 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 184 | # html_file_suffix = None 185 | 186 | # Output file base name for HTML help builder. 187 | htmlhelp_basename = "telnetlib3_doc" 188 | 189 | 190 | # -- Options for LaTeX output ------------------------------------------------- 191 | 192 | # The paper size ('letter' or 'a4'). 193 | # latex_paper_size = 'letter' 194 | 195 | # The font size ('10pt', '11pt' or '12pt'). 196 | # latex_font_size = '10pt' 197 | 198 | # Grouping the document tree into LaTeX files. List of tuples 199 | # (source start file, target name, title, author, documentclass 200 | # [howto/manual]). 201 | latex_documents = [ 202 | ("index", "telnetlib3.tex", "Telnetlib3 Documentation", "Jeff Quast", "manual"), 203 | ] 204 | 205 | # The name of an image file (relative to this directory) to place at the top of 206 | # the title page. 207 | # latex_logo = None 208 | 209 | # For "manual" documents, if this is true, then toplevel headings are parts, 210 | # not chapters. 211 | # latex_use_parts = False 212 | 213 | # If true, show page references after internal links. 214 | # latex_show_pagerefs = False 215 | 216 | # If true, show URL addresses after external links. 217 | # latex_show_urls = False 218 | 219 | # Additional stuff for the LaTeX preamble. 220 | # latex_preamble = '' 221 | 222 | # Documents to append as an appendix to all manuals. 223 | # latex_appendices = [] 224 | 225 | # If false, no module index is generated. 226 | # latex_domain_indices = True 227 | 228 | 229 | # -- Options for manual page output ------------------------------------------- 230 | 231 | # One entry per manual page. List of tuples 232 | # (source start file, name, description, authors, manual section). 233 | man_pages = [("index", "telnetlib3", "Telnetlib3 Documentation", ["Jeff Quast"], 1)] 234 | 235 | # sort order of API documentation is by their appearance in source code 236 | autodoc_member_order = "bysource" 237 | 238 | # when linking to standard python library, use and prefer python 3 239 | # documentation. 240 | 241 | intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} 242 | 243 | # Both the class’ and the __init__ method’s docstring are concatenated and 244 | # inserted. 245 | autoclass_content = "both" 246 | -------------------------------------------------------------------------------- /telnetlib3/tests/test_ttype.py: -------------------------------------------------------------------------------- 1 | """Test TTYPE, rfc-930_.""" 2 | 3 | # std imports 4 | import asyncio 5 | 6 | # local imports 7 | import telnetlib3 8 | import telnetlib3.stream_writer 9 | from telnetlib3.tests.accessories import unused_tcp_port, bind_host 10 | 11 | # 3rd party 12 | import pytest 13 | 14 | 15 | async def test_telnet_server_on_ttype(bind_host, unused_tcp_port): 16 | """Test Server's callback method on_ttype().""" 17 | # given 18 | from telnetlib3.telopt import IAC, WILL, SB, SE, IS, TTYPE 19 | 20 | _waiter = asyncio.Future() 21 | 22 | class ServerTestTtype(telnetlib3.TelnetServer): 23 | def on_ttype(self, ttype): 24 | super().on_ttype(ttype) 25 | _waiter.set_result(self) 26 | 27 | await telnetlib3.create_server( 28 | protocol_factory=ServerTestTtype, host=bind_host, port=unused_tcp_port 29 | ) 30 | 31 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 32 | 33 | # exercise, 34 | writer.write(IAC + WILL + TTYPE) 35 | writer.write(IAC + SB + TTYPE + IS + b"ALPHA" + IAC + SE) 36 | writer.write(IAC + SB + TTYPE + IS + b"ALPHA" + IAC + SE) 37 | 38 | # verify, 39 | srv_instance = await asyncio.wait_for(_waiter, 0.5) 40 | assert "ALPHA" == srv_instance.get_extra_info("ttype1") 41 | assert "ALPHA" == srv_instance.get_extra_info("ttype2") 42 | assert "ALPHA" == srv_instance.get_extra_info("TERM") 43 | 44 | 45 | async def test_telnet_server_on_ttype_beyond_max(bind_host, unused_tcp_port): 46 | """ 47 | Test Server's callback method on_ttype() with long list. 48 | 49 | After TTYPE_LOOPMAX, we stop requesting and tracking further 50 | terminal types; something of an error (a warning is emitted), 51 | and assume the use of the first we've seen. This is to prevent 52 | an infinite loop with a distant end that is not conforming. 53 | """ 54 | # given 55 | from telnetlib3.telopt import IAC, WILL, SB, SE, IS, TTYPE 56 | 57 | _waiter = asyncio.Future() 58 | given_ttypes = ( 59 | "ALPHA", 60 | "BETA", 61 | "GAMMA", 62 | "DETLA", 63 | "EPSILON", 64 | "ZETA", 65 | "ETA", 66 | "THETA", 67 | "IOTA", 68 | "KAPPA", 69 | "LAMBDA", 70 | "MU", 71 | ) 72 | 73 | class ServerTestTtype(telnetlib3.TelnetServer): 74 | def on_ttype(self, ttype): 75 | super().on_ttype(ttype) 76 | if ttype == given_ttypes[-1]: 77 | _waiter.set_result(self) 78 | 79 | await telnetlib3.create_server( 80 | protocol_factory=ServerTestTtype, host=bind_host, port=unused_tcp_port 81 | ) 82 | 83 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 84 | 85 | # exercise, 86 | writer.write(IAC + WILL + TTYPE) 87 | for send_ttype in given_ttypes: 88 | writer.write(IAC + SB + TTYPE + IS + send_ttype.encode("ascii") + IAC + SE) 89 | 90 | # verify, 91 | srv_instance = await asyncio.wait_for(_waiter, 0.5) 92 | for idx in range(telnetlib3.TelnetServer.TTYPE_LOOPMAX): 93 | key = "ttype{0}".format(idx + 1) 94 | expected = given_ttypes[idx] 95 | assert srv_instance.get_extra_info(key) == expected, (idx, key) 96 | 97 | # ttype{max} gets overwritten continiously, so the last given 98 | # ttype is the last value. 99 | key = "ttype{0}".format(telnetlib3.TelnetServer.TTYPE_LOOPMAX + 1) 100 | expected = given_ttypes[-1] 101 | assert srv_instance.get_extra_info(key) == expected, (idx, key) 102 | assert srv_instance.get_extra_info("TERM") == given_ttypes[-1] 103 | 104 | 105 | async def test_telnet_server_on_ttype_empty(bind_host, unused_tcp_port): 106 | """Test Server's callback method on_ttype(): empty value is ignored.""" 107 | # given 108 | from telnetlib3.telopt import IAC, WILL, SB, SE, IS, TTYPE 109 | 110 | _waiter = asyncio.Future() 111 | given_ttypes = ("ALPHA", "", "BETA") 112 | 113 | class ServerTestTtype(telnetlib3.TelnetServer): 114 | def on_ttype(self, ttype): 115 | super().on_ttype(ttype) 116 | if ttype == given_ttypes[-1]: 117 | _waiter.set_result(self) 118 | 119 | await telnetlib3.create_server( 120 | protocol_factory=ServerTestTtype, host=bind_host, port=unused_tcp_port 121 | ) 122 | 123 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 124 | 125 | # exercise, 126 | writer.write(IAC + WILL + TTYPE) 127 | for send_ttype in given_ttypes: 128 | writer.write(IAC + SB + TTYPE + IS + send_ttype.encode("ascii") + IAC + SE) 129 | 130 | # verify, 131 | srv_instance = await asyncio.wait_for(_waiter, 0.5) 132 | assert srv_instance.get_extra_info("ttype1") == "ALPHA" 133 | assert srv_instance.get_extra_info("ttype2") == "BETA" 134 | assert srv_instance.get_extra_info("TERM") == "BETA" 135 | 136 | 137 | async def test_telnet_server_on_ttype_looped(bind_host, unused_tcp_port): 138 | """Test Server's callback method on_ttype() when value looped.""" 139 | # given 140 | from telnetlib3.telopt import IAC, WILL, SB, SE, IS, TTYPE 141 | 142 | _waiter = asyncio.Future() 143 | given_ttypes = ("ALPHA", "BETA", "GAMMA", "ALPHA") 144 | 145 | class ServerTestTtype(telnetlib3.TelnetServer): 146 | count = 1 147 | 148 | def on_ttype(self, ttype): 149 | super().on_ttype(ttype) 150 | if self.count == len(given_ttypes): 151 | _waiter.set_result(self) 152 | self.count += 1 153 | 154 | await telnetlib3.create_server( 155 | protocol_factory=ServerTestTtype, host=bind_host, port=unused_tcp_port 156 | ) 157 | 158 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 159 | 160 | # exercise, 161 | writer.write(IAC + WILL + TTYPE) 162 | for send_ttype in given_ttypes: 163 | writer.write(IAC + SB + TTYPE + IS + send_ttype.encode("ascii") + IAC + SE) 164 | 165 | # verify, 166 | srv_instance = await asyncio.wait_for(_waiter, 0.5) 167 | assert srv_instance.get_extra_info("ttype1") == "ALPHA" 168 | assert srv_instance.get_extra_info("ttype2") == "BETA" 169 | assert srv_instance.get_extra_info("ttype3") == "GAMMA" 170 | assert srv_instance.get_extra_info("ttype4") == "ALPHA" 171 | assert srv_instance.get_extra_info("TERM") == "ALPHA" 172 | 173 | 174 | async def test_telnet_server_on_ttype_repeated(bind_host, unused_tcp_port): 175 | """Test Server's callback method on_ttype() when value repeats.""" 176 | # given 177 | from telnetlib3.telopt import IAC, WILL, SB, SE, IS, TTYPE 178 | 179 | _waiter = asyncio.Future() 180 | given_ttypes = ("ALPHA", "BETA", "GAMMA", "GAMMA") 181 | 182 | class ServerTestTtype(telnetlib3.TelnetServer): 183 | count = 1 184 | 185 | def on_ttype(self, ttype): 186 | super().on_ttype(ttype) 187 | if self.count == len(given_ttypes): 188 | _waiter.set_result(self) 189 | self.count += 1 190 | 191 | await telnetlib3.create_server( 192 | protocol_factory=ServerTestTtype, host=bind_host, port=unused_tcp_port 193 | ) 194 | 195 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 196 | 197 | # exercise, 198 | writer.write(IAC + WILL + TTYPE) 199 | for send_ttype in given_ttypes: 200 | writer.write(IAC + SB + TTYPE + IS + send_ttype.encode("ascii") + IAC + SE) 201 | 202 | # verify, 203 | srv_instance = await asyncio.wait_for(_waiter, 0.5) 204 | assert srv_instance.get_extra_info("ttype1") == "ALPHA" 205 | assert srv_instance.get_extra_info("ttype2") == "BETA" 206 | assert srv_instance.get_extra_info("ttype3") == "GAMMA" 207 | assert srv_instance.get_extra_info("ttype4") == "GAMMA" 208 | assert srv_instance.get_extra_info("TERM") == "GAMMA" 209 | 210 | 211 | async def test_telnet_server_on_ttype_mud(bind_host, unused_tcp_port): 212 | """Test Server's callback method on_ttype() for MUD clients (MTTS).""" 213 | # given 214 | from telnetlib3.telopt import IAC, WILL, SB, SE, IS, TTYPE 215 | 216 | _waiter = asyncio.Future() 217 | given_ttypes = ("ALPHA", "BETA", "MTTS 137") 218 | 219 | class ServerTestTtype(telnetlib3.TelnetServer): 220 | count = 1 221 | 222 | def on_ttype(self, ttype): 223 | super().on_ttype(ttype) 224 | if self.count == len(given_ttypes): 225 | _waiter.set_result(self) 226 | self.count += 1 227 | 228 | await telnetlib3.create_server( 229 | protocol_factory=ServerTestTtype, host=bind_host, port=unused_tcp_port 230 | ) 231 | 232 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 233 | 234 | # exercise, 235 | writer.write(IAC + WILL + TTYPE) 236 | for send_ttype in given_ttypes: 237 | writer.write(IAC + SB + TTYPE + IS + send_ttype.encode("ascii") + IAC + SE) 238 | 239 | # verify, 240 | srv_instance = await asyncio.wait_for(_waiter, 0.5) 241 | assert srv_instance.get_extra_info("ttype1") == "ALPHA" 242 | assert srv_instance.get_extra_info("ttype2") == "BETA" 243 | assert srv_instance.get_extra_info("ttype3") == "MTTS 137" 244 | assert srv_instance.get_extra_info("TERM") == "BETA" 245 | -------------------------------------------------------------------------------- /docs/example_readline.py: -------------------------------------------------------------------------------- 1 | """ 2 | КОСМОС/300 Lunar server: a demonstration Telnet Server shell. 3 | 4 | With this shell, multiple clients may instruct the lander to: 5 | 6 | - collect rock samples 7 | - launch sample return capsule 8 | - relay a message (talker). 9 | 10 | All input and output is forced uppercase, but unicode is otherwise supported. 11 | 12 | A simple character-at-a-time repl is provided, supporting backspace. 13 | """ 14 | 15 | # std imports 16 | import collections 17 | import contextlib 18 | import logging 19 | import asyncio 20 | 21 | 22 | class Client(collections.namedtuple("Client", ["reader", "writer", "notify_queue"])): 23 | def __str__(self): 24 | return "#{1}".format(*self.writer.get_extra_info("peername")) 25 | 26 | 27 | class Lander(object): 28 | """ 29 | КОСМОС/300 Lunar module. 30 | """ 31 | 32 | collecting = False 33 | capsule_amount = 0 34 | capsule_launched = False 35 | capsule_launching = False 36 | 37 | def __init__(self): 38 | self.log = logging.getLogger("lunar.lander") 39 | self.clients = [] 40 | self._loop = asyncio.get_event_loop() 41 | 42 | def __str__(self): 43 | collector = "RUNNING" if self.collecting else "READY" 44 | capsule = ( 45 | "LAUNCH IN-PROGRESS" 46 | if self.capsule_launching 47 | else ( 48 | "LAUNCHED" 49 | if self.capsule_launched 50 | else "{}/4".format(self.capsule_amount) 51 | ) 52 | ) 53 | clients = ", ".join(map(str, self.clients)) 54 | return "COLLECTOR {}\r\nCAPSULE {}\r\nUPLINKS: {}".format( 55 | collector, capsule, clients 56 | ) 57 | 58 | @contextlib.contextmanager 59 | def register_link(self, reader, writer): 60 | client = Client(reader, writer, notify_queue=asyncio.Queue()) 61 | self.clients.append(client) 62 | try: 63 | self.notify_event("LINK ESTABLISHED TO {}".format(client)) 64 | yield client 65 | 66 | finally: 67 | self.clients.remove(client) 68 | self.notify_event("LOST CONNECTION TO {}".format(client)) 69 | 70 | def notify_event(self, event_msg): 71 | self.log.info(event_msg) 72 | for client in self.clients: 73 | client.notify_queue.put_nowait(event_msg) 74 | 75 | def repl_readline(self, client): 76 | """ 77 | Lander REPL, provides no process, local echo. 78 | """ 79 | from telnetlib3 import WONT, ECHO, SGA 80 | 81 | client.writer.iac(WONT, ECHO) 82 | client.writer.iac(WONT, SGA) 83 | readline = asyncio.ensure_future(client.reader.readline()) 84 | recv_msg = asyncio.ensure_future(client.notify_queue.get()) 85 | client.writer.write("КОСМОС/300: READY\r\n") 86 | wait_for = set([readline, recv_msg]) 87 | try: 88 | while True: 89 | client.writer.write("? ") 90 | 91 | # await (1) client input or (2) system notification 92 | done, pending = await asyncio.wait( 93 | wait_for, return_when=asyncio.FIRST_COMPLETED 94 | ) 95 | 96 | task = done.pop() 97 | wait_for.remove(task) 98 | if task == readline: 99 | # (1) client input 100 | cmd = task.result().rstrip().upper() 101 | 102 | client.writer.echo(cmd) 103 | self.process_command(client, cmd) 104 | 105 | # await next, 106 | readline = asyncio.ensure_future(client.reader.readline()) 107 | wait_for.add(readline) 108 | 109 | else: 110 | # (2) system notification 111 | msg = task.result() 112 | 113 | # await next, 114 | recv_msg = asyncio.ensure_future(client.notify_queue.get()) 115 | wait_for.add(recv_msg) 116 | 117 | # show and display prompt, 118 | client.writer.write("\r\x1b[K{}\r\n".format(msg)) 119 | 120 | finally: 121 | for task in wait_for: 122 | task.cancel() 123 | 124 | async def repl_catime(self, client): 125 | """ 126 | Lander REPL providing character-at-a-time processing. 127 | """ 128 | read_one = asyncio.ensure_future(client.reader.read(1)) 129 | recv_msg = asyncio.ensure_future(client.notify_queue.get()) 130 | wait_for = set([read_one, recv_msg]) 131 | 132 | client.writer.write("КОСМОС/300: READY\r\n") 133 | 134 | while True: 135 | cmd = "" 136 | 137 | # prompt 138 | client.writer.write("? ") 139 | while True: 140 | # await (1) client input (2) system notification 141 | done, pending = await asyncio.wait( 142 | wait_for, return_when=asyncio.FIRST_COMPLETED 143 | ) 144 | 145 | task = done.pop() 146 | wait_for.remove(task) 147 | if task == read_one: 148 | # (1) client input 149 | char = task.result().upper() 150 | 151 | # await next, 152 | read_one = asyncio.ensure_future(client.reader.read(1)) 153 | wait_for.add(read_one) 154 | 155 | if char == "": 156 | # disconnect, exit 157 | return 158 | 159 | elif char in "\r\n": 160 | # carriage return, process command. 161 | break 162 | 163 | elif char in "\b\x7f": 164 | # backspace 165 | cmd = cmd[:-1] 166 | client.writer.echo("\b") 167 | 168 | else: 169 | # echo input 170 | cmd += char 171 | client.writer.echo(char) 172 | 173 | else: 174 | # (2) system notification 175 | msg = task.result() 176 | 177 | # await next, 178 | recv_msg = asyncio.ensure_future(client.notify_queue.get()) 179 | wait_for.add(recv_msg) 180 | 181 | # show and display prompt, 182 | client.writer.write("\r\x1b[K{}\r\n".format(msg)) 183 | client.writer.write("? {}".format(cmd)) 184 | 185 | # reached when user pressed return by inner 'break' statement. 186 | self.process_command(client, cmd) 187 | 188 | def process_command(self, client, cmd): 189 | result = "\r\n" 190 | if cmd == "HELP": 191 | result += ( 192 | "COLLECT COLLECT ROCK SAMPLE\r\n" 193 | " STATUS DEVICE STATUS\r\n" 194 | " LAUNCH LAUNCH RETURN CAPSULE\r\n" 195 | " RELAY MESSAGE TRANSMISSION RELAY" 196 | ) 197 | elif cmd == "STATUS": 198 | result += str(self) 199 | elif cmd == "COLLECT": 200 | result += self.collect_sample(client) 201 | elif cmd == "LAUNCH": 202 | result += self.launch_capsule(client) 203 | elif cmd == "RELAY" or cmd.startswith("RELAY ") or cmd.startswith("R "): 204 | cmd, *args = cmd.split(None, 1) 205 | if args: 206 | self.notify_event("RELAY FROM {}: {}".format(client, args[0])) 207 | result = "" 208 | elif cmd: 209 | result += "NOT A COMMAND, {!r}".format(cmd) 210 | client.writer.write(result + "\r\n") 211 | 212 | def launch_capsule(self, client): 213 | if self.capsule_launched: 214 | return "ERROR: NO CAPSULE" 215 | elif self.capsule_launching: 216 | return "ERROR: LAUNCH SEQUENCE IN-PROGRESS" 217 | elif self.collecting: 218 | return "ERROR: COLLECTOR ACTIVE" 219 | self.capsule_launching = True 220 | self.notify_event("CAPSULE LAUNCH SEQUENCE INITIATED!") 221 | asyncio.get_event_loop().call_later(10, self.after_launch) 222 | for count in range(1, 10): 223 | asyncio.get_event_loop().call_later( 224 | count, self.notify_event, "{} ...".format(10 - count) 225 | ) 226 | return "OK" 227 | 228 | def collect_sample(self, client): 229 | if self.collecting: 230 | return "ERROR: COLLECTION ALREADY IN PROGRESS" 231 | elif self.capsule_launched: 232 | return "ERROR: COLLECTOR CAPSULE NOT CONNECTED" 233 | elif self.capsule_launching: 234 | return "ERROR: LAUNCH SEQUENCE IN-PROGRESS." 235 | elif self.capsule_amount >= 4: 236 | return "ERROR: COLLECTOR CAPSULE FULL" 237 | self.collecting = True 238 | self.notify_event("SAMPLE COLLECTION HAS BEGUN") 239 | self._loop.call_later(7, self.collected_sample) 240 | return "OK" 241 | 242 | def collected_sample(self): 243 | self.notify_event("SAMPLE COLLECTED") 244 | self.capsule_amount += 1 245 | self.collecting = False 246 | 247 | def after_launch(self): 248 | self.capsule_launching = False 249 | self.capsule_launched = True 250 | self.notify_event("CAPSULE LAUNCHED SUCCESSFULLY") 251 | 252 | 253 | # each client shares, even communicates through lunar 'lander' instance. 254 | lander = Lander() 255 | 256 | 257 | def shell(reader, writer): 258 | global lander 259 | with lander.register_link(reader, writer) as client: 260 | await lander.repl_readline(client) 261 | -------------------------------------------------------------------------------- /telnetlib3/server_shell.py: -------------------------------------------------------------------------------- 1 | import types 2 | import asyncio 3 | 4 | CR, LF, NUL = "\r\n\x00" 5 | from . import slc 6 | from . import telopt 7 | from . import accessories 8 | 9 | __all__ = ("telnet_server_shell",) 10 | 11 | 12 | async def telnet_server_shell(reader, writer): 13 | """ 14 | A default telnet shell, appropriate for use with telnetlib3.create_server. 15 | 16 | This shell provides a very simple REPL, allowing introspection and state 17 | toggling of the connected client session. 18 | """ 19 | linereader = readline(reader, writer) 20 | linereader.send(None) 21 | 22 | writer.write("Ready." + CR + LF) 23 | 24 | command = None 25 | while not writer.is_closing(): 26 | if command: 27 | writer.write(CR + LF) 28 | writer.write("tel:sh> ") 29 | await writer.drain() 30 | 31 | command = None 32 | while command is None: 33 | await writer.drain() 34 | inp = await reader.read(1) 35 | if not inp: 36 | # close/eof by client at prompt 37 | return 38 | command = linereader.send(inp) 39 | writer.write(CR + LF) 40 | 41 | if command == "quit": 42 | # server hangs up on client 43 | writer.write("Goodbye." + CR + LF) 44 | break 45 | elif command == "help": 46 | writer.write("quit, writer, slc, toggle [option|all], reader, proto, dump") 47 | elif command == "writer": 48 | # show 'writer' status 49 | writer.write(repr(writer)) 50 | elif command == "reader": 51 | # show 'reader' status 52 | writer.write(repr(reader)) 53 | elif command == "proto": 54 | # show 'proto' details of writer 55 | writer.write(repr(writer.protocol)) 56 | elif command == "version": 57 | writer.write(accessories.get_version()) 58 | elif command == "slc": 59 | # show 'slc' support and data tables 60 | writer.write(get_slcdata(writer)) 61 | elif command.startswith("toggle"): 62 | # toggle specified options 63 | option = command[len("toggle ") :] or None 64 | writer.write(do_toggle(writer, option)) 65 | elif command.startswith("dump"): 66 | # dump [kb] [ms_delay] [drain|nodrain] [close|noclose] 67 | # 68 | # this allows you to experiment with the effects of 'drain', and, 69 | # some longer-running programs that check for early break through 70 | # writer.is_closing(). 71 | try: 72 | kb_limit = int(command.split()[1]) 73 | except (ValueError, IndexError): 74 | kb_limit = 1000 75 | try: 76 | delay = int(float(command.split()[2]) / 1000) 77 | except (ValueError, IndexError): 78 | delay = 0 79 | # experiment with large sizes and 'nodrain', the server pretty much 80 | # locks up and stops talking to new clients. 81 | try: 82 | drain = command.split()[3].lower() == "nodrain" 83 | except IndexError: 84 | drain = True 85 | try: 86 | do_close = command.split()[4].lower() == "close" 87 | except IndexError: 88 | do_close = False 89 | writer.write( 90 | "kb_limit={}, delay={}, drain={}, do_close={}:\r\n".format( 91 | kb_limit, delay, drain, do_close 92 | ) 93 | ) 94 | for lineout in character_dump(kb_limit): 95 | if writer.is_closing(): 96 | break 97 | writer.write(lineout) 98 | if drain: 99 | await writer.drain() 100 | if delay: 101 | await asyncio.sleep(delay) 102 | 103 | if not writer.is_closing(): 104 | writer.write("\r\n{} OK".format(kb_limit)) 105 | if do_close: 106 | break 107 | elif command: 108 | writer.write("no such command.") 109 | writer.close() 110 | 111 | 112 | def character_dump(kb_limit): 113 | num_bytes = 0 114 | while (num_bytes) < (kb_limit * 1024): 115 | for char in ("/", "\\"): 116 | lineout = (char * 80) + "\033[1G" 117 | yield lineout 118 | num_bytes += len(lineout) 119 | yield ("\033[1G" + "wrote " + str(num_bytes) + " bytes") 120 | 121 | 122 | async def get_next_ascii(reader, writer): 123 | """ 124 | A coroutine that accepts the next character from `reader` that is not a 125 | part of an ANSI escape sequence. 126 | """ 127 | escape_sequence = False 128 | while not writer.is_closing(): 129 | next_char = await reader.read(1) 130 | if next_char == "\x1b": 131 | escape_sequence = True 132 | elif escape_sequence: 133 | if 61 <= ord(next_char) <= 90 or 97 <= ord(next_char) <= 122: 134 | escape_sequence = False 135 | else: 136 | return next_char 137 | return None 138 | 139 | 140 | @types.coroutine 141 | def readline(reader, writer): 142 | """ 143 | A very crude readline coroutine interface. This is a legacy function 144 | designed for Python 3.4 and remains here for compatibility, superseded by 145 | :func:`~.readline2` 146 | """ 147 | command, inp, last_inp = "", "", "" 148 | inp = yield None 149 | while True: 150 | if inp in (LF, NUL) and last_inp == CR: 151 | last_inp = inp 152 | inp = yield None 153 | 154 | elif inp in (CR, LF): 155 | # first CR or LF yields command 156 | last_inp = inp 157 | inp = yield command 158 | command = "" 159 | 160 | elif inp in ("\b", "\x7f"): 161 | # backspace over input 162 | if command: 163 | command = command[:-1] 164 | writer.echo("\b \b") 165 | last_inp = inp 166 | inp = yield None 167 | 168 | else: 169 | # buffer and echo input 170 | command += inp 171 | writer.echo(inp) 172 | last_inp = inp 173 | inp = yield None 174 | 175 | 176 | async def readline2(reader, writer): 177 | """ 178 | Another crude readline interface as a more amiable asynchronous function 179 | than :func:`readline` supplied with the earliest version of this library. 180 | 181 | This version attempts to filter away escape sequences, such as when a user 182 | presses an arrow or function key. Delete key is backspace. 183 | 184 | However, this function does not handle all possible types of carriage 185 | returns and so it is not used by default shell, :func:`telnet_server_shell`. 186 | """ 187 | command = "" 188 | while True: 189 | next_char = await filter_ansi(reader, writer) 190 | 191 | if next_char == CR: 192 | return command 193 | 194 | elif next_char in (LF, NUL) and len(command) == 0: 195 | continue 196 | 197 | elif next_char in ("\b", "\x7f"): 198 | # backspace over input 199 | if len(command) > 0: 200 | command = command[:-1] 201 | writer.echo("\b \b") 202 | 203 | elif next_char == "": 204 | return None 205 | 206 | else: 207 | command += next_char 208 | writer.echo(next_char) 209 | 210 | 211 | def get_slcdata(writer): 212 | """Display Special Line Editing (SLC) characters.""" 213 | _slcs = sorted( 214 | [ 215 | "{:>15}: {}".format(slc.name_slc_command(slc_func), slc_def) 216 | for (slc_func, slc_def) in sorted(writer.slctab.items()) 217 | if not (slc_def.nosupport or slc_def.val == slc.theNULL) 218 | ] 219 | ) 220 | _unset = sorted( 221 | [ 222 | slc.name_slc_command(slc_func) 223 | for (slc_func, slc_def) in sorted(writer.slctab.items()) 224 | if slc_def.val == slc.theNULL 225 | ] 226 | ) 227 | _nosupport = sorted( 228 | [ 229 | slc.name_slc_command(slc_func) 230 | for (slc_func, slc_def) in sorted(writer.slctab.items()) 231 | if slc_def.nosupport 232 | ] 233 | ) 234 | 235 | return ( 236 | "Special Line Characters:\r\n" 237 | + "\r\n".join(_slcs) 238 | + "\r\nUnset by client: " 239 | + ", ".join(_unset) 240 | + "\r\nNot supported by server: " 241 | + ", ".join(_nosupport) 242 | ) 243 | 244 | 245 | def do_toggle(writer, option): 246 | """Display or toggle telnet session parameters.""" 247 | tbl_opt = { 248 | "echo": writer.local_option.enabled(telopt.ECHO), 249 | "goahead": not writer.local_option.enabled(telopt.SGA), 250 | "outbinary": writer.outbinary, 251 | "inbinary": writer.inbinary, 252 | "binary": writer.outbinary and writer.inbinary, 253 | "xon-any": writer.xon_any, 254 | "lflow": writer.lflow, 255 | } 256 | 257 | if not option: 258 | return "\r\n".join( 259 | "{0} {1}".format(opt, "ON" if enabled else "off") 260 | for opt, enabled in sorted(tbl_opt.items()) 261 | ) 262 | 263 | msgs = [] 264 | if option in ("echo", "all"): 265 | cmd = telopt.WONT if tbl_opt["echo"] else telopt.WILL 266 | writer.iac(cmd, telopt.ECHO) 267 | msgs.append("{} echo.".format(telopt.name_command(cmd).lower())) 268 | 269 | if option in ("goahead", "all"): 270 | cmd = telopt.WILL if tbl_opt["goahead"] else telopt.WONT 271 | writer.iac(cmd, telopt.SGA) 272 | msgs.append("{} suppress go-ahead.".format(telopt.name_command(cmd).lower())) 273 | 274 | if option in ("outbinary", "binary", "all"): 275 | cmd = telopt.WONT if tbl_opt["outbinary"] else telopt.WILL 276 | writer.iac(cmd, telopt.BINARY) 277 | msgs.append("{} outbinary.".format(telopt.name_command(cmd).lower())) 278 | 279 | if option in ("inbinary", "binary", "all"): 280 | cmd = telopt.DONT if tbl_opt["inbinary"] else telopt.DO 281 | writer.iac(cmd, telopt.BINARY) 282 | msgs.append("{} inbinary.".format(telopt.name_command(cmd).lower())) 283 | 284 | if option in ("xon-any", "all"): 285 | writer.xon_any = not tbl_opt["xon-any"] 286 | writer.send_lineflow_mode() 287 | msgs.append("xon-any {}abled.".format("en" if writer.xon_any else "dis")) 288 | 289 | if option in ("lflow", "all"): 290 | writer.lflow = not tbl_opt["lflow"] 291 | writer.send_lineflow_mode() 292 | msgs.append("lineflow {}abled.".format("en" if writer.lflow else "dis")) 293 | 294 | if option not in tbl_opt and option != "all": 295 | msgs.append("toggle: not an option.") 296 | 297 | return "\r\n".join(msgs) 298 | -------------------------------------------------------------------------------- /telnetlib3/tests/test_encoding.py: -------------------------------------------------------------------------------- 1 | """Test Server encoding mixin.""" 2 | 3 | # std imports 4 | import asyncio 5 | 6 | # local imports 7 | import telnetlib3 8 | import telnetlib3.stream_writer 9 | from telnetlib3.tests.accessories import unused_tcp_port, bind_host 10 | 11 | # 3rd party 12 | import pytest 13 | 14 | 15 | async def test_telnet_server_encoding_default(bind_host, unused_tcp_port): 16 | """Default encoding US-ASCII unless it can be negotiated/confirmed!""" 17 | from telnetlib3.telopt import IAC, WONT, TTYPE 18 | 19 | # given 20 | _waiter = asyncio.Future() 21 | 22 | await telnetlib3.create_server( 23 | host=bind_host, 24 | port=unused_tcp_port, 25 | _waiter_connected=_waiter, 26 | connect_maxwait=0.05, 27 | ) 28 | 29 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 30 | 31 | # exercise, quickly failing negotiation/encoding. 32 | writer.write(IAC + WONT + TTYPE) 33 | 34 | # verify, 35 | srv_instance = await asyncio.wait_for(_waiter, 0.5) 36 | assert srv_instance.encoding(incoming=True) == "US-ASCII" 37 | assert srv_instance.encoding(outgoing=True) == "US-ASCII" 38 | assert srv_instance.encoding(incoming=True, outgoing=True) == "US-ASCII" 39 | with pytest.raises(TypeError): 40 | # at least one direction should be specified 41 | srv_instance.encoding() 42 | 43 | 44 | async def test_telnet_client_encoding_default(bind_host, unused_tcp_port): 45 | """Default encoding US-ASCII unless it can be negotiated/confirmed!""" 46 | from telnetlib3.telopt import IAC, WONT, TTYPE 47 | 48 | # given 49 | _waiter = asyncio.Future() 50 | 51 | await asyncio.get_event_loop().create_server( 52 | asyncio.Protocol, bind_host, unused_tcp_port 53 | ) 54 | 55 | reader, writer = await telnetlib3.open_connection( 56 | host=bind_host, port=unused_tcp_port, connect_minwait=0.05 57 | ) 58 | 59 | # after MIN_CONNECT elapsed, client is in US-ASCII state. 60 | assert writer.protocol.encoding(incoming=True) == "US-ASCII" 61 | assert writer.protocol.encoding(outgoing=True) == "US-ASCII" 62 | assert writer.protocol.encoding(incoming=True, outgoing=True) == "US-ASCII" 63 | with pytest.raises(TypeError): 64 | # at least one direction should be specified 65 | writer.protocol.encoding() 66 | 67 | 68 | async def test_telnet_server_encoding_client_will(bind_host, unused_tcp_port): 69 | """Server Default encoding (utf8) incoming when client WILL.""" 70 | from telnetlib3.telopt import IAC, WONT, WILL, TTYPE, BINARY 71 | 72 | # given 73 | _waiter = asyncio.Future() 74 | 75 | await telnetlib3.create_server( 76 | host=bind_host, port=unused_tcp_port, _waiter_connected=_waiter 77 | ) 78 | 79 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 80 | 81 | # exercise, quickly failing negotiation/encoding. 82 | writer.write(IAC + WILL + BINARY) 83 | writer.write(IAC + WONT + TTYPE) 84 | 85 | # verify, 86 | srv_instance = await asyncio.wait_for(_waiter, 0.5) 87 | assert srv_instance.encoding(incoming=True) == "utf8" 88 | assert srv_instance.encoding(outgoing=True) == "US-ASCII" 89 | assert srv_instance.encoding(incoming=True, outgoing=True) == "US-ASCII" 90 | 91 | 92 | async def test_telnet_server_encoding_server_do(bind_host, unused_tcp_port): 93 | """Server's default encoding.""" 94 | from telnetlib3.telopt import IAC, WONT, DO, TTYPE, BINARY 95 | 96 | # given 97 | _waiter = asyncio.Future() 98 | 99 | await telnetlib3.create_server( 100 | host=bind_host, port=unused_tcp_port, _waiter_connected=_waiter 101 | ) 102 | 103 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 104 | 105 | # exercise, server will binary 106 | writer.write(IAC + DO + BINARY) 107 | writer.write(IAC + WONT + TTYPE) 108 | 109 | # verify, 110 | srv_instance = await asyncio.wait_for(_waiter, 0.5) 111 | assert srv_instance.encoding(incoming=True) == "US-ASCII" 112 | assert srv_instance.encoding(outgoing=True) == "utf8" 113 | assert srv_instance.encoding(incoming=True, outgoing=True) == "US-ASCII" 114 | 115 | 116 | async def test_telnet_server_encoding_bidirectional(bind_host, unused_tcp_port): 117 | """Server's default encoding with bi-directional BINARY negotiation.""" 118 | from telnetlib3.telopt import IAC, WONT, DO, WILL, TTYPE, BINARY 119 | 120 | # given 121 | _waiter = asyncio.Future() 122 | 123 | await telnetlib3.create_server( 124 | host=bind_host, 125 | port=unused_tcp_port, 126 | _waiter_connected=_waiter, 127 | connect_maxwait=0.05, 128 | ) 129 | 130 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 131 | 132 | # exercise, bi-directional BINARY with quickly failing negotiation. 133 | writer.write(IAC + DO + BINARY) 134 | writer.write(IAC + WILL + BINARY) 135 | writer.write(IAC + WONT + TTYPE) 136 | 137 | # verify, 138 | srv_instance = await asyncio.wait_for(_waiter, 0.5) 139 | assert srv_instance.encoding(incoming=True) == "utf8" 140 | assert srv_instance.encoding(outgoing=True) == "utf8" 141 | assert srv_instance.encoding(incoming=True, outgoing=True) == "utf8" 142 | 143 | 144 | async def test_telnet_client_and_server_encoding_bidirectional( 145 | bind_host, unused_tcp_port 146 | ): 147 | """Given a default encoding for client and server, client always wins!""" 148 | # given: server prefers latin1, client prefers cp437, client gets their wish 149 | _waiter = asyncio.Future() 150 | 151 | await telnetlib3.create_server( 152 | host=bind_host, 153 | port=unused_tcp_port, 154 | _waiter_connected=_waiter, 155 | encoding="latin1", 156 | connect_maxwait=1.0, 157 | ) 158 | 159 | reader, writer = await telnetlib3.open_connection( 160 | host=bind_host, port=unused_tcp_port, encoding="cp437", connect_minwait=1.0 161 | ) 162 | 163 | srv_instance = await asyncio.wait_for(_waiter, 1.5) 164 | 165 | assert srv_instance.encoding(incoming=True) == "cp437" 166 | assert srv_instance.encoding(outgoing=True) == "cp437" 167 | assert srv_instance.encoding(incoming=True, outgoing=True) == "cp437" 168 | assert writer.protocol.encoding(incoming=True) == "cp437" 169 | assert writer.protocol.encoding(outgoing=True) == "cp437" 170 | assert writer.protocol.encoding(incoming=True, outgoing=True) == "cp437" 171 | 172 | 173 | async def test_telnet_server_encoding_by_LANG(bind_host, unused_tcp_port): 174 | """Server's encoding negotiated by LANG value.""" 175 | from telnetlib3.telopt import ( 176 | IAC, 177 | WONT, 178 | DO, 179 | WILL, 180 | TTYPE, 181 | BINARY, 182 | WILL, 183 | SB, 184 | SE, 185 | IS, 186 | NEW_ENVIRON, 187 | ) 188 | 189 | # given 190 | _waiter = asyncio.Future() 191 | 192 | await telnetlib3.create_server( 193 | host=bind_host, port=unused_tcp_port, _waiter_connected=_waiter 194 | ) 195 | 196 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 197 | 198 | # exercise, bi-direction binary with LANG variable. 199 | writer.write(IAC + DO + BINARY) 200 | writer.write(IAC + WILL + BINARY) 201 | writer.write(IAC + WILL + NEW_ENVIRON) 202 | writer.write( 203 | IAC 204 | + SB 205 | + NEW_ENVIRON 206 | + IS 207 | + telnetlib3.stream_writer._encode_env_buf( 208 | { 209 | "LANG": "uk_UA.KOI8-U", 210 | } 211 | ) 212 | + IAC 213 | + SE 214 | ) 215 | writer.write(IAC + WONT + TTYPE) 216 | 217 | # verify, 218 | srv_instance = await asyncio.wait_for(_waiter, 0.5) 219 | assert srv_instance.encoding(incoming=True) == "KOI8-U" 220 | assert srv_instance.encoding(outgoing=True) == "KOI8-U" 221 | assert srv_instance.encoding(incoming=True, outgoing=True) == "KOI8-U" 222 | assert srv_instance.get_extra_info("LANG") == "uk_UA.KOI8-U" 223 | 224 | 225 | async def test_telnet_server_binary_mode(bind_host, unused_tcp_port): 226 | """Server's encoding=False creates a binary reader/writer interface.""" 227 | from telnetlib3.telopt import IAC, WONT, DO, TTYPE, BINARY 228 | 229 | # given 230 | _waiter = asyncio.Future() 231 | 232 | async def binary_shell(reader, writer): 233 | # our reader and writer should provide binary output 234 | writer.write(b"server_output") 235 | 236 | val = await reader.readexactly(1) 237 | assert val == b"c" 238 | val = await reader.readexactly(len(b"lient ")) 239 | assert val == b"lient " 240 | writer.close() 241 | val = await reader.read() 242 | assert val == b"output" 243 | 244 | await telnetlib3.create_server( 245 | host=bind_host, 246 | port=unused_tcp_port, 247 | shell=binary_shell, 248 | _waiter_connected=_waiter, 249 | encoding=False, 250 | ) 251 | 252 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 253 | 254 | # exercise, server will binary 255 | val = await reader.readexactly(len(IAC + DO + TTYPE)) 256 | assert val == IAC + DO + TTYPE 257 | 258 | writer.write(IAC + WONT + TTYPE) 259 | writer.write(b"client output") 260 | 261 | val = await reader.readexactly(len(b"server_output")) 262 | assert val == b"server_output" 263 | 264 | eof = await reader.read() 265 | assert eof == b"" 266 | 267 | 268 | async def test_telnet_client_and_server_escape_iac_encoding(bind_host, unused_tcp_port): 269 | """Ensure that IAC (byte 255) may be sent across the wire by encoding.""" 270 | # given 271 | _waiter = asyncio.Future() 272 | given_string = "".join(chr(val) for val in list(range(256))) * 2 273 | 274 | await telnetlib3.create_server( 275 | host=bind_host, 276 | port=unused_tcp_port, 277 | _waiter_connected=_waiter, 278 | encoding="iso8859-1", 279 | connect_maxwait=0.05, 280 | ) 281 | 282 | client_reader, client_writer = await telnetlib3.open_connection( 283 | host=bind_host, port=unused_tcp_port, encoding="iso8859-1", connect_minwait=0.05 284 | ) 285 | 286 | server = await asyncio.wait_for(_waiter, 0.5) 287 | 288 | server.writer.write(given_string) 289 | result = await client_reader.readexactly(len(given_string)) 290 | assert result == given_string 291 | server.writer.close() 292 | eof = await asyncio.wait_for(client_reader.read(), 0.5) 293 | assert eof == "" 294 | 295 | 296 | async def test_telnet_client_and_server_escape_iac_binary(bind_host, unused_tcp_port): 297 | """Ensure that IAC (byte 255) may be sent across the wire in binary.""" 298 | # given 299 | _waiter = asyncio.Future() 300 | given_string = bytes(range(256)) * 2 301 | 302 | await telnetlib3.create_server( 303 | host=bind_host, 304 | port=unused_tcp_port, 305 | _waiter_connected=_waiter, 306 | encoding=False, 307 | connect_maxwait=0.05, 308 | ) 309 | 310 | client_reader, client_writer = await telnetlib3.open_connection( 311 | host=bind_host, port=unused_tcp_port, encoding=False, connect_minwait=0.05 312 | ) 313 | 314 | server = await asyncio.wait_for(_waiter, 0.5) 315 | 316 | server.writer.write(given_string) 317 | result = await client_reader.readexactly(len(given_string)) 318 | assert result == given_string 319 | server.writer.close() 320 | eof = await asyncio.wait_for(client_reader.read(), 0.5) 321 | assert eof == b"" 322 | -------------------------------------------------------------------------------- /telnetlib3/tests/test_shell.py: -------------------------------------------------------------------------------- 1 | """Test the server's shell(reader, writer) callback.""" 2 | 3 | # std imports 4 | import asyncio 5 | import logging 6 | 7 | # local imports 8 | import telnetlib3 9 | from telnetlib3.tests.accessories import unused_tcp_port, bind_host 10 | 11 | 12 | async def test_telnet_server_shell_as_coroutine(bind_host, unused_tcp_port): 13 | """Test callback shell(reader, writer) as coroutine of create_server().""" 14 | from telnetlib3.telopt import IAC, DO, WONT, TTYPE 15 | 16 | # given, 17 | _waiter = asyncio.Future() 18 | send_input = "Alpha" 19 | expect_output = "Beta" 20 | expect_hello = IAC + DO + TTYPE 21 | hello_reply = IAC + WONT + TTYPE 22 | 23 | async def shell(reader, writer): 24 | _waiter.set_result(True) 25 | inp = await reader.readexactly(len(send_input)) 26 | assert inp == send_input 27 | writer.write(expect_output) 28 | await writer.drain() 29 | writer.close() 30 | await writer.wait_closed() 31 | 32 | # exercise, 33 | await telnetlib3.create_server(host=bind_host, port=unused_tcp_port, shell=shell) 34 | 35 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 36 | 37 | # given, verify IAC DO TTYPE 38 | hello = await asyncio.wait_for(reader.readexactly(len(expect_hello)), 0.5) 39 | assert hello == expect_hello 40 | 41 | # exercise, 42 | # respond 'WONT TTYPE' to quickly complete negotiation as failed. 43 | writer.write(hello_reply) 44 | 45 | # await for the shell callback to be ready, 46 | await asyncio.wait_for(_waiter, 0.5) 47 | 48 | # client sends input, reads shell output response 49 | writer.write(send_input.encode("ascii")) 50 | server_output = await asyncio.wait_for(reader.readexactly(len(expect_output)), 0.5) 51 | 52 | # verify, 53 | assert server_output.decode("ascii") == expect_output 54 | 55 | # nothing more to read from server; server writer closed in shell. 56 | result = await reader.read() 57 | assert result == b"" 58 | 59 | 60 | async def test_telnet_client_shell_as_coroutine(bind_host, unused_tcp_port): 61 | """Test callback shell(reader, writer) as coroutine of create_server().""" 62 | _waiter = asyncio.Future() 63 | 64 | async def shell(reader, writer): 65 | # just hang up 66 | _waiter.set_result(True) 67 | 68 | # a server that doesn't care 69 | await asyncio.get_event_loop().create_server( 70 | asyncio.Protocol, bind_host, unused_tcp_port 71 | ) 72 | 73 | reader, writer = await telnetlib3.open_connection( 74 | host=bind_host, 75 | port=unused_tcp_port, 76 | shell=shell, 77 | connect_minwait=0.05, 78 | ) 79 | 80 | await asyncio.wait_for(_waiter, 0.5) 81 | 82 | 83 | async def test_telnet_server_shell_make_coro_by_function(bind_host, unused_tcp_port): 84 | """Test callback shell(reader, writer) as function, for create_server().""" 85 | from telnetlib3.telopt import IAC, DO, WONT, TTYPE 86 | 87 | # given, 88 | _waiter = asyncio.Future() 89 | 90 | def shell(reader, writer): 91 | _waiter.set_result(True) 92 | 93 | # exercise, 94 | await telnetlib3.create_server(host=bind_host, port=unused_tcp_port, shell=shell) 95 | 96 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 97 | 98 | # exercise, cancel negotiation and await for the shell callback 99 | writer.write(IAC + WONT + TTYPE) 100 | 101 | # verify, 102 | await asyncio.wait_for(_waiter, 0.5) 103 | 104 | 105 | async def test_telnet_server_no_shell(bind_host, unused_tcp_port): 106 | """Test telnetlib3.TelnetServer() instantiation and connection_made().""" 107 | from telnetlib3.telopt import IAC, DO, WONT, TTYPE 108 | 109 | _waiter = asyncio.Future() 110 | client_expected = IAC + DO + TTYPE + b"beta" 111 | server_expected = IAC + WONT + TTYPE + b"alpha" 112 | # given, 113 | await telnetlib3.create_server( 114 | _waiter_connected=_waiter, host=bind_host, port=unused_tcp_port 115 | ) 116 | 117 | # exercise, 118 | client_reader, client_writer = await asyncio.open_connection( 119 | host=bind_host, port=unused_tcp_port 120 | ) 121 | 122 | client_writer.write(IAC + WONT + TTYPE + b"alpha") 123 | 124 | server = await asyncio.wait_for(_waiter, 0.5) 125 | server.writer.write("beta") 126 | server.writer.close() 127 | client_recv = await client_reader.read() 128 | assert client_recv == client_expected 129 | 130 | 131 | async def test_telnet_server_given_shell(bind_host, unused_tcp_port): 132 | """Iterate all state-reading commands of default telnet_server_shell.""" 133 | from telnetlib3.telopt import IAC, WILL, DO, WONT, ECHO, SGA, BINARY, TTYPE 134 | from telnetlib3 import telnet_server_shell 135 | 136 | # given 137 | _waiter = asyncio.Future() 138 | await telnetlib3.create_server( 139 | host=bind_host, 140 | port=unused_tcp_port, 141 | shell=telnet_server_shell, 142 | _waiter_connected=_waiter, 143 | connect_maxwait=0.05, 144 | timeout=1.25, 145 | limit=13377, 146 | ) 147 | 148 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 149 | 150 | expected = IAC + DO + TTYPE 151 | result = await asyncio.wait_for(reader.readexactly(len(expected)), 0.5) 152 | assert result == expected 153 | 154 | writer.write(IAC + WONT + TTYPE) 155 | 156 | expected = b"Ready.\r\ntel:sh> " 157 | result = await asyncio.wait_for(reader.readexactly(len(expected)), 0.5) 158 | assert result == expected 159 | 160 | server = await asyncio.wait_for(_waiter, 0.5) 161 | server_port = str(server._transport.get_extra_info("peername")[1]) 162 | 163 | # Command & Response table 164 | cmd_output_table = ( 165 | # exercise backspace in input for help command 166 | ( 167 | (b"\bhel\blp\r"), 168 | ( 169 | b"\r\nquit, writer, slc, toggle [option|all], reader, proto, dump" 170 | b"\r\ntel:sh> " 171 | ), 172 | ), 173 | ( 174 | b"writer\r\x00", 175 | ( 176 | b"\r\n" 177 | b"\r\ntel:sh> " 178 | ), 179 | ), 180 | ( 181 | b"reader\r\n", 182 | ( 183 | b"\r\n" 184 | b"\r\ntel:sh> " 185 | ), 186 | ), 187 | ( 188 | b"proto\n", 189 | ( 190 | b"\r\n" 195 | + b"\r\ntel:sh> " 196 | ), 197 | ), 198 | ( 199 | b"slc\r\n", 200 | ( 201 | b"\r\nSpecial Line Characters:" 202 | b"\r\n SLC_AO: (^O, variable|flushout)" 203 | b"\r\n SLC_EC: (^?, variable)" 204 | b"\r\n SLC_EL: (^U, variable)" 205 | b"\r\n SLC_EW: (^W, variable)" 206 | b"\r\n SLC_IP: (^C, variable|flushin|flushout)" 207 | b"\r\n SLC_RP: (^R, variable)" 208 | b"\r\n SLC_AYT: (^T, variable)" 209 | b"\r\n SLC_EOF: (^D, variable)" 210 | b"\r\n SLC_XON: (^Q, variable)" 211 | b"\r\n SLC_SUSP: (^Z, variable|flushin)" 212 | b"\r\n SLC_XOFF: (^S, variable)" 213 | b"\r\n SLC_ABORT: (^\\, variable|flushin|flushout)" 214 | b"\r\n SLC_LNEXT: (^V, variable)" 215 | b"\r\nUnset by client: SLC_BRK, SLC_EOR, SLC_SYNCH" 216 | b"\r\nNot supported by server: SLC_EBOL, SLC_ECR, SLC_EEOL, " 217 | b"SLC_EWR, SLC_FORW1, SLC_FORW2, SLC_INSRT, SLC_MCBOL, " 218 | b"SLC_MCEOL, SLC_MCL, SLC_MCR, SLC_MCWL, SLC_MCWR, SLC_OVER" 219 | b"\r\ntel:sh> " 220 | ), 221 | ), 222 | ( 223 | b"toggle\n", 224 | ( 225 | b"\r\nbinary off" 226 | b"\r\necho off" 227 | b"\r\ngoahead ON" 228 | b"\r\ninbinary off" 229 | b"\r\nlflow ON" 230 | b"\r\noutbinary off" 231 | b"\r\nxon-any off" 232 | b"\r\ntel:sh> " 233 | ), 234 | ), 235 | (b"toggle not-an-option\r", (b"\r\ntoggle: not an option." b"\r\ntel:sh> ")), 236 | ( 237 | b"toggle all\r\n", 238 | ( 239 | b"\r\n" + 240 | # negotiation options received, 241 | # though ignored by our dumb client. 242 | IAC 243 | + WILL 244 | + ECHO 245 | + IAC 246 | + WILL 247 | + SGA 248 | + IAC 249 | + WILL 250 | + BINARY 251 | + IAC 252 | + DO 253 | + BINARY 254 | + b"will echo." 255 | b"\r\nwill suppress go-ahead." 256 | b"\r\nwill outbinary." 257 | b"\r\ndo inbinary." 258 | b"\r\nxon-any enabled." 259 | b"\r\nlineflow disabled." 260 | b"\r\ntel:sh> " 261 | ), 262 | ), 263 | ( 264 | b"toggle\n", 265 | ( 266 | # and therefor the same state values remain unchanged -- 267 | # with exception of lineflow and xon-any, which are 268 | # states toggled by the shell directly (and presumably 269 | # knows what to do with it!) 270 | b"\r\nbinary off" 271 | b"\r\necho off" 272 | b"\r\ngoahead ON" 273 | b"\r\ninbinary off" 274 | b"\r\nlflow off" # flipped 275 | b"\r\noutbinary off" 276 | b"\r\nxon-any ON" # flipped 277 | b"\r\ntel:sh> " 278 | ), 279 | ), 280 | (b"\r\n", (b"\r\ntel:sh> ")), 281 | (b"not-a-command\n", (b"\r\nno such command." b"\r\ntel:sh> ")), 282 | (b"quit\r", b"\r\nGoodbye.\r\n"), 283 | ) 284 | 285 | for cmd, output_expected in cmd_output_table: 286 | logging.debug("cmd=%r, output_expected=%r", cmd, output_expected) 287 | writer.write(cmd) 288 | await writer.drain() 289 | timed_out = False 290 | try: 291 | result = await asyncio.wait_for( 292 | reader.readexactly(len(output_expected)), 0.5 293 | ) 294 | except asyncio.IncompleteReadError as err: 295 | result = err.partial 296 | except TimeoutError: 297 | result = await reader.read(1024) 298 | else: 299 | if result != output_expected: 300 | # fetch extra output, if any, for better understanding of error 301 | result += await reader.read(1024) 302 | assert result == output_expected and timed_out == False 303 | 304 | # nothing more to read. 305 | result = await reader.read() 306 | assert result == b"" 307 | 308 | 309 | async def test_telnet_server_shell_eof(bind_host, unused_tcp_port): 310 | """Test EOF in telnet_server_shell().""" 311 | from telnetlib3.telopt import IAC, WONT, TTYPE 312 | from telnetlib3 import telnet_server_shell 313 | 314 | # given 315 | _waiter_connected = asyncio.Future() 316 | _waiter_closed = asyncio.Future() 317 | 318 | await telnetlib3.create_server( 319 | host=bind_host, 320 | port=unused_tcp_port, 321 | _waiter_connected=_waiter_connected, 322 | _waiter_closed=_waiter_closed, 323 | shell=telnet_server_shell, 324 | timeout=0.25, 325 | ) 326 | 327 | reader, writer = await asyncio.open_connection(host=bind_host, port=unused_tcp_port) 328 | writer.write(IAC + WONT + TTYPE) 329 | 330 | await asyncio.wait_for(_waiter_connected, 0.5) 331 | writer.close() 332 | await asyncio.wait_for(_waiter_closed, 0.5) 333 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/pypi/v/telnetlib3.svg 2 | :alt: Latest Version 3 | :target: https://pypi.python.org/pypi/telnetlib3 4 | 5 | .. image:: https://img.shields.io/pypi/dm/telnetlib3.svg?logo=pypi 6 | :alt: Downloads 7 | :target: https://pypi.python.org/pypi/telnetlib3 8 | 9 | .. image:: https://codecov.io/gh/jquast/telnetlib3/branch/master/graph/badge.svg 10 | :alt: codecov.io Code Coverage 11 | :target: https://codecov.io/gh/jquast/telnetlib3/ 12 | 13 | Introduction 14 | ============ 15 | 16 | telnetlib3 is a Telnet Client and Server library for python. This project 17 | requires python 3.7 and later, using the asyncio_ module. 18 | 19 | .. _asyncio: http://docs.python.org/3.11/library/asyncio.html 20 | 21 | Legacy 'telnetlib' 22 | ------------------ 23 | 24 | This library *also* contains a copy of telnetlib.py_ from the standard library of 25 | Python 3.12 before it was removed in Python 3.13. asyncio_ is not required. 26 | 27 | To migrate code from Python 3.11 and earlier, install this library and change 28 | instances of `telnetlib` to `telnetlib3`: 29 | 30 | .. code-block:: python 31 | 32 | # OLD imports: 33 | import telnetlib 34 | # - or - 35 | from telnetlib import Telnet, ECHO, BINARY 36 | 37 | # NEW imports: 38 | import telnetlib3.telnetlib as telnetlib 39 | # - or - 40 | from telnetlib3 import Telnet, ECHO, BINARY 41 | from telnetlib3.telnetlib import Telnet, ECHO, BINARY 42 | 43 | .. _telnetlib.py: https://docs.python.org/3.12/library/telnetlib.html 44 | 45 | 46 | Quick Example 47 | ------------- 48 | 49 | Writing a Telnet Server that offers a basic "war game": 50 | 51 | .. code-block:: python 52 | 53 | import asyncio, telnetlib3 54 | 55 | async def shell(reader, writer): 56 | writer.write('\r\nWould you like to play a game? ') 57 | inp = await reader.read(1) 58 | if inp: 59 | writer.echo(inp) 60 | writer.write('\r\nThey say the only way to win ' 61 | 'is to not play at all.\r\n') 62 | await writer.drain() 63 | writer.close() 64 | 65 | async def main(): 66 | server = await telnetlib3.create_server('127.0.0.1', 6023, shell=shell) 67 | await server.wait_closed() 68 | 69 | asyncio.run(main()) 70 | 71 | Writing a Telnet Client that plays the "war game" against this server: 72 | 73 | .. code-block:: python 74 | 75 | import asyncio, telnetlib3 76 | 77 | async def shell(reader, writer): 78 | while True: 79 | # read stream until '?' mark is found 80 | outp = await reader.read(1024) 81 | if not outp: 82 | # End of File 83 | break 84 | elif '?' in outp: 85 | # reply all questions with 'y'. 86 | writer.write('y') 87 | 88 | # display all server output 89 | print(outp, flush=True) 90 | 91 | # EOF 92 | print() 93 | 94 | async def main(): 95 | reader, writer = await telnetlib3.open_connection('localhost', 6023, shell=shell) 96 | await writer.protocol.waiter_closed 97 | 98 | asyncio.run(main()) 99 | 100 | Command-line 101 | ------------ 102 | 103 | Two command-line scripts are distributed with this package, 104 | `telnetlib3-client` and `telnetlib3-server`. 105 | 106 | Both command-line scripts accept argument ``--shell=my_module.fn_shell`` 107 | describing a python module path to an function of signature 108 | ``async def shell(reader, writer)``, as in the above examples. 109 | 110 | These scripts also serve as more advanced server and client examples that 111 | perform advanced telnet option negotation and may serve as a basis for 112 | creating your own custom negotiation behaviors. 113 | 114 | Find their filepaths using command:: 115 | 116 | python -c 'import telnetlib3.server;print(telnetlib3.server.__file__, telnetlib3.client.__file__)' 117 | 118 | telnetlib3-client 119 | ~~~~~~~~~~~~~~~~~ 120 | 121 | This is an entry point for command ``python -m telnetlib3.client`` 122 | 123 | Small terminal telnet client. Some example destinations and options:: 124 | 125 | telnetlib3-client --loglevel warn 1984.ws 126 | telnetlib3-client --loglevel debug --logfile logfile.txt nethack.alt.org 127 | telnetlib3-client --encoding=cp437 --force-binary blackflag.acid.org 128 | 129 | See section Encoding_ about arguments, ``--encoding=cp437`` and ``--force-binary``. 130 | 131 | :: 132 | 133 | usage: telnetlib3-client [-h] [--term TERM] [--loglevel LOGLEVEL] 134 | [--logfmt LOGFMT] [--logfile LOGFILE] [--shell SHELL] 135 | [--encoding ENCODING] [--speed SPEED] 136 | [--encoding-errors {replace,ignore,strict}] 137 | [--force-binary] [--connect-minwait CONNECT_MINWAIT] 138 | [--connect-maxwait CONNECT_MAXWAIT] 139 | host [port] 140 | 141 | Telnet protocol client 142 | 143 | positional arguments: 144 | host hostname 145 | port port number (default: 23) 146 | 147 | optional arguments: 148 | -h, --help show this help message and exit 149 | --term TERM terminal type (default: xterm-256color) 150 | --loglevel LOGLEVEL log level (default: warn) 151 | --logfmt LOGFMT log format (default: %(asctime)s %(levelname)s 152 | %(filename)s:%(lineno)d %(message)s) 153 | --logfile LOGFILE filepath (default: None) 154 | --shell SHELL module.function_name (default: 155 | telnetlib3.telnet_client_shell) 156 | --encoding ENCODING encoding name (default: utf8) 157 | --speed SPEED connection speed (default: 38400) 158 | --encoding-errors {replace,ignore,strict} 159 | handler for encoding errors (default: replace) 160 | --force-binary force encoding (default: True) 161 | --connect-minwait CONNECT_MINWAIT 162 | shell delay for negotiation (default: 1.0) 163 | --connect-maxwait CONNECT_MAXWAIT 164 | timeout for pending negotiation (default: 4.0) 165 | 166 | telnetlib3-server 167 | ~~~~~~~~~~~~~~~~~ 168 | 169 | This is an entry point for command ``python -m telnetlib3.server`` 170 | 171 | Telnet server providing the default debugging shell. This provides a simple 172 | shell server that allows introspection of the session's values. 173 | 174 | Example session:: 175 | 176 | tel:sh> help 177 | quit, writer, slc, toggle [option|all], reader, proto 178 | 179 | tel:sh> writer 180 | 181 | 182 | tel:sh> reader 183 | 184 | 185 | tel:sh> toggle all 186 | wont echo. 187 | wont suppress go-ahead. 188 | wont outbinary. 189 | dont inbinary. 190 | xon-any enabled. 191 | lineflow disabled. 192 | 193 | tel:sh> reader 194 | 195 | 196 | tel:sh> writer 197 | 198 | 199 | :: 200 | 201 | usage: telnetlib3-server [-h] [--loglevel LOGLEVEL] [--logfile LOGFILE] 202 | [--logfmt LOGFMT] [--shell SHELL] 203 | [--encoding ENCODING] [--force-binary] 204 | [--timeout TIMEOUT] 205 | [--connect-maxwait CONNECT_MAXWAIT] 206 | [host] [port] 207 | 208 | Telnet protocol server 209 | 210 | positional arguments: 211 | host bind address (default: localhost) 212 | port bind port (default: 6023) 213 | 214 | optional arguments: 215 | -h, --help show this help message and exit 216 | --loglevel LOGLEVEL level name (default: info) 217 | --logfile LOGFILE filepath (default: None) 218 | --logfmt LOGFMT log format (default: %(asctime)s %(levelname)s 219 | %(filename)s:%(lineno)d %(message)s) 220 | --shell SHELL module.function_name (default: telnet_server_shell) 221 | --encoding ENCODING encoding name (default: utf8) 222 | --force-binary force binary transmission (default: False) 223 | --timeout TIMEOUT idle disconnect (0 disables) (default: 300) 224 | --connect-maxwait CONNECT_MAXWAIT 225 | timeout for pending negotiation (default: 4.0) 226 | 227 | Encoding 228 | -------- 229 | 230 | In this client connection example:: 231 | 232 | telnetlib3-client --encoding=cp437 --force-binary blackflag.acid.org 233 | 234 | Note the use of `--encoding=cp437` to translate input and output characters of 235 | the remote end. This example legacy telnet BBS is unable to negotiate about 236 | or present characters in any other encoding but CP437. Without these arguments, 237 | Telnet protocol would dictate our session to be US-ASCII. 238 | 239 | Argument `--force-binary` is *also* required in many cases, with both 240 | ``telnetlib3-client`` and ``telnetlib3-server``. In the original Telnet protocol 241 | specifications, the Network Virtual Terminal (NVT) is defined as 7-bit US-ASCII, 242 | and this is the default state for both ends until negotiated otherwise by RFC-856_ 243 | by negotiation of BINARY TRANSMISSION. 244 | 245 | However, **many common telnet clients and servers fail to negotiate for BINARY** 246 | correctly or at all. Using ``--force-binary`` allows non-ASCII encodings to be 247 | used with those kinds of clients. 248 | 249 | A Telnet Server that prefers "utf8" encoding, and, transmits it even in the case 250 | of failed BINARY negotiation, to support a "dumb" telnet client like netcat:: 251 | 252 | telnetlib3-server --encoding=utf8 --force-binary 253 | 254 | Connecting with "dumb" client:: 255 | 256 | nc -t localhost 6023 257 | 258 | Features 259 | -------- 260 | 261 | The following RFC specifications are implemented: 262 | 263 | * `rfc-727`_, "Telnet Logout Option," Apr 1977. 264 | * `rfc-779`_, "Telnet Send-Location Option", Apr 1981. 265 | * `rfc-854`_, "Telnet Protocol Specification", May 1983. 266 | * `rfc-855`_, "Telnet Option Specifications", May 1983. 267 | * `rfc-856`_, "Telnet Binary Transmission", May 1983. 268 | * `rfc-857`_, "Telnet Echo Option", May 1983. 269 | * `rfc-858`_, "Telnet Suppress Go Ahead Option", May 1983. 270 | * `rfc-859`_, "Telnet Status Option", May 1983. 271 | * `rfc-860`_, "Telnet Timing mark Option", May 1983. 272 | * `rfc-885`_, "Telnet End of Record Option", Dec 1983. 273 | * `rfc-1073`_, "Telnet Window Size Option", Oct 1988. 274 | * `rfc-1079`_, "Telnet Terminal Speed Option", Dec 1988. 275 | * `rfc-1091`_, "Telnet Terminal-Type Option", Feb 1989. 276 | * `rfc-1096`_, "Telnet X Display Location Option", Mar 1989. 277 | * `rfc-1123`_, "Requirements for Internet Hosts", Oct 1989. 278 | * `rfc-1184`_, "Telnet Linemode Option (extended options)", Oct 1990. 279 | * `rfc-1372`_, "Telnet Remote Flow Control Option", Oct 1992. 280 | * `rfc-1408`_, "Telnet Environment Option", Jan 1993. 281 | * `rfc-1571`_, "Telnet Environment Option Interoperability Issues", Jan 1994. 282 | * `rfc-1572`_, "Telnet Environment Option", Jan 1994. 283 | * `rfc-2066`_, "Telnet Charset Option", Jan 1997. 284 | 285 | .. _rfc-727: https://www.rfc-editor.org/rfc/rfc727.txt 286 | .. _rfc-779: https://www.rfc-editor.org/rfc/rfc779.txt 287 | .. _rfc-854: https://www.rfc-editor.org/rfc/rfc854.txt 288 | .. _rfc-855: https://www.rfc-editor.org/rfc/rfc855.txt 289 | .. _rfc-856: https://www.rfc-editor.org/rfc/rfc856.txt 290 | .. _rfc-857: https://www.rfc-editor.org/rfc/rfc857.txt 291 | .. _rfc-858: https://www.rfc-editor.org/rfc/rfc858.txt 292 | .. _rfc-859: https://www.rfc-editor.org/rfc/rfc859.txt 293 | .. _rfc-860: https://www.rfc-editor.org/rfc/rfc860.txt 294 | .. _rfc-885: https://www.rfc-editor.org/rfc/rfc885.txt 295 | .. _rfc-1073: https://www.rfc-editor.org/rfc/rfc1073.txt 296 | .. _rfc-1079: https://www.rfc-editor.org/rfc/rfc1079.txt 297 | .. _rfc-1091: https://www.rfc-editor.org/rfc/rfc1091.txt 298 | .. _rfc-1096: https://www.rfc-editor.org/rfc/rfc1096.txt 299 | .. _rfc-1123: https://www.rfc-editor.org/rfc/rfc1123.txt 300 | .. _rfc-1184: https://www.rfc-editor.org/rfc/rfc1184.txt 301 | .. _rfc-1372: https://www.rfc-editor.org/rfc/rfc1372.txt 302 | .. _rfc-1408: https://www.rfc-editor.org/rfc/rfc1408.txt 303 | .. _rfc-1571: https://www.rfc-editor.org/rfc/rfc1571.txt 304 | .. _rfc-1572: https://www.rfc-editor.org/rfc/rfc1572.txt 305 | .. _rfc-2066: https://www.rfc-editor.org/rfc/rfc2066.txt 306 | 307 | Further Reading 308 | --------------- 309 | 310 | Further documentation available at https://telnetlib3.readthedocs.io/ 311 | -------------------------------------------------------------------------------- /telnetlib3/client_shell.py: -------------------------------------------------------------------------------- 1 | # std imports 2 | import collections 3 | import contextlib 4 | import logging 5 | import asyncio 6 | import sys 7 | 8 | # local 9 | from . import accessories 10 | 11 | __all__ = ("telnet_client_shell",) 12 | 13 | 14 | # TODO: needs 'wait_for' implementation (see DESIGN.rst) 15 | # task = telnet_writer.wait_for(lambda: telnet_writer.local_mode[ECHO] == True) 16 | 17 | if sys.platform == "win32": 18 | 19 | async def telnet_client_shell(telnet_reader, telnet_writer): 20 | raise NotImplementedError( 21 | "win32 not yet supported as telnet client. Please contribute!" 22 | ) 23 | 24 | else: 25 | import termios 26 | import os 27 | import signal 28 | 29 | @contextlib.contextmanager 30 | def _set_tty(fobj, tty_func): 31 | """ 32 | return context manager for manipulating stdin tty state. 33 | 34 | if stdin is not attached to a terminal, no action is performed 35 | before or after yielding. 36 | """ 37 | 38 | class Terminal(object): 39 | """ 40 | Context manager that yields (sys.stdin, sys.stdout) for POSIX systems. 41 | 42 | When sys.stdin is a attached to a terminal, it is configured for 43 | the matching telnet modes negotiated for the given telnet_writer. 44 | """ 45 | 46 | ModeDef = collections.namedtuple( 47 | "mode", ["iflag", "oflag", "cflag", "lflag", "ispeed", "ospeed", "cc"] 48 | ) 49 | 50 | def __init__(self, telnet_writer): 51 | self.telnet_writer = telnet_writer 52 | self._fileno = sys.stdin.fileno() 53 | self._istty = os.path.sameopenfile(0, 1) 54 | 55 | def __enter__(self): 56 | self._save_mode = self.get_mode() 57 | if self._istty: 58 | self.set_mode(self.determine_mode(self._save_mode)) 59 | return self 60 | 61 | def __exit__(self, *_): 62 | if self._istty: 63 | termios.tcsetattr( 64 | self._fileno, termios.TCSAFLUSH, list(self._save_mode) 65 | ) 66 | 67 | def get_mode(self): 68 | if self._istty: 69 | return self.ModeDef(*termios.tcgetattr(self._fileno)) 70 | 71 | def set_mode(self, mode): 72 | termios.tcsetattr(sys.stdin.fileno(), termios.TCSAFLUSH, list(mode)) 73 | 74 | def determine_mode(self, mode): 75 | """ 76 | Return copy of 'mode' with changes suggested for telnet connection. 77 | """ 78 | from telnetlib3.telopt import ECHO 79 | 80 | if not self.telnet_writer.will_echo: 81 | # return mode as-is 82 | self.telnet_writer.log.debug("local echo, linemode") 83 | return mode 84 | self.telnet_writer.log.debug("server echo, kludge mode") 85 | 86 | # "Raw mode", see tty.py function setraw. This allows sending 87 | # of ^J, ^C, ^S, ^\, and others, which might otherwise 88 | # interrupt with signals or map to another character. We also 89 | # trust the remote server to manage CR/LF without mapping. 90 | # 91 | iflag = mode.iflag & ~( 92 | termios.BRKINT 93 | | termios.ICRNL # Do not send INTR signal on break 94 | | termios.INPCK # Do not map CR to NL on input 95 | | termios.ISTRIP # Disable input parity checking 96 | | termios.IXON # Do not strip input characters to 7 bits 97 | ) # Disable START/STOP output control 98 | 99 | # Disable parity generation and detection, 100 | # Select eight bits per byte character size. 101 | cflag = mode.cflag & ~(termios.CSIZE | termios.PARENB) 102 | cflag = cflag | termios.CS8 103 | 104 | # Disable canonical input (^H and ^C processing), 105 | # disable any other special control characters, 106 | # disable checking for INTR, QUIT, and SUSP input. 107 | lflag = mode.lflag & ~( 108 | termios.ICANON | termios.IEXTEN | termios.ISIG | termios.ECHO 109 | ) 110 | 111 | # Disable post-output processing, 112 | # such as mapping LF('\n') to CRLF('\r\n') in output. 113 | oflag = mode.oflag & ~(termios.OPOST | termios.ONLCR) 114 | 115 | # "A pending read is not satisfied until MIN bytes are received 116 | # (i.e., the pending read until MIN bytes are received), or a 117 | # signal is received. A program that uses this case to read 118 | # record-based terminal I/O may block indefinitely in the read 119 | # operation." 120 | cc = list(mode.cc) 121 | cc[termios.VMIN] = 1 122 | cc[termios.VTIME] = 0 123 | 124 | return self.ModeDef( 125 | iflag=iflag, 126 | oflag=oflag, 127 | cflag=cflag, 128 | lflag=lflag, 129 | ispeed=mode.ispeed, 130 | ospeed=mode.ospeed, 131 | cc=cc, 132 | ) 133 | 134 | async def make_stdio(self): 135 | """ 136 | Return (reader, writer) pair for sys.stdin, sys.stdout. 137 | """ 138 | reader = asyncio.StreamReader() 139 | reader_protocol = asyncio.StreamReaderProtocol(reader) 140 | 141 | # Thanks: 142 | # 143 | # https://gist.github.com/nathan-hoad/8966377 144 | # 145 | # After some experimentation, this 'sameopenfile' conditional seems 146 | # allow us to handle stdin as a pipe or a keyboard. In the case of 147 | # a tty, 0 and 1 are the same open file, we use: 148 | # 149 | # https://github.com/orochimarufan/.files/blob/master/bin/mpr 150 | write_fobj = sys.stdout 151 | if self._istty: 152 | write_fobj = sys.stdin 153 | loop = asyncio.get_event_loop() 154 | writer_transport, writer_protocol = await loop.connect_write_pipe( 155 | asyncio.streams.FlowControlMixin, write_fobj 156 | ) 157 | 158 | writer = asyncio.StreamWriter(writer_transport, writer_protocol, None, loop) 159 | 160 | await loop.connect_read_pipe(lambda: reader_protocol, sys.stdin) 161 | 162 | return reader, writer 163 | 164 | async def telnet_client_shell(telnet_reader, telnet_writer): 165 | """ 166 | Minimal telnet client shell for POSIX terminals. 167 | 168 | This shell performs minimal tty mode handling when a terminal is 169 | attached to standard in (keyboard), notably raw mode is often set 170 | and this shell may exit only by disconnect from server, or the 171 | escape character, ^]. 172 | 173 | stdin or stdout may also be a pipe or file, behaving much like nc(1). 174 | """ 175 | keyboard_escape = "\x1d" 176 | 177 | with Terminal(telnet_writer=telnet_writer) as term: 178 | linesep = "\n" 179 | if term._istty and telnet_writer.will_echo: 180 | linesep = "\r\n" 181 | stdin, stdout = await term.make_stdio() 182 | stdout.write( 183 | "Escape character is '{escape}'.{linesep}".format( 184 | escape=accessories.name_unicode(keyboard_escape), linesep=linesep 185 | ).encode() 186 | ) 187 | 188 | # Setup SIGWINCH handler to send NAWS on terminal resize (POSIX only). 189 | # We debounce to avoid flooding on continuous resizes. 190 | loop = asyncio.get_event_loop() 191 | winch_pending = {"h": None} 192 | remove_winch = False 193 | if term._istty: 194 | try: 195 | 196 | def _send_naws(): 197 | from .telopt import NAWS 198 | 199 | try: 200 | if ( 201 | telnet_writer.local_option.enabled(NAWS) 202 | and not telnet_writer.is_closing() 203 | ): 204 | telnet_writer._send_naws() 205 | except Exception: 206 | # Avoid surfacing errors from signal context 207 | pass 208 | 209 | def _on_winch(): 210 | h = winch_pending.get("h") 211 | if h is not None and not h.cancelled(): 212 | try: 213 | h.cancel() 214 | except Exception: 215 | pass 216 | # small delay to debounce rapid resize events 217 | winch_pending["h"] = loop.call_later(0.05, _send_naws) 218 | 219 | if hasattr(signal, "SIGWINCH"): 220 | loop.add_signal_handler(signal.SIGWINCH, _on_winch) 221 | remove_winch = True 222 | except Exception: 223 | # add_signal_handler may be unsupported in some environments 224 | remove_winch = False 225 | 226 | stdin_task = accessories.make_reader_task(stdin) 227 | telnet_task = accessories.make_reader_task(telnet_reader, size=2**24) 228 | wait_for = set([stdin_task, telnet_task]) 229 | while wait_for: 230 | done, pending = await asyncio.wait( 231 | wait_for, return_when=asyncio.FIRST_COMPLETED 232 | ) 233 | 234 | # Prefer handling stdin events first to avoid starvation under heavy output 235 | if stdin_task in done: 236 | task = stdin_task 237 | done.discard(task) 238 | else: 239 | task = done.pop() 240 | wait_for.discard(task) 241 | 242 | telnet_writer.log.debug("task=%s, wait_for=%s", task, wait_for) 243 | 244 | # client input 245 | if task == stdin_task: 246 | inp = task.result() 247 | if inp: 248 | if keyboard_escape in inp.decode(): 249 | # on ^], close connection to remote host 250 | try: 251 | telnet_writer.close() 252 | except Exception: 253 | pass 254 | if telnet_task in wait_for: 255 | telnet_task.cancel() 256 | wait_for.remove(telnet_task) 257 | stdout.write( 258 | "\033[m{linesep}Connection closed.{linesep}".format( 259 | linesep=linesep 260 | ).encode() 261 | ) 262 | # Cleanup resize handler on local escape close 263 | if term._istty and remove_winch: 264 | try: 265 | loop.remove_signal_handler(signal.SIGWINCH) 266 | except Exception: 267 | pass 268 | h = winch_pending.get("h") 269 | if h is not None: 270 | try: 271 | h.cancel() 272 | except Exception: 273 | pass 274 | break 275 | else: 276 | telnet_writer.write(inp.decode()) 277 | stdin_task = accessories.make_reader_task(stdin) 278 | wait_for.add(stdin_task) 279 | else: 280 | telnet_writer.log.debug("EOF from client stdin") 281 | 282 | # server output 283 | if task == telnet_task: 284 | out = task.result() 285 | 286 | # TODO: We should not require to check for '_eof' value, 287 | # but for some systems, htc.zapto.org, it is required, 288 | # where b'' is received even though connection is on?. 289 | if not out and telnet_reader._eof: 290 | if stdin_task in wait_for: 291 | stdin_task.cancel() 292 | wait_for.remove(stdin_task) 293 | stdout.write( 294 | ( 295 | "\033[m{linesep}Connection closed " 296 | "by foreign host.{linesep}" 297 | ) 298 | .format(linesep=linesep) 299 | .encode() 300 | ) 301 | # Cleanup resize handler on remote close 302 | if term._istty and remove_winch: 303 | try: 304 | loop.remove_signal_handler(signal.SIGWINCH) 305 | except Exception: 306 | pass 307 | h = winch_pending.get("h") 308 | if h is not None: 309 | try: 310 | h.cancel() 311 | except Exception: 312 | pass 313 | else: 314 | stdout.write(out.encode() or b":?!?:") 315 | telnet_task = accessories.make_reader_task( 316 | telnet_reader, size=2**24 317 | ) 318 | wait_for.add(telnet_task) 319 | --------------------------------------------------------------------------------