Skip to content

Commit ba41a8c

Browse files
committed
feat: add SSL/WSS connection support
- Add use_ssl parameter to connect() for WSS protocol - Add skip_ssl_verify parameter to control certificate verification - Support endpoint with ws:// or wss:// prefix - Update demo.py with --use-ssl and --skip-ssl-verify CLI args - Update documentation - Add spec-driven development docs
1 parent 64f8aea commit ba41a8c

File tree

7 files changed

+329
-11
lines changed

7 files changed

+329
-11
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77

88
## [Unreleased]
99

10+
### Added
11+
- `FnosClient.connect()` 新增 SSL/WSS 连接支持
12+
- `use_ssl` 参数:是否使用 WSS 协议连接(默认 False)
13+
- `skip_ssl_verify` 参数:是否跳过 SSL 证书验证(默认 True,便于自签名证书场景)
14+
- 支持 endpoint 带协议前缀(`wss://``ws://`),前缀优先于 `use_ssl` 参数
15+
1016
## [0.11.0] - 2026-02-15
1117

1218
### Added

README.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ async def main():
3030
parser.add_argument('--user', type=str, required=True, help='用户名')
3131
parser.add_argument('--password', type=str, required=True, help='密码')
3232
parser.add_argument('-e', '--endpoint', type=str, default='your-custom-endpoint.com:5666', help='服务器地址 (默认: your-custom-endpoint.com:5666)')
33+
parser.add_argument('--use-ssl', action='store_true', help='使用 SSL/WSS 连接')
34+
parser.add_argument('--skip-ssl-verify', type=lambda x: x.lower() == 'true', default=True, help='跳过 SSL 证书验证 (默认: True)')
3335

3436
args = parser.parse_args()
3537

@@ -39,7 +41,8 @@ async def main():
3941
client.on_message(on_message_handler)
4042

4143
# 连接到服务器(必须指定endpoint)
42-
await client.connect(args.endpoint)
44+
# 可选参数:use_ssl=True 使用 WSS 连接,skip_ssl_verify=False 验证证书
45+
await client.connect(args.endpoint, use_ssl=args.use_ssl, skip_ssl_verify=args.skip_ssl_verify)
4346

4447
# 登录
4548
result = await client.login(args.user, args.password)
@@ -54,7 +57,7 @@ async def main():
5457
# 演示重连功能(手动方式)
5558
await client.close() # 先关闭连接
5659
print("连接已关闭,尝试重连...")
57-
await client.connect(args.endpoint) # 重新连接(现在会等待连接完成)
60+
await client.connect(args.endpoint, use_ssl=args.use_ssl, skip_ssl_verify=args.skip_ssl_verify) # 重新连接(现在会等待连接完成)
5861
result = await client.login(args.user, args.password) # 重新登录
5962
print("重连登录结果:", result)
6063

