├── 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 |
--------------------------------------------------------------------------------