Skip to content

Latest commit

 

History

History
609 lines (422 loc) · 29.8 KB

File metadata and controls

609 lines (422 loc) · 29.8 KB

七、使用套接字编程

在用 Python 与各种客户机/服务器交互之后,您将渴望为您选择的任何协议创建自己的定制客户机和服务器。Python 提供了对低级网络接口的良好覆盖。这一切都从 BSD 套接字接口开始。可以假设,Python 有一个socket模块,它为您提供了使用套接字接口所需的功能。如果您曾经使用任何其他语言(如 C/C++)进行过套接字编程,那么您一定会喜欢 Pythonsocket模块。

在本章中,我们将通过创建各种各样的 Python 脚本来探索 socket 模块。

以下是本章的重点内容:

  • 套接字的基础知识
  • 使用 TCP 套接字
  • 使用 UDP 套接字
  • TCP 端口转发
  • 非阻塞套接字 I/O
  • 使用 SSL/TLS 保护套接字
  • 创建自定义 SSL 客户端/服务器

插座基础

任何编程语言中的网络编程都可以从套接字开始。但什么是插座?简单地说,网络套接字是实体可以执行进程间通信的虚拟端点。例如,位于计算机中的一个进程与位于同一台或另一台计算机上的另一个进程交换数据。我们通常将启动通信的第一个进程标记为客户端,后一个进程标记为服务器。

Python 有一种非常简单的方法可以从套接字接口开始。为了更好地理解这一点,让我们先看看全局。下图显示了客户机/服务器交互的流程。这将使您了解如何使用 socket API。

Basics of sockets

通过套接字的客户机/服务器交互

在典型客户机和服务器之间的交互中,正如您可能认为的那样,服务器进程必须工作得更多。创建套接字对象后,服务器进程将该套接字绑定到特定的 IP 地址和端口。这非常类似于带有分机号码的电话连接。在公司办公室,新员工被分配办公桌电话后,通常会被分配到新的分机号码。因此,如果有人打电话给该员工,可以使用他的电话号码和分机建立连接。成功绑定后,服务器进程将开始侦听新的客户端连接。对于有效的客户端会话,服务器进程可以接受客户端进程的请求。此时,我们可以说服务器和客户端之间的连接已经建立。

然后,客户机/服务器进入请求/响应循环。客户端进程向服务器进程发送数据,服务器进程处理数据并向客户端返回响应。当客户端进程完成时,它通过关闭连接退出。此时,服务器进程可能返回到侦听状态。

客户机和服务器之间的上述交互非常简单地表示了实际情况。实际上,任何生产服务器进程都有多个线程或子进程来处理来自数千个客户机通过各自虚拟通道的并发连接。

使用 TCP 套接字

在 Python 中创建套接字对象非常简单。您只需导入socket模块并调用socket()类:

from socket import*
import socket

#create a TCP socket (SOCK_STREAM)
s = socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0)
print('Socket created')

传统上,类需要大量的参数。以下列出了其中一些:

  • 套接字家族:这是套接字的域,如AF_INET(互联网上约 90%的套接字属于这一类)或AF_UNIX,,有时也会用到。在 Python 3 中,您可以使用AF_BLUETOOTH创建蓝牙套接字。
  • 插座类型:根据的需要,您需要指定插座的类型。例如,通过分别指定SOCK_STREAMSOCK_DGRAM,来创建基于 TCP 和 UDP 的套接字。
  • 协议:指定套接字族和类型内协议的变化。通常,它保留为零。

由于许多原因,套接字操作可能不会成功。例如,如果您没有作为普通用户访问特定端口的权限,则可能无法绑定到套接字。这就是为什么在创建套接字或进行某些网络绑定通信时,最好进行适当的错误处理。

让我们尝试将客户端套接字连接到服务器进程。以下代码是连接到服务器套接字的 TCP 客户端套接字的示例:

import socket
import sys 

if __name__ == '__main__':

    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    except socket.error as err:
        print("Failed to crate a socket")
        print("Reason: %s" %str(err))
        sys.exit();

    print('Socket created')

    target_host = input("Enter the target host name to connect: ")
    target_port = input("Enter the target port: ") 

    try:
        sock.connect((target_host, int(target_port)))
        print("Socket Connected to %s on port: %s" %(target_host, target_port))
    sock.shutdown(2)
    except socket.error as err:
        print("Failed to connect to %s on port %s" %(target_host, target_port))
        print("Reason: %s" %str(err))
        sys.exit();