@@ -74,7 +77,7 @@ if __name__ == "__main__":
7477
| 类名 | 方法名 | 简介 |
7578
| ---- | ---- | ---- |
7679
| FnosClient | `__init__` | 初始化客户端,支持type参数("main"、"timer"或"file",默认为"main") |
77-
| FnosClient | `connect` | 连接到WebSocket服务器(必填参数:endpoint) |
80+
| FnosClient | `connect` | 连接到WebSocket服务器(必填参数:endpoint;可选参数:use_ssl、skip_ssl_verify|
7881
| FnosClient | `login` | 用户登录方法 |
7982
| FnosClient | `get_decrypted_secret` | 获取解密后的secret |
8083
| FnosClient | `on_message` | 设置消息回调函数 |
@@ -135,6 +138,9 @@ if __name__ == "__main__":
135138
- `--user`: 用户名(必填)
136139
- `--password`: 密码(必填)
137140
- `-e, --endpoint`: 服务器地址(可选,默认为 your-custom-endpoint.com:5666)
141+
- 支持协议前缀:`wss://host:port``ws://host:port`
142+
- `--use-ssl`: 使用 SSL/WSS 连接(可选,默认不使用)
143+
- `--skip-ssl-verify`: 跳过 SSL 证书验证(可选,默认为 True)
138144

139145
## 运行示例
140146

@@ -167,4 +173,19 @@ uv run examples/user.py --user myuser --password mypassword -e my-server.com:566
167173
| `docker_manager.py` | 演示DockerManager模块的各种功能(Docker Compose项目、容器、统计信息、系统设置) |
168174
| `event_logger.py` | 演示EventLogger模块的功能(获取事件日志) |
169175
| `share.py` | 演示Share模块的功能(获取SMB共享配置信息) |
170-
| `notify.py` | 演示Notify模块的功能(获取未读通知数) |
176+
| `notify.py` | 演示Notify模块的功能(获取未读通知数) |
177+
178+
### SSL/WSS 连接示例
179+
180+
如果服务器使用 HTTPS/WSS 协议,可以通过以下方式连接:
181+
182+
```bash
183+
# 方式1:使用 wss:// 前缀
184+
uv run examples/user.py --user myuser --password mypassword -e wss://my-server.com:5667
185+
186+
# 方式2:使用 --use-ssl 参数
187+
uv run examples/user.py --user myuser --password mypassword -e my-server.com:5667 --use-ssl
188+
189+
# 禁用证书验证跳过(验证服务器证书)
190+
uv run examples/user.py --user myuser --password mypassword -e wss://my-server.com:5667 --skip-ssl-verify false
191+
```

demo.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ async def main():
2727
parser.add_argument('--user', type=str, required=True, help='用户名')
2828
parser.add_argument('--password', type=str, required=True, help='密码')
2929
parser.add_argument('-e', '--endpoint', type=str, default='your-custom-endpoint.com:5666', help='服务器地址 (默认: your-custom-endpoint.com:5666)')
30+
parser.add_argument('--use-ssl', action='store_true', help='使用 SSL/WSS 连接')
31+
parser.add_argument('--skip-ssl-verify', type=lambda x: x.lower() == 'true', default=True, help='跳过 SSL 证书验证 (默认: True)')
3032

3133
args = parser.parse_args()
3234

@@ -36,7 +38,7 @@ async def main():
3638
client.on_message(on_message_handler)
3739

3840
# 连接到服务器(必须指定endpoint)
39-
await client.connect(args.endpoint)
41+
await client.connect(args.endpoint, use_ssl=args.use_ssl, skip_ssl_verify=args.skip_ssl_verify)
4042

4143
if client.connected:
4244
print("连接成功,尝试登录...")

fnos/client.py

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import hashlib
2222
import hmac
2323
import logging
24+
import ssl
2425
from Crypto.PublicKey import RSA
2526
from Crypto.Cipher import AES, PKCS1_v1_5
2627
from Crypto.Random import get_random_bytes
@@ -74,6 +75,9 @@ def __init__(self, type: str = "main"):
7475
self.password = None
7576
self.token = None
7677
self.long_token = None
78+
# SSL 配置
79+
self.use_ssl = False
80+
self.skip_ssl_verify = True
7781

7882
def _generate_reqid(self):
7983
"""生成唯一的reqid"""
@@ -92,6 +96,21 @@ def _generate_did(self):
9296
n = base64.b32encode(str(random.random()).encode()).decode()[:15]
9397
return f"{t}-{e}-{n}".lower().replace('=', '')
9498

99+
def _parse_endpoint(self, endpoint: str, use_ssl: bool) -> tuple:
100+
"""
101+
解析 endpoint,返回 (host_port, actual_use_ssl)
102+
103+
- 如果 endpoint 以 wss:// 开头,返回去掉前缀的地址和 True
104+
- 如果 endpoint 以 ws:// 开头,返回去掉前缀的地址和 False
105+
- 否则返回原地址和 use_ssl 参数值
106+
"""
107+
if endpoint.startswith("wss://"):
108+
return endpoint[6:], True
109+
elif endpoint.startswith("ws://"):
110+
return endpoint[5:], False
111+
else:
112+
return endpoint, use_ssl
113+
95114
def _encrypt_login_data(self, username, password):
96115
"""加密登录数据"""
97116
# 生成随机AES密钥
@@ -172,14 +191,39 @@ def _decrypt_login_secret(self, encrypted_secret):
172191
logger.error(f"解密登录secret失败: {e}")
173192
return None
174193

175-
async def connect(self, endpoint, timeout: float = 3.0):
176-
"""连接到WebSocket服务器"""
194+
async def connect(self, endpoint, timeout: float = 3.0, use_ssl: bool = False, skip_ssl_verify: bool = True):
195+
"""连接到WebSocket服务器
196+
197+
Args:
198+
endpoint: 服务器地址,可以是 "host:port" 格式或带协议前缀 "ws://host:port" / "wss://host:port"
199+
timeout: 连接超时时间(秒)
200+
use_ssl: 是否使用 SSL/WSS 连接(默认 False)
201+
skip_ssl_verify: 是否跳过 SSL 证书验证(默认 True)
202+
"""
177203
try:
178204
logger.info("正在连接到WebSocket服务器...")
179-
# 保存endpoint用于重连
180-
self.endpoint = endpoint
181-
# 创建WebSocket连接,使用构造函数设置的type参数
182-
self.ws = await websockets.connect(f"ws://{endpoint}/websocket?type={self.type}")
205+
# 解析 endpoint,处理协议前缀
206+
parsed_endpoint, actual_use_ssl = self._parse_endpoint(endpoint, use_ssl)
207+
208+
# 保存连接信息用于重连
209+
self.endpoint = parsed_endpoint
210+
self.use_ssl = actual_use_ssl
211+
self.skip_ssl_verify = skip_ssl_verify
212+
213+
# 根据 use_ssl 选择协议
214+
protocol = "wss" if actual_use_ssl else "ws"
215+
uri = f"{protocol}://{parsed_endpoint}/websocket?type={self.type}"
216+
217+
# 配置 SSL 上下文
218+
ssl_context = None
219+
if actual_use_ssl:
220+
ssl_context = ssl.create_default_context()
221+
if skip_ssl_verify:
222+
ssl_context.check_hostname = False
223+
ssl_context.verify_mode = ssl.CERT_NONE
224+
225+
# 创建WebSocket连接
226+
self.ws = await websockets.connect(uri, ssl=ssl_context)
183227
logger.debug("websockets.connect returned")
184228

185229
logger.debug("Creating async message handler...")

specs/ssl-support-plan.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# WSS/SSL 连接支持 - 技术实现计划
2+
3+
## 修改范围
4+
5+
### 主要文件
6+
7+
| 文件 | 修改类型 | 说明 |
8+
|-----|---------|-----|
9+
| `fnos/client.py` | 修改 | 添加 SSL 支持逻辑 |
10+
| `demo.py` | 修改 | 添加命令行参数支持 |
11+
12+
## 实现细节
13+
14+
### 1. FnosClient 类变更
15+
16+
#### 1.1 新增实例属性
17+
18+
`__init__()` 中添加:
19+
20+
```python
21+
self.use_ssl = False
22+
self.skip_ssl_verify = True
23+
```
24+
25+
#### 1.2 connect() 方法变更
26+
27+
**新增参数:**
28+
- `use_ssl: bool = False` - 是否使用 SSL 连接
29+
- `skip_ssl_verify: bool = True` - 是否跳过证书验证
30+
31+
**endpoint 解析逻辑:**
32+
```python
33+
def _parse_endpoint(self, endpoint: str, use_ssl: bool) -> tuple[str, bool]:
34+
"""
35+
解析 endpoint,返回 (host_port, actual_use_ssl)
36+
37+
- 如果 endpoint 以 wss:// 开头,返回去掉前缀的地址和 True
38+
- 如果 endpoint 以 ws:// 开头,返回去掉前缀的地址和 False
39+
- 否则返回原地址和 use_ssl 参数值
40+
"""
41+
if endpoint.startswith("wss://"):
42+
return endpoint[6:], True
43+
elif endpoint.startswith("ws://"):
44+
return endpoint[5:], False
45+
else:
46+
return endpoint, use_ssl
47+
```
48+
49+
**WebSocket 连接构建:**
50+
```python
51+
# 根据 use_ssl 选择协议
52+
protocol = "wss" if use_ssl else "ws"
53+
uri = f"{protocol}://{endpoint}/websocket?type={self.type}"
54+
55+
# 配置 SSL 上下文
56+
ssl_context = None
57+
if use_ssl and skip_ssl_verify:
58+
ssl_context = ssl.create_default_context()
59+
ssl_context.check_hostname = False
60+
ssl_context.verify_mode = ssl.CERT_NONE
61+
62+
# 创建连接
63+
self.ws = await websockets.connect(uri, ssl=ssl_context)
64+
```
65+
66+
#### 1.3 reconnect() 方法变更
67+
68+
无需修改,因为 `connect()` 会自动使用保存的 SSL 配置。
69+
70+
### 2. demo.py 变更
71+
72+
添加命令行参数:
73+
74+
```python
75+
parser.add_argument('--use-ssl', action='store_true', help='使用 SSL/WSS 连接')
76+
parser.add_argument('--skip-ssl-verify', type=lambda x: x.lower() == 'true',
77+
default=True, help='跳过 SSL 证书验证 (默认: True)')
78+
```
79+
80+
## 依赖
81+
82+
- `ssl` 模块(Python 标准库)
83+
- `websockets` 库已支持 SSL 参数
84+
85+
## 测试策略
86+
87+
### 单元测试
88+
89+
- 测试 endpoint 解析逻辑(各种前缀组合)
90+
- 测试协议选择逻辑
91+
92+
### 集成测试
93+
94+
- 测试 ws:// 连接(现有行为)
95+
- 测试 wss:// 连接(需要测试服务器)
96+
- 测试自签名证书跳过验证
97+
98+
## 风险与缓解
99+
100+
| 风险 | 缓解措施 |
101+
|-----|---------|
102+
| 向后兼容性破坏 | 默认值保持 `use_ssl=False` |
103+
| SSL 验证失败 | 默认 `skip_ssl_verify=True` 便于自签名证书场景 |

specs/ssl-support-spec.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# WSS/SSL 连接支持规范
2+
3+
## 概述
4+
5+
`FnosClient` 添加 HTTPS/WSS (安全 WebSocket) 连接支持,允许用户通过 SSL/TLS 加密连接到服务器。
6+
7+
## 功能需求
8+
9+
### FR-1: use_ssl 参数
10+
11+
**WHEN** 用户调用 `connect()` 方法时,
12+
**THEN THE SYSTEM SHALL** 接受一个 `use_ssl` 布尔参数(默认 `False`),
13+
**AND**`use_ssl=True` 时使用 `wss://` 协议连接。
14+
15+
### FR-2: endpoint 协议前缀支持
16+
17+
**WHEN** 用户在 `endpoint` 参数中指定 `ws://``wss://` 前缀时,
18+
**THEN THE SYSTEM SHALL** 自动解析并使用该协议,
19+
**AND** endpoint 中指定的协议前缀优先于 `use_ssl` 参数。
20+
21+
### FR-3: SSL 证书验证控制
22+
23+
**WHEN** 用户连接到使用自签名证书的服务器时,
24+
**THEN THE SYSTEM SHALL** 提供 `skip_ssl_verify` 参数(默认 `True`)控制是否跳过证书验证,
25+
**AND**`skip_ssl_verify=True` 时跳过 SSL 证书验证,
26+
**AND**`skip_ssl_verify=False` 时验证 SSL 证书。
27+
28+
### FR-4: 重连时保持 SSL 配置
29+
30+
**WHEN** 用户调用 `reconnect()` 方法时,
31+
**THEN THE SYSTEM SHALL** 自动使用之前保存的 SSL 配置(`use_ssl``skip_ssl_verify`)。
32+
33+
## 参数行为矩阵
34+
35+
| endpoint 格式 | use_ssl | 实际协议 |
36+
|--------------|---------|---------|
37+
| `wss://host:port` | 任意 | wss:// |
38+
| `ws://host:port` | 任意 | ws:// |
39+
| `host:port` | False | ws:// |
40+
| `host:port` | True | wss:// |
41+
42+
## API 变更
43+
44+
### connect() 方法签名变更
45+
46+
```python
47+
async def connect(
48+
self,
49+
endpoint,
50+
timeout: float = 3.0,
51+
use_ssl: bool = False,
52+
skip_ssl_verify: bool = True
53+
):
54+
```
55+
56+
### 新增实例属性
57+
58+
- `self.use_ssl: bool` - 保存 SSL 配置用于重连
59+
- `self.skip_ssl_verify: bool` - 保存证书验证配置用于重连
60+
61+
## 验收标准
62+
63+
- [ ] `use_ssl=True` 时使用 wss:// 协议连接
64+
- [ ] `use_ssl=False`(默认)时使用 ws:// 协议连接
65+
- [ ] endpoint 包含 `wss://` 前缀时使用 wss:// 协议
66+
- [ ] endpoint 包含 `ws://` 前缀时使用 ws:// 协议
67+
- [ ] endpoint 中的协议前缀优先于 use_ssl 参数
68+
- [ ] `skip_ssl_verify=True` 时跳过证书验证
69+
- [ ] `skip_ssl_verify=False` 时验证证书
70+
- [ ] `reconnect()` 方法使用保存的 SSL 配置
71+
- [ ] 向后兼容:现有代码无需修改即可正常工作

0 commit comments

Comments
 (0)