首页 > 解决方案 > 无法使用 python 3 websocket 库连接到远程安全 web socket服务器

问题描述

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

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

出于测试目的,我使用的是使用 openssl 生成的自签名证书:证书文件cert.pem以及key.pem中的配套私钥。

在客户端,我在一台 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”),但随后脚本似乎陷入了一些同步阻塞 - 无一例外,屏幕上没有列出任何内容,但也没有收到任何内容在服务器端。

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

标签: python-3.xsslwebsocketopenssl

解决方案


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

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

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

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

在 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


推荐阅读