如果运行前面的 TCP 客户端,将显示类似于以下内容的输出:

# python 7_1_tcp_client_socket.py
Socket created
Enter the target host name to connect: 'www.python.org'
Enter the target port: 80
Socket Connected to www.python.org on port: 80

但是,如果套接字创建由于某些原因(例如无效 DNS)失败,将显示类似于以下内容的输出:

# python 7_1_tcp_client_socket.py
Socket created
Enter the target host name to connect: www.asgdfdfdkflakslalalasdsdsds.invalid
Enter the target port: 80
Failed to connect to www.asgdfdfdkflakslalalasdsdsds.invalid on port 80
Reason: [Errno -2] Name or service not known

现在,让我们与服务器交换一些数据。以下代码是简单 TCP 客户端的示例:

import socket

HOST = 'www.linux.org' # or 'localhost'
PORT = 80
BUFSIZ = 4096
ADDR = (HOST, PORT)

if __name__ == '__main__':
    client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_sock.connect(ADDR)

    while True:
        data = 'GET / HTTP/1.0\r\n\r\n'
        if not data:
            break
        client_sock.send(data.encode('utf-8'))
        data = client_sock.recv(BUFSIZ)
        if not data:
            break
        print(data.decode('utf-8'))

    client_sock.close()

如果仔细观察,您会发现前面的代码实际上创建了一个原始 HTTP 客户机,用于从 web 服务器获取网页。发送 HTTPGET请求拉取主页:

# python 7_2_simple_tcp_client.py
HTTP/1.1 200 OK
Date: Sat, 07 Mar 2015 16:23:02 GMT
Server: Apache
Last-Modified: Mon, 17 Feb 2014 03:19:34 GMT
Accept-Ranges: bytes
Content-Length: 111
Connection: close
Content-Type: text/html

<html><head><META HTTP-EQUIV="refresh" CONTENT="0;URL=/cgi- sys/defaultwebpage.cgi"></head><body></body></html>

检查客户端/服务器通信

客户机和服务器之间通过网络数据包交换进行的交互可以使用任何网络数据包捕获工具(如 Wireshark)进行分析。您可以将 Wireshark 配置为按端口或主机过滤数据包。在这种情况下,我们可以通过端口 80 进行过滤。您可以在捕获****选项菜单下获取选项,并在捕获过滤器选项旁边的输入框中输入port 80,如下图所示:

Inspecting the client/server communication

接口选项中,我们选择捕获通过任何接口的数据包。现在,如果您运行前面的 TCP 客户端连接到www.linux.org,您可以看到 Wireshark 中交换的数据包序列,如下图所示:

Inspecting the client/server communication

如您所见,前三个数据包通过客户端和服务器之间的三方握手过程建立 TCP 连接。我们更感兴趣的是向服务器发出 HTTPGET请求的第四个数据包。如果双击所选行,可以看到 HTTP 请求的详细信息,如以下屏幕截图所示:

Inspecting the client/server communication

如您所见,HTTPGET请求还有其他组件,如Request URI、版本等。现在,您可以检查从 web 服务器到客户端的 HTTP 响应。它位于 TCP 确认数据包之后,即第六个数据包。在此,服务器通常发送 HTTP 响应代码(在本例中为200)、内容长度以及数据或网页内容。此数据包的结构如以下屏幕截图所示:

Inspecting the client/server communication

通过前面对客户机和服务器之间交互的分析,您现在可以从一个基本的层面上理解当您使用 web 浏览器访问网页时,幕后发生了什么。在下一节中,将向您展示如何创建自己的 TCP 服务器,并检查您的个人 TCP 客户端和服务器之间的交互。

TCP 服务器

正如您从第一个客户机/服务器交互图中所理解的,服务器进程需要执行一些额外的工作。它需要绑定到套接字地址并侦听传入的连接。以下代码段显示了如何创建 TCP 服务器:

import socket
from time import ctime

