无法使用 python 3 websocket 库连接到远程安全 web 套接字服务器

Cannot connect to remote secure web socket server using python 3 websocket library

我在地址 RASPI_ADDRESS 的 RaspberryPI 设备上 python 3 运行 中实现了一个安全的网络套接字服务器,暴露在端口 8000 上。 在 RaspberryPI 设备上,ssl 版本显示为:

>>> import ssl
>>> print(ssl.OPENSSL_VERSION)
OpenSSL 1.1.1d  10 Sep 2019

出于测试目的,我使用的是使用 openssl 生成的自签名证书:证书文件 cert.pem 以及 [ 中的伴随私钥=33=].

在客户端,我在 Windows 机器上,我按如下方式实现了客户端(上面的相同 cert.pem 文件可在此处作为本地副本使用):

import ssl
import websocket

ws = websocket.WebSocket(sslopt={"ssl_version": ssl.PROTOCOL_TLSv1, "certfile": "cert.pem"})
try:
    ws.connect("wss://RASPI_ADDRESS:8000")
    ws.send("Hello, Server")
    print(ws.recv())
    ws.close()
except Exception as e:
    print("Exception: ", e)

我在 ws.connect(...) 上收到此异常:

Exception:  [SSL] PEM lib (_ssl.c:4065)

(如果我使用“ws://...”以非安全方式连接,它会起作用)

不幸的是,我在搜索此错误时没有得到很多相关结果。我也尝试在 sslopt 中提供私钥(“keyfile”:“key.pem”),但随后脚本似乎陷入了某种同步阻塞 - 无一例外,屏幕上没有列出任何内容,但也服务器端没有收到任何东西。

关于我做错了什么的任何指示?

最后,我通过使用 websockets 库重写服务器和客户端解决了这个问题: https://pypi.org/project/websockets/

也许它也会有 运行 和 websocket-clienthttps://pypi.org/project/websocket-client/ 我以前用过,但文档部分不一致且令人困惑。在这里写一个简化的工作解决方案以供将来参考,以虚拟回显服务器的形式。

RasPI 上的服务器 运行ning(在 LAN 中可见 IP 地址 RASPI_IP)

import asyncio
import pathlib
import ssl
import websockets

async def hello(websocket, path):
    name = await websocket.recv()
    print(f"<<< {name}")

    greeting = f"Hello {name}!"

    await websocket.send(greeting)
    print(f">>> {greeting}")

ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
localhost_pem = pathlib.Path(__file__).with_name("key_cert.pem")
ssl_context.load_cert_chain(localhost_pem)

async def main():
    async with websockets.serve(hello, "0.0.0.0", 8765, ssl=ssl_context):
        await asyncio.Future()  # run forever

asyncio.run(main())

注意 websockets.serve() 中的“0.0.0.0”主机 IP!如果我们将它设置为“localhost”,客户端将看到一个以这个错误结尾的堆栈跟踪:

ConnectionRefusedError: [WinError 1225] The remote computer refused the network connection

客户端 运行ning 在 Windows 机器上:

import asyncio
import pathlib
import ssl
import websockets

ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
localhost_pem = pathlib.Path(__file__).with_name("key_cert.pem")
ssl_context.load_verify_locations(localhost_pem)
uri_linux = "wss://RASPI_IP:8765"

async def hello():
    uri = uri_linux
    async with websockets.connect(uri, ssl=ssl_context) as websocket:
        name = input("What's your name? ")

        await websocket.send(name)
        print(f">>> {name}")

        greeting = await websocket.recv()
        print(f"<<< {greeting}")

asyncio.run(hello())

与最初的实现相比,这至少让我有所反应,因为当时我 运行 遇到了这个错误:

ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: IP address mismatch, certificate is not valid for *RASPI_IP*. (_ssl.c:1129)

这可以通过使用 SAN 而不是仅使用 CN 生成证书来解决:https://serverfault.com/a/880809 此外,我将证书和密钥合并到一个文件中:cat key.pem cert.pem > key_cert.pem