Files
xiaozhi-esp32-server/main/xiaozhi-server/core/api/ota_handler.py
T

200 lines
7.9 KiB
Python
Raw Normal View History

2025-05-21 13:18:12 +08:00
import json
import time
2025-09-22 14:18:03 +08:00
import base64
import hashlib
import hmac
2025-05-21 13:18:12 +08:00
from aiohttp import web
2025-10-11 15:27:18 +08:00
2025-10-13 17:52:28 +08:00
from core.auth import AuthManager
2025-05-29 23:56:34 +08:00
from core.utils.util import get_local_ip
from core.api.base_handler import BaseHandler
2025-05-21 13:18:12 +08:00
TAG = __name__
class OTAHandler(BaseHandler):
2025-05-21 13:18:12 +08:00
def __init__(self, config: dict):
super().__init__(config)
2025-10-11 15:27:18 +08:00
auth_config = config["server"].get("auth", {})
self.auth_enable = auth_config.get("enabled", False)
# 设备白名单
2025-10-13 17:52:28 +08:00
self.allowed_devices = set(auth_config.get("allowed_devices", []))
secret_key = config["server"]["auth_key"]
2025-10-11 15:27:18 +08:00
expire_seconds = auth_config.get("expire_seconds")
self.auth = AuthManager(secret_key=secret_key, expire_seconds=expire_seconds)
2025-10-13 17:52:28 +08:00
2025-09-22 14:18:03 +08:00
def generate_password_signature(self, content: str, secret_key: str) -> str:
"""生成MQTT密码签名
2025-10-13 17:52:28 +08:00
2025-09-22 14:18:03 +08:00
Args:
content: 签名内容 (clientId + '|' + username)
secret_key: 密钥
2025-10-13 17:52:28 +08:00
2025-09-22 14:18:03 +08:00
Returns:
str: Base64编码的HMAC-SHA256签名
"""
try:
2025-10-13 17:52:28 +08:00
hmac_obj = hmac.new(
secret_key.encode("utf-8"), content.encode("utf-8"), hashlib.sha256
)
2025-09-22 14:18:03 +08:00
signature = hmac_obj.digest()
2025-10-13 17:52:28 +08:00
return base64.b64encode(signature).decode("utf-8")
2025-09-22 14:18:03 +08:00
except Exception as e:
self.logger.bind(tag=TAG).error(f"生成MQTT密码签名失败: {e}")
return ""
2025-05-21 13:18:12 +08:00
2025-09-23 11:29:05 +08:00
def _get_websocket_url(self, local_ip: str, port: int) -> str:
"""获取websocket地址
Args:
local_ip: 本地IP地址
port: 端口号
Returns:
str: websocket地址
"""
server_config = self.config["server"]
websocket_config = server_config.get("websocket", "")
if "你的" not in websocket_config:
return websocket_config
else:
return f"ws://{local_ip}:{port}/xiaozhi/v1/"
async def handle_post(self, request):
2025-09-23 11:29:05 +08:00
"""处理 OTA POST 请求"""
2025-05-21 13:18:12 +08:00
try:
data = await request.text()
self.logger.bind(tag=TAG).debug(f"OTA请求方法: {request.method}")
self.logger.bind(tag=TAG).debug(f"OTA请求头: {request.headers}")
self.logger.bind(tag=TAG).debug(f"OTA请求数据: {data}")
device_id = request.headers.get("device-id", "")
2025-09-23 11:29:05 +08:00
if device_id:
self.logger.bind(tag=TAG).info(f"OTA请求设备ID: {device_id}")
else:
2025-05-21 13:18:12 +08:00
raise Exception("OTA请求设备ID为空")
2025-10-11 15:27:18 +08:00
client_id = request.headers.get("client-id", "")
if client_id:
self.logger.bind(tag=TAG).info(f"OTA请求ClientID: {client_id}")
else:
raise Exception("OTA请求ClientID为空")
2025-05-21 13:18:12 +08:00
data_json = json.loads(data)
server_config = self.config["server"]
2025-09-23 11:29:05 +08:00
port = int(server_config.get("port", 8000))
local_ip = get_local_ip()
2025-05-21 13:18:12 +08:00
return_json = {
"server_time": {
"timestamp": int(round(time.time() * 1000)),
"timezone_offset": server_config.get("timezone_offset", 8) * 60,
},
"firmware": {
"version": data_json["application"].get("version", "1.0.0"),
"url": "",
},
2025-09-23 11:29:05 +08:00
}
2025-09-23 11:37:28 +08:00
mqtt_gateway_endpoint = server_config.get("mqtt_gateway")
2025-10-13 17:52:28 +08:00
2025-09-23 11:37:28 +08:00
if mqtt_gateway_endpoint: # 如果配置了非空字符串
2025-09-23 11:29:05 +08:00
# 尝试从请求数据中获取设备型号
device_model = "default"
try:
if "device" in data_json and isinstance(data_json["device"], dict):
device_model = data_json["device"].get("model", "default")
elif "model" in data_json:
device_model = data_json["model"]
group_id = f"GID_{device_model}".replace(":", "_").replace(" ", "_")
except Exception as e:
self.logger.bind(tag=TAG).error(f"获取设备型号失败: {e}")
2025-09-23 11:37:28 +08:00
group_id = "GID_default"
2025-09-23 11:29:05 +08:00
mac_address_safe = device_id.replace(":", "_")
mqtt_client_id = f"{group_id}@@@{mac_address_safe}@@@{mac_address_safe}"
2025-09-23 11:37:28 +08:00
# 构建用户数据
2025-10-13 17:52:28 +08:00
user_data = {"ip": "unknown"}
2025-09-23 11:29:05 +08:00
try:
user_data_json = json.dumps(user_data)
2025-10-13 17:52:28 +08:00
username = base64.b64encode(user_data_json.encode("utf-8")).decode(
"utf-8"
)
2025-09-23 11:29:05 +08:00
except Exception as e:
self.logger.bind(tag=TAG).error(f"生成用户名失败: {e}")
username = ""
2025-09-23 11:37:28 +08:00
# 生成密码
2025-09-23 11:29:05 +08:00
password = ""
signature_key = server_config.get("mqtt_signature_key", "")
if signature_key:
2025-10-13 17:52:28 +08:00
password = self.generate_password_signature(
mqtt_client_id + "|" + username, signature_key
)
2025-09-23 11:29:05 +08:00
if not password:
2025-09-23 11:37:28 +08:00
password = "" # 签名失败则留空,由设备决定是否允许无密码
2025-09-23 11:29:05 +08:00
else:
2025-09-23 11:37:28 +08:00
self.logger.bind(tag=TAG).warning("缺少MQTT签名密钥,密码留空")
2025-09-23 11:29:05 +08:00
2025-09-23 11:37:28 +08:00
# 构建MQTT配置(直接使用 mqtt_gateway 字符串)
2025-09-23 11:29:05 +08:00
return_json["mqtt_gateway"] = {
2025-09-23 11:37:28 +08:00
"endpoint": mqtt_gateway_endpoint,
2025-09-22 14:18:03 +08:00
"client_id": mqtt_client_id,
"username": username,
"password": password,
"publish_topic": "device-server",
2025-10-13 17:52:28 +08:00
"subscribe_topic": f"devices/p2p/{mac_address_safe}",
2025-09-22 14:18:03 +08:00
}
2025-09-23 11:29:05 +08:00
self.logger.bind(tag=TAG).info(f"为设备 {device_id} 下发MQTT网关配置")
2025-10-13 17:52:28 +08:00
2025-09-23 11:37:28 +08:00
else: # 未配置 mqtt_gateway,下发 WebSocket
2025-10-11 15:27:18 +08:00
# 如果开启了认证,则进行认证校验
token = ""
if self.auth_enable:
if self.allowed_devices:
2025-10-13 17:52:28 +08:00
if device_id not in self.allowed_devices:
2025-10-11 15:27:18 +08:00
token = self.auth.generate_token(client_id, device_id)
else:
token = self.auth.generate_token(client_id, device_id)
2025-09-23 11:29:05 +08:00
return_json["websocket"] = {
"url": self._get_websocket_url(local_ip, port),
2025-10-13 17:52:28 +08:00
"token": token,
2025-09-23 11:29:05 +08:00
}
2025-10-13 17:52:28 +08:00
self.logger.bind(tag=TAG).info(
f"未配置MQTT网关,为设备 {device_id} 下发WebSocket配置"
)
2025-10-11 15:27:18 +08:00
self.logger.bind(tag=TAG).info(f"{return_json}")
2025-10-13 17:52:28 +08:00
2025-05-21 13:18:12 +08:00
response = web.Response(
text=json.dumps(return_json, separators=(",", ":")),
content_type="application/json",
)
except Exception as e:
2025-09-23 11:29:05 +08:00
return_json = {"success": False, "message": "request error."}
2025-05-21 13:18:12 +08:00
response = web.Response(
text=json.dumps(return_json, separators=(",", ":")),
content_type="application/json",
)
finally:
self._add_cors_headers(response)
2025-05-21 13:18:12 +08:00
return response
async def handle_get(self, request):
"""处理 OTA GET 请求"""
2025-05-21 13:18:12 +08:00
try:
server_config = self.config["server"]
2025-09-23 11:29:05 +08:00
local_ip = get_local_ip()
port = int(server_config.get("port", 8000))
websocket_url = self._get_websocket_url(local_ip, port)
message = f"OTA接口运行正常,向设备发送的websocket地址是:{websocket_url}"
2025-05-21 13:18:12 +08:00
response = web.Response(text=message, content_type="text/plain")
except Exception as e:
self.logger.bind(tag=TAG).error(f"OTA GET请求异常: {e}")
response = web.Response(text="OTA接口异常", content_type="text/plain")
finally:
self._add_cors_headers(response)
2025-10-13 17:52:28 +08:00
return response