HOST = 'localhost'
PORT = 12345
BUFSIZ = 1024
ADDR = (HOST, PORT)

if __name__ == '__main__':
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.bind(ADDR)
    server_socket.listen(5)
    server_socket.setsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 )

    while True:
        print('Server waiting for connection...')
        client_sock, addr = server_socket.accept()
        print('Client connected from: ', addr)

        while True:
            data = client_sock.recv(BUFSIZ)
            if not data or data.decode('utf-8') == 'END':
                break
            print("Received from client: %s" % data.decode('utf- 8'))
            print("Sending the server time to client: %s"  %ctime())
            try:
                client_sock.send(bytes(ctime(), 'utf-8'))
            except KeyboardInterrupt:
                print("Exited by user")
        client_sock.close()
    server_socket.close()

让我们修改之前的 TCP 客户端,将任意数据发送到任何服务器。以下是增强型 TCP 客户端的示例:

import socket

HOST = 'localhost'
PORT = 12345
BUFSIZ = 256

if __name__ == '__main__':
    client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    host = input("Enter hostname [%s]: " %HOST) or HOST
    port = input("Enter port [%s]: " %PORT) or PORT

    sock_addr = (host, int(port))
    client_sock.connect(sock_addr)

    payload = 'GET TIME'
    try:
        while True:
            client_sock.send(payload.encode('utf-8'))
            data = client_sock.recv(BUFSIZ)
            print(repr(data))
            more = input("Want to send more data to server[y/n] :")
            if more.lower() == 'y':
               payload = input("Enter payload: ")
            else:
                break
    except KeyboardInterrupt:
        print("Exited by user") 

    client_sock.close()

如果在一个控制台中运行前面的 TCP 服务器,在另一个控制台中运行 TCP 客户端,则可以看到客户端和服务器之间的以下交互。运行 TCP 服务器脚本后,您将获得以下输出:

# python 7_3_tcp_server.py 
Server waiting for connection...
Client connected from:  ('127.0.0.1', 59961)
Received from client: GET TIME

Sending the server time to client: Sun Mar 15 12:09:16 2015
Server waiting for connection...

当您在另一个终端上运行 TCP 客户端脚本时,您将获得以下输出:

# python 7_4_tcp_client_socket_send_data.py 
Enter hostname [www.linux.org]: localhost
Enter port [80]: 12345
b'Sun Mar 15 12:09:16 2015'
Want to send more data to server[y/n] :n

检查客户端/服务器交互

