How to use `pexpect` on windows to read the "welcome message" like `telnetlib3`?

290 Views Asked by At

I want to use pexpect on Windows to open a telnet connection and to be able to read the "welcome message" (i.e. the output you get when you connect to some device). This is working with telnetlib3 using the following code

import asyncio
import telnetlib3

async def foo():
    reader, writer = await telnetlib3.open_connection("192.168.200.10", 9000)
    data = await asyncio.wait_for(reader.read(4096), timeout=2)
    print(data)

asyncio.run(foo())

but this unfortunately uses asynchronous methods and hence is very cumbersome to use.

To avoid these issues I want to use pexpect to do the same, but with the following code I am not able to get that "welcome message" as for telnetlib3:

import time
from pexpect import popen_spawn

session = popen_spawn.PopenSpawn("telnet 192.168.200.10 9000")
time.sleep(5)
data = session.read_nonblocking(4096, 1)
print(data)

In that case, I do not get any output. Is there something I am missing? How to get the "welcome message" using pexpect on Windows?

Running the command

telnet 192.168.200.10 9000

in a Windows terminal works as expected, i.e. I get this "welcome message".

The problem seems to be part of the "telnet" "negotiation" when the server sends commands like "IAC" and "DO", which I do not read on the client side (see HERE). But why does this work for telnetlib3 but not for telnetlib? What is the difference?

3

There are 3 best solutions below

2
larsks On

This might do what you want:

import time
import pexpect
from pexpect import popen_spawn

session = popen_spawn.PopenSpawn("telnet telehack.com")
try:
    session.expect(pexpect.EOF, timeout=2)
except pexpect.TIMEOUT:
    pass

print(session.before.decode())

Running this against telehack (on my linux system; I am assuming that Windows telnet will behave similarly) produces:

Trying 64.13.139.230...
Connected to telehack.com.
Escape character is '^]'.

Connected to TELEHACK port 218

It is 5:36 am on Monday, July 17, 2023 in Mountain View, California, USA.
There are 125 local users. There are 26647 hosts on the network.
.
.
.

This code works by:

  1. Waiting for a disconnect (pyexpect.EOF)
  2. Using the timeout parameter on the expect method to time out if we don't receive it in 2 seconds
  3. Looking at the before attribute (text received before the expected string) to get the information we want
8
Alireza Roshanzamir On

I've attempted to solve your question using pexcept, but it appears that it doesn't work properly on Windows (refer to these open issues). They all recommend using the wexpect alternative. I also tried wexpect, but it exhibits similar issues to pexpect. Finally, I came across this issue which suggests that the actual problem lies with the Telnet Client in Windows.

By the way, I understand that you want to use pexcept, but I highly recommend considering a pure Python solution. Currently, there are three good alternatives available:

Standard telnetlib library:

First implementation: Utilize the expect method with a timeout and always expect "unavailable data" to prompt the client to wait until the timeout is reached. Alternatively, you can implement the timeout logic manually, but using the library to handle it is recommended:

from telnetlib import Telnet

with Telnet("telehack.com") as telnet:
    index, match, data = telnet.expect([b"unavailable data"], timeout=5)
    print(data.decode())

By executing it, the following result is obtained:

enter image description here

Second implementation: Utilize the read_some, read_eager, or preferably read_very_eager methods, iterating over them for a specific number of attempts, and introducing a sleep period for each attempt:

from telnetlib import Telnet
from time import sleep

MAX_ATTEMPTS = 10
EACH_ATTEMPT_SLEEP = 0.2

with Telnet("telehack.com") as telnet:
    data = ""
    for i in range(MAX_ATTEMPTS):
        data += telnet.read_very_eager().decode()
        sleep(EACH_ATTEMPT_SLEEP)

    print(data)

By executing it, the following result is obtained:

enter image description here

Both implementations work perfectly. However, as stated here, the module will be removed in Python 3.13:

Deprecated since version 3.11, will be removed in version 3.13: The telnetlib module is deprecated (see PEP 594 for details and alternatives).

The synchronous Exscript library:

It offers a highly similar interface to the standard telnetlib library and is endorsed by PEP 0594:

from time import sleep
from Exscript.protocols.telnetlib import Telnet

MAX_ATTEMPTS = 10
EACH_ATTEMPT_SLEEP = 0.5

telnet = Telnet(host="telehack.com")

data = ""
for i in range(MAX_ATTEMPTS):
    data += telnet.read_very_eager()
    sleep(EACH_ATTEMPT_SLEEP)

telnet.close()

print(data)

I attempted this on Windows, but unfortunately, it resulted in a TypeError: ord() expected a character, but string of length 0 found error, which appears to be a bug. Consequently, I made a modification to Exscript\protocols\telnetlib.py, line 48, changing to self.msg('IAC DO %s', ord(opt) if opt else '') with the intention of resolving the issue. After implementing this change, the error disappeared, and the code worked flawlessly:

enter image description here

The asynchronous telnetlib3 library:

This is also a renowned library that is endorsed by PEP 0594. However, it operates entirely in an asynchronous manner, as you have mentioned.

4
DrDark On

I think the reason it is not working may be due to the time.sleep(5) line since connections can take varying amounts of time, therefore it might be executing too soon or too late. This might work:

import time
from pexpect import popen_spawn

session = popen_spawn.PopenSpawn("telnet 192.168.200.10 9000")

try:
    session.expect('Connected.*Escape character is', timeout=5)
except pexpect.TIMEOUT:
    print("ERROR: failed to find expected pattern within timeout period")
    # do something such as retry the operation or exit the program

else:
    data = session.read_nonblocking(4096, 1)
    print(data)

Edit:

I'm not entirely sure, but apparently it is better to use:

session = pexpect.spawn("telnet 192.168.200.10 9000")

I don't have much experience with telnet or pexpect, so this is just what I can find online and through the documentation. Sorry if it doesn't change anything