python-web-6

python-web-6

Telnet 和 SSH

  • Telnet
    • Telnet 取名自 Telecommunications 和 Networks 的联合缩写,这是一种在 UNIX 平台上最为人所熟知的网络协议。
    • Telnet 使用端口 23,它是专门为局域网设计的。
    • Telnet 不是一种安全通信协议,因为它并不使用任何安全机制,通过网络/互联网传输明文格式的数据,包括密码,所以谁都能嗅探数据包。
    • Telnet 中没有使用任何验证策略及数据加密方法,因而带来了巨大的安全威胁,这就是为什么telnet不再用于通过公共网络访问网络设备和服务器。
  • SSH
    • SSH 取名自安全外壳(Secure Shell),它现在是通过互联网访问网络设备和服务器的唯一的主要协议。
    • SSH 默认情况下通过端口22运行,该端口号可以更改。
    • SSH 是一种非常安全的协议,因为它共享并发送经过加密的信息,从而为通过互联网等不安全的网络访问的数据提供了机密性和安全性。
    • 一旦通讯的数据使用 SSH 经过加密,就极难解压和读取该数据,所以我们的密码在公共网络上传输也变得很安全。
    • SSH 还使用公钥用于对访问服务器的用户验证身份,这是一种很好的做法,为我们提供了极高的安全性。

终端的特别之处

在使用 Python 进行远程连接的时候,可能会和除了 shell 以外的其他程序进行交互。对于接收到的数据流,往往需要进行观察,以获取所运行的命令生成的数据或错误信息。有时候还需要返回数据,用作远程程序的输入或是远程程序提出问题的响应。

进行上面这些操作时,有时可能会发现程序被无限期地挂起了,始终无法接收到需要的内容。同样地,发送的数据也可能无法被成功传输。为了解决这一问题,这里简要介绍一下 UNIX 终端。

终端是这样一台设备:用户可以通过终端来输入文本,而计算机的响应也会显示在终端的屏幕上。如果 UNIX 机器拥有能够支持物理终端的物理序列端口,那么该设备的目录中就会包含类似 /dev/ttyS1 的项。程序可以用它来向终端发送字符串或者从终端来接收字符串。不过,现在大多数终端其实就是其他程序如:xterm 终端,Gnome 或 KDE 终端程序,或者 Windows 机器上的 PuTTY 客户端。

在计算机终端运行的程序,往往会自动检测正在与其交互的是否是人类用户。只有连接到终端设备时,这些程序才会对其输入进行格式化,使之便于人类用户理解。因此,UNIX 操作系统提供了一些列的 “伪终端” 设备(虚拟终端),其名称类似于 /dev/tty42。如果希望程序认定其在与人类用户进行交互的话,也可以将运行程序的进程连接到这些”伪终端”设备。当启动一个 xterm 或者通过 SSH 进行连接时,其进程会新建一个伪终端,并且对其进行配置,然后运行绑定到 xterm 或者 SSH 的 shell。该 shell 会检查标准输入,如果来自另一个终端,则认为与其交互的是人类用户,并显示命令提示符。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ cat | bash
echo Here we are inside of a bash, with no prompt
# 立即输出
Here we are inside of a bash, with no prompt

python3
print("Hello World")
import sys
print("Is this a terminal?", sys.stdin.isatty())

# Ctrl + D 后出现输出
Hello World
Is this a terminal? False

不仅 bash 不会显示命令提示符,Python 也没有了。但至少 bash 可以对 echo 做出回应。

由于输入并非来自终端,导致 Python 认为它只需要无条件从标准输入读取整个 Python 脚本即可。毕竟输入是一个文件,而文件包含了整个脚本,Python 可能会进行无限的读取操作,直到读到文件的结尾。我们按下 Ctrl + D 就给 cat 发送了文件结束的信号,结束了 cat 自身的输出,完成了整个操作。同时关闭了 Python 的输入,这样就拿到了输出。

在 Python 中,通过调用前面提到的 isatty() 来检查程序是否在和终端交互,然后根据该调用的返回值来设计程序的具体行为。下面是几种常见行为:

  • 交互式程序在与终端进行交互时会显示易于人类用户理解的命令行提示符。当程序认为其输入来自文件的时候,就不会显示命令行提示符。防止屏幕中出现大量冗余的命令行提示符。
  • 当程序不由终端控制时,会关闭命令行编辑功能,并且将控制字符也当做输入流中的普通字符。
  • 人类用户往往希望在键入一条命令后马上能得到响应,因此许多程序在读取终端的输入时会每次只读取一行的输入。但从管道或者文件读取输入时,这些程序会一次性读取大量的字符,然后再对读取的输入进行解释。从前面看到,bash 每次只读一行而 Python 就会一次性读完整个脚本然后从第一行开始执行。
  • 多数程序会根据是否在与终端进行通信来对输出进行调整。如果与程序交互的只是文件或管道,那么程序会等待整块输入都读完后再一次性发送整块输入的响应。

最后两点都涉及了缓冲。最常见的当我们执行到 print() 函数时,并没有立刻在终端显示字符。这是由于 print() 会先将输出结果存入缓冲区,等缓冲区满了才会一次性输出。我们可以调用 flush() 来清空输出的缓冲区。

同样,UNIX 终端设备会对键盘输入进行缓冲,每次读取一行输入。如果只希望每次从输入读入一个字符,可以使用 stty 关闭标准处理模式(每次处理一行)

stty -icanon

ps:记得使用 reset 重置回来。

Telnet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import argparse
import getpass
import telnetlib


def main(hostname, username, password):
t = telnetlib.Telnet(hostname)
# t.set_debuglevel(1)
t.read_until(b"login:")
t.write(username.encode("utf-8"))
t.write(b"\n")
print(t.read_all())
t.read_until(b"assword:") # Password or password
t.write(password.encode("utf-8"))
t.write(b"\n")
# br: b 二进制 r 不转义原生字符
n, match, previous_text = t.expect([br"Login incorrect", br"\$"], 10)
if n == 0:
print("Username and password failed - giving up")
else:
t.write(b"exec uptime\n")
print(t.read_all().decode("utf-8")) # read until the socket closes


if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Use Telnet to login")
parser.add_argument("hostname", help="Remote host to telnet to")
parser.add_argument("username", help="Remote username")
args = parser.parse_args()
# Prompt the user for a password without echoing
# https://docs.python.org/3/library/getpass.html
password = getpass.getpass("Password: ")
main(args.hostname, args.username, password)

注意:这个只是针对 Linux 的连接方法(书上的),现在的很多的服务器不支持23端口的telnet了(尤其是云服务器为了安全就关闭了23端口,改用了22的ssh),所以大部分情况下感觉还是使用 telnet 连接 winserver 的情况多,需要注意的是换行符,不同的系统是不同的,还有一些小的细节问题,比如说 winserver 返回的内容需要用 gbk 来解码等等。

SSH

Author

Ctwo

Posted on

2020-10-18

Updated on

2020-10-25

Licensed under

Comments