现在,再一次,您可以配置 Wireshark 以捕获数据包,如上一节所述。但是,在这种情况下,您需要指定服务器正在侦听的端口(在上例中为12345,如以下屏幕截图所示:

Inspecting client/server interaction

当我们在非标准端口上捕获数据包时,WiReSARK 不会在 ALE T1 数据 DAT2 T2 节中解码它(如前面屏幕截图的中间窗格所示)。但是,您可以在底部窗格中看到解码的文本,其中服务器的时间戳显示在右侧。

使用 UDP 套接字

与 TCP 不同,UDP 不检查交换数据报中的错误。我们可以创建类似于 TCP 客户机/服务器的 UDP 客户机/服务器。唯一的区别是在创建套接字对象时必须指定SOCK_DGRAM而不是SOCK_STREAM

让我们创建一个 UDP 服务器。使用以下代码创建 UDP 服务器:

from socket import socket, AF_INET, SOCK_DGRAM
maxsize = 4096

sock = socket(AF_INET,SOCK_DGRAM)
sock.bind(('',12345))
while True:    
  data, addr = sock.recvfrom(maxsize)
    resp = "UDP server sending data"    
  sock.sendto(resp,addr)

现在,您可以创建一个 UDP 客户端,向 UDP 服务器发送一些数据,如下代码所示:

from socket import socket, AF_INET, SOCK_DGRAM

MAX_SIZE = 4096
PORT = 12345

if __name__ == '__main__':
    sock = socket(AF_INET,SOCK_DGRAM)
    msg = "Hello UDP server"
    sock.sendto(msg.encode(),('', PORT))
    data, addr = sock.recvfrom(MAX_SIZE)
    print("Server says:")
    print(repr(data))

在前面的代码片段中,UDP 客户端发送一行文本Hello UDP server并从服务器接收响应。以下屏幕截图显示了从客户端发送到服务器的请求:

Working with UDP sockets

下面的屏幕截图显示了服务器发送到客户端的响应。在检查 UDP 客户机/服务器数据包后,我们可以很容易地看到 UDP 比 TCP 简单得多。它通常被称为无连接协议,因为不涉及确认或错误检查。

Working with UDP sockets

TCP 端口转发

我们可以用 TCP 套接字编程做的一个有趣的实验是设置一个 TCP 端口转发。这有非常好的用例。例如,如果您在公共服务器上运行不安全的程序(如 FTP),而该服务器没有任何 SSL 功能来进行安全通信(FTP 密码可以通过电线以明文形式显示)。由于可以从 Internet 访问此服务器,因此在未确保密码已加密的情况下,您不得使用密码登录服务器。一种方法是使用安全的 FTP 或 SFTP。我们可以使用一个简单的 SSH 隧道来展示这种方法是如何工作的。因此,本地 FTP 客户端和远程 FTP 服务器之间的任何通信都将通过此加密通道进行。

让我们在同一个 SSH 服务器主机上运行 FTP 程序。但是,从本地计算机创建一个 SSH 隧道,它将为您提供一个本地端口号,并将您直接连接到远程 FTP 服务器守护进程。

Python 有一个第三方sshtunnel模块,它是 ParamikoSSH库的包装器。以下是 TCP 端口转发的代码片段,展示了如何实现这一概念:

import sshtunnel
from getpass import getpass

ssh_host = '192.168.56.101'
ssh_port = 22
ssh_user = 'YOUR_SSH_USERNAME'

REMOTE_HOST = '192.168.56.101'
REMOTE_PORT = 21

from sshtunnel import SSHTunnelForwarder
ssh_password = getpass('Enter YOUR_SSH_PASSWORD: ')

server = SSHTunnelForwarder(
    ssh_address=(ssh_host, ssh_port),
    ssh_username=ssh_user,
    ssh_password=ssh_password,
    remote_bind_address=(REMOTE_HOST, REMOTE_PORT))

server.start()
print('Connect the remote service via local port: %s'  %server.local_bind_port)
# work with FTP SERVICE via the `server.local_bind_port.
try:
    while True:
        pass
except KeyboardInterrupt:
    print("Exiting user user request.\n")
    server.stop()

让我们捕获从本地机器192.168.0.102到远程机器192.168.0.101的数据包传输。您将看到所有网络流量都已加密。运行上述脚本时,您将获得一个本地端口号。使用ftp命令连接到该本地端口号:

$ ftp <localhost> <local_bind_port>

如果运行上述命令,则将获得以下屏幕截图:

TCP port forwarding

在前面的截图中,您看不到任何 FTP 流量。如您所见,首先我们连接到本地端口5815(参见前三个数据包),然后突然与远程主机启动了加密会话。您可以继续监视远程通信,但没有 FTP 的痕迹。

如果您还可以在远程机器(192.168.56.101上捕获数据包,您可以看到 FTP 流量,如以下屏幕截图所示:

TCP port forwarding

有趣的是,您可以看到从本地计算机(通过 SSH 隧道)发送的 FTP 密码仅在远程设备上显示为明文,而不是通过网络,如以下屏幕截图所示:

TCP port forwarding

因此,通过这种方式,您可以在 SSL 隧道中隐藏任何敏感的网络流量。不仅是 FTP,还可以通过 SSH 通道传递加密的远程桌面会话。

非阻塞插座 I/O

在本节中,我们将看到一个测试非阻塞套接字 I/O 的小示例代码段。如果您知道同步阻塞连接对于您的程序来说不是必需的,那么这将非常有用。以下是非阻塞 I/O 的示例:

import socket

if __name__ == '__main__':
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setblocking(0)
    sock.settimeout(0.5)
    sock.bind(("127.0.0.1", 0))

    socket_address =sock.getsockname()
    print("Asynchronous socket server launched on socket: %s" %str(socket_address))
    while(1):
        sock.listen(1)

此脚本将运行套接字服务器并以非阻塞方式侦听。这意味着您可以连接更多不必为 I/O 而被阻止的客户端。

使用 TLS/SSL 固定插座

您可能遇到过关于使用安全套接字层SSL)或更准确地说是传输层安全TLS)进行安全 web 通信的讨论,这是许多其他高级协议所采用的。让我们看看如何用 SSL 封装普通套接字连接。Python 有内置的ssl模块,用于此目的。

