xfcloudcard 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xfcloudcard-0.1.0/PKG-INFO +97 -0
- xfcloudcard-0.1.0/README.md +74 -0
- xfcloudcard-0.1.0/pyproject.toml +41 -0
- xfcloudcard-0.1.0/setup.cfg +4 -0
- xfcloudcard-0.1.0/xfcloudcard/__init__.py +24 -0
- xfcloudcard-0.1.0/xfcloudcard/client.py +453 -0
- xfcloudcard-0.1.0/xfcloudcard/crypto.py +104 -0
- xfcloudcard-0.1.0/xfcloudcard/device_info.py +99 -0
- xfcloudcard-0.1.0/xfcloudcard.egg-info/PKG-INFO +97 -0
- xfcloudcard-0.1.0/xfcloudcard.egg-info/SOURCES.txt +11 -0
- xfcloudcard-0.1.0/xfcloudcard.egg-info/dependency_links.txt +1 -0
- xfcloudcard-0.1.0/xfcloudcard.egg-info/requires.txt +2 -0
- xfcloudcard-0.1.0/xfcloudcard.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xfcloudcard
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 云控卡密验证客户端库 - 支持 AES+HMAC 加密通信、心跳保活、离线上报
|
|
5
|
+
Author: songxf
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/songxf/xfcloudcard
|
|
8
|
+
Project-URL: Source, https://github.com/songxf/xfcloudcard
|
|
9
|
+
Project-URL: Issues, https://github.com/songxf/xfcloudcard/issues
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Topic :: Security :: Cryptography
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: requests>=2.25.0
|
|
22
|
+
Requires-Dist: pycryptodome>=3.15.0
|
|
23
|
+
|
|
24
|
+
# xfcloudcard
|
|
25
|
+
|
|
26
|
+
云控卡密验证客户端库,支持 AES-256-CBC + HMAC-SHA256 加密通信、心跳保活、离线上报。
|
|
27
|
+
|
|
28
|
+
## 安装
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install xfcloudcard
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 快速开始
|
|
35
|
+
|
|
36
|
+
### 方式一:上下文管理器(推荐)
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from xfcloudcard import CardClient
|
|
40
|
+
|
|
41
|
+
with CardClient(server_url="http://localhost:8000", key="your-key") as client:
|
|
42
|
+
result = client.verify("CARD-XXXX-XXXX-XXXX-XXXX")
|
|
43
|
+
if result['success']:
|
|
44
|
+
# 业务代码写这里
|
|
45
|
+
pass
|
|
46
|
+
# 退出 with 块时自动停止心跳 + 发送离线通知
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 方式二:装饰器(最简洁)
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from xfcloudcard import require_card
|
|
53
|
+
|
|
54
|
+
@require_card(server_url="http://localhost:8000", key="your-key")
|
|
55
|
+
def main():
|
|
56
|
+
# 仅当卡密验证通过后才执行
|
|
57
|
+
print("验证通过,执行业务逻辑...")
|
|
58
|
+
|
|
59
|
+
main()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 方式三:命令行
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# 交互模式
|
|
66
|
+
xfcloudcard
|
|
67
|
+
|
|
68
|
+
# 直接验证
|
|
69
|
+
xfcloudcard --card CARD-XXXX-XXXX-XXXX-XXXX
|
|
70
|
+
|
|
71
|
+
# 只验证,不启动心跳
|
|
72
|
+
xfcloudcard --card CARD-XXXX-... --once
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## API
|
|
76
|
+
|
|
77
|
+
### `CardClient(server_url, key, heartbeat_interval=60)`
|
|
78
|
+
|
|
79
|
+
| 方法 | 说明 |
|
|
80
|
+
|------|------|
|
|
81
|
+
| `verify(card_key)` | 验证卡密,成功后自动启动心跳 |
|
|
82
|
+
| `verify_only(card_key)` | 只验证,不启动心跳 |
|
|
83
|
+
| `close()` | 停止心跳并发送离线通知 |
|
|
84
|
+
| `is_online()` | 返回心跳是否运行中 |
|
|
85
|
+
|
|
86
|
+
### `require_card(server_url, key, heartbeat_interval=60, exit_on_fail=True)`
|
|
87
|
+
|
|
88
|
+
装饰器,在业务函数执行前自动验证卡密。
|
|
89
|
+
|
|
90
|
+
## 依赖
|
|
91
|
+
|
|
92
|
+
- `requests >= 2.25.0`
|
|
93
|
+
- `pycryptodome >= 3.15.0`
|
|
94
|
+
|
|
95
|
+
## 协议
|
|
96
|
+
|
|
97
|
+
MIT
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# xfcloudcard
|
|
2
|
+
|
|
3
|
+
云控卡密验证客户端库,支持 AES-256-CBC + HMAC-SHA256 加密通信、心跳保活、离线上报。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install xfcloudcard
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 快速开始
|
|
12
|
+
|
|
13
|
+
### 方式一:上下文管理器(推荐)
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from xfcloudcard import CardClient
|
|
17
|
+
|
|
18
|
+
with CardClient(server_url="http://localhost:8000", key="your-key") as client:
|
|
19
|
+
result = client.verify("CARD-XXXX-XXXX-XXXX-XXXX")
|
|
20
|
+
if result['success']:
|
|
21
|
+
# 业务代码写这里
|
|
22
|
+
pass
|
|
23
|
+
# 退出 with 块时自动停止心跳 + 发送离线通知
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 方式二:装饰器(最简洁)
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from xfcloudcard import require_card
|
|
30
|
+
|
|
31
|
+
@require_card(server_url="http://localhost:8000", key="your-key")
|
|
32
|
+
def main():
|
|
33
|
+
# 仅当卡密验证通过后才执行
|
|
34
|
+
print("验证通过,执行业务逻辑...")
|
|
35
|
+
|
|
36
|
+
main()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 方式三:命令行
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# 交互模式
|
|
43
|
+
xfcloudcard
|
|
44
|
+
|
|
45
|
+
# 直接验证
|
|
46
|
+
xfcloudcard --card CARD-XXXX-XXXX-XXXX-XXXX
|
|
47
|
+
|
|
48
|
+
# 只验证,不启动心跳
|
|
49
|
+
xfcloudcard --card CARD-XXXX-... --once
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## API
|
|
53
|
+
|
|
54
|
+
### `CardClient(server_url, key, heartbeat_interval=60)`
|
|
55
|
+
|
|
56
|
+
| 方法 | 说明 |
|
|
57
|
+
|------|------|
|
|
58
|
+
| `verify(card_key)` | 验证卡密,成功后自动启动心跳 |
|
|
59
|
+
| `verify_only(card_key)` | 只验证,不启动心跳 |
|
|
60
|
+
| `close()` | 停止心跳并发送离线通知 |
|
|
61
|
+
| `is_online()` | 返回心跳是否运行中 |
|
|
62
|
+
|
|
63
|
+
### `require_card(server_url, key, heartbeat_interval=60, exit_on_fail=True)`
|
|
64
|
+
|
|
65
|
+
装饰器,在业务函数执行前自动验证卡密。
|
|
66
|
+
|
|
67
|
+
## 依赖
|
|
68
|
+
|
|
69
|
+
- `requests >= 2.25.0`
|
|
70
|
+
- `pycryptodome >= 3.15.0`
|
|
71
|
+
|
|
72
|
+
## 协议
|
|
73
|
+
|
|
74
|
+
MIT
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "xfcloudcard"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "云控卡密验证客户端库 - 支持 AES+HMAC 加密通信、心跳保活、离线上报"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "songxf"},
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.8",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Topic :: Security :: Cryptography",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"requests >= 2.25.0",
|
|
28
|
+
"pycryptodome >= 3.15.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/songxf/xfcloudcard"
|
|
33
|
+
Source = "https://github.com/songxf/xfcloudcard"
|
|
34
|
+
Issues = "https://github.com/songxf/xfcloudcard/issues"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["."]
|
|
38
|
+
include = ["xfcloudcard*"]
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.package-data]
|
|
41
|
+
xfcloudcard = ["py.typed"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
xfcloudcard - 云控卡密验证客户端库
|
|
3
|
+
|
|
4
|
+
支持 AES-256-CBC + HMAC-SHA256 加密通信、
|
|
5
|
+
心跳保活、离线上报,可作为库集成到业务代码中。
|
|
6
|
+
|
|
7
|
+
快速开始:
|
|
8
|
+
>>> from xfcloudcard import CardClient
|
|
9
|
+
>>> with CardClient(server_url="http://localhost:8000", key="your-key") as client:
|
|
10
|
+
... result = client.verify("CARD-XXXX-XXXX-XXXX-XXXX")
|
|
11
|
+
... if result['success']:
|
|
12
|
+
... pass # 业务代码
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .client import CardClient, require_card
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
__author__ = "songxf"
|
|
19
|
+
__license__ = "MIT"
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"CardClient",
|
|
23
|
+
"require_card",
|
|
24
|
+
]
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"""
|
|
2
|
+
卡密验证客户端库
|
|
3
|
+
支持作为库导入使用,也支持命令行运行。
|
|
4
|
+
|
|
5
|
+
作为库使用:
|
|
6
|
+
from client import CardClient
|
|
7
|
+
|
|
8
|
+
with CardClient(server_url="http://...", key="...") as client:
|
|
9
|
+
result = client.verify("CARD-XXXX-XXXX-XXXX-XXXX")
|
|
10
|
+
if result['success']:
|
|
11
|
+
# 业务代码
|
|
12
|
+
pass
|
|
13
|
+
# 退出 with 块时自动停止心跳并发送离线通知
|
|
14
|
+
|
|
15
|
+
或使用装饰器:
|
|
16
|
+
from client import require_card
|
|
17
|
+
|
|
18
|
+
@require_card(server_url="http://...", key="...")
|
|
19
|
+
def main():
|
|
20
|
+
# 仅当卡密验证通过时才执行
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
main()
|
|
24
|
+
"""
|
|
25
|
+
import sys
|
|
26
|
+
import os
|
|
27
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import time
|
|
31
|
+
import signal
|
|
32
|
+
import threading
|
|
33
|
+
from typing import Optional, Callable, Any
|
|
34
|
+
|
|
35
|
+
from crypto import CryptoManager
|
|
36
|
+
from device_info import get_device_sn, get_ip_address
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# 默认配置(可通过环境变量或参数覆盖)
|
|
40
|
+
DEFAULT_SERVER_URL = "http://localhost:8000"
|
|
41
|
+
DEFAULT_KEY = "cloud-card-system-key-32bytes!!"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CardClient:
|
|
45
|
+
"""
|
|
46
|
+
卡密验证客户端(支持上下文管理器)
|
|
47
|
+
|
|
48
|
+
用法:
|
|
49
|
+
with CardClient(server_url="http://...", key="...") as client:
|
|
50
|
+
result = client.verify("CARD-XXXX-...")
|
|
51
|
+
if result['success']:
|
|
52
|
+
# 业务代码
|
|
53
|
+
pass
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
server_url: str = None,
|
|
59
|
+
key: str = None,
|
|
60
|
+
heartbeat_interval: int = 60,
|
|
61
|
+
):
|
|
62
|
+
"""
|
|
63
|
+
Args:
|
|
64
|
+
server_url: 服务器URL,默认 http://localhost:8000
|
|
65
|
+
key: 加密密钥(必须与服务器端相同,32字节或任意长度)
|
|
66
|
+
heartbeat_interval: 心跳间隔秒数(默认60秒)
|
|
67
|
+
"""
|
|
68
|
+
self.server_url = (server_url or DEFAULT_SERVER_URL).rstrip('/')
|
|
69
|
+
raw_key = (key or DEFAULT_KEY).encode('utf-8')
|
|
70
|
+
self.crypto = CryptoManager(raw_key)
|
|
71
|
+
self.device_sn = get_device_sn()
|
|
72
|
+
self.ip_address = get_ip_address()
|
|
73
|
+
self.heartbeat_interval = heartbeat_interval
|
|
74
|
+
|
|
75
|
+
self._heartbeat_running = False
|
|
76
|
+
self._heartbeat_thread: Optional[threading.Thread] = None
|
|
77
|
+
self._current_card_key: Optional[str] = None
|
|
78
|
+
self._lock = threading.Lock()
|
|
79
|
+
|
|
80
|
+
# ─────────────────────────────────────────────
|
|
81
|
+
# 上下文管理器
|
|
82
|
+
# ─────────────────────────────────────────────
|
|
83
|
+
def __enter__(self):
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
87
|
+
self.close()
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
# ─────────────────────────────────────────────
|
|
91
|
+
# 公开 API
|
|
92
|
+
# ─────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
def verify(self, card_key: str) -> dict:
|
|
95
|
+
"""
|
|
96
|
+
验证卡密。
|
|
97
|
+
验证成功时自动启动心跳线程;验证失败或卡密不变时复用已有心跳。
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
dict: {
|
|
101
|
+
'success': bool,
|
|
102
|
+
'message': str,
|
|
103
|
+
'remaining_seconds': int,
|
|
104
|
+
'rate_limited': bool, # 是否因频率限制被拒
|
|
105
|
+
}
|
|
106
|
+
"""
|
|
107
|
+
result = self._send_verify_request(card_key)
|
|
108
|
+
|
|
109
|
+
if result.get('success') and not result.get('rate_limited'):
|
|
110
|
+
with self._lock:
|
|
111
|
+
self._current_card_key = card_key
|
|
112
|
+
self._start_heartbeat_unsafe()
|
|
113
|
+
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
def verify_only(self, card_key: str) -> dict:
|
|
117
|
+
"""
|
|
118
|
+
只验证卡密,不启动心跳(适用于一次性检查场景)。
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
同 verify()
|
|
122
|
+
"""
|
|
123
|
+
return self._send_verify_request(card_key)
|
|
124
|
+
|
|
125
|
+
def close(self):
|
|
126
|
+
"""停止心跳并发送离线通知。可重复调用。"""
|
|
127
|
+
self._stop_heartbeat()
|
|
128
|
+
self._send_offline()
|
|
129
|
+
self._current_card_key = None
|
|
130
|
+
|
|
131
|
+
def is_online(self) -> bool:
|
|
132
|
+
"""当前是否有活跃心跳"""
|
|
133
|
+
return self._heartbeat_running
|
|
134
|
+
|
|
135
|
+
# ─────────────────────────────────────────────
|
|
136
|
+
# 内部方法
|
|
137
|
+
# ─────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
def _send_verify_request(self, card_key: str) -> dict:
|
|
140
|
+
"""构造、加密、发送验证请求,返回解析后的结果字典。"""
|
|
141
|
+
import requests
|
|
142
|
+
|
|
143
|
+
request_data = {
|
|
144
|
+
'card_key': card_key,
|
|
145
|
+
'device_sn': self.device_sn,
|
|
146
|
+
'ip_address': self.ip_address,
|
|
147
|
+
'timestamp': int(time.time()),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
payload = self._encrypt_and_sign(request_data)
|
|
152
|
+
except Exception as e:
|
|
153
|
+
return _err(f"加密失败: {e}")
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
response = requests.post(
|
|
157
|
+
f"{self.server_url}/api/card/verify",
|
|
158
|
+
json=payload,
|
|
159
|
+
timeout=10,
|
|
160
|
+
)
|
|
161
|
+
if response.status_code != 200:
|
|
162
|
+
return _err(f"服务器错误: {response.status_code}")
|
|
163
|
+
except requests.exceptions.RequestException as e:
|
|
164
|
+
return _err(f"网络错误: {e}")
|
|
165
|
+
|
|
166
|
+
result = self._decrypt_response(response.json())
|
|
167
|
+
return result
|
|
168
|
+
|
|
169
|
+
def _start_heartbeat_unsafe(self):
|
|
170
|
+
"""调用前需持有 self._lock"""
|
|
171
|
+
if self._heartbeat_running:
|
|
172
|
+
return
|
|
173
|
+
self._heartbeat_running = True
|
|
174
|
+
self._heartbeat_thread = threading.Thread(
|
|
175
|
+
target=self._heartbeat_loop, daemon=True
|
|
176
|
+
)
|
|
177
|
+
self._heartbeat_thread.start()
|
|
178
|
+
|
|
179
|
+
def _stop_heartbeat(self):
|
|
180
|
+
with self._lock:
|
|
181
|
+
if not self._heartbeat_running:
|
|
182
|
+
return
|
|
183
|
+
self._heartbeat_running = False
|
|
184
|
+
if self._heartbeat_thread:
|
|
185
|
+
self._heartbeat_thread.join(timeout=2)
|
|
186
|
+
|
|
187
|
+
def _heartbeat_loop(self):
|
|
188
|
+
import requests
|
|
189
|
+
|
|
190
|
+
while self._heartbeat_running and self._current_card_key:
|
|
191
|
+
try:
|
|
192
|
+
request_data = {
|
|
193
|
+
'device_sn': self.device_sn,
|
|
194
|
+
'ip_address': self.ip_address,
|
|
195
|
+
'card_key': self._current_card_key,
|
|
196
|
+
'timestamp': int(time.time()),
|
|
197
|
+
}
|
|
198
|
+
payload = self._encrypt_and_sign(request_data)
|
|
199
|
+
|
|
200
|
+
resp = requests.post(
|
|
201
|
+
f"{self.server_url}/api/client/heartbeat",
|
|
202
|
+
json=payload,
|
|
203
|
+
timeout=5,
|
|
204
|
+
)
|
|
205
|
+
if resp.status_code != 200:
|
|
206
|
+
print(f"[心跳] 发送失败: {resp.status_code}")
|
|
207
|
+
except Exception as e:
|
|
208
|
+
print(f"[心跳] 发送失败: {e}")
|
|
209
|
+
|
|
210
|
+
# 可中断的休眠
|
|
211
|
+
for _ in range(self.heartbeat_interval):
|
|
212
|
+
if not self._heartbeat_running:
|
|
213
|
+
break
|
|
214
|
+
time.sleep(1)
|
|
215
|
+
|
|
216
|
+
def _send_offline(self):
|
|
217
|
+
if not self._current_card_key:
|
|
218
|
+
return
|
|
219
|
+
import requests
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
request_data = {
|
|
223
|
+
'device_sn': self.device_sn,
|
|
224
|
+
'timestamp': int(time.time()),
|
|
225
|
+
}
|
|
226
|
+
payload = self._encrypt_and_sign(request_data)
|
|
227
|
+
|
|
228
|
+
resp = requests.post(
|
|
229
|
+
f"{self.server_url}/api/client/offline",
|
|
230
|
+
json=payload,
|
|
231
|
+
timeout=5,
|
|
232
|
+
)
|
|
233
|
+
if resp.status_code == 200:
|
|
234
|
+
print("[离线] 已通知服务器")
|
|
235
|
+
else:
|
|
236
|
+
print(f"[离线] 通知失败: {resp.status_code}")
|
|
237
|
+
except Exception as e:
|
|
238
|
+
print(f"[离线] 发送失败: {e}")
|
|
239
|
+
|
|
240
|
+
# ─────────────────────────────────────────────
|
|
241
|
+
# 加密 / 解密辅助
|
|
242
|
+
# ─────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
def _encrypt_and_sign(self, data: dict) -> dict:
|
|
245
|
+
plaintext = json.dumps(data)
|
|
246
|
+
encrypted = self.crypto.encrypt(plaintext)
|
|
247
|
+
data_to_sign = encrypted['ciphertext'] + encrypted['iv']
|
|
248
|
+
signature = self.crypto.generate_hmac(data_to_sign)
|
|
249
|
+
return {
|
|
250
|
+
'ciphertext': encrypted['ciphertext'],
|
|
251
|
+
'iv': encrypted['iv'],
|
|
252
|
+
'signature': signature,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
def _decrypt_response(self, response_data: dict) -> dict:
|
|
256
|
+
try:
|
|
257
|
+
to_verify = response_data['ciphertext'] + response_data['iv']
|
|
258
|
+
if not self.crypto.verify_hmac(to_verify, response_data['signature']):
|
|
259
|
+
return _err("响应签名验证失败")
|
|
260
|
+
|
|
261
|
+
decrypted = self.crypto.decrypt(
|
|
262
|
+
response_data['ciphertext'],
|
|
263
|
+
response_data['iv'],
|
|
264
|
+
)
|
|
265
|
+
return json.loads(decrypted)
|
|
266
|
+
except Exception as e:
|
|
267
|
+
return _err(f"解析响应失败: {e}")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
271
|
+
# 装饰器:@require_card(...)
|
|
272
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
def require_card(
|
|
275
|
+
server_url: str = None,
|
|
276
|
+
key: str = None,
|
|
277
|
+
heartbeat_interval: int = 60,
|
|
278
|
+
exit_on_fail: bool = True,
|
|
279
|
+
):
|
|
280
|
+
"""
|
|
281
|
+
装饰器:在业务函数执行前自动验证卡密。
|
|
282
|
+
验证成功时自动管理心跳和离线通知。
|
|
283
|
+
|
|
284
|
+
用法:
|
|
285
|
+
@require_card(server_url="http://...", key="...")
|
|
286
|
+
def main():
|
|
287
|
+
# 仅当验证通过时才执行
|
|
288
|
+
pass()
|
|
289
|
+
|
|
290
|
+
参数:
|
|
291
|
+
exit_on_fail: 验证失败时是否直接退出进程(默认 True)
|
|
292
|
+
"""
|
|
293
|
+
def decorator(func: Callable) -> Callable:
|
|
294
|
+
def wrapper(*args, **kwargs):
|
|
295
|
+
with CardClient(
|
|
296
|
+
server_url=server_url,
|
|
297
|
+
key=key,
|
|
298
|
+
heartbeat_interval=heartbeat_interval,
|
|
299
|
+
) as client:
|
|
300
|
+
card_key = _prompt_or_env_card_key()
|
|
301
|
+
if not card_key:
|
|
302
|
+
print("未提供卡密,退出。")
|
|
303
|
+
if exit_on_fail:
|
|
304
|
+
sys.exit(1)
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
result = client.verify(card_key)
|
|
308
|
+
_print_result(result)
|
|
309
|
+
|
|
310
|
+
if not result.get('success'):
|
|
311
|
+
if exit_on_fail:
|
|
312
|
+
sys.exit(1)
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
return func(*args, **kwargs)
|
|
316
|
+
return wrapper
|
|
317
|
+
return decorator
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
321
|
+
# CLI 辅助函数(供 python client.py 使用)
|
|
322
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
323
|
+
|
|
324
|
+
def _prompt_or_env_card_key() -> Optional[str]:
|
|
325
|
+
"""优先从环境变量 CARD_KEY 读取,否则交互式输入"""
|
|
326
|
+
import os
|
|
327
|
+
key = os.environ.get("CARD_KEY")
|
|
328
|
+
if key:
|
|
329
|
+
return key.strip()
|
|
330
|
+
try:
|
|
331
|
+
return input("请输入卡密: ").strip() or None
|
|
332
|
+
except (EOFError, KeyboardInterrupt):
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _print_result(result: dict):
|
|
337
|
+
remaining = result.get('remaining_seconds', 0)
|
|
338
|
+
if result['success']:
|
|
339
|
+
print(f"✅ 验证成功: {result['message']}")
|
|
340
|
+
print(f" 剩余时间: {_format_time(remaining)}")
|
|
341
|
+
else:
|
|
342
|
+
print(f"❌ 验证失败: {result['message']}")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _format_time(seconds: int) -> str:
|
|
346
|
+
if seconds <= 0:
|
|
347
|
+
return "已过期"
|
|
348
|
+
d, h = divmod(seconds, 86400)
|
|
349
|
+
h, m = divmod(h, 3600)
|
|
350
|
+
m, s = divmod(m, 60)
|
|
351
|
+
parts = []
|
|
352
|
+
if d:
|
|
353
|
+
parts.append(f"{d}天")
|
|
354
|
+
if h:
|
|
355
|
+
parts.append(f"{h}小时")
|
|
356
|
+
if m:
|
|
357
|
+
parts.append(f"{m}分钟")
|
|
358
|
+
if s or not parts:
|
|
359
|
+
parts.append(f"{s}秒")
|
|
360
|
+
return "".join(parts)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _err(msg: str) -> dict:
|
|
364
|
+
return {'success': False, 'message': msg, 'remaining_seconds': 0}
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
368
|
+
# CLI 入口(python client.py ...)
|
|
369
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
def _cli_main():
|
|
372
|
+
import argparse
|
|
373
|
+
|
|
374
|
+
parser = argparse.ArgumentParser(description='卡密验证客户端')
|
|
375
|
+
parser.add_argument('--server', default=DEFAULT_SERVER_URL, help='服务器URL')
|
|
376
|
+
parser.add_argument('--key', default=DEFAULT_KEY, help='加密密钥')
|
|
377
|
+
parser.add_argument('--card', help='卡密(直接验证模式)')
|
|
378
|
+
parser.add_argument('--once', action='store_true', help='只验证一次,不启动心跳')
|
|
379
|
+
parser.add_argument('--interactive', action='store_true', help='交互模式')
|
|
380
|
+
parser.add_argument('--heartbeat', type=int, default=60, help='心跳间隔秒数')
|
|
381
|
+
args = parser.parse_args()
|
|
382
|
+
|
|
383
|
+
client = CardClient(
|
|
384
|
+
server_url=args.server,
|
|
385
|
+
key=args.key,
|
|
386
|
+
heartbeat_interval=args.heartbeat,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# 信号处理:Ctrl+C 时优雅退出
|
|
390
|
+
def _cleanup(signum=None, frame=None):
|
|
391
|
+
print("\n正在退出...")
|
|
392
|
+
client.close()
|
|
393
|
+
print("感谢使用,再见!")
|
|
394
|
+
sys.exit(0)
|
|
395
|
+
|
|
396
|
+
try:
|
|
397
|
+
signal.signal(signal.SIGINT, _cleanup)
|
|
398
|
+
signal.signal(signal.SIGTERM, _cleanup)
|
|
399
|
+
except ValueError:
|
|
400
|
+
pass # 非主线程中无法设置 signal
|
|
401
|
+
|
|
402
|
+
print("=" * 60)
|
|
403
|
+
print(f"设备序列号: {client.device_sn}")
|
|
404
|
+
print(f"IP地址: {client.ip_address}")
|
|
405
|
+
print("=" * 60)
|
|
406
|
+
|
|
407
|
+
if args.card:
|
|
408
|
+
result = client.verify_only(args.card) if args.once else client.verify(args.card)
|
|
409
|
+
_print_result(result)
|
|
410
|
+
if result.get('success') and not args.once:
|
|
411
|
+
print(f"\n💓 心跳已启动(间隔 {args.heartbeat} 秒),按 Ctrl+C 退出...")
|
|
412
|
+
_wait_forever()
|
|
413
|
+
return
|
|
414
|
+
|
|
415
|
+
if args.once:
|
|
416
|
+
card_key = _prompt_or_env_card_key()
|
|
417
|
+
if not card_key:
|
|
418
|
+
return
|
|
419
|
+
result = client.verify_only(card_key)
|
|
420
|
+
_print_result(result)
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
# 交互模式(默认)
|
|
424
|
+
print("交互模式(输入 'quit' 退出)\n")
|
|
425
|
+
while True:
|
|
426
|
+
try:
|
|
427
|
+
card_key = input("请输入卡密: ").strip()
|
|
428
|
+
if card_key.lower() == 'quit':
|
|
429
|
+
_cleanup()
|
|
430
|
+
if not card_key:
|
|
431
|
+
print("卡密不能为空!")
|
|
432
|
+
continue
|
|
433
|
+
result = client.verify(card_key)
|
|
434
|
+
_print_result(result)
|
|
435
|
+
if result.get('success'):
|
|
436
|
+
print(f"\n💓 心跳已启动,按 Ctrl+C 退出...")
|
|
437
|
+
_wait_forever()
|
|
438
|
+
except KeyboardInterrupt:
|
|
439
|
+
_cleanup()
|
|
440
|
+
except Exception as e:
|
|
441
|
+
print(f"错误: {e}")
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _wait_forever():
|
|
445
|
+
try:
|
|
446
|
+
while True:
|
|
447
|
+
time.sleep(1)
|
|
448
|
+
except KeyboardInterrupt:
|
|
449
|
+
pass
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
if __name__ == "__main__":
|
|
453
|
+
_cli_main()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
客户端加密解密模块
|
|
3
|
+
使用AES-256-CBC加密和HMAC SHA256签名
|
|
4
|
+
密钥派生:从主密钥派生独立的加密密钥和HMAC密钥(与服务端保持一致)
|
|
5
|
+
"""
|
|
6
|
+
import base64
|
|
7
|
+
import hashlib
|
|
8
|
+
import hmac
|
|
9
|
+
from Crypto.Cipher import AES
|
|
10
|
+
from Crypto.Util.Padding import pad, unpad
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CryptoManager:
|
|
14
|
+
"""加密管理器(客户端版本)"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, key: bytes):
|
|
17
|
+
"""
|
|
18
|
+
初始化加密管理器,从主密钥派生独立密钥
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
key: 主密钥(必须与服务器端相同,任意长度)
|
|
22
|
+
"""
|
|
23
|
+
# 主密钥归一化为32字节
|
|
24
|
+
if len(key) != 32:
|
|
25
|
+
key = hashlib.sha256(key).digest()
|
|
26
|
+
# 派生独立密钥:加密密钥 和 HMAC密钥(与服务端一致)
|
|
27
|
+
self.enc_key = hashlib.sha256(key + b"enc").digest()
|
|
28
|
+
self.hmac_key = hashlib.sha256(key + b"hmac").digest()
|
|
29
|
+
|
|
30
|
+
def encrypt(self, data: str) -> dict:
|
|
31
|
+
"""
|
|
32
|
+
加密数据
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
data: 要加密的字符串数据
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
包含加密数据和IV的字典
|
|
39
|
+
"""
|
|
40
|
+
from Crypto.Random import get_random_bytes
|
|
41
|
+
|
|
42
|
+
# 生成随机IV (16字节)
|
|
43
|
+
iv = get_random_bytes(16)
|
|
44
|
+
|
|
45
|
+
# 创建AES加密器
|
|
46
|
+
cipher = AES.new(self.enc_key, AES.MODE_CBC, iv)
|
|
47
|
+
|
|
48
|
+
# 加密数据
|
|
49
|
+
ciphertext = cipher.encrypt(pad(data.encode('utf-8'), AES.block_size))
|
|
50
|
+
|
|
51
|
+
# 返回base64编码的密文和IV
|
|
52
|
+
return {
|
|
53
|
+
'ciphertext': base64.b64encode(ciphertext).decode('utf-8'),
|
|
54
|
+
'iv': base64.b64encode(iv).decode('utf-8')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def decrypt(self, ciphertext: str, iv: str) -> str:
|
|
58
|
+
"""
|
|
59
|
+
解密数据
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
ciphertext: base64编码的密文
|
|
63
|
+
iv: base64编码的IV
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
解密后的字符串
|
|
67
|
+
"""
|
|
68
|
+
# 解码base64
|
|
69
|
+
ciphertext_bytes = base64.b64decode(ciphertext)
|
|
70
|
+
iv_bytes = base64.b64decode(iv)
|
|
71
|
+
|
|
72
|
+
# 创建AES解密器
|
|
73
|
+
cipher = AES.new(self.enc_key, AES.MODE_CBC, iv_bytes)
|
|
74
|
+
|
|
75
|
+
# 解密并去除填充
|
|
76
|
+
plaintext = unpad(cipher.decrypt(ciphertext_bytes), AES.block_size)
|
|
77
|
+
|
|
78
|
+
return plaintext.decode('utf-8')
|
|
79
|
+
|
|
80
|
+
def generate_hmac(self, data: str) -> str:
|
|
81
|
+
"""
|
|
82
|
+
生成HMAC签名
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
data: 要签名的数据
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
HMAC签名(十六进制字符串)
|
|
89
|
+
"""
|
|
90
|
+
return hmac.new(self.hmac_key, data.encode('utf-8'), hashlib.sha256).hexdigest()
|
|
91
|
+
|
|
92
|
+
def verify_hmac(self, data: str, signature: str) -> bool:
|
|
93
|
+
"""
|
|
94
|
+
验证HMAC签名
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
data: 原始数据
|
|
98
|
+
signature: HMAC签名
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
签名是否有效
|
|
102
|
+
"""
|
|
103
|
+
expected_signature = self.generate_hmac(data)
|
|
104
|
+
return hmac.compare_digest(expected_signature, signature)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
设备信息收集模块
|
|
3
|
+
"""
|
|
4
|
+
import socket
|
|
5
|
+
import uuid
|
|
6
|
+
import platform
|
|
7
|
+
import hashlib
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_device_sn() -> str:
|
|
11
|
+
"""
|
|
12
|
+
获取设备序列号
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
设备序列号(基于硬件信息生成的唯一标识)
|
|
16
|
+
"""
|
|
17
|
+
# 尝试获取真实的硬件序列号
|
|
18
|
+
try:
|
|
19
|
+
# Windows
|
|
20
|
+
if platform.system() == "Windows":
|
|
21
|
+
import wmi
|
|
22
|
+
c = wmi.WMI()
|
|
23
|
+
for item in c.Win32_BIOS():
|
|
24
|
+
return item.SerialNumber.strip()
|
|
25
|
+
# Linux
|
|
26
|
+
elif platform.system() == "Linux":
|
|
27
|
+
try:
|
|
28
|
+
with open('/etc/machine-id', 'r') as f:
|
|
29
|
+
return f.read().strip()
|
|
30
|
+
except:
|
|
31
|
+
try:
|
|
32
|
+
with open('/var/lib/dbus/machine-id', 'r') as f:
|
|
33
|
+
return f.read().strip()
|
|
34
|
+
except:
|
|
35
|
+
pass
|
|
36
|
+
# macOS
|
|
37
|
+
elif platform.system() == "Darwin":
|
|
38
|
+
import subprocess
|
|
39
|
+
output = subprocess.check_output(['system_profiler', 'SPHardwareDataType'])
|
|
40
|
+
for line in output.decode('utf-8').split('\n'):
|
|
41
|
+
if 'Serial Number' in line:
|
|
42
|
+
return line.split(':')[1].strip()
|
|
43
|
+
except:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
# 如果无法获取真实序列号,则基于MAC地址和硬件信息生成一个
|
|
47
|
+
mac = uuid.getnode()
|
|
48
|
+
machine_info = f"{platform.node()}-{platform.machine()}-{platform.processor()}-{mac}"
|
|
49
|
+
device_sn = hashlib.sha256(machine_info.encode()).hexdigest()[:32]
|
|
50
|
+
|
|
51
|
+
return device_sn
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_ip_address() -> str:
|
|
55
|
+
"""
|
|
56
|
+
获取本机IP地址
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
本机IP地址
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
# 创建一个UDP socket连接到外部地址(不会真正发送数据)
|
|
63
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
64
|
+
s.connect(("8.8.8.8", 80))
|
|
65
|
+
ip_address = s.getsockname()[0]
|
|
66
|
+
s.close()
|
|
67
|
+
return ip_address
|
|
68
|
+
except:
|
|
69
|
+
# 如果无法获取,返回回环地址
|
|
70
|
+
return "127.0.0.1"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_device_info() -> dict:
|
|
74
|
+
"""
|
|
75
|
+
获取设备完整信息
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
设备信息字典
|
|
79
|
+
"""
|
|
80
|
+
return {
|
|
81
|
+
'device_sn': get_device_sn(),
|
|
82
|
+
'ip_address': get_ip_address(),
|
|
83
|
+
'hostname': platform.node(),
|
|
84
|
+
'platform': platform.system(),
|
|
85
|
+
'platform_release': platform.release(),
|
|
86
|
+
'platform_version': platform.version(),
|
|
87
|
+
'architecture': platform.machine(),
|
|
88
|
+
'processor': platform.processor()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
# 测试
|
|
94
|
+
print("设备序列号:", get_device_sn())
|
|
95
|
+
print("IP地址:", get_ip_address())
|
|
96
|
+
print("\n完整设备信息:")
|
|
97
|
+
info = get_device_info()
|
|
98
|
+
for key, value in info.items():
|
|
99
|
+
print(f" {key}: {value}")
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xfcloudcard
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 云控卡密验证客户端库 - 支持 AES+HMAC 加密通信、心跳保活、离线上报
|
|
5
|
+
Author: songxf
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/songxf/xfcloudcard
|
|
8
|
+
Project-URL: Source, https://github.com/songxf/xfcloudcard
|
|
9
|
+
Project-URL: Issues, https://github.com/songxf/xfcloudcard/issues
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Topic :: Security :: Cryptography
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: requests>=2.25.0
|
|
22
|
+
Requires-Dist: pycryptodome>=3.15.0
|
|
23
|
+
|
|
24
|
+
# xfcloudcard
|
|
25
|
+
|
|
26
|
+
云控卡密验证客户端库,支持 AES-256-CBC + HMAC-SHA256 加密通信、心跳保活、离线上报。
|
|
27
|
+
|
|
28
|
+
## 安装
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install xfcloudcard
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 快速开始
|
|
35
|
+
|
|
36
|
+
### 方式一:上下文管理器(推荐)
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from xfcloudcard import CardClient
|
|
40
|
+
|
|
41
|
+
with CardClient(server_url="http://localhost:8000", key="your-key") as client:
|
|
42
|
+
result = client.verify("CARD-XXXX-XXXX-XXXX-XXXX")
|
|
43
|
+
if result['success']:
|
|
44
|
+
# 业务代码写这里
|
|
45
|
+
pass
|
|
46
|
+
# 退出 with 块时自动停止心跳 + 发送离线通知
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 方式二:装饰器(最简洁)
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from xfcloudcard import require_card
|
|
53
|
+
|
|
54
|
+
@require_card(server_url="http://localhost:8000", key="your-key")
|
|
55
|
+
def main():
|
|
56
|
+
# 仅当卡密验证通过后才执行
|
|
57
|
+
print("验证通过,执行业务逻辑...")
|
|
58
|
+
|
|
59
|
+
main()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 方式三:命令行
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# 交互模式
|
|
66
|
+
xfcloudcard
|
|
67
|
+
|
|
68
|
+
# 直接验证
|
|
69
|
+
xfcloudcard --card CARD-XXXX-XXXX-XXXX-XXXX
|
|
70
|
+
|
|
71
|
+
# 只验证,不启动心跳
|
|
72
|
+
xfcloudcard --card CARD-XXXX-... --once
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## API
|
|
76
|
+
|
|
77
|
+
### `CardClient(server_url, key, heartbeat_interval=60)`
|
|
78
|
+
|
|
79
|
+
| 方法 | 说明 |
|
|
80
|
+
|------|------|
|
|
81
|
+
| `verify(card_key)` | 验证卡密,成功后自动启动心跳 |
|
|
82
|
+
| `verify_only(card_key)` | 只验证,不启动心跳 |
|
|
83
|
+
| `close()` | 停止心跳并发送离线通知 |
|
|
84
|
+
| `is_online()` | 返回心跳是否运行中 |
|
|
85
|
+
|
|
86
|
+
### `require_card(server_url, key, heartbeat_interval=60, exit_on_fail=True)`
|
|
87
|
+
|
|
88
|
+
装饰器,在业务函数执行前自动验证卡密。
|
|
89
|
+
|
|
90
|
+
## 依赖
|
|
91
|
+
|
|
92
|
+
- `requests >= 2.25.0`
|
|
93
|
+
- `pycryptodome >= 3.15.0`
|
|
94
|
+
|
|
95
|
+
## 协议
|
|
96
|
+
|
|
97
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
xfcloudcard/__init__.py
|
|
4
|
+
xfcloudcard/client.py
|
|
5
|
+
xfcloudcard/crypto.py
|
|
6
|
+
xfcloudcard/device_info.py
|
|
7
|
+
xfcloudcard.egg-info/PKG-INFO
|
|
8
|
+
xfcloudcard.egg-info/SOURCES.txt
|
|
9
|
+
xfcloudcard.egg-info/dependency_links.txt
|
|
10
|
+
xfcloudcard.egg-info/requires.txt
|
|
11
|
+
xfcloudcard.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
xfcloudcard
|