在本例中,我们希望创建一个普通 TCP 套接字并连接到启用 HTTPS 的 web 服务器。然后,我们可以使用 SSL 包装该连接,并检查该连接的各种属性。例如,要检查远程 web 服务器的标识,我们可以查看 SSL 证书中的主机名是否与预期的相同。以下是基于安全套接字的客户端的示例:

import socket
import ssl
from ssl import wrap_socket, CERT_NONE, PROTOCOL_TLSv1, SSLError
from ssl import SSLContext
from ssl import HAS_SNI

from pprint import pprint

TARGET_HOST = 'www.google.com'
SSL_PORT = 443
# Use the path of CA certificate file in your system
CA_CERT_PATH = '/usr/local/lib/python3.3/dist- packages/requests/cacert.pem'

def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, ca_certs=None, server_hostname=None, ssl_version=None):

    context = SSLContext(ssl_version)
    context.verify_mode = cert_reqs

    if ca_certs:
        try:
            context.load_verify_locations(ca_certs)
        except Exception as e:
            raise SSLError(e)

    if certfile:
        context.load_cert_chain(certfile, keyfile)

    if HAS_SNI:  # OpenSSL enabled SNI
        return context.wrap_socket(sock, server_hostname=server_hostname)

    return context.wrap_socket(sock)

if __name__ == '__main__':
    hostname = input("Enter target host:") or TARGET_HOST
    client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_sock.connect((hostname, 443))

    ssl_socket = ssl_wrap_socket(client_sock, ssl_version=PROTOCOL_TLSv1, cert_reqs=ssl.CERT_REQUIRED, ca_certs=CA_CERT_PATH, server_hostname=hostname)

    print("Extracting remote host certificate details:")
    cert = ssl_socket.getpeercert()
    pprint(cert)
    if not cert or ('commonName', TARGET_HOST) not in cert['subject'][4]:
        raise Exception("Invalid SSL cert for host %s. Check if this is a man-in-the-middle attack!" )
    ssl_socket.write('GET / \n'.encode('utf-8'))
    #pprint(ssl_socket .recv(1024).split(b"\r\n"))
    ssl_socket.close()
    client_sock.close()

如果您运行前面的示例,您将看到远程 web 服务器(如的 SSL 证书)的详细信息 http://www.google.com 。这里我们创建了一个 TCP 套接字并将其连接到 HTTPS 端口443。然后,使用我们的ssl_wrap_socket()函数将套接字连接包装到 SSL 数据包中。此函数将以下参数作为参数:

  • sock:TCP 套接字
  • keyfile:SSL 私钥文件路径
  • certfile:SSL 公共证书路径
  • cert_reqs:确认连接是否需要对方的证书,是否需要验证测试
  • ca_certs:公共证书颁发机构证书路径
  • server_hostname:目标远程服务器的主机名
  • ssl_version:客户端使用的预期 SSL 版本

在 SSL 套接字包装过程的开始时,我们使用SSLContext()类创建了一个 SSL 上下文。这是设置 SSL 连接特定属性所必需的。除了使用自定义上下文之外,我们还可以使用默认上下文(默认情况下随ssl模块一起提供),使用create_default_context()函数。您可以使用常量指定是要创建客户端套接字还是服务器端套接字。以下是创建客户端套接字的示例:

context = ssl.create_default_context(Purpose.SERVER_AUTH)

SSLContext对象采用 SSL 版本参数,在我们的示例中,该参数设置为PROTOCOL_TLSv1,或者您应该使用最新版本。请注意,SSLv2 和 SSLv3 已损坏,不得在任何生产代码中用于严重的安全问题。

在前面的示例中,CERT_REQUIRED表示连接继续需要服务器证书,该证书将在以后进行验证。

如果 CA certificate 参数已提供证书路径,则使用load_verify_locations()方法加载 CA 证书文件。这将用于验证对等服务器证书。如果您想在系统上使用默认证书路径,您可能会调用另一个上下文方法;load_default_certs(purpose=Purpose.SERVER_AUTH)

当我们在服务器端操作时,通常使用load_cert_chain()方法加载密钥和证书文件,以便客户端可以验证服务器的真实性。

最后,调用wrap_socket()方法返回 SSL 包装的套接字。请注意,如果OpenSSL库附带服务器名称指示SNI支持已启用,则可以在包装套接字时传递远程服务器的主机名。当远程服务器使用单个 IP 地址(例如,基于名称的虚拟主机)为不同的安全服务使用不同的 SSL 证书时,这非常有用。

如果运行前面的 SSL 客户端代码,您将看到远程服务器的 SSL 证书的各种属性,如下面的屏幕截图所示。这用于通过调用getpeercert()方法并将其与返回的主机名进行比较来验证远程服务器的真实性。

Securing sockets with TLS/SSL

有趣的是,如果任何其他假冒的 web 服务器想冒充 Google 的 web 服务器,它根本无法做到这一点,只要您检查由认证机构签署的 SSL 证书,除非认证的 CA 已被破坏/颠覆。对 Web 浏览器的这种攻击形式通常被称为 OutT1。

检查标准 SSL 客户端/服务器通信

下面的屏幕截图显示了 SSL 客户端和远程服务器之间的交互:

Inspecting standard SSL client/server communication

让我们检查一下客户端和服务器之间的 SSL 握手过程。在 SSL 握手的第一步中,客户机向远程服务器发送Hello消息,说明其在处理密钥文件、加密消息、执行消息完整性检查等方面的能力。在下面的屏幕截图中,您可以看到客户端正在向服务器提供一组38密码套件,以选择相关算法。它还发送 TLS 版本号1.0和一个随机数,以生成一个主密钥,用于加密后续的消息交换。这有助于防止任何第三方查看数据包内部。hello 消息中的随机数用于生成预主密钥,两端将进一步处理以获得主密钥,然后使用该密钥生成对称密钥。

Inspecting standard SSL client/server communication

在从服务器到客户端的第二个数据包中,服务器选择密码套件TLS_ECDHE_RSA_WITH_RC4_128_SHA连接到客户端。这大致意味着服务器希望使用 RSA 算法进行密钥处理,使用 RC4 进行加密,使用 SHA 进行完整性检查(散列)。这显示在以下屏幕截图中:

Inspecting standard SSL client/server communication

在 SSL 握手的第二个阶段,服务器向客户端发送 SSL 证书。如前所述,本证书由 CA 颁发。它包含序列号、公钥、有效期以及主体和发行人的详细信息。以下屏幕截图显示了远程服务器证书。您能在数据包中找到服务器的公钥吗?

Inspecting standard SSL client/server communication

在握手的第三个阶段,客户端交换密钥并计算主密钥,以加密消息并继续进一步通信。客户端还发送更改上一阶段商定的密码规范的请求。然后指示开始加密消息。以下屏幕截图显示了此过程:

Inspecting standard SSL client/server communication

在 SSL 握手过程的最后一个任务中,服务器将为客户端的特定会话生成一个新的会话票证。这是由于 TLS 扩展导致的,其中客户端通过在客户端Hello消息中发送空会话票证扩展来宣传其支持。服务器在其服务器Hello消息中使用空会话票证扩展名进行应答。此会话票证机制使客户端能够记住整个会话状态,并且服务器在维护服务器端会话缓存方面的参与度降低。以下屏幕截图显示了显示 SSL 会话票证的示例:

Inspecting standard SSL client/server communication

创建自定义 SSL 客户端/服务器

到目前为止,我们一直在处理更多的 SSL 或 TLS 客户端。现在,让我们简单地看一下服务器端。由于您已经熟悉 TCP/UDP 套接字服务器的创建过程,让我们跳过这一部分,只关注 SSL 包装部分。以下代码段显示了一个简单 SSL 服务器的示例:

import socket
import ssl

SSL_SERVER_PORT = 8000

if __name__ == '__main__':
    server_socket = socket.socket()
    server_socket.bind(('', SSL_SERVER_PORT))
    server_socket.listen(5)
    print("Waiting for ssl client on port %s" %SSL_SERVER_PORT)
    newsocket, fromaddr = server_socket.accept()
    # Generate your server's  public certificate and private key pairs.
    ssl_conn = ssl.wrap_socket(newsocket, server_side=True, certfile="server.crt", keyfile="server.key", ssl_version=ssl.PROTOCOL_TLSv1)
    print(ssl_conn.read())
    ssl_conn.write('200 OK\r\n\r\n'.encode())
    print("Served ssl client. Exiting...")
    ssl_conn.close()
    server_socket.close()

正如您所看到的,服务器套接字是用wrap_socket()方法包装的,它使用了一些直观的参数,如certfilekeyfileSSL版本号。您可以按照 Internet 上的任何分步指南轻松生成证书。例如,http://www.akadia.com/services/ssh_test_certificate.html 建议分几个步骤生成 SSL 证书。

现在,让我们制作一个简化版本的 SSL 客户机,以便与上面的 SSL 服务器进行通信。以下代码段显示了一个简单 SSL 客户端的示例:

from socket import socket
import ssl

from pprint import pprint

TARGET_HOST ='localhost'
TARGET_PORT = 8000
CA_CERT_PATH = 'server.crt'

if __name__ == '__main__':

    sock = socket()
    ssl_conn = ssl.wrap_socket(sock, cert_reqs=ssl.CERT_REQUIRED, ssl_version=ssl.PROTOCOL_TLSv1, ca_certs=CA_CERT_PATH)
    target_host = TARGET_HOST 
    target_port = TARGET_PORT 
    ssl_conn.connect((target_host, int(target_port)))
    # get remote cert
    cert = ssl_conn.getpeercert()
    print("Checking server certificate")
    pprint(cert)
    if not cert or ssl.match_hostname(cert, target_host):
        raise Exception("Invalid SSL cert for host %s. Check if this is a man-in-the-middle attack!" %target_host )
    print("Server certificate OK.\n Sending some custom request... GET ")
    ssl_conn.write('GET / \n'.encode('utf-8'))
    print("Response received from server:")
    print(ssl_conn.read())
    ssl_conn.close()

运行客户端/服务器将显示类似于以下屏幕截图的输出。与上一个示例客户机/服务器通信相比,您能看到任何差异吗?

Creating a custom SSL client/server

检查自定义 SSL 客户端/服务器之间的交互

让我们再次检查 SSL 客户机/服务器交互,以观察差异。第一个屏幕截图显示了整个通信序列。在下面的屏幕截图中,我们可以看到服务器的Hello和证书组合在同一条消息中。

Inspecting interaction between a custom SSL client/server

客户端的客户端 Hello数据包看起来与我们之前的 SSL 连接非常相似,如以下屏幕截图所示:

Inspecting interaction between a custom SSL client/server

服务器的服务器 Hello数据包有点不同。你能辨别出这些区别吗?密码规范与TLS_RSA_WITH_AES_256_CBC_SHA不同,如下图所示:

Inspecting interaction between a custom SSL client/server

客户端密钥交换包看起来也很熟悉,如下图所示:

Inspecting interaction between a custom SSL client/server

以下屏幕截图显示了此连接提供的新会话票证数据包:

Inspecting interaction between a custom SSL client/server

现在让我们看看应用数据。那是加密的吗?对于捕获的数据包,它看起来像垃圾。下面的屏幕截图显示了隐藏真实数据的加密消息。这就是我们希望使用 SSL/TLS 实现的目标。

Inspecting interaction between a custom SSL client/server

总结

在本章中,我们讨论了使用 Python 的socketssl模块进行的基本 TCP/IP 套接字编程。我们演示了如何使用 TLS 包装简单的 TCP 套接字,并将其用于传输加密数据。我们还找到了使用 SSL 证书验证远程服务器真实性的方法。还介绍了套接字编程的一些其他小问题,如非阻塞套接字 I/O。每一节中的详细数据包分析都有助于我们理解套接字编程练习中的幕后操作。

在下一章中,我们将学习 socket 服务器设计,特别是将涉及到流行的多线程和事件驱动方法。