From: 孙春阳 Date: Wed, 12 Nov 2025 10:19:37 +0000 (+0800) Subject: N706A H111 T1108 版本AT指令上网固件,python验证测试脚本提交。https/http下载,tcp 连接已经验证完毕。证书已经生成。 X-Git-Url: http://git.snicker.com.cn/?a=commitdiff_plain;h=refs%2Fchanges%2F2733%2Fhead;p=mirrors%2FFD230.git N706A H111 T1108 版本AT指令上网固件,python验证测试脚本提交。https/http下载,tcp 连接已经验证完毕。证书已经生成。 --- diff --git a/cat1modem/leiniao/N706A-H111-STD-BZ_CN1X-001_T1108.fwpkg b/cat1modem/leiniao/N706A-H111-STD-BZ_CN1X-001_T1108.fwpkg new file mode 100644 index 000000000..162f336b8 Binary files /dev/null and b/cat1modem/leiniao/N706A-H111-STD-BZ_CN1X-001_T1108.fwpkg differ diff --git a/cat1modem/src/origImg/N706A-H111-STD-BZ_CN1X-001_T1108.tar.gz b/cat1modem/src/origImg/N706A-H111-STD-BZ_CN1X-001_T1108.tar.gz new file mode 100644 index 000000000..192051abc Binary files /dev/null and b/cat1modem/src/origImg/N706A-H111-STD-BZ_CN1X-001_T1108.tar.gz differ diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_connect_tcp.py b/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_connect_tcp.py new file mode 100644 index 000000000..d68a08e58 --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_connect_tcp.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +通过TCP客户端连接个人测试服务器 +使用重构后的公共TCP客户端模块实现 +通过被动接收的方式接收服务器的消息 +""" + +import time +from n706a_at_handler import ATCommandHandler +from n706a_tcp_client import N706ATCPClient + +# -------------------------- 1. 配置参数 -------------------------- +# 服务器配置 +SERVER_HOST = "snicker.com.cn" +SERVER_PORT = 8910 + +# AT模块串口配置(根据实际硬件配置修改) +SERIAL_PORT = "COM19" # 修改为实际的串口设备 +SERIAL_BAUDRATE = 1000000 +SERIAL_TIMEOUT = 10 + +# Socket ID +SOCKET_ID = 0 + +# 是否使用SSL/TLS加密 +USE_SSL = False # 设置为True使用TCP+SSL连接 + +# SSL配置(仅USE_SSL=True时需要) +SSL_AUTH_MODE = 0 # 0=不认证, 1=单向认证, 2=双向认证 +SSL_CA_CERT = None # CA证书文件名(单向/双向认证需要) +SSL_CLIENT_CERT = None # 客户端证书文件名(双向认证需要) +SSL_CLIENT_KEY = None # 客户端密钥文件名(双向认证需要) + +# 接收超时时间(秒) +RECEIVE_TIMEOUT = 30 + +# -------------------------- 2. 连接测试服务器函数 -------------------------- +def connect_test_server(): + """连接测试服务器并被动接收消息""" + + # 初始化AT指令处理器 + at_handler = ATCommandHandler(SERIAL_PORT, SERIAL_BAUDRATE, SERIAL_TIMEOUT) + + try: + # 连接串口 + if not at_handler.connect(): + print("❌ 无法连接串口,请检查串口配置") + return False + + # 测试AT连接 + success, response = at_handler.send_at_command("AT", timeout=3) + if not success: + print("❌ AT模块无响应,请检查连接") + return False + print("✅ AT模块连接正常") + + # 检查PPP拨号状态 + print("\n" + "=" * 50) + print("【步骤1: 检查PPP拨号状态】") + print("=" * 50) + is_ppp_connected, ppp_ip = at_handler.check_ppp_status() + + if is_ppp_connected: + print(f"✅ PPP已连接,IP地址: {ppp_ip}") + else: + print(f"❌ PPP未连接,IP地址: {ppp_ip or '未知'}") + print("\n尝试建立PPP连接...") + + apn_name = None + success, ip = at_handler.establish_ppp_connection(apn_name=apn_name) + + if not success: + print(f"❌ PPP连接建立失败,无法继续TCP连接") + return False + + ppp_ip = ip + + # 配置网络省电模式 + print("\n" + "=" * 50) + print("【步骤2: 配置网络省电模式】") + print("=" * 50) + success, response = at_handler.send_at_command("AT+NWENPWRSAVE=1", timeout=5) + if not success: + print(f"⚠️ 网络省电模式配置失败: {response}") + print("继续执行...") + else: + print("✅ 网络省电模式已启用 (AT+NWENPWRSAVE=1)") + + # 初始化TCP客户端 + print("\n" + "=" * 50) + print("【步骤3: 初始化TCP客户端】") + print("=" * 50) + tcp_client = N706ATCPClient(at_handler, socket_id=SOCKET_ID) + + # 配置SSL(如果需要) + if USE_SSL: + print("\n" + "=" * 50) + print("【步骤4: 配置SSL参数】") + print("=" * 50) + if not tcp_client.configure_ssl( + ssl_version=3, # TLS1.2 + auth_mode=SSL_AUTH_MODE, + ca_cert=SSL_CA_CERT, + client_cert=SSL_CLIENT_CERT, + client_key=SSL_CLIENT_KEY + ): + print("❌ SSL配置失败,终止流程") + return False + + # 建立TCP连接 + protocol_name = "TCP+SSL" if USE_SSL else "TCP" + print("\n" + "=" * 50) + print(f"【步骤{4 if not USE_SSL else 5}: 建立{protocol_name}连接】") + print("=" * 50) + print(f"连接服务器: {SERVER_HOST}:{SERVER_PORT}") + + if not tcp_client.connect( + server_ip=SERVER_HOST, + server_port=SERVER_PORT, + use_ssl=USE_SSL, + transparent_mode=False # 非透传模式,使用被动接收 + ): + print(f"❌ {protocol_name}连接失败") + return False + + print(f"✅ {protocol_name}连接成功") + + # 被动接收服务器消息 + print("\n" + "=" * 50) + print("【步骤6: 被动接收服务器消息】") + print("=" * 50) + print(f"等待服务器发送消息(超时时间: {RECEIVE_TIMEOUT}秒)...") + print("(按Ctrl+C中断)") + print("=" * 50) + + message_count = 0 + start_time = time.time() + + try: + while True: + # 等待接收数据 + data = tcp_client.wait_for_data(timeout=RECEIVE_TIMEOUT) + + if data: + message_count += 1 + elapsed_time = time.time() - start_time + + print(f"\n【消息 #{message_count}】 (连接后 {elapsed_time:.1f}秒)") + print(f"数据长度: {len(data)} 字节") + + # 尝试解码为文本 + try: + text_data = data.decode('utf-8') + print(f"文本内容: {text_data}") + except: + print(f"二进制数据(hex): {data[:100].hex()}...") # 只显示前100字节 + + print("-" * 50) + else: + print(f"\n⚠️ 接收超时({RECEIVE_TIMEOUT}秒),继续等待...") + print("-" * 50) + + except KeyboardInterrupt: + print("\n\n用户中断接收") + + # 关闭连接 + print("\n" + "=" * 50) + print(f"【步骤7: 关闭{protocol_name}连接】") + print("=" * 50) + tcp_client.close() + + # 统计信息 + total_time = time.time() - start_time + print("\n" + "=" * 50) + print("【连接统计】") + print("=" * 50) + print(f"连接持续时间: {total_time:.1f} 秒") + print(f"接收消息数量: {message_count}") + if message_count > 0: + print(f"平均消息间隔: {total_time/message_count:.1f} 秒") + print("=" * 50) + + return True + + except Exception as e: + print("\n" + "=" * 50) + print("【错误处理】") + print("=" * 50) + print(f"❌ 连接异常: {str(e)}") + import traceback + traceback.print_exc() + return False + finally: + # 断开串口连接 + print("\n断开串口连接...") + at_handler.disconnect() + +# -------------------------- 3. 主程序 -------------------------- +if __name__ == "__main__": + print("=" * 50) + print("TCP测试服务器连接工具(AT指令版本)") + print("=" * 50) + print(f"服务器地址: {SERVER_HOST}:{SERVER_PORT}") + print(f"连接类型: {'TCP+SSL' if USE_SSL else 'TCP'}") + print(f"Socket ID: {SOCKET_ID}") + print("=" * 50) + + # 执行连接 + success = connect_test_server() + + if success: + print("\n✅ 连接测试完成!") + else: + print("\n❌ 连接测试失败!") + exit(1) + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_download_file.py b/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_download_file.py new file mode 100644 index 000000000..edbec903a --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_download_file.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +使用AT指令通过HTTP/HTTPS下载文件 +使用重构后的公共模块实现 +""" + +import os +import time +from n706a_at_handler import ATCommandHandler +from n706a_http_client import N706AHTTPClient, load_certificate_file + +# -------------------------- 1. 配置参数 -------------------------- +# 目标文件URL +FILE_URL = "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/RoundEyes/Blue/Angry.mp4" +# FILE_URL = "http://snicker.com.cn:8089/LEINIAO/blue/angry.zip" + +# 保存路径 +OUTPUT_DIR = "downloads" +OUTPUT_FILENAME = "downloaded_file" + +# AT模块串口配置(根据实际硬件配置修改) +SERIAL_PORT = "COM19" # 修改为实际的串口设备 +SERIAL_BAUDRATE = 1000000 +SERIAL_TIMEOUT = 10 + +# 认证模式配置 +AUTH_MODE = 1 # 0=无需认证, 1=单向认证, 2=双向认证 +# CA_CERT_PATH = None # 例如: "certs/ca.crt" +# CA_CERT_PATH = "certs/ca_chain.pem" +CA_CERT_PATH = "certs/intermediate_cas.pem" + +# -------------------------- 2. 下载文件函数 -------------------------- +def download_file(url, output_path, auth_mode=0, ca_cert_path=None): + """ + 使用AT指令通过HTTP/HTTPS下载文件 + + Args: + url: 文件的HTTP/HTTPS URL + output_path: 保存路径 + auth_mode: 认证模式,0=无需认证, 1=单向认证, 2=双向认证(仅HTTPS需要) + ca_cert_path: CA证书路径(HTTPS单向认证时需要) + """ + # 初始化AT指令处理器 + at_handler = ATCommandHandler(SERIAL_PORT, SERIAL_BAUDRATE, SERIAL_TIMEOUT) + + try: + # 连接串口 + if not at_handler.connect(): + print("❌ 无法连接串口,请检查串口配置") + return False + + # 测试AT连接 + success, response = at_handler.send_at_command("AT", timeout=3) + if not success: + print("❌ AT模块无响应,请检查连接") + return False + print("✅ AT模块连接正常") + + # 检查PPP拨号状态 + print("\n" + "=" * 50) + print("【步骤1: 检查PPP拨号状态】") + print("=" * 50) + is_ppp_connected, ppp_ip = at_handler.check_ppp_status() + + if is_ppp_connected: + print(f"✅ PPP已连接,IP地址: {ppp_ip}") + else: + print(f"❌ PPP未连接,IP地址: {ppp_ip or '未知'}") + print("\n尝试建立PPP连接...") + + apn_name = None + success, ip = at_handler.establish_ppp_connection(apn_name=apn_name) + + if not success: + print(f"❌ PPP连接建立失败,无法继续HTTP/HTTPS访问") + return False + + ppp_ip = ip + + # 初始化HTTP/HTTPS客户端(自动判断协议类型) + http_client = N706AHTTPClient(at_handler) + + # 先设置URL以判断协议类型 + http_client.set_url(url) + + # 处理证书(如果需要) + ca_cert_name = None + if http_client.is_https and auth_mode == 1 and ca_cert_path: + print("\n" + "=" * 50) + print("【步骤2: 处理证书】") + print("=" * 50) + + ca_cert_name = os.path.basename(ca_cert_path) + + # 检查证书是否已存在 + if http_client.certificate_exists(ca_cert_name): + print(f"✅ 证书 {ca_cert_name} 已存在于模组中,跳过添加") + else: + # 证书不存在,需要添加 + print(f"证书 {ca_cert_name} 不存在,开始添加...") + + # 加载证书文件 + print(f"加载证书文件: {ca_cert_path}") + ca_cert_data = load_certificate_file(ca_cert_path) + if not ca_cert_data: + print("❌ 无法加载CA证书,终止流程") + return False + + # 添加证书到模组 + print(f"添加证书到模组: {ca_cert_name}") + if not http_client.add_certificate(ca_cert_name, ca_cert_data): + print("❌ CA证书添加失败,终止流程") + return False + + # 确认证书添加成功 + if not http_client.check_certificate(ca_cert_name): + print("❌ CA证书确认失败,终止流程") + return False + + print(f"✅ 证书 {ca_cert_name} 已成功添加到模组") + + # ========== 开始计时:HTTP/HTTPS访问耗时 ========== + access_start_time = time.time() + + # 配置HTTPS参数(如果需要) + # 重要:这里会明确指定使用哪个证书,确保不会混用其他证书 + if http_client.is_https: + print("\n" + "=" * 50) + print("【步骤3: 配置HTTPS参数】") + print("=" * 50) + if ca_cert_name: + print(f"明确指定使用证书: {ca_cert_name}") + if not http_client.configure_https( + ssl_version=3, # TLS1.2 + auth_mode=auth_mode, + ca_cert=ca_cert_name # 明确指定:本次HTTPS连接使用此证书 + ): + print("❌ HTTPS配置失败,终止流程") + return False + + # 确认HTTPS配置 + print("\n确认HTTPS配置...") + config = http_client.verify_https_config() + if config: + print("✅ HTTPS配置确认成功") + else: + print("⚠️ HTTPS配置确认失败,但继续执行") + + # URL已在前面设置,这里只显示信息 + protocol_name = "HTTPS" if http_client.is_https else "HTTP" + print("\n" + "=" * 50) + print(f"【步骤{3 if not http_client.is_https else 4}: {protocol_name}请求参数已设置】") + print("=" * 50) + print(f"请求URL: {url}") + + # 建立连接 + print("\n" + "=" * 50) + print(f"【步骤{4 if not http_client.is_https else 5}: 建立{protocol_name}连接】") + print("=" * 50) + http_client.setup_connection() + print(f"✅ {protocol_name}连接建立成功") + + # 发送GET请求 + print("\n" + "=" * 50) + print(f"【步骤{5 if not http_client.is_https else 6}: 发送{protocol_name} GET请求】") + print("=" * 50) + + # 使用标准GET模式(不传headers参数) + file_data = http_client.send_get_request(headers=None, timeout=600) + + # 保存文件 + print("\n" + "=" * 50) + print("【步骤7: 保存文件】") + print("=" * 50) + + # 创建输出目录 + os.makedirs(os.path.dirname(output_path), exist_ok=True) + + # 写入文件 + with open(output_path, 'wb') as f: + f.write(file_data) + + file_size = len(file_data) + print(f"✅ 文件保存成功: {output_path}") + print(f" 文件大小: {file_size} 字节 ({file_size/1024/1024:.2f} MB)") + + # 关闭连接 + print("\n" + "=" * 50) + print(f"【步骤8: 关闭{protocol_name}连接】") + print("=" * 50) + try: + http_client.close_connection() + print(f"✅ {protocol_name}连接已关闭") + except Exception as e: + print(f"警告: 关闭{protocol_name}连接时出错: {str(e)}") + + # ========== 结束计时:HTTP/HTTPS访问耗时 ========== + access_end_time = time.time() + access_duration = access_end_time - access_start_time + + print("\n" + "=" * 50) + print(f"【{protocol_name}访问耗时统计】") + print("=" * 50) + print(f"{protocol_name}访问总耗时: {access_duration:.3f} 秒 ({access_duration*1000:.1f} 毫秒)") + print(f"下载速度: {file_size/1024/1024/access_duration:.2f} MB/s") + print("\n耗时包含以下步骤:") + if http_client.is_https: + print(" - HTTPS配置和验证") + print(" - URL和端口设置") + print(f" - {protocol_name}连接建立") + print(" - HTTP请求发送") + print(" - 文件数据接收") + print(" - 连接关闭") + print("=" * 50) + + return True + + except Exception as e: + print("\n" + "=" * 50) + print("【错误处理】") + print("=" * 50) + print(f"❌ 下载异常: {str(e)}") + import traceback + traceback.print_exc() + return False + finally: + # 断开串口连接 + print("\n断开串口连接...") + at_handler.disconnect() + +# -------------------------- 3. 主程序 -------------------------- +if __name__ == "__main__": + print("=" * 50) + print("HTTP/HTTPS文件下载工具(AT指令版本)") + print("=" * 50) + print(f"目标URL: {FILE_URL}") + + # 构建输出路径 + output_path = os.path.join(OUTPUT_DIR, OUTPUT_FILENAME) + print(f"保存路径: {output_path}") + print("=" * 50) + + # 执行下载 + success = download_file( + url=FILE_URL, + output_path=output_path, + auth_mode=AUTH_MODE, + ca_cert_path=CA_CERT_PATH + ) + + if success: + print("\n✅ 文件下载完成!") + else: + print("\n❌ 文件下载失败!") + exit(1) + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_get_device_token.py b/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_get_device_token.py new file mode 100644 index 000000000..9600d56be --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_get_device_token.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +使用AT指令通过HTTPS获取设备Token +使用重构后的公共模块实现 +""" + +import hashlib +import hmac +import random +import string +import json +import time +import os +from n706a_at_handler import ATCommandHandler +from n706a_http_client import N706AHTTPClient, load_certificate_file + +# -------------------------- 1. 接口基础配置 -------------------------- +BASE_URL = "https://ambybox-api.test.leiniao.com/ambybox-device-api" +INTERFACE_PATH = "/v1/auth/getDeviceToken" +FULL_URL = BASE_URL + INTERFACE_PATH +SIGN_SECRET = "vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus=" + +# AT模块串口配置(根据实际硬件配置修改) +SERIAL_PORT = "COM19" # 修改为实际的串口设备 +SERIAL_BAUDRATE = 1000000 +SERIAL_TIMEOUT = 10 + +# -------------------------- 2. 生成合法请求参数 -------------------------- +def generate_request_params(): + device_id = "DEVICE_PC_TEST_001" + device_iccid = "86123456789012345678" + + # 1. 时间戳格式修正:确保是字符串类型的毫秒级时间戳 + timestamp = str(int(time.time() * 1000)) + + # 2. 随机数生成:确保长度在8-16位之间(当前12位符合要求) + nonce_length = 12 + nonce = ''.join(random.choices(string.ascii_letters + string.digits, k=nonce_length)) + + # 3. 签名生成(保持不变,符合协议要求) + sign_data = f"{device_id}:{device_iccid}:{timestamp}:{nonce}" + hmac_obj = hmac.new(SIGN_SECRET.encode("utf-8"), sign_data.encode("utf-8"), hashlib.sha256) + signature = hmac_obj.hexdigest().lower() + + # 4. 返回参数(键名严格匹配文档,保持不变) + return { + "deviceId": device_id, + "deviceIccid": device_iccid, + "timestamp": timestamp, + "nonce": nonce, + "signature": signature, + "deviceModel": "AB101", + "deviceVersion": "1.0.0" + } + +# -------------------------- 3. 发送请求并解析响应 -------------------------- +def send_get_device_token_request(auth_mode=0, ca_cert_path=None, client_cert_path=None, client_key_path=None): + """使用AT指令发送HTTPS GET请求""" + + # 初始化AT指令处理器 + at_handler = ATCommandHandler(SERIAL_PORT, SERIAL_BAUDRATE, SERIAL_TIMEOUT) + + try: + # 连接串口 + if not at_handler.connect(): + print("❌ 无法连接串口,请检查串口配置") + return + + # 测试AT连接 + success, response = at_handler.send_at_command("AT", timeout=3) + if not success: + print("❌ AT模块无响应,请检查连接") + return + print("✅ AT模块连接正常") + + # 查询版本号 + print("\n查询模块版本号...") + success, response = at_handler.send_at_command("AT+GMR", timeout=5) + if success: + print("✅ 版本信息:") + for line in response.split('\n'): + line = line.strip() + if line and line not in ['OK', 'AT+GMR']: + print(f" {line}") + else: + print(f"⚠️ 版本查询失败: {response}") + + # 检查PPP拨号状态 + print("\n" + "=" * 50) + print("【步骤1: 检查PPP拨号状态】") + print("=" * 50) + is_ppp_connected, ppp_ip = at_handler.check_ppp_status() + + if is_ppp_connected: + print(f"✅ PPP已连接,IP地址: {ppp_ip}") + else: + print(f"❌ PPP未连接,IP地址: {ppp_ip or '未知'}") + print("\n尝试建立PPP连接...") + + apn_name = None + success, ip = at_handler.establish_ppp_connection(apn_name=apn_name) + + if not success: + print("❌ PPP连接建立失败,无法继续HTTPS访问") + return + + ppp_ip = ip + + # 初始化HTTP/HTTPS客户端 + http_client = N706AHTTPClient(at_handler) + + # 查询设备中已有的证书列表(可选,用于调试) + if auth_mode in [1, 2]: + print("\n" + "=" * 50) + print("【步骤2: 查询设备证书列表】") + print("=" * 50) + cert_list = http_client.list_certificates() + if cert_list: + print(f"设备中已有 {len(cert_list)} 个证书:") + for cert in cert_list: + print(f" - {cert}") + else: + print("设备中暂无证书") + + # 配置HTTPS(根据认证模式) + print("\n" + "=" * 50) + print("【步骤3: 配置HTTPS参数】") + print("=" * 50) + + # 根据认证模式处理证书 + ca_cert_name = None + client_cert_name = None + client_key_name = None + + if auth_mode == 1: # 单向认证 + print("配置模式: 单向认证") + if not ca_cert_path: + print("❌ 错误: 单向认证需要提供CA证书路径") + return + + ca_cert_name = os.path.basename(ca_cert_path) + + print(f"\n检查证书是否已存在: {ca_cert_name}") + if http_client.certificate_exists(ca_cert_name): + print(f"✅ 证书 {ca_cert_name} 已存在于设备中,跳过添加步骤") + else: + print(f"证书 {ca_cert_name} 不存在,开始添加...") + + ca_cert_data = load_certificate_file(ca_cert_path) + if not ca_cert_data: + print("❌ 无法加载CA证书,终止流程") + return + + print(f"\n添加CA证书: {ca_cert_name}") + if not http_client.add_certificate(ca_cert_name, ca_cert_data): + print("❌ CA证书添加失败,终止流程") + return + + if not http_client.check_certificate(ca_cert_name): + print("❌ CA证书确认失败,终止流程") + return + + elif auth_mode == 2: # 双向认证 + print("配置模式: 双向认证") + if not ca_cert_path or not client_cert_path or not client_key_path: + print("❌ 错误: 双向认证需要提供CA证书、客户端证书和密钥路径") + return + + ca_cert_name = os.path.basename(ca_cert_path) + client_cert_name = os.path.basename(client_cert_path) + client_key_name = os.path.basename(client_key_path) + + # 检查并添加CA证书 + print(f"\n检查CA证书: {ca_cert_name}") + if http_client.certificate_exists(ca_cert_name): + print(f"✅ 证书 {ca_cert_name} 已存在,跳过添加") + else: + ca_cert_data = load_certificate_file(ca_cert_path) + if not ca_cert_data: + return + if not http_client.add_certificate(ca_cert_name, ca_cert_data): + return + if not http_client.check_certificate(ca_cert_name): + return + + # 检查并添加客户端证书 + print(f"\n检查客户端证书: {client_cert_name}") + if http_client.certificate_exists(client_cert_name): + print(f"✅ 证书 {client_cert_name} 已存在,跳过添加") + else: + client_cert_data = load_certificate_file(client_cert_path) + if not client_cert_data: + return + if not http_client.add_certificate(client_cert_name, client_cert_data): + return + if not http_client.check_certificate(client_cert_name): + return + + # 检查并添加客户端密钥 + print(f"\n检查客户端密钥: {client_key_name}") + if http_client.certificate_exists(client_key_name): + print(f"✅ 密钥 {client_key_name} 已存在,跳过添加") + else: + client_key_data = load_certificate_file(client_key_path) + if not client_key_data: + return + if not http_client.add_certificate(client_key_name, client_key_data): + return + if not http_client.check_certificate(client_key_name): + return + else: + print("配置模式: 无需认证") + + # ========== 开始计时:HTTPS访问耗时 ========== + https_start_time = time.time() + + # 配置HTTPS参数 + print("\n" + "=" * 50) + print("【步骤4: 配置HTTPS参数】") + print("=" * 50) + if not http_client.configure_https( + ssl_version=3, # TLS1.2 + auth_mode=auth_mode, + ca_cert=ca_cert_name, + client_cert=client_cert_name, + client_key=client_key_name + ): + print("❌ HTTPS配置失败,终止流程") + return + + # 确认HTTPS配置 + print("\n确认HTTPS配置...") + config = http_client.verify_https_config() + if config: + print("✅ HTTPS配置确认成功") + else: + print("⚠️ HTTPS配置确认失败,但继续执行") + + # 设置URL + print("\n" + "=" * 50) + print("【步骤5: 设置HTTPS请求参数】") + print("=" * 50) + print(f"设置请求URL: {FULL_URL}") + http_client.set_url(FULL_URL) + + # 建立HTTPS连接 + print("\n" + "=" * 50) + print("【步骤6: 建立HTTPS连接】") + print("=" * 50) + http_client.setup_connection() + print("✅ HTTPS连接建立成功") + + # 生成请求参数 + params = generate_request_params() + + # 构建HTTP头 + headers = {} + for key, value in params.items(): + headers[key] = str(value) + headers["Content-Type"] = "application/json" + + print("\n" + "=" * 50) + print("【步骤7: 发送HTTPS GET请求】") + print("=" * 50) + print("【请求信息】") + print(f"请求 URL: {FULL_URL}") + print(f"请求头: {headers}") + print("=" * 50) + + # 发送请求(获取完整响应,包括响应头和响应体) + try: + full_response, header_text, body_data = http_client._send_https_get_request(headers=headers) + except Exception as e: + print(f"❌ HTTPS请求发送失败: {str(e)}") + raise + + # 解析响应(文本模式) + print("\n" + "=" * 50) + print("【步骤8: 解析HTTPS响应】") + print("=" * 50) + try: + # 使用响应头和响应体解析 + parsed_response = http_client.parse_http_response(None, header_text=header_text, body_data=body_data) + except Exception as e: + print(f"❌ HTTP响应解析失败: {str(e)}") + print(f"响应体数据: {body_data[:500] if body_data else 'None'}...") + raise + + print(f"HTTP状态码: {parsed_response['status_code']}") + print(f"响应头: {parsed_response['headers']}") + print(f"响应体: {parsed_response['body']}") + + # 解析JSON响应 + print("\n" + "=" * 50) + print("【步骤9: 解析JSON响应】") + print("=" * 50) + try: + response_data = json.loads(parsed_response['body']) + print("\n【JSON响应】") + print(json.dumps(response_data, ensure_ascii=False, indent=2)) + + if response_data.get("errorCode") is None: + print("\n✅ 请求成功!") + if "data" in response_data: + print(f"设备 Token: {response_data['data'].get('token')}") + print(f"Token 过期时间: {response_data['data'].get('expireTime')} 秒({response_data['data'].get('expireTime', 0)//86400} 天)") + else: + print(f"\n❌ 请求失败!错误码: {response_data['errorCode']}, 原因: {response_data.get('message', '未知错误')}") + except json.JSONDecodeError as e: + print(f"\n❌ JSON解析失败: {e}") + print(f"原始响应体: {parsed_response['body']}") + raise + + # 关闭连接 + print("\n" + "=" * 50) + print("【步骤10: 关闭HTTPS连接】") + print("=" * 50) + try: + http_client.close_connection() + print("✅ HTTPS连接已关闭") + except Exception as e: + print(f"警告: 关闭HTTPS连接时出错: {str(e)}") + + # ========== 结束计时:HTTPS访问耗时 ========== + https_end_time = time.time() + https_duration = https_end_time - https_start_time + + print("\n" + "=" * 50) + print("【HTTPS访问耗时统计】") + print("=" * 50) + print(f"HTTPS访问总耗时: {https_duration:.3f} 秒 ({https_duration*1000:.1f} 毫秒)") + print("\n耗时包含以下步骤:") + print(" - HTTPS配置和验证") + print(" - URL和端口设置") + print(" - HTTPS连接建立") + print(" - HTTP请求发送") + print(" - HTTP响应接收") + print(" - 响应解析") + print(" - 连接关闭") + print("=" * 50) + + except Exception as e: + print("\n" + "=" * 50) + print("【错误处理】") + print("=" * 50) + print(f"❌ 请求异常: {str(e)}") + import traceback + traceback.print_exc() + finally: + # 断开串口连接 + print("\n断开串口连接...") + at_handler.disconnect() + +# -------------------------- 4. 执行请求 -------------------------- +if __name__ == "__main__": + # 认证模式配置 + # auth_mode: 0=无需认证, 1=单向认证, 2=双向认证 + # AUTH_MODE = 0 # 默认无需认证 + + # 证书文件路径(单向/双向认证时需要) + # CA_CERT_PATH = None # 例如: "certs/ca.crt" + CLIENT_CERT_PATH = None # 例如: "certs/client.crt" + CLIENT_KEY_PATH = None # 例如: "certs/client.key" + + # 如果使用单向认证,取消下面的注释并设置证书路径 + AUTH_MODE = 1 + CA_CERT_PATH = "certs/ca_chain.pem" + + # 如果使用双向认证,取消下面的注释并设置所有证书路径 + # AUTH_MODE = 2 + # CA_CERT_PATH = "certs/ca.crt" + # CLIENT_CERT_PATH = "certs/client.crt" + # CLIENT_KEY_PATH = "certs/client.key" + + send_get_device_token_request( + auth_mode=AUTH_MODE, + ca_cert_path=CA_CERT_PATH, + client_cert_path=CLIENT_CERT_PATH, + client_key_path=CLIENT_KEY_PATH + ) diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/atcmd.txt b/code/sdk/zhihui/leiniao/leiniao_python_test/atcmd.txt new file mode 100644 index 000000000..3d7c5348c --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/atcmd.txt @@ -0,0 +1,586 @@ +at+clac + +AT+CFUN + +AT+CSQ + +AT+COPS + +AT+CGATT + +AT+CTZR + +AT+CCLK + +AT+CEREG + +AT+CPLS + +AT+CPOL + +AT+COPN + +AT+NPSMR + +AT+NPSMSET + +AT+NEARFCN + +AT+CPSMS + +AT+NBAND + +AT+CNEC + +AT+CTZU + +AT+NDRX + +AT+CSCON + +AT+NCSEARFCN + +AT+NUESTATS + +AT+NPOWD + +AT+CREG + +AT+CGREG + +AT+NSQ + +AT+NNMPSD + +AT+NGSBLACKCELL + +AT+NCSBLACKCELL + +AT+NBACKOFF + +AT+NROAMING + +AT+NRESELCFG + +AT+NSIMAC + +AT+NDRXS + +AT+CGTFT + +AT+CGPADDR + +AT+CGDCONT + +AT+CGACT + +AT+CGCMOD + +AT+CGDSCONT + +AT+CGTFTRDP + +AT+CGEQOSRDP + +AT+CGAUTH + +AT+CGEQOS + +AT+CEER + +AT+CGCONTRDP + +AT+NCONFIG + +AT+CIMI + +AT+CGLA + +AT+CRSM + +AT+CSIM + +AT+CCHC + +AT+CCHO + +AT+USIMHOTPLUG + +AT+NUICC + +AT+ICCID + +AT+PHONENUM + +AT+NSIMSRS + +AT+CLEARFPLMN + +AT+USIMRETRYTIMES + +AT+CPIN + +AT+CLCK + +AT+CPWD + +AT+NPIN + +AT+SETHWLOCK + +AT+CARDLOCK + +AT+SIMLOCK + +AT+CPINR + +AT+CMEE + +AT+CGMI + +AT+CGSN + +AT+CLAC + +AT+RESET + +AT+NLOGLEVEL + +AT+NATSPEED + +AT+NFWUPD + +AT+NDSDELAY + +AT+NCRASHINFO + +AT+NTSETID + +AT+NTPERMID + +AT+NCHIPTYPE + +AT+NQSTATE + +AT+USIMSEL + +AT+NVETOSUPERDS + +AT+NBANKD + +AT+NVER + +AT+MODEMTEMP + +AT+NUSBPSM + +AT+NRATEMODE + +AT+NFLIST + +AT+RECONNDEL + +AT+NRCUPD + +AT+NLOGMODFT + +AT+NRDCTRL + +AT+NRDSET + +AT+NRDEXEC + +AT+NGOLD + +AT+CSCA + +AT+CNMI + +AT+CMGS + +AT+CMMS + +AT+CMGF + +AT+CSMP + +AT+CSDH + +AT+CMGR + +AT+CMGL + +AT+CMGD + +AT+CMGW + +AT+CPMS + +AT+CMSS + +AT+CSCS + +AT+NDISDUP + +AT+NPING + +AT+NDNS + +AT+NSOCR + +AT+NSOST + +AT+NSOCO + +AT+NSOSD + +AT+NQSOS + +AT+NSORF + +AT+NSOCL + +AT+NSOSTATUS + +AT+NCIDSTATUS + +AT+NDATATRAN + +AT+NIPINFO + +AT+NKEEPALIVE + +AT+NISEND + +AT+NDATARATE + +AT+NFCMEMBYTE + +AT+TTSBATLV + +AT+TTSAMOUNT + +AT+TTSPRESET + +AT+NRDTEST + +AT+NWIFISCAN + +AT+NWIFISCANSTOP + +AT+NTXPOWER + +AT+DEMO + +AT+IVSN + +AT+GMR + +AT+FTCAL + +AT+FTRF + +AT+IFC + +AT+ICF + +AT+IPR + +AT+NWIDCFG + +AT+DUMP + +AT+CERTADD + +AT+CERTCHECK + +AT+CERTDEL + +AT+IPINFO + +AT+IPSTATUS + +AT+NCUSTSWITCH + +AT+PDPKEEPALIVE + +AT+PDPSTATUS + +AT+RECVMODE + +AT+TCPACK + +AT+TCPCLOSE + +AT+TCPKEEPALIVE + +AT+TCPREAD + +AT+TCPSEND + +AT+TCPSETUP + +AT+TCPTRANS + +AT+TRANSCLOSE + +AT+UDPCLOSE + +AT+UDPREAD + +AT+UDPSEND + +AT+UDPSETUP + +AT+UDPTRANS + +AT+CESQ + +AT+CPSERVICE + +AT+ENPWRSAVE + +AT+IMEI + +AT+NFTCHIPTYPE + +AT+NFTFBACKUP + +AT+NFTIMEI + +AT+NFTRCAL + +AT+NFTRCREG + +AT+NFTRCSQ + +AT+NFTRFBAK + +AT+NFTRFIT + +AT+NWENPWRSAVE + +AT+NWPWROFF + +AT+QCNVERSION + +AT+READADC + +AT+CCID + +AT+CNUM + +AT+NWCCID + +AT+NWSIMCFG + +AT+NWSIMDET + +AT+NWSIMWRCNT + +AT+NWSIMWRCNTCTL + +AT+SIMCOUNTCTL + +AT+SIMCOUNTREAD + +AT+SIMCROSS + +AT+SIMHOTSWAP + +AT+NETSHAREACT + +AT+XIIC + +AT+CGMM + +AT+CGMR + +AT+GMM + +AT+GSN + +AT+HEAPINFO + +AT+NETSHAREMODE + +AT+RSPACK + +AT+SIGNAL + +AT+BANDLOCK + +AT+MYCELLINFO + +AT+MYCGED + +AT+MYLACID + +AT+MYNETINFO + +AT+MYSYSINFO + +AT+MYSYSINFOURC + +AT+MYURCSYSINFO + +AT+NBANDLOCK + +AT+NETDMSG + +AT+NETMSG + +AT+NFREQLOCK + +AT+NRSP + +AT+NSTGETRSSI + +AT+NWENG + +AT+NWRFTEST + +AT+NWRRCRELEASE + +AT+NWUEDRX + +AT+MQTTCONN + +AT+MQTTCONNPARAM + +AT+MQTTDISCONN + +AT+MQTTMUX + +AT+MQTTPUB + +AT+MQTTPUBS + +AT+MQTTSTATE + +AT+MQTTSUB + +AT+MQTTTLS + +AT+MQTTUNSUB + +AT+MQTTWILLMSG + +AT+MQTTWILLPARAM + +AT+SSLCIPHERSET + +AT+SSLTCPCFG + +AT+SSLTCPCFGA + +AT+SSLTCPCLOSE + +AT+SSLTCPREAD + +AT+SSLTCPSEND + +AT+SSLTCPSETUP + +AT+FILEHTTPACTION + +AT+FILEHTTPSACTION + +AT+HTTPACTION + +AT+HTTPCLOSE + +AT+HTTPGET + +AT+HTTPGETSTAT + +AT+HTTPPARA + +AT+HTTPSACTION + +AT+HTTPSCFG + +AT+HTTPSCLOSE + +AT+HTTPSETUP + +AT+HTTPSGET + +AT+HTTPSGETSTAT + +AT+HTTPSPARA + +AT+HTTPSSETUP + +AT+FSDF + +AT+FSFAT + +AT+FSFS + +AT+FSLIST + +AT+FSLS + +AT+FSRF + +AT+FSRFEX + +AT+FSRN + +AT+FSWF + +AT+NWATFOTA + +AT+NWFOTA + +AT+NWFOTASCFG + +AT+NWFTPFOTA + +AT+FILEFTPGET + +AT+FTPGET + +AT+FTPLOGI +N + +AT+FTPLOGOUT + +AT+FTPPUT + +AT+FTPSIZE + +AT+FTPSTATUS + +AT+DNSSERVER + +AT+NWDNS + +AT+PING + +AT+UPDATETIME + +AT+CIPGSMLOC + +AT+NWURCCFG + +AT&F + +AT&W + +AT&V + +ATD + +ATH + +ATS0 + +ATE + +ATI + +ATO + +OK \ No newline at end of file diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certificate_storage_and_usage.md b/code/sdk/zhihui/leiniao/leiniao_python_test/certificate_storage_and_usage.md new file mode 100644 index 000000000..12e7350ba --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/certificate_storage_and_usage.md @@ -0,0 +1,150 @@ +# 证书存储和使用机制说明 + +## 证书存储方式 + +### 1. 存储机制 +- **存储方式**:通过 `AT+CERTADD=,` 添加证书 +- **存储位置**:证书存储在模组的非易失性存储器中 +- **存储格式**:以**文件名**作为唯一标识符存储证书内容 +- **持久化**:证书会持久保存,即使模组重启也不会丢失 + +### 2. 证书数量 +- 模组可以存储**多个证书**(具体数量限制取决于模组型号和存储空间) +- 每个证书通过**唯一的文件名**区分 +- 可以通过 `AT+CERTCHECK?` 查询所有已存储的证书列表 + +### 3. 证书管理 +- **添加证书**:`AT+CERTADD=,` +- **检查证书**:`AT+CERTCHECK=` 或 `AT+CERTCHECK?` +- **删除证书**:`AT+CERTDEL=` + +## HTTPS下载时证书的指定方式 + +### 关键机制:每次连接前明确指定证书 + +**重要**:HTTPS下载时,**不会混用证书**,因为每次连接前都会**明确指定**使用哪个证书。 + +### 工作流程 + +1. **添加证书到模组**(如果不存在) + ```python + http_client.add_certificate(ca_cert_name, ca_cert_data) + ``` + - 执行:`AT+CERTADD=,` + - 作用:将证书存储到模组中 + +2. **配置HTTPS使用指定证书**(每次连接前) + ```python + http_client.configure_https( + ssl_version=3, + auth_mode=1, # 单向认证 + ca_cert=ca_cert_name # 明确指定证书名称 + ) + ``` + - 执行:`AT+HTTPSCFG="cacert",` + - 作用:**明确指定**本次HTTPS连接使用哪个证书 + +3. **建立HTTPS连接** + - 模组会使用步骤2中配置的证书来验证服务器证书 + - **不会使用其他证书** + +### 证书配置的作用范围 + +- **会话级别**:`AT+HTTPSCFG="cacert"` 的配置是**会话级别的** +- **每次连接都需要重新配置**:每次建立新的HTTPS连接前,都需要重新调用 `configure_https()` +- **不会影响其他证书**:配置只影响当前HTTPS会话,不会影响模组中存储的其他证书 + +## 为什么不会混用证书? + +### 原因1:明确指定机制 +```python +# 每次HTTPS连接前都会执行 +AT+HTTPSCFG="cacert", # 明确指定使用哪个证书 +``` + +### 原因2:会话隔离 +- 每个HTTPS连接都是独立的会话 +- 每个会话都会明确指定使用的证书 +- 不同会话之间不会互相影响 + +### 原因3:配置覆盖 +- 如果之前配置过其他证书,新的配置会**覆盖**之前的配置 +- 但只影响**当前会话**,不会影响模组中存储的证书 + +## 实际代码流程 + +```python +# 1. 添加证书(如果不存在) +if not http_client.certificate_exists(ca_cert_name): + http_client.add_certificate(ca_cert_name, ca_cert_data) + +# 2. 配置HTTPS使用指定证书(每次连接前都会执行) +http_client.configure_https( + ssl_version=3, + auth_mode=1, + ca_cert=ca_cert_name # 明确指定:使用这个证书 +) + +# 3. 建立HTTPS连接 +# 模组会使用步骤2中指定的证书来验证服务器 +http_client.setup_connection() +http_client.send_get_request() +``` + +## 关于删除证书的必要性 + +### 如果证书名称不同 +- **不需要删除**:因为每个证书都有唯一的名称 +- **不会混用**:因为每次都会明确指定使用哪个证书 + +### 如果证书名称相同 +- **理论上不需要删除**:如果保证同一个名称的证书内容相同 +- **但建议删除**:如果证书内容可能不同,建议先删除再添加,确保使用最新证书 + +### 当前代码的处理方式 + +```python +# 当前代码:如果证书已存在,先删除再添加 +if http_client.certificate_exists(ca_cert_name): + http_client.delete_certificate(ca_cert_name) + +http_client.add_certificate(ca_cert_name, ca_cert_data) +``` + +**对于您的场景**(每个证书名称都不同,且保证同名证书内容相同): +- **可以优化**:如果证书已存在且内容相同,可以跳过添加步骤 +- **不会混用**:因为每次都会通过 `configure_https(ca_cert=ca_cert_name)` 明确指定 + +## 建议的优化方案 + +### 方案1:检查证书是否存在,存在则跳过(推荐) + +```python +# 如果证书已存在,跳过添加(假设同名证书内容相同) +if http_client.certificate_exists(ca_cert_name): + print(f"✅ 证书 {ca_cert_name} 已存在,跳过添加") +else: + # 只有不存在时才添加 + http_client.add_certificate(ca_cert_name, ca_cert_data) + http_client.check_certificate(ca_cert_name) + +# 无论证书是否已存在,都需要配置HTTPS使用该证书 +http_client.configure_https( + ssl_version=3, + auth_mode=1, + ca_cert=ca_cert_name # 明确指定使用这个证书 +) +``` + +### 方案2:验证证书内容(如果模组支持) + +如果模组支持验证证书内容,可以比较证书内容是否相同,但通常不需要这么复杂。 + +## 总结 + +1. **证书存储**:模组可以存储多个证书,每个证书通过文件名唯一标识 +2. **证书指定**:每次HTTPS连接前,通过 `AT+HTTPSCFG="cacert",` 明确指定使用哪个证书 +3. **不会混用**:因为每次都会明确指定,不会使用其他证书 +4. **删除必要性**:如果证书名称不同,不需要删除;如果名称相同但内容可能不同,建议先删除再添加 +5. **优化建议**:如果保证同名证书内容相同,可以检查存在后跳过添加,但**必须**每次都调用 `configure_https()` 明确指定证书 + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certificate_usage_guide.md b/code/sdk/zhihui/leiniao/leiniao_python_test/certificate_usage_guide.md new file mode 100644 index 000000000..56a80262a --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/certificate_usage_guide.md @@ -0,0 +1,176 @@ +# AT指令HTTP/HTTPS证书使用指南 + +## 证书管理AT指令 + +### 1. 添加证书:`AT+CERTADD` +``` +AT+CERTADD=, +``` +- 发送后会返回 `CONNECT` +- 然后发送证书数据(二进制) +- 最后返回 `+CERTADD: ,OK` 或 `+CERTADD: ERROR` + +### 2. 检查证书:`AT+CERTCHECK` +``` +AT+CERTCHECK= # 检查指定证书是否存在 +AT+CERTCHECK? # 列出所有证书 +``` + +### 3. 删除证书:`AT+CERTDEL` +``` +AT+CERTDEL= # 删除指定证书 +``` + +## 证书使用流程 + +### 当前实现方式 + +在 `at_test_download_file.py` 中,证书添加流程如下: + +1. **检查证书是否存在**(可选) + ```python + if http_client.certificate_exists(ca_cert_name): + print(f"✅ 证书 {ca_cert_name} 已存在,跳过添加") + ``` + +2. **添加证书** + ```python + http_client.add_certificate(ca_cert_name, ca_cert_data) + ``` + +3. **确认证书** + ```python + http_client.check_certificate(ca_cert_name) + ``` + +4. **配置HTTPS使用证书** + ```python + http_client.configure_https( + ssl_version=3, + auth_mode=1, # 单向认证 + ca_cert=ca_cert_name + ) + ``` + +## 关于证书覆盖的问题 + +### 问题分析 + +**如果使用相同的证书名称添加证书:** +- 模组可能会**覆盖**旧证书 +- 但为了**确保验证的是当前证书**,建议先删除旧证书 + +### 为什么需要删除旧证书? + +1. **确保证书一致性**:删除旧证书可以确保模组中只有最新的证书 +2. **避免证书冲突**:如果证书内容不同但名称相同,可能导致验证失败 +3. **清理不需要的证书**:删除不再使用的证书,释放存储空间 + +## 推荐的最佳实践 + +### 方案1:先删除再添加(推荐) + +```python +# 1. 检查证书是否存在 +if http_client.certificate_exists(ca_cert_name): + print(f"证书 {ca_cert_name} 已存在,先删除旧证书...") + http_client.delete_certificate(ca_cert_name) + +# 2. 添加新证书 +http_client.add_certificate(ca_cert_name, ca_cert_data) + +# 3. 确认证书 +http_client.check_certificate(ca_cert_name) +``` + +### 方案2:使用唯一证书名称 + +```python +# 使用带时间戳或版本的证书名称 +import time +ca_cert_name = f"ca_{int(time.time())}.crt" +# 或 +ca_cert_name = f"ca_v1.crt" +``` + +### 方案3:列出所有证书并清理 + +```python +# 列出所有证书 +cert_list = http_client.list_certificates() +print(f"当前证书列表: {cert_list}") + +# 删除不需要的证书 +for old_cert in old_cert_list: + http_client.delete_certificate(old_cert) + +# 添加新证书 +http_client.add_certificate(ca_cert_name, ca_cert_data) +``` + +## 证书验证机制 + +### HTTPS单向认证流程 + +1. **客户端(模组)** 使用 `AT+HTTPSCFG="cacert",` 配置CA证书 +2. **建立HTTPS连接** 时,模组会: + - 接收服务器的证书 + - 使用配置的CA证书验证服务器证书 + - 如果验证通过,建立连接 + - 如果验证失败,返回错误 + +### 证书选择 + +- 模组会使用 `AT+HTTPSCFG="cacert"` 中指定的证书名称 +- 如果该证书不存在,HTTPS连接会失败 +- **如果模组中有多个证书,只有配置的证书会被使用** + +## 注意事项 + +1. **证书名称唯一性**:如果使用相同的证书名称,新证书会覆盖旧证书 +2. **证书存储限制**:模组可能有证书存储数量限制 +3. **证书大小限制**:单个证书可能有大小限制 +4. **证书格式**:必须是PEM格式的证书 + +## 建议的代码改进 + +在 `n706a_http_client.py` 中添加删除证书的方法: + +```python +def delete_certificate(self, cert_name): + """ + 删除SSL证书 + + Args: + cert_name: 证书文件名 + + Returns: + bool: 是否删除成功 + """ + success, response = self.at.send_at_command( + f'AT+CERTDEL={cert_name}', + timeout=5 + ) + if not success: + return False + + if "OK" in response: + print(f"✅ 证书 {cert_name} 已删除") + return True + else: + print(f"❌ 证书删除失败: {response}") + return False +``` + +然后在添加证书前先删除旧证书: + +```python +# 如果证书已存在,先删除 +if http_client.certificate_exists(ca_cert_name): + print(f"证书 {ca_cert_name} 已存在,先删除...") + http_client.delete_certificate(ca_cert_name) + +# 添加新证书 +http_client.add_certificate(ca_cert_name, ca_cert_data) +``` + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/.gitignore b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/.gitignore new file mode 100644 index 000000000..d7d42907c --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/.gitignore @@ -0,0 +1,18 @@ +# 证书文件(包含敏感信息,不应提交到版本控制) +*.crt +*.key +# *.pem +*.der +*.csr +*.srl + +# 但保留示例和说明文件 +!README.md +!certificate_preparation_guide.md +!generate_test_certs.sh +!download_server_ca.py +!check_certificate.py +!extract_ca_from_chain.py +!verify_ca_for_https.py +!ca_cert_usage_explanation.md + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/ca_cert_usage_explanation.md b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/ca_cert_usage_explanation.md new file mode 100644 index 000000000..6bcbf326b --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/ca_cert_usage_explanation.md @@ -0,0 +1,157 @@ +# CA证书使用说明:服务器证书 vs CA证书 + +## 问题:直接拿服务器的ca.crt可以用到client上吗? + +**简短回答**:**可以,但需要确认证书类型**。 + +## 详细说明 + +### 1. 证书类型区分 + +#### CA证书(Certificate Authority Certificate) +- **用途**:用于验证其他证书的有效性 +- **特点**:是根证书或中间证书,用于验证服务器证书是否由可信CA签发 +- **客户端使用**:✅ **可以直接使用**,用于单向认证验证服务器身份 + +#### 服务器证书(Server Certificate) +- **用途**:服务器自己的身份证书 +- **特点**:由CA签发的终端实体证书 +- **客户端使用**:❌ **不能直接使用**,但可以从中提取CA证书链 + +### 2. 如何判断证书类型 + +#### 方法1:查看证书主题(Subject) + +```bash +openssl x509 -in ca.crt -noout -subject -issuer +``` + +**CA证书特征**: +- Subject 和 Issuer **相同**(自签名) +- 通常包含 "CA"、"Certificate Authority" 等字样 +- CN(Common Name)通常是 "Root CA"、"Intermediate CA" 等 + +**服务器证书特征**: +- Subject 和 Issuer **不同** +- Subject 通常是服务器域名(如 `ambybox-api.test.leiniao.com`) +- Issuer 是CA的名称 + +#### 方法2:查看证书扩展 + +```bash +openssl x509 -in ca.crt -noout -text | grep -A 5 "X509v3 extensions" +``` + +**CA证书**:通常包含 `Basic Constraints: CA:TRUE` +**服务器证书**:通常包含 `Basic Constraints: CA:FALSE` + +### 3. 使用场景 + +#### 场景A:服务器提供的是CA证书 ✅ + +如果服务器提供的 `ca.crt` 是真正的CA证书(根证书或中间证书),那么: + +```python +# 可以直接使用 +AUTH_MODE = 1 +CA_CERT_PATH = "certs/ca.crt" # 直接使用 +``` + +**这是最常见的情况**,因为: +- 客户端需要CA证书来验证服务器证书 +- 服务器通常会提供其证书链中的CA证书 + +#### 场景B:服务器提供的是服务器证书 ⚠️ + +如果服务器提供的是服务器自己的证书(不是CA证书),需要: + +**选项1:提取CA证书链** + +```bash +# 使用OpenSSL获取完整证书链(包括CA证书) +openssl s_client -showcerts -connect ambybox-api.test.leiniao.com:443 /dev/null | \ + openssl x509 -outform PEM > server_chain.pem + +# 从证书链中提取CA证书(通常是第二个或最后一个证书) +openssl x509 -in server_chain.pem -outform PEM -out ca.crt +``` + +**选项2:从服务器管理员获取CA证书** + +联系服务器管理员,获取用于验证服务器证书的CA证书。 + +### 4. 实际使用建议 + +#### 推荐方法:从服务器下载完整证书链 + +使用提供的脚本: + +```bash +cd leiniao/leiniao_python_test/certs +python3 download_server_ca.py https://ambybox-api.test.leiniao.com -o ca.crt +``` + +这个脚本会: +1. 连接到服务器 +2. 获取服务器证书 +3. 提取CA证书链 +4. 保存为 `ca.crt` + +#### 验证证书是否可用 + +```bash +# 检查证书类型 +openssl x509 -in ca.crt -noout -subject -issuer + +# 验证证书格式 +python3 check_certificate.py ca.crt +``` + +### 5. 常见问题 + +#### Q1: 服务器给的ca.crt是服务器证书怎么办? + +**A**: 需要提取CA证书链,或联系服务器管理员获取真正的CA证书。 + +#### Q2: 如何确认证书是否可用? + +**A**: +1. 检查证书的 Subject 和 Issuer +2. 如果 Subject == Issuer,通常是CA证书,可以直接使用 +3. 如果 Subject != Issuer,可能是服务器证书,需要提取CA证书 + +#### Q3: 证书链中有多个证书怎么办? + +**A**: +- 对于单向认证,通常只需要**æ ¹CA证书**(最后一个证书) +- 某些情况下可能需要**中间CA证书** +- 可以尝试使用根CA证书,如果验证失败,再添加中间CA证书 + +### 6. 代码中的使用 + +```python +# 在 get_device_token_at.py 中 +if __name__ == "__main__": + # 单向认证配置 + AUTH_MODE = 1 + CA_CERT_PATH = "certs/ca.crt" # 使用从服务器获取的CA证书 + + send_get_device_token_request( + auth_mode=AUTH_MODE, + ca_cert_path=CA_CERT_PATH + ) +``` + +### 7. 总结 + +| 证书类型 | 能否直接用于客户端 | 说明 | +|---------|------------------|------| +| CA证书(根证书/中间证书) | ✅ **可以** | 直接用于单向认证验证服务器 | +| 服务器证书 | ❌ **不能** | 需要提取CA证书链 | +| 证书链文件 | ⚠️ **需要提取** | 从中提取CA证书部分 | + +**最佳实践**: +1. 使用 `download_server_ca.py` 脚本从服务器下载CA证书 +2. 验证证书类型(Subject == Issuer) +3. 如果验证失败,联系服务器管理员获取正确的CA证书 + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/ca_chain.pem b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/ca_chain.pem new file mode 100644 index 000000000..abb5e1eaa --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/ca_chain.pem @@ -0,0 +1,91 @@ +-----BEGIN CERTIFICATE----- +MIIF4jCCA8qgAwIBAgIRANVuJGyU7WOrsUbvwZa2T7AwDQYJKoZIhvcNAQEMBQAw +gYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtK +ZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYD +VQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTIw +MDEwODAwMDAwMFoXDTMwMDEwNzIzNTk1OVowXDELMAkGA1UEBhMCQ04xGjAYBgNV +BAoTEVdvVHJ1cyBDQSBMaW1pdGVkMTEwLwYDVQQDDChXb1RydXMgRFYgU2VydmVy +IENBICBbUnVuIGJ5IHRoZSBJc3N1ZXJdMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEA7IE0rmVTVdRdNOzK1jsR/VppyukZ/XbQgakJHOhg6XGDsiHe/l5B +3PxyXw18jEdN+7YxP0qsGz+HlQbsQh6XlwIyjpz/2gFMiqa7y1v+dHOgj6xNOF5a +oaPm/Qhb0N+JYQidgaC+1Zp6W+YeC736rzCMr9vL1Usa3QLzRoQEo0DzbG4sPeP1 +US0Ia/i8o6szArH+DAcvrzCZ2kkpTScQ9QfOsvkMBP1W2otICdKUyZHaBc+ztTAd +ovSlOR+GPf29dYfGQkZAp0tffIRw/na3WB86WGZPpNFfo2QxxsHYoL3oSWKfSWTY +FgW22J8eA03TFHYowm/NqYuJ7GW553HppQIDAQABo4IBcDCCAWwwHwYDVR0jBBgw +FoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFJmbLfaL8KPbidSe++V0 +L2jSkE/kMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1Ud +JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAiBgNVHSAEGzAZMA0GCysGAQQBsjEB +AgIWMAgGBmeBDAECATBQBgNVHR8ESTBHMEWgQ6BBhj9odHRwOi8vY3JsLnVzZXJ0 +cnVzdC5jb20vVVNFUlRydXN0UlNBQ2VydGlmaWNhdGlvbkF1dGhvcml0eS5jcmww +cQYIKwYBBQUHAQEEZTBjMDoGCCsGAQUFBzAChi5odHRwOi8vY3J0LnVzZXJ0cnVz +dC5jb20vVVNFUlRydXN0UlNBQUFBQ0EuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8v +b2NzcC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEBDAUAA4ICAQB5t8v3uYzHa4EL +0rOb9g/YAmptUbILcBMKk1x188ucsGVPaG1DG9bpVamxbmCtFA1MlrA7iUC8SGop +KBnuWFsNKiC7jCbRoahT1/FSwFsSuDlDmOjr1MqDXE+or08UkXsJB57XxXxdVOPl +DcZHII4qHi1XKK4iurMqb+kbdpAWadyfidRRCGPopYCVYLLYhRJgpFGtfr6Gk8N0 +j81jq/7QbN0dRSDzMNdadKTc7c3+i9fIrXj79lV5Wvva+OL7nh8MxQhG1Ekek7Rv +en++jSZvaEhCrMsSedFTA/aIy7oJg85tfglF2ybK61HsobjYzdDNICKJlIm4chlA +XIDDqw2mw0Kz2snrkp9dpvMBqahF/Uy1kHzPcrq1/w5OqZWAuDKxZ68PuZ/ME2hI +YbIDG9dWT6Y7eqtjQ2TmAQbOqdAG2LeikPMl2DMrPEa4lcKJzsFbHfHAW3hVgPSQ +hRfS4TtbNnxijbsp8GguMHxP2R7dpAAYybwfZdXP7WYAnwEr1mzIf0Y3J0m7GDyX +JhaflN3G2wIm2HzRd39NvnDRmFEraqui/YYO9ym0pwq1d0S+bGG6876QCto0u3Cg +ItFh3Za2ZeIY+g5mWrejSaDs9LT7eu44iCyebfgekdMRqFeCuGAsJdsun3LOHHJo +tCVPRjyFg9NDeJeMa4Z8QuXAXLd9cw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgTCCBGmgAwIBAgIQOXJEOvkit1HX02wQ3TE1lTANBgkqhkiG9w0BAQwFADB7 +MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD +VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE +AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTE5MDMxMjAwMDAwMFoXDTI4 +MTIzMTIzNTk1OVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5 +MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO +ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sI +s9CsVw127c0n00ytUINh4qogTQktZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnG +vDoZtF+mvX2do2NCtnbyqTsrkfjib9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQ +Ijy8/hPwhxR79uQfjtTkUcYRZ0YIUcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfb +IWax1Jt4A8BQOujM8Ny8nkz+rwWWNR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0 +tyA9yn8iNK5+O2hmAUTnAU5GU5szYPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97E +xwzf4TKuzJM7UXiVZ4vuPVb+DNBpDxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNV +icQNwZNUMBkTrNN9N6frXTpsNVzbQdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5 +D9kCnusSTJV882sFqV4Wg8y4Z+LoE53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJ +WBp/kjbmUZIO8yZ9HE0XvMnsQybQv0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ +5lhCLkMaTLTwJUdZ+gQek9QmRkpQgbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzG +KAgEJTm4Diup8kyXHAc/DVL17e8vgg8CAwEAAaOB8jCB7zAfBgNVHSMEGDAWgBSg +EQojPpbxB+zirynvgqV/0DCktDAdBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rID +ZsswDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAG +BgRVHSAAMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29t +L0FBQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggr +BgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUA +A4IBAQAYh1HcdCE9nIrgJ7cz0C7M7PDmy14R3iJvm3WOnnL+5Nb+qh+cli3vA0p+ +rvSNb3I8QzvAP+u431yqqcau8vzY7qN7Q/aGNnwU4M309z/+3ri0ivCRlv79Q2R+ +/czSAaF9ffgZGclCKxO/WIu6pKJmBHaIkU4MiRTOok3JMrO66BQavHHxW/BBC5gA +CiIDEOUMsfnNkjcZ7Tvx5Dq2+UUTJnWvu6rvP3t3O9LEApE9GQDTF1w52z97GA1F +zZOFli9d31kWTz9RvdVFGD/tSo7oBmF0Ixa1DVBzJ0RHfxBdiSprhTEUxOipakyA +vGp4z7h/jnZymQyd/teRCBaho1+V +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/cert_chain.pem b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/cert_chain.pem new file mode 100644 index 000000000..7312de278 --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/cert_chain.pem @@ -0,0 +1,127 @@ +-----BEGIN CERTIFICATE----- +MIIGLzCCBRegAwIBAgIQDoeEtRS9PUI03KGd2JIoazANBgkqhkiG9w0BAQsFADBc +MQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29UcnVzIENBIExpbWl0ZWQxMTAvBgNV +BAMMKFdvVHJ1cyBEViBTZXJ2ZXIgQ0EgIFtSdW4gYnkgdGhlIElzc3Vlcl0wHhcN +MjUwODA1MDAwMDAwWhcNMjYwODIxMjM1OTU5WjAdMRswGQYDVQQDDBIqLnRlc3Qu +bGVpbmlhby5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAYTnC +rJxPLTWj7w3k75qmqOrcue6+0zEv2OG2DcV7ZANB0+j3dCXLeAUmWHAyoIYX92Nb +ApJ67vCAoz8EjwxWDmy0xrvm8J6y9+CaXggfpdkJmBGB2fjlDGvjWEY9QVl0YIOJ +eeGHWKuPIf0JJ1m1aeZdlP04NHB1ppBw4JC9pekfDWiJj0X4sGy44zdjx0voAjiM ++nSAeO3IDWTeG7nlWgnrQryCA74l/wBkggz2Nlnjzwm857pDFZAn8fx4GY2IAAvY +3WISfGOkeixzStZmTk+CxGEy2SxTRGTLXSICDF41gkfgTcK7gqK2J1xL2hPzRjLE +EvF8EVLvQj1LBCd/AgMBAAGjggMqMIIDJjAfBgNVHSMEGDAWgBSZmy32i/Cj24nU +nvvldC9o0pBP5DAdBgNVHQ4EFgQUHboPw04c8wuw9nagnR/WA25F0V8wDgYDVR0P +AQH/BAQDAgWgMAwGA1UdEwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG +AQUFBwMCMEkGA1UdIARCMEAwNAYLKwYBBAGyMQECAhYwJTAjBggrBgEFBQcCARYX +aHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQIBMD0GA1UdHwQ2MDQwMqAw +oC6GLGh0dHA6Ly9jcmwuY3Jsb2NzcC5jbi9Xb1RydXNEVlNlcnZlckNBXzIuY3Js +MGwGCCsGAQUFBwEBBGAwXjA4BggrBgEFBQcwAoYsaHR0cDovL2FpYS5jcmxvY3Nw +LmNuL1dvVHJ1c0RWU2VydmVyQ0FfMi5jcnQwIgYIKwYBBQUHMAGGFmh0dHA6Ly9v +Y3NwLmNybG9jc3AuY24wLwYDVR0RBCgwJoISKi50ZXN0LmxlaW5pYW8uY29tghB0 +ZXN0LmxlaW5pYW8uY29tMIIBfAYKKwYBBAHWeQIEAgSCAWwEggFoAWYAdQDYCVU7 +lE96/8gWGW+UT4WrsPj8XodVJg8V0S5yu0VLFAAAAZh5caiPAAAEAwBGMEQCIEDI +9cjdBO2+Syn6MGoIkuElcBaLDqcZx90pcRLVDC0vAiAh7jVQwA7L1O/kPQHiejUY +mD76It9dmNO8yapW4rt2+gB2AKyrMHBs6+yEMfQT0vSRXxEeQiRDsfKmjE88Kzun +HgLDAAABmHlxqFwAAAQDAEcwRQIgcxpTxyrEt4HGRBIZrJAjcQHCebXx67Xc3m5R +LoJ1QRYCIQCK3gzJOfVtI7HDit/DF0MoABE9S4mpEvJBJlXGgEA7bgB1ANdtfRDR +p/V3wsfpX9cAv/mCyTNaZeHQswFzF8DIxWl3AAABmHlxqEwAAAQDAEYwRAIgFzKE +BggU2u8IELOYhUhsHUf0mj16mTIiCRrM7N3czpACIAfIat8Wg/QrGG7CfVq3dwak +2nZCOj8lRgWAHr332biXMA0GCSqGSIb3DQEBCwUAA4IBAQAb8cZRhW51AHv/qqrh +yvZ7Hj2MMbZhLgzmWU5VHIfIpM9FKK1K7jOT8Bj+CVm4uQs9VmPqmUGcFLksX3H+ +oYlXZ7P3rAhe1Scve/nF/10Gb5I2A842KMXg1us9FNazPC7L54rALB2CfjdkvBF1 +xIz8eSq1I8jwsEb7MDeYELfu1tks08ijVBHyRsHyKrUmWPVsc43MSC4/YS6PdppK +tBpkI4m9nISNo/TkWv+cBk3MlMGVI+n6p//hhQ8ws6Mb7isGv9+nYxk1bMRXvW4s +1alSdOG31gUUg+ewEFcMB7rYCJsvZ6ymSFgKCKoJmDJPajG6Ay03YGPc1CtZnfQF +2bvf +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF4jCCA8qgAwIBAgIRANVuJGyU7WOrsUbvwZa2T7AwDQYJKoZIhvcNAQEMBQAw +gYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtK +ZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYD +VQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTIw +MDEwODAwMDAwMFoXDTMwMDEwNzIzNTk1OVowXDELMAkGA1UEBhMCQ04xGjAYBgNV +BAoTEVdvVHJ1cyBDQSBMaW1pdGVkMTEwLwYDVQQDDChXb1RydXMgRFYgU2VydmVy +IENBICBbUnVuIGJ5IHRoZSBJc3N1ZXJdMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEA7IE0rmVTVdRdNOzK1jsR/VppyukZ/XbQgakJHOhg6XGDsiHe/l5B +3PxyXw18jEdN+7YxP0qsGz+HlQbsQh6XlwIyjpz/2gFMiqa7y1v+dHOgj6xNOF5a +oaPm/Qhb0N+JYQidgaC+1Zp6W+YeC736rzCMr9vL1Usa3QLzRoQEo0DzbG4sPeP1 +US0Ia/i8o6szArH+DAcvrzCZ2kkpTScQ9QfOsvkMBP1W2otICdKUyZHaBc+ztTAd +ovSlOR+GPf29dYfGQkZAp0tffIRw/na3WB86WGZPpNFfo2QxxsHYoL3oSWKfSWTY +FgW22J8eA03TFHYowm/NqYuJ7GW553HppQIDAQABo4IBcDCCAWwwHwYDVR0jBBgw +FoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFJmbLfaL8KPbidSe++V0 +L2jSkE/kMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1Ud +JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAiBgNVHSAEGzAZMA0GCysGAQQBsjEB +AgIWMAgGBmeBDAECATBQBgNVHR8ESTBHMEWgQ6BBhj9odHRwOi8vY3JsLnVzZXJ0 +cnVzdC5jb20vVVNFUlRydXN0UlNBQ2VydGlmaWNhdGlvbkF1dGhvcml0eS5jcmww +cQYIKwYBBQUHAQEEZTBjMDoGCCsGAQUFBzAChi5odHRwOi8vY3J0LnVzZXJ0cnVz +dC5jb20vVVNFUlRydXN0UlNBQUFBQ0EuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8v +b2NzcC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEBDAUAA4ICAQB5t8v3uYzHa4EL +0rOb9g/YAmptUbILcBMKk1x188ucsGVPaG1DG9bpVamxbmCtFA1MlrA7iUC8SGop +KBnuWFsNKiC7jCbRoahT1/FSwFsSuDlDmOjr1MqDXE+or08UkXsJB57XxXxdVOPl +DcZHII4qHi1XKK4iurMqb+kbdpAWadyfidRRCGPopYCVYLLYhRJgpFGtfr6Gk8N0 +j81jq/7QbN0dRSDzMNdadKTc7c3+i9fIrXj79lV5Wvva+OL7nh8MxQhG1Ekek7Rv +en++jSZvaEhCrMsSedFTA/aIy7oJg85tfglF2ybK61HsobjYzdDNICKJlIm4chlA +XIDDqw2mw0Kz2snrkp9dpvMBqahF/Uy1kHzPcrq1/w5OqZWAuDKxZ68PuZ/ME2hI +YbIDG9dWT6Y7eqtjQ2TmAQbOqdAG2LeikPMl2DMrPEa4lcKJzsFbHfHAW3hVgPSQ +hRfS4TtbNnxijbsp8GguMHxP2R7dpAAYybwfZdXP7WYAnwEr1mzIf0Y3J0m7GDyX +JhaflN3G2wIm2HzRd39NvnDRmFEraqui/YYO9ym0pwq1d0S+bGG6876QCto0u3Cg +ItFh3Za2ZeIY+g5mWrejSaDs9LT7eu44iCyebfgekdMRqFeCuGAsJdsun3LOHHJo +tCVPRjyFg9NDeJeMa4Z8QuXAXLd9cw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgTCCBGmgAwIBAgIQOXJEOvkit1HX02wQ3TE1lTANBgkqhkiG9w0BAQwFADB7 +MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD +VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE +AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTE5MDMxMjAwMDAwMFoXDTI4 +MTIzMTIzNTk1OVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5 +MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO +ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sI +s9CsVw127c0n00ytUINh4qogTQktZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnG +vDoZtF+mvX2do2NCtnbyqTsrkfjib9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQ +Ijy8/hPwhxR79uQfjtTkUcYRZ0YIUcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfb +IWax1Jt4A8BQOujM8Ny8nkz+rwWWNR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0 +tyA9yn8iNK5+O2hmAUTnAU5GU5szYPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97E +xwzf4TKuzJM7UXiVZ4vuPVb+DNBpDxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNV +icQNwZNUMBkTrNN9N6frXTpsNVzbQdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5 +D9kCnusSTJV882sFqV4Wg8y4Z+LoE53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJ +WBp/kjbmUZIO8yZ9HE0XvMnsQybQv0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ +5lhCLkMaTLTwJUdZ+gQek9QmRkpQgbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzG +KAgEJTm4Diup8kyXHAc/DVL17e8vgg8CAwEAAaOB8jCB7zAfBgNVHSMEGDAWgBSg +EQojPpbxB+zirynvgqV/0DCktDAdBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rID +ZsswDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAG +BgRVHSAAMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29t +L0FBQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggr +BgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUA +A4IBAQAYh1HcdCE9nIrgJ7cz0C7M7PDmy14R3iJvm3WOnnL+5Nb+qh+cli3vA0p+ +rvSNb3I8QzvAP+u431yqqcau8vzY7qN7Q/aGNnwU4M309z/+3ri0ivCRlv79Q2R+ +/czSAaF9ffgZGclCKxO/WIu6pKJmBHaIkU4MiRTOok3JMrO66BQavHHxW/BBC5gA +CiIDEOUMsfnNkjcZ7Tvx5Dq2+UUTJnWvu6rvP3t3O9LEApE9GQDTF1w52z97GA1F +zZOFli9d31kWTz9RvdVFGD/tSo7oBmF0Ixa1DVBzJ0RHfxBdiSprhTEUxOipakyA +vGp4z7h/jnZymQyd/teRCBaho1+V +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/cert_chain_ca.pem b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/cert_chain_ca.pem new file mode 100644 index 000000000..33c71ba9d --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/cert_chain_ca.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/check_certificate.py b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/check_certificate.py new file mode 100644 index 000000000..8274c2c1f --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/check_certificate.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +检查证书文件信息 +用于验证证书文件是否正确 +""" + +import sys +import os +import ssl +import socket +from datetime import datetime + +def check_certificate_file(cert_path): + """ + 检查证书文件 + + Args: + cert_path: 证书文件路径 + """ + if not os.path.exists(cert_path): + print(f"❌ 证书文件不存在: {cert_path}") + return False + + # 读取文件大小 + file_size = os.path.getsize(cert_path) + print(f"文件: {cert_path}") + print(f"大小: {file_size} 字节") + print() + + try: + # 读取证书内容 + with open(cert_path, 'rb') as f: + cert_data = f.read() + + # 尝试解析证书 + try: + cert = ssl.PEM_cert_to_DER_cert(cert_data.decode('utf-8')) + cert_info = ssl.DER_cert_to_PEM_cert(cert) + + # 使用OpenSSL解析(如果可用) + import subprocess + result = subprocess.run( + ['openssl', 'x509', '-in', cert_path, '-noout', '-text'], + capture_output=True, + text=True, + timeout=5 + ) + + if result.returncode == 0: + print("✅ 证书格式正确") + print() + print("证书详细信息:") + print("-" * 50) + print(result.stdout) + print("-" * 50) + + # 提取关键信息 + subject_result = subprocess.run( + ['openssl', 'x509', '-in', cert_path, '-noout', '-subject'], + capture_output=True, + text=True, + timeout=5 + ) + issuer_result = subprocess.run( + ['openssl', 'x509', '-in', cert_path, '-noout', '-issuer'], + capture_output=True, + text=True, + timeout=5 + ) + dates_result = subprocess.run( + ['openssl', 'x509', '-in', cert_path, '-noout', '-dates'], + capture_output=True, + text=True, + timeout=5 + ) + + if subject_result.returncode == 0: + print(subject_result.stdout.strip()) + if issuer_result.returncode == 0: + print(issuer_result.stdout.strip()) + if dates_result.returncode == 0: + print(dates_result.stdout.strip()) + + return True + else: + print("⚠️ 无法使用OpenSSL解析证书,但文件格式可能正确") + print(f"错误: {result.stderr}") + return False + + except Exception as e: + print(f"❌ 证书解析失败: {e}") + return False + + except Exception as e: + print(f"❌ 读取证书文件失败: {e}") + return False + +def main(): + if len(sys.argv) < 2: + print("用法: python3 check_certificate.py <证书文件路径>") + print("示例: python3 check_certificate.py ca.crt") + sys.exit(1) + + cert_path = sys.argv[1] + + print("=" * 50) + print("证书文件检查") + print("=" * 50) + print() + + success = check_certificate_file(cert_path) + + if success: + print() + print("=" * 50) + print("✅ 证书检查完成") + print("=" * 50) + print() + print("在 get_device_token_at.py 中使用:") + print(f" CA_CERT_PATH = \"{cert_path}\"") + else: + print() + print("=" * 50) + print("❌ 证书检查失败") + print("=" * 50) + sys.exit(1) + +if __name__ == "__main__": + main() + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/download_server_ca.py b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/download_server_ca.py new file mode 100644 index 000000000..a41b2ef83 --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/download_server_ca.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +从HTTPS服务器下载CA证书 +用于单向认证场景 +""" + +import ssl +import socket +import sys +import argparse +from urllib.parse import urlparse + +def download_server_certificate(hostname, port=443, output_file="ca.crt", extract_ca=True): + """ + 从HTTPS服务器下载证书 + + Args: + hostname: 服务器主机名或IP + port: 端口号(默认443) + output_file: 输出文件名 + extract_ca: 是否提取CA证书(True=提取CA证书,False=保存服务器证书) + """ + try: + # 创建SSL上下文 + context = ssl.create_default_context() + + # 连接到服务器 + print(f"正在连接到 {hostname}:{port}...") + with socket.create_connection((hostname, port), timeout=10) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + # 获取证书链(Python的getpeercert只返回服务器证书,需要从SSL连接获取完整链) + cert_der = ssock.getpeercert(binary_form=True) + cert_pem = ssl.DER_cert_to_PEM_cert(cert_der) + + # 获取证书信息 + cert_info = ssock.getpeercert() + + print(f"\n服务器证书信息:") + print(f" 主题: {cert_info.get('subject', 'N/A')}") + print(f" 颁发者: {cert_info.get('issuer', 'N/A')}") + + # 判断是服务器证书还是CA证书 + subject = str(cert_info.get('subject', '')) + issuer = str(cert_info.get('issuer', '')) + is_ca_cert = (subject == issuer) + + if extract_ca and not is_ca_cert: + # 如果提取CA证书,但当前是服务器证书,提示用户 + print(f"\n⚠️ 注意: 下载的是服务器证书,不是CA证书") + print(f" 建议使用OpenSSL获取完整证书链:") + print(f" openssl s_client -showcerts -connect {hostname}:{port} /dev/null | \\") + print(f" openssl x509 -outform PEM > {output_file}") + print(f"\n 或者联系服务器管理员获取CA证书") + print(f"\n 当前保存服务器证书到: {output_file}") + print(f" (可以尝试使用,但可能无法验证服务器身份)") + elif extract_ca and is_ca_cert: + print(f"\n✅ 这是CA证书(Subject == Issuer),可以直接使用") + else: + print(f"\n✅ 保存服务器证书到: {output_file}") + + # 保存证书 + with open(output_file, 'w') as f: + f.write(cert_pem) + + print(f" 文件大小: {len(cert_pem)} 字节") + print(f" 有效期: {cert_info.get('notBefore', 'N/A')} 至 {cert_info.get('notAfter', 'N/A')}") + + return True + + except socket.gaierror as e: + print(f"❌ DNS解析失败: {e}") + return False + except socket.timeout: + print(f"❌ 连接超时") + return False + except ssl.SSLError as e: + print(f"❌ SSL错误: {e}") + return False + except Exception as e: + print(f"❌ 错误: {e}") + import traceback + traceback.print_exc() + return False + +def main(): + parser = argparse.ArgumentParser(description='从HTTPS服务器下载证书') + parser.add_argument('url', help='HTTPS URL (例如: https://example.com)') + parser.add_argument('-o', '--output', default='ca.crt', help='输出文件名 (默认: ca.crt)') + parser.add_argument('-p', '--port', type=int, help='端口号 (默认从URL解析或443)') + parser.add_argument('--server-cert', action='store_true', help='下载服务器证书而不是CA证书') + + args = parser.parse_args() + + # 解析URL + parsed = urlparse(args.url) + hostname = parsed.hostname + port = args.port or parsed.port or 443 + + if not hostname: + print("❌ 错误: 无法从URL解析主机名") + sys.exit(1) + + print("=" * 50) + if args.server_cert: + print("从HTTPS服务器下载服务器证书") + else: + print("从HTTPS服务器下载证书(尝试提取CA证书)") + print("=" * 50) + print(f"服务器: {hostname}:{port}") + print(f"输出文件: {args.output}") + print("=" * 50) + + success = download_server_certificate(hostname, port, args.output, extract_ca=not args.server_cert) + + if success: + print("\n" + "=" * 50) + print("✅ 证书下载成功!") + print("=" * 50) + print(f"\n使用方法:") + print(f" 在 get_device_token_at.py 中设置:") + print(f" AUTH_MODE = 1") + print(f" CA_CERT_PATH = \"{args.output}\"") + print(f"\n注意:") + print(f" - 如果下载的是服务器证书,可能需要提取CA证书链") + print(f" - 使用以下命令获取完整证书链:") + print(f" openssl s_client -showcerts -connect {hostname}:{port} /dev/null | \\") + print(f" openssl x509 -outform PEM > {args.output}") + else: + print("\n❌ 证书下载失败!") + sys.exit(1) + +if __name__ == "__main__": + main() + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/extract_ca_from_chain.py b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/extract_ca_from_chain.py new file mode 100644 index 000000000..f5d83d1c3 --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/extract_ca_from_chain.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +从证书链中提取CA证书 +用于从cert_chain.pem中提取可用于设备端验证的CA证书 +""" + +import sys +import os +import re +import subprocess +import tempfile + +def extract_certificates(chain_file): + """ + 从证书链文件中提取所有证书 + + Returns: + list: 证书列表,每个元素是证书的PEM格式字符串 + """ + with open(chain_file, 'r') as f: + content = f.read() + + # 使用正则表达式提取所有证书 + pattern = r'-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----' + cert_matches = re.findall(pattern, content, re.DOTALL) + + certificates = [] + for match in cert_matches: + cert_text = f'-----BEGIN CERTIFICATE-----{match}-----END CERTIFICATE-----\n' + certificates.append(cert_text) + + return certificates + +def get_cert_info(cert_pem): + """ + 获取证书信息 + + Returns: + dict: 包含subject, issuer, is_ca等信息 + """ + # 写入临时文件 + with tempfile.NamedTemporaryFile(mode='w', suffix='.pem', delete=False) as tmp: + tmp.write(cert_pem) + tmp_path = tmp.name + + try: + # 获取subject和issuer + subject_result = subprocess.run( + ['openssl', 'x509', '-in', tmp_path, '-noout', '-subject'], + capture_output=True, text=True, timeout=5 + ) + issuer_result = subprocess.run( + ['openssl', 'x509', '-in', tmp_path, '-noout', '-issuer'], + capture_output=True, text=True, timeout=5 + ) + + # 检查是否是CA证书 + text_result = subprocess.run( + ['openssl', 'x509', '-in', tmp_path, '-noout', '-text'], + capture_output=True, text=True, timeout=5 + ) + + subject = subject_result.stdout.strip() if subject_result.returncode == 0 else None + issuer = issuer_result.stdout.strip() if issuer_result.returncode == 0 else None + + # 提取subject和issuer的实际值(去除前缀) + subject_value = subject.replace('subject=', '').strip() if subject else None + issuer_value = issuer.replace('issuer=', '').strip() if issuer else None + + is_ca = 'CA:TRUE' in text_result.stdout if text_result.returncode == 0 else False + is_self_signed = (subject_value == issuer_value) if subject_value and issuer_value else False + + return { + 'subject': subject, + 'issuer': issuer, + 'is_ca': is_ca, + 'is_self_signed': is_self_signed, + 'pem': cert_pem + } + finally: + os.unlink(tmp_path) + +def extract_ca_certificates(chain_file, output_file=None): + """ + 从证书链中提取CA证书 + + Args: + chain_file: 证书链文件路径 + output_file: 输出文件路径(如果为None,则自动生成) + + Returns: + str: 输出文件路径 + """ + print("=" * 60) + print("从证书链中提取CA证书") + print("=" * 60) + print(f"输入文件: {chain_file}\n") + + # 提取所有证书 + certificates = extract_certificates(chain_file) + print(f"证书链包含 {len(certificates)} 个证书\n") + + # 分析每个证书 + cert_infos = [] + for i, cert in enumerate(certificates): + print(f"分析证书 {i+1}/{len(certificates)}...") + info = get_cert_info(cert) + cert_infos.append(info) + + print(f" Subject: {info['subject']}") + print(f" Issuer: {info['issuer']}") + print(f" CA证书: {'是' if info['is_ca'] else '否'}") + print(f" 自签名: {'是' if info['is_self_signed'] else '否'}") + print() + + # 查找CA证书(优先选择自签名的根CA) + ca_certs = [] + root_ca = None + + for info in cert_infos: + if info['is_ca']: + ca_certs.append(info) + if info['is_self_signed']: + root_ca = info + + # 选择要使用的CA证书 + if root_ca: + selected_ca = root_ca + print(f"✅ 找到根CA证书(自签名):") + elif ca_certs: + # 如果没有根CA,使用最后一个CA证书(通常是中间CA) + selected_ca = ca_certs[-1] + print(f"⚠️ 未找到根CA证书,使用中间CA证书:") + else: + print("❌ 未找到CA证书!") + print(" 证书链中可能只包含服务器证书") + print(" 建议:") + print(" 1. 使用authmode=0(不验证证书)") + print(" 2. 或联系服务器管理员获取CA根证书") + return None + + print(f" Subject: {selected_ca['subject']}") + print(f" Issuer: {selected_ca['issuer']}") + print() + + # 确定输出文件名 + if output_file is None: + base_name = os.path.splitext(chain_file)[0] + output_file = f"{base_name}_ca.pem" + + # 构建完整CA链(包含所有中间CA和根CA) + # 通常需要中间CA证书才能验证服务器证书 + ca_chain_pem = "" + intermediate_cas = [info for info in ca_certs if not info['is_self_signed']] + + if intermediate_cas: + print(f"找到 {len(intermediate_cas)} 个中间CA证书") + for info in intermediate_cas: + ca_chain_pem += info['pem'] + + # 添加根CA + if root_ca: + ca_chain_pem += root_ca['pem'] + + # 保存完整CA链(推荐) + base_name = os.path.splitext(output_file)[0] + ca_chain_file = f"{base_name}_chain.pem" + with open(ca_chain_file, 'w') as f: + f.write(ca_chain_pem) + + # 也保存单独的根CA(某些设备可能只需要根CA) + with open(output_file, 'w') as f: + f.write(selected_ca['pem']) + + file_size = os.path.getsize(output_file) + chain_size = os.path.getsize(ca_chain_file) + + print(f"✅ æ ¹CA证书已保存到: {output_file}") + print(f" 文件大小: {file_size} 字节") + print() + print(f"✅ 完整CA链已保存到: {ca_chain_file}") + print(f" 文件大小: {chain_size} 字节") + print(f" (包含 {len(intermediate_cas)} 个中间CA + 1 个根CA)") + print() + + # 验证证书是否能验证服务器证书 + print("验证证书链...") + server_cert = cert_infos[0] if cert_infos else None + if server_cert: + # 将服务器证书保存到临时文件 + with tempfile.NamedTemporaryFile(mode='w', suffix='.pem', delete=False) as tmp_server: + tmp_server.write(server_cert['pem']) + tmp_server_path = tmp_server.name + + try: + # 先尝试使用完整CA链验证 + verify_result = subprocess.run( + ['openssl', 'verify', '-CAfile', ca_chain_file, tmp_server_path], + capture_output=True, text=True, timeout=5 + ) + + if verify_result.returncode == 0: + print(f"✅ 使用完整CA链验证成功!") + print(f" 推荐使用: {ca_chain_file}") + else: + # 尝试只用根CA验证 + verify_result2 = subprocess.run( + ['openssl', 'verify', '-CAfile', output_file, tmp_server_path], + capture_output=True, text=True, timeout=5 + ) + + if verify_result2.returncode == 0: + print(f"✅ 使用根CA验证成功!") + print(f" 可以使用: {output_file}") + else: + print(f"⚠️ 证书验证失败") + print(f" æ ¹CA验证: {verify_result2.stderr.strip()}") + print(f" 完整链验证: {verify_result.stderr.strip()}") + print(f" 建议:尝试使用完整证书链文件 {ca_chain_file}") + finally: + os.unlink(tmp_server_path) + + print() + print("=" * 60) + print("使用说明") + print("=" * 60) + print(f"在 get_device_token_at.py 中设置:") + print(f" AUTH_MODE = 1") + print() + print(f"推荐使用完整CA链(包含中间CA和根CA):") + print(f" CA_CERT_PATH = \"{ca_chain_file}\"") + print() + print(f"或者只使用根CA(如果设备支持):") + print(f" CA_CERT_PATH = \"{output_file}\"") + print() + print("注意:") + print(" - 某些设备可能需要完整的CA链才能验证服务器证书") + print(" - 如果验证失败,尝试使用完整CA链文件") + print() + + return ca_chain_file # 返回完整CA链文件路径 + +def main(): + parser = argparse.ArgumentParser(description='从证书链中提取CA证书') + parser.add_argument('chain_file', help='证书链文件路径 (例如: cert_chain.pem)') + parser.add_argument('-o', '--output', help='输出CA证书文件路径 (默认: _ca.pem)') + + args = parser.parse_args() + + if not os.path.exists(args.chain_file): + print(f"❌ 错误: 证书链文件不存在: {args.chain_file}") + sys.exit(1) + + output_file = extract_ca_certificates(args.chain_file, args.output) + + if output_file: + print("✅ 处理完成!") + else: + print("❌ 处理失败!") + sys.exit(1) + +if __name__ == "__main__": + import argparse + main() + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/generate_test_certs.sh b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/generate_test_certs.sh new file mode 100644 index 000000000..85f4c674e --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/generate_test_certs.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# 生成测试用自签名证书脚本 +# 注意:这些证书仅用于测试,生产环境请使用正式的CA签发的证书 + +set -e + +CERT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$CERT_DIR" + +echo "==========================================" +echo "生成测试用SSL/TLS证书" +echo "==========================================" +echo "" + +# 创建临时目录 +TMP_DIR=$(mktemp -d) +trap "rm -rf $TMP_DIR" EXIT + +# 1. 生成CA私钥 +echo "1. 生成CA私钥..." +openssl genrsa -out "$TMP_DIR/ca.key" 2048 + +# 2. 生成CA证书 +echo "2. 生成CA证书..." +openssl req -new -x509 -days 3650 -key "$TMP_DIR/ca.key" -out "$TMP_DIR/ca.crt" \ + -subj "/C=CN/ST=Guangdong/L=Shenzhen/O=Test CA/CN=Test CA" + +# 3. 生成客户端私钥 +echo "3. 生成客户端私钥..." +openssl genrsa -out "$TMP_DIR/client.key" 2048 + +# 4. 生成客户端证书请求 +echo "4. 生成客户端证书请求..." +openssl req -new -key "$TMP_DIR/client.key" -out "$TMP_DIR/client.csr" \ + -subj "/C=CN/ST=Guangdong/L=Shenzhen/O=Test Client/CN=test-client" + +# 5. 使用CA签名客户端证书 +echo "5. 使用CA签名客户端证书..." +openssl x509 -req -days 3650 -in "$TMP_DIR/client.csr" -CA "$TMP_DIR/ca.crt" \ + -CAkey "$TMP_DIR/ca.key" -CAcreateserial -out "$TMP_DIR/client.crt" + +# 6. 复制证书到当前目录 +echo "6. 复制证书文件..." +cp "$TMP_DIR/ca.crt" "$CERT_DIR/ca.crt" +cp "$TMP_DIR/client.crt" "$CERT_DIR/client.crt" +cp "$TMP_DIR/client.key" "$CERT_DIR/client.key" + +# 7. 显示证书信息 +echo "" +echo "==========================================" +echo "证书生成完成!" +echo "==========================================" +echo "" +echo "证书文件:" +echo " - ca.crt ($(wc -c < "$CERT_DIR/ca.crt") 字节)" +echo " - client.crt ($(wc -c < "$CERT_DIR/client.crt") 字节)" +echo " - client.key ($(wc -c < "$CERT_DIR/client.key") 字节)" +echo "" +echo "证书信息:" +echo "" +echo "CA证书:" +openssl x509 -in "$CERT_DIR/ca.crt" -noout -subject -issuer -dates +echo "" +echo "客户端证书:" +openssl x509 -in "$CERT_DIR/client.crt" -noout -subject -issuer -dates +echo "" +echo "==========================================" +echo "注意:这些是测试证书,仅用于开发测试!" +echo "生产环境请使用正式的CA签发的证书。" +echo "==========================================" + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/intermediate_cas.pem b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/intermediate_cas.pem new file mode 100644 index 000000000..7f9e38732 --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/intermediate_cas.pem @@ -0,0 +1,66 @@ +-----BEGIN CERTIFICATE----- +MIIF4jCCA8qgAwIBAgIRANVuJGyU7WOrsUbvwZa2T7AwDQYJKoZIhvcNAQEMBQAw +gYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtK +ZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYD +VQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTIw +MDEwODAwMDAwMFoXDTMwMDEwNzIzNTk1OVowXDELMAkGA1UEBhMCQ04xGjAYBgNV +BAoTEVdvVHJ1cyBDQSBMaW1pdGVkMTEwLwYDVQQDDChXb1RydXMgRFYgU2VydmVy +IENBICBbUnVuIGJ5IHRoZSBJc3N1ZXJdMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEA7IE0rmVTVdRdNOzK1jsR/VppyukZ/XbQgakJHOhg6XGDsiHe/l5B +3PxyXw18jEdN+7YxP0qsGz+HlQbsQh6XlwIyjpz/2gFMiqa7y1v+dHOgj6xNOF5a +oaPm/Qhb0N+JYQidgaC+1Zp6W+YeC736rzCMr9vL1Usa3QLzRoQEo0DzbG4sPeP1 +US0Ia/i8o6szArH+DAcvrzCZ2kkpTScQ9QfOsvkMBP1W2otICdKUyZHaBc+ztTAd +ovSlOR+GPf29dYfGQkZAp0tffIRw/na3WB86WGZPpNFfo2QxxsHYoL3oSWKfSWTY +FgW22J8eA03TFHYowm/NqYuJ7GW553HppQIDAQABo4IBcDCCAWwwHwYDVR0jBBgw +FoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFJmbLfaL8KPbidSe++V0 +L2jSkE/kMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1Ud +JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAiBgNVHSAEGzAZMA0GCysGAQQBsjEB +AgIWMAgGBmeBDAECATBQBgNVHR8ESTBHMEWgQ6BBhj9odHRwOi8vY3JsLnVzZXJ0 +cnVzdC5jb20vVVNFUlRydXN0UlNBQ2VydGlmaWNhdGlvbkF1dGhvcml0eS5jcmww +cQYIKwYBBQUHAQEEZTBjMDoGCCsGAQUFBzAChi5odHRwOi8vY3J0LnVzZXJ0cnVz +dC5jb20vVVNFUlRydXN0UlNBQUFBQ0EuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8v +b2NzcC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEBDAUAA4ICAQB5t8v3uYzHa4EL +0rOb9g/YAmptUbILcBMKk1x188ucsGVPaG1DG9bpVamxbmCtFA1MlrA7iUC8SGop +KBnuWFsNKiC7jCbRoahT1/FSwFsSuDlDmOjr1MqDXE+or08UkXsJB57XxXxdVOPl +DcZHII4qHi1XKK4iurMqb+kbdpAWadyfidRRCGPopYCVYLLYhRJgpFGtfr6Gk8N0 +j81jq/7QbN0dRSDzMNdadKTc7c3+i9fIrXj79lV5Wvva+OL7nh8MxQhG1Ekek7Rv +en++jSZvaEhCrMsSedFTA/aIy7oJg85tfglF2ybK61HsobjYzdDNICKJlIm4chlA +XIDDqw2mw0Kz2snrkp9dpvMBqahF/Uy1kHzPcrq1/w5OqZWAuDKxZ68PuZ/ME2hI +YbIDG9dWT6Y7eqtjQ2TmAQbOqdAG2LeikPMl2DMrPEa4lcKJzsFbHfHAW3hVgPSQ +hRfS4TtbNnxijbsp8GguMHxP2R7dpAAYybwfZdXP7WYAnwEr1mzIf0Y3J0m7GDyX +JhaflN3G2wIm2HzRd39NvnDRmFEraqui/YYO9ym0pwq1d0S+bGG6876QCto0u3Cg +ItFh3Za2ZeIY+g5mWrejSaDs9LT7eu44iCyebfgekdMRqFeCuGAsJdsun3LOHHJo +tCVPRjyFg9NDeJeMa4Z8QuXAXLd9cw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgTCCBGmgAwIBAgIQOXJEOvkit1HX02wQ3TE1lTANBgkqhkiG9w0BAQwFADB7 +MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD +VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE +AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTE5MDMxMjAwMDAwMFoXDTI4 +MTIzMTIzNTk1OVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5 +MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO +ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sI +s9CsVw127c0n00ytUINh4qogTQktZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnG +vDoZtF+mvX2do2NCtnbyqTsrkfjib9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQ +Ijy8/hPwhxR79uQfjtTkUcYRZ0YIUcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfb +IWax1Jt4A8BQOujM8Ny8nkz+rwWWNR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0 +tyA9yn8iNK5+O2hmAUTnAU5GU5szYPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97E +xwzf4TKuzJM7UXiVZ4vuPVb+DNBpDxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNV +icQNwZNUMBkTrNN9N6frXTpsNVzbQdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5 +D9kCnusSTJV882sFqV4Wg8y4Z+LoE53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJ +WBp/kjbmUZIO8yZ9HE0XvMnsQybQv0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ +5lhCLkMaTLTwJUdZ+gQek9QmRkpQgbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzG +KAgEJTm4Diup8kyXHAc/DVL17e8vgg8CAwEAAaOB8jCB7zAfBgNVHSMEGDAWgBSg +EQojPpbxB+zirynvgqV/0DCktDAdBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rID +ZsswDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAG +BgRVHSAAMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29t +L0FBQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggr +BgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUA +A4IBAQAYh1HcdCE9nIrgJ7cz0C7M7PDmy14R3iJvm3WOnnL+5Nb+qh+cli3vA0p+ +rvSNb3I8QzvAP+u431yqqcau8vzY7qN7Q/aGNnwU4M309z/+3ri0ivCRlv79Q2R+ +/czSAaF9ffgZGclCKxO/WIu6pKJmBHaIkU4MiRTOok3JMrO66BQavHHxW/BBC5gA +CiIDEOUMsfnNkjcZ7Tvx5Dq2+UUTJnWvu6rvP3t3O9LEApE9GQDTF1w52z97GA1F +zZOFli9d31kWTz9RvdVFGD/tSo7oBmF0Ixa1DVBzJ0RHfxBdiSprhTEUxOipakyA +vGp4z7h/jnZymQyd/teRCBaho1+V +-----END CERTIFICATE----- diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/root_ca.pem b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/root_ca.pem new file mode 100644 index 000000000..33c71ba9d --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/root_ca.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/verify_ca_for_https.py b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/verify_ca_for_https.py new file mode 100644 index 000000000..fc555f7fe --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/verify_ca_for_https.py @@ -0,0 +1,561 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +验证CA证书是否可以用来做HTTPS下载的服务器认证(单向认证) + +功能: +1. 检查证书类型(CA证书、服务器证书、客户端证书) +2. 验证CA证书是否可以验证目标服务器的证书 +3. 测试HTTPS连接(使用CA证书进行服务器认证) +""" + +import ssl +import socket +import sys +import os +import subprocess +from urllib.parse import urlparse + +# 可选依赖:用于HTTPS下载测试 +try: + import requests + from requests.adapters import HTTPAdapter + from urllib3.util.ssl_ import create_urllib3_context + from urllib3.poolmanager import PoolManager + REQUESTS_AVAILABLE = True +except ImportError: + REQUESTS_AVAILABLE = False + + +def extract_certificates_from_file(cert_path): + """ + 从证书文件中提取所有证书(支持证书链文件) + + Returns: + list: 证书PEM字符串列表 + """ + import re + + with open(cert_path, 'r') as f: + content = f.read() + + # 使用正则表达式提取所有证书 + pattern = r'-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----' + cert_matches = re.findall(pattern, content, re.DOTALL) + + certificates = [] + for match in cert_matches: + cert_text = f'-----BEGIN CERTIFICATE-----{match}-----END CERTIFICATE-----\n' + certificates.append(cert_text) + + return certificates + + +def check_single_certificate(cert_pem): + """ + 检查单个证书的类型 + + Args: + cert_pem: 证书的PEM格式字符串 + + Returns: + dict: 包含证书类型信息的字典 + """ + import tempfile + + # 写入临时文件 + with tempfile.NamedTemporaryFile(mode='w', suffix='.pem', delete=False) as tmp: + tmp.write(cert_pem) + tmp_path = tmp.name + + try: + result = subprocess.run( + ['openssl', 'x509', '-in', tmp_path, '-noout', '-text'], + capture_output=True, + text=True, + check=True + ) + cert_text = result.stdout + + cert_info = { + 'is_ca': False, + 'is_server': False, + 'is_client': False, + 'basic_constraints': None, + 'key_usage': None, + 'extended_key_usage': None, + 'subject': None, + 'issuer': None, + 'is_self_signed': False + } + + # 检查Basic Constraints + if 'CA:TRUE' in cert_text or 'CA:true' in cert_text: + cert_info['is_ca'] = True + cert_info['basic_constraints'] = 'CA:TRUE' + elif 'CA:FALSE' in cert_text or 'CA:false' in cert_text: + cert_info['basic_constraints'] = 'CA:FALSE' + + # 检查Key Usage + if 'Key Usage' in cert_text: + key_usage_parts = [] + if 'Certificate Sign' in cert_text: + key_usage_parts.append('Certificate Sign') + if 'Digital Signature' in cert_text: + key_usage_parts.append('Digital Signature') + if key_usage_parts: + cert_info['key_usage'] = ', '.join(key_usage_parts) + + # 检查Extended Key Usage + extended_key_usage_parts = [] + if 'TLS Web Server Authentication' in cert_text: + cert_info['is_server'] = True + extended_key_usage_parts.append('TLS Web Server Authentication') + if 'TLS Web Client Authentication' in cert_text: + cert_info['is_client'] = True + extended_key_usage_parts.append('TLS Web Client Authentication') + if extended_key_usage_parts: + cert_info['extended_key_usage'] = ', '.join(extended_key_usage_parts) + + # 提取Subject和Issuer + subject_result = subprocess.run( + ['openssl', 'x509', '-in', tmp_path, '-noout', '-subject'], + capture_output=True, + text=True, + check=True + ) + cert_info['subject'] = subject_result.stdout.strip() + + issuer_result = subprocess.run( + ['openssl', 'x509', '-in', tmp_path, '-noout', '-issuer'], + capture_output=True, + text=True, + check=True + ) + cert_info['issuer'] = issuer_result.stdout.strip() + + # 检查是否自签名 + subject_value = cert_info['subject'].replace('subject=', '').strip() + issuer_value = cert_info['issuer'].replace('issuer=', '').strip() + cert_info['is_self_signed'] = (subject_value == issuer_value) + + return cert_info + + finally: + os.unlink(tmp_path) + + +def check_certificate_type(cert_path): + """ + 检查证书类型(支持证书链文件) + + Returns: + dict: 包含证书类型信息的字典 + """ + try: + # 提取所有证书 + certificates = extract_certificates_from_file(cert_path) + + if not certificates: + print(f"❌ 证书文件中未找到任何证书") + return None + + print(f"证书文件包含 {len(certificates)} 个证书") + + # 检查每个证书 + cert_infos = [] + ca_certs = [] + root_ca = None + + for i, cert_pem in enumerate(certificates): + cert_info = check_single_certificate(cert_pem) + if cert_info: + cert_infos.append(cert_info) + + # 收集CA证书 + if cert_info['is_ca']: + ca_certs.append(cert_info) + if cert_info['is_self_signed']: + root_ca = cert_info + + # 如果只有一个证书,返回该证书的信息 + if len(cert_infos) == 1: + return cert_infos[0] + + # 如果有多个证书,返回汇总信息 + summary = { + 'is_certificate_chain': True, + 'cert_count': len(cert_infos), + 'ca_count': len(ca_certs), + 'has_root_ca': root_ca is not None, + 'certificates': cert_infos, + 'ca_certificates': ca_certs, + 'root_ca': root_ca + } + + # 判断是否可以作为CA证书使用 + # 如果包含CA证书(特别是根CA),可以用于服务器认证 + if ca_certs: + summary['can_use_for_server_auth'] = True + if root_ca: + summary['recommended_cert'] = root_ca + else: + summary['recommended_cert'] = ca_certs[-1] # 使用最后一个CA证书 + else: + summary['can_use_for_server_auth'] = False + + return summary + + except Exception as e: + print(f"❌ 检查证书类型失败: {e}") + import traceback + traceback.print_exc() + return None + + +def verify_ca_with_server(ca_cert_path, server_host, server_port=443): + """ + 验证CA证书是否可以验证服务器的证书 + + Args: + ca_cert_path: CA证书路径 + server_host: 服务器主机名 + server_port: 服务器端口,默认443 + + Returns: + bool: 是否可以验证 + """ + try: + # 创建SSL上下文 + context = ssl.create_default_context() + context.check_hostname = False # 先不检查主机名 + context.verify_mode = ssl.CERT_REQUIRED + + # 加载CA证书 + context.load_verify_locations(ca_cert_path) + + # 建立连接 + with socket.create_connection((server_host, server_port), timeout=10) as sock: + with context.wrap_socket(sock, server_hostname=server_host) as ssock: + server_cert = ssock.getpeercert(binary_form=False) + server_cert_binary = ssock.getpeercert(binary_form=True) + + print(f"✅ SSL连接成功") + print(f" 服务器证书主题: {server_cert.get('subject', 'N/A')}") + print(f" 服务器证书颁发者: {server_cert.get('issuer', 'N/A')}") + + # 验证证书链 + try: + # 使用openssl验证 + result = subprocess.run( + ['openssl', 'verify', '-CAfile', ca_cert_path, '-'], + input=server_cert_binary, + capture_output=True, + check=False + ) + + if result.returncode == 0: + print(f"✅ CA证书可以验证服务器证书") + return True + else: + print(f"❌ CA证书无法验证服务器证书") + print(f" 错误: {result.stderr.decode('utf-8', errors='ignore')}") + return False + except Exception as e: + print(f"⚠️ 无法使用openssl验证,但SSL连接成功: {e}") + return True # SSL连接成功说明证书验证通过 + + except ssl.SSLError as e: + print(f"❌ SSL验证失败: {e}") + return False + except socket.timeout: + print(f"❌ 连接超时") + return False + except Exception as e: + print(f"❌ 连接失败: {e}") + return False + + +def test_https_download_with_ca(ca_cert_path, test_url): + """ + 测试使用CA证书进行HTTPS下载 + + Args: + ca_cert_path: CA证书路径 + test_url: 测试URL + + Returns: + bool: 是否成功 + """ + if not REQUESTS_AVAILABLE: + print("⚠️ requests库未安装,跳过HTTPS下载测试") + print(" 安装方法: pip install requests urllib3") + return None + + try: + parsed = urlparse(test_url) + if parsed.scheme != 'https': + print(f"❌ URL必须是HTTPS协议: {test_url}") + return False + + print(f"\n测试HTTPS下载: {test_url}") + print(f"使用CA证书: {ca_cert_path}") + + # 创建自定义的HTTPAdapter,使用CA证书 + class CAAdapter(HTTPAdapter): + def init_poolmanager(self, *args, **kwargs): + context = create_urllib3_context() + context.load_verify_locations(ca_cert_path) + kwargs['ssl_context'] = context + return super().init_poolmanager(*args, **kwargs) + + # 创建Session + session = requests.Session() + session.mount('https://', CAAdapter()) + + # 发送HEAD请求(只获取响应头,不下载内容) + print("发送HTTPS HEAD请求...") + response = session.head(test_url, timeout=10, allow_redirects=True) + + if response.status_code == 200: + print(f"✅ HTTPS请求成功") + print(f" 状态码: {response.status_code}") + print(f" 内容类型: {response.headers.get('Content-Type', 'N/A')}") + print(f" 内容长度: {response.headers.get('Content-Length', 'N/A')} 字节") + return True + else: + print(f"⚠️ HTTPS请求返回状态码: {response.status_code}") + return False + + except requests.exceptions.SSLError as e: + print(f"❌ SSL验证失败: {e}") + return False + except requests.exceptions.RequestException as e: + print(f"❌ 请求失败: {e}") + return False + except Exception as e: + print(f"❌ 测试失败: {e}") + import traceback + traceback.print_exc() + return False + + +def main(): + """主函数""" + import argparse + + parser = argparse.ArgumentParser( + description='验证CA证书是否可以用来做HTTPS下载的服务器认证', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +示例: + # 验证CA证书类型 + python verify_ca_for_https.py ca_chain.pem + + # 验证CA证书是否可以验证特定服务器 + python verify_ca_for_https.py ca_chain.pem --server ambybox-api.test.leiniao.com + + # 测试HTTPS下载 + python verify_ca_for_https.py ca_chain.pem --test-url https://ambybox-api.test.leiniao.com/ambybox-device-api/v1/auth/getDeviceToken + """ + ) + + parser.add_argument('ca_cert', help='CA证书文件路径') + parser.add_argument('--server', help='服务器主机名(用于验证CA证书是否可以验证服务器证书)') + parser.add_argument('--port', type=int, default=443, help='服务器端口,默认443') + parser.add_argument('--test-url', help='测试HTTPS下载的URL') + + args = parser.parse_args() + + # 检查证书文件是否存在 + if not os.path.exists(args.ca_cert): + print(f"❌ 证书文件不存在: {args.ca_cert}") + sys.exit(1) + + print("=" * 70) + print("CA证书HTTPS验证工具") + print("=" * 70) + print(f"证书文件: {args.ca_cert}") + print() + + # 1. 检查证书类型 + print("【步骤1: 检查证书类型】") + print("-" * 70) + cert_info = check_certificate_type(args.ca_cert) + + if not cert_info: + print("❌ 无法检查证书类型") + sys.exit(1) + + # 处理证书链文件 + if cert_info.get('is_certificate_chain'): + print(f"✅ 这是一个证书链文件,包含 {cert_info['cert_count']} 个证书") + print(f" 其中包含 {cert_info['ca_count']} 个CA证书") + + if cert_info['has_root_ca']: + print(" ✅ 包含根CA证书(自签名)") + root_ca = cert_info['root_ca'] + print(f" æ ¹CA主题: {root_ca['subject']}") + print(f" æ ¹CA颁发者: {root_ca['issuer']}") + else: + print(" ⚠️ 未找到根CA证书(自签名),但包含中间CA证书") + if cert_info['ca_certificates']: + last_ca = cert_info['ca_certificates'][-1] + print(f" 最后一个CA证书主题: {last_ca['subject']}") + + print() + print("证书链中的证书:") + for i, cert in enumerate(cert_info['certificates'], 1): + cert_type = [] + if cert['is_ca']: + cert_type.append("CA证书") + if cert['is_self_signed']: + cert_type.append("(æ ¹CA)") + if cert['is_server']: + cert_type.append("服务器证书") + if cert['is_client']: + cert_type.append("客户端证书") + + print(f" {i}. {', '.join(cert_type) if cert_type else '未知类型'}") + print(f" Subject: {cert['subject']}") + print(f" Issuer: {cert['issuer']}") + + print() + + if cert_info.get('can_use_for_server_auth'): + print("✅ 证书链可以用于HTTPS服务器认证(单向认证)") + if cert_info.get('recommended_cert'): + print(f" 推荐使用: æ ¹CA证书或完整证书链") + else: + print("⚠️ 证书链中未找到CA证书,可能无法用于服务器认证") + else: + # 单个证书 + print(f"证书主题: {cert_info['subject']}") + print(f"证书颁发者: {cert_info['issuer']}") + print() + + # 判断证书类型 + if cert_info['is_ca']: + print("✅ 这是一个CA证书(证书颁发机构证书)") + if cert_info['is_self_signed']: + print(" ✅ 这是根CA证书(自签名)") + print(f" Basic Constraints: {cert_info['basic_constraints']}") + if cert_info['key_usage']: + print(f" Key Usage: {cert_info['key_usage']}") + else: + print("⚠️ 这不是一个CA证书") + if cert_info['is_server']: + print(" 这是一个服务器证书") + elif cert_info['is_client']: + print(" 这是一个客户端证书") + else: + print(" 证书类型未知") + + if cert_info['extended_key_usage']: + print(f" Extended Key Usage: {cert_info['extended_key_usage']}") + + print() + + # 2. 如果指定了服务器,验证CA证书是否可以验证服务器证书 + if args.server: + print("【步骤2: 验证CA证书是否可以验证服务器证书】") + print("-" * 70) + print(f"服务器: {args.server}:{args.port}") + print() + + can_verify = verify_ca_with_server(args.ca_cert, args.server, args.port) + print() + + if not can_verify: + print("❌ CA证书无法验证服务器证书,不能用于HTTPS下载") + sys.exit(1) + else: + print("【步骤2: 跳过服务器验证(未指定--server参数)】") + print() + + # 3. 如果指定了测试URL,测试HTTPS下载 + if args.test_url: + print("【步骤3: 测试HTTPS下载】") + print("-" * 70) + + success = test_https_download_with_ca(args.ca_cert, args.test_url) + print() + + if success: + print("✅ CA证书可以用于HTTPS下载") + else: + print("❌ CA证书无法用于HTTPS下载") + sys.exit(1) + else: + print("【步骤3: 跳过HTTPS下载测试(未指定--test-url参数)】") + print() + print("提示: 使用 --test-url 参数可以测试实际的HTTPS下载") + + # 总结 + print("=" * 70) + print("【验证总结】") + print("=" * 70) + + # 判断是否可以用于服务器认证 + can_use = False + if cert_info.get('is_certificate_chain'): + can_use = cert_info.get('can_use_for_server_auth', False) + if can_use: + print("✅ 证书类型: 证书链文件(包含CA证书,可用于服务器认证)") + else: + print("⚠️ 证书类型: 证书链文件(但未找到CA证书)") + else: + can_use = cert_info.get('is_ca', False) + if can_use: + print("✅ 证书类型: CA证书(可用于服务器认证)") + else: + print("⚠️ 证书类型: 不是CA证书(可能无法用于服务器认证)") + + if args.server: + server_verified = verify_ca_with_server(args.ca_cert, args.server, args.port) + if server_verified: + print("✅ 服务器验证: 通过") + can_use = True # 实际验证通过,即使类型检查不通过也认为可用 + else: + print("❌ 服务器验证: 失败") + else: + print("⚠️ 服务器验证: 未测试(建议使用 --server 参数测试)") + + if args.test_url: + download_tested = test_https_download_with_ca(args.ca_cert, args.test_url) + if download_tested is True: + print("✅ HTTPS下载: 通过") + can_use = True # 实际测试通过,认为可用 + elif download_tested is False: + print("❌ HTTPS下载: 失败") + else: + print("⚠️ HTTPS下载: 未测试(requests库未安装)") + else: + print("⚠️ HTTPS下载: 未测试(建议使用 --test-url 参数测试)") + + print() + print("使用建议:") + if can_use: + print("✅ 证书可以用于HTTPS单向认证(服务器认证)") + print(" 在 get_device_token_at.py 或 download_file_at.py 中设置:") + print(f" AUTH_MODE = 1 # 单向认证") + print(f" CA_CERT_PATH = \"{args.ca_cert}\"") + else: + print("⚠️ 证书可能无法用于HTTPS服务器认证") + print(" 建议:") + print(" 1. 使用 --server 参数测试服务器验证") + print(" 2. 使用 --test-url 参数测试实际HTTPS下载") + print(" 3. 如果实际使用中成功,说明证书可用(可能是证书链文件)") + + print("=" * 70) + + # 如果实际测试通过,即使类型检查不通过也返回成功 + if can_use: + sys.exit(0) + else: + sys.exit(1) + + +if __name__ == "__main__": + main() + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/download_emoji_mp4.py b/code/sdk/zhihui/leiniao/leiniao_python_test/download_emoji_mp4.py new file mode 100644 index 000000000..d8a012b93 --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/download_emoji_mp4.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +根据日志中的URL模式,下载所有表情mp4文件到本地 +""" + +import os +import requests +from pathlib import Path +from urllib.parse import urlparse + +# 基础URL +BASE_URL = "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions" + +# 从日志中提取的表情列表 +EMOTIONS = [ + "happy", + "laughing", + "angry", + "wronged", + "heartbeat", + "awkward", + "confused", + "shocked", + "crying", + "blowing-kiss", + "sleepy", + "mischievous", + "speechless", + "dizzy", + "thinking", + "sleeping", + "working", + "exercising", + "playing", + "daydreaming" +] + +# 表情名称到URL文件名的映射(从日志中看到的URL是 Laugh.mp4,而emotion是laughing) +EMOTION_TO_FILENAME = { + "happy": "Happy", + "laughing": "Laugh", + "angry": "Angry", + "wronged": "Wronged", + "heartbeat": "Heartbeat", + "awkward": "Awkward", + "confused": "Confused", + "shocked": "Shocked", + "crying": "Crying", + "blowing-kiss": "BlowingKiss", + "sleepy": "Sleepy", + "mischievous": "Mischievous", + "speechless": "Speechless", + "dizzy": "Dizzy", + "thinking": "Thinking", + "sleeping": "Sleeping", + "working": "Working", + "exercising": "Exercising", + "playing": "Playing", + "daydreaming": "Daydreaming" +} + +# 从日志中看到的配置 +EYES_SHAPE = "RoundEyes" # URL中的格式 +EYES_COLOR = "Blue" # URL中的格式 + +# 本地保存路径(对应日志中的路径格式) +LOCAL_BASE_PATH = "./emoji/round-eyes/blue" + +def download_file(url, save_path): + """下载文件""" + try: + # 创建目录 + os.makedirs(os.path.dirname(save_path), exist_ok=True) + + # 下载文件 + print(f"正在下载: {url}") + response = requests.get(url, stream=True, timeout=30) + response.raise_for_status() + + # 保存文件 + total_size = int(response.headers.get('content-length', 0)) + downloaded = 0 + + with open(save_path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) + downloaded += len(chunk) + if total_size > 0: + percent = (downloaded / total_size) * 100 + print(f"\r进度: {percent:.1f}% ({downloaded}/{total_size} bytes)", end='', flush=True) + + print(f"\n✓ 下载完成: {save_path} ({downloaded} bytes)") + return True + + except requests.exceptions.RequestException as e: + print(f"\n✗ 下载失败: {url}") + print(f" 错误: {e}") + return False + except Exception as e: + print(f"\n✗ 保存失败: {save_path}") + print(f" 错误: {e}") + return False + +def main(): + """主函数""" + print("=" * 60) + print("表情MP4文件下载工具") + print("=" * 60) + print(f"基础URL: {BASE_URL}") + print(f"眼型: {EYES_SHAPE}") + print(f"颜色: {EYES_COLOR}") + print(f"本地保存路径: {LOCAL_BASE_PATH}") + print(f"表情数量: {len(EMOTIONS)}") + print("=" * 60) + print() + + success_count = 0 + fail_count = 0 + + for emotion in EMOTIONS: + # 获取URL文件名 + filename = EMOTION_TO_FILENAME.get(emotion, emotion.capitalize()) + + # 构建URL + url = f"{BASE_URL}/{EYES_SHAPE}/{EYES_COLOR}/{filename}.mp4" + + # 构建本地保存路径 + local_path = os.path.join(LOCAL_BASE_PATH, f"{emotion}.mp4") + + # 检查文件是否已存在 + if os.path.exists(local_path): + file_size = os.path.getsize(local_path) + print(f"⏭ 跳过(已存在): {emotion}.mp4 ({file_size} bytes)") + success_count += 1 + continue + + # 下载文件 + if download_file(url, local_path): + success_count += 1 + else: + fail_count += 1 + + print() # 空行分隔 + + # 统计结果 + print("=" * 60) + print("下载完成统计") + print("=" * 60) + print(f"成功: {success_count}/{len(EMOTIONS)}") + print(f"失败: {fail_count}/{len(EMOTIONS)}") + print(f"本地路径: {os.path.abspath(LOCAL_BASE_PATH)}") + print("=" * 60) + +if __name__ == "__main__": + main() + + + + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/download_mp4_at.py b/code/sdk/zhihui/leiniao/leiniao_python_test/download_mp4_at.py new file mode 100644 index 000000000..c19025a8e --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/download_mp4_at.py @@ -0,0 +1,496 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +使用AT指令通过HTTPS下载MP4文件 +参考 get_device_token_at.py 实现 +""" + +import serial +import time +import re +import urllib.parse +import os +from get_device_token_at import ATCommandHandler, HTTPSATClient, HTTPATClient + +# -------------------------- 1. 配置参数 -------------------------- +# 目标文件URL +MP4_URL = "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/RoundEyes/Blue/Angry.mp4" +# MP4_URL = "http://snicker.com.cn:8089/LEINIAO/blue/angry.zip" + +# 保存路径 +OUTPUT_DIR = "downloads" +OUTPUT_FILENAME = "Angry.mp4" + +# AT模块串口配置(根据实际硬件配置修改) +SERIAL_PORT = "COM19" # 修改为实际的串口设备 +SERIAL_BAUDRATE = 2000000 +SERIAL_TIMEOUT = 10 + +# 认证模式配置 +AUTH_MODE = 1 # 0=无需认证, 1=单向认证, 2=双向认证 +# CA_CERT_PATH = None # 例如: "certs/ca.crt" +CA_CERT_PATH = "certs/ca_chain.pem" + +# -------------------------- 2. 下载MP4文件函数 -------------------------- +def download_mp4_file(url, output_path, auth_mode=0, ca_cert_path=None): + """ + 使用AT指令通过HTTP/HTTPS下载MP4文件 + + Args: + url: MP4文件的HTTP/HTTPS URL + output_path: 保存路径 + auth_mode: 认证模式,0=无需认证, 1=单向认证, 2=双向认证(仅HTTPS需要) + ca_cert_path: CA证书路径(HTTPS单向认证时需要) + """ + # 解析URL,判断协议类型 + parsed_url = urllib.parse.urlparse(url) + is_https = parsed_url.scheme.lower() == 'https' + is_http = parsed_url.scheme.lower() == 'http' + + if not (is_http or is_https): + raise ValueError(f"不支持的URL协议: {parsed_url.scheme},仅支持 http:// 或 https://") + + protocol_name = "HTTPS" if is_https else "HTTP" + print(f"检测到协议类型: {protocol_name}") + # 初始化AT指令处理器 + at_handler = ATCommandHandler(SERIAL_PORT, SERIAL_BAUDRATE, SERIAL_TIMEOUT) + + try: + # 连接串口 + if not at_handler.connect(): + print("❌ 无法连接串口,请检查串口配置") + return False + + # 测试AT连接 + success, response = at_handler.send_at_command("AT", timeout=3) + if not success: + print("❌ AT模块无响应,请检查连接") + return False + print("✅ AT模块连接正常") + + # 检查PPP拨号状态 + print("\n" + "=" * 50) + print("【步骤1: 检查PPP拨号状态】") + print("=" * 50) + is_ppp_connected, ppp_ip = at_handler.check_ppp_status() + + if is_ppp_connected: + print(f"✅ PPP已连接,IP地址: {ppp_ip}") + else: + print(f"❌ PPP未连接,IP地址: {ppp_ip or '未知'}") + print("\n尝试建立PPP连接...") + + apn_name = None + success, ip = at_handler.establish_ppp_connection(apn_name=apn_name) + + if not success: + print(f"❌ PPP连接建立失败,无法继续{protocol_name}访问") + return False + + ppp_ip = ip + + # 根据协议类型初始化客户端 + if is_https: + # 初始化HTTPS客户端 + http_client = HTTPSATClient(at_handler) + + # 处理证书(如果需要) + ca_cert_name = None + if auth_mode == 1 and ca_cert_path: + print("\n" + "=" * 50) + print("【步骤2: 处理证书】") + print("=" * 50) + + ca_cert_name = os.path.basename(ca_cert_path) + + # 检查证书是否已存在 + if http_client.certificate_exists(ca_cert_name): + print(f"✅ 证书 {ca_cert_name} 已存在,跳过添加") + else: + print(f"证书 {ca_cert_name} 不存在,开始添加...") + from get_device_token_at import load_certificate_file + + ca_cert_data = load_certificate_file(ca_cert_path) + if not ca_cert_data: + print("❌ 无法加载CA证书,终止流程") + return False + + if not http_client.add_certificate(ca_cert_name, ca_cert_data): + print("❌ CA证书添加失败,终止流程") + return False + + if not http_client.check_certificate(ca_cert_name): + print("❌ CA证书确认失败,终止流程") + return False + + # ========== 开始计时:HTTPS访问耗时 ========== + access_start_time = time.time() + + # 配置HTTPS参数 + print("\n" + "=" * 50) + print("【步骤3: 配置HTTPS参数】") + print("=" * 50) + if not http_client.configure_https( + ssl_version=3, # TLS1.2 + auth_mode=auth_mode, + ca_cert=ca_cert_name + ): + print("❌ HTTPS配置失败,终止流程") + return False + + # 确认HTTPS配置 + print("\n确认HTTPS配置...") + config = http_client.verify_https_config() + if config: + print("✅ HTTPS配置确认成功") + else: + print("⚠️ HTTPS配置确认失败,但继续执行") + + # 设置URL + print("\n" + "=" * 50) + print("【步骤4: 设置HTTPS请求参数】") + print("=" * 50) + print(f"设置请求URL: {url}") + http_client.set_url(url) + + # 建立HTTPS连接 + print("\n" + "=" * 50) + print("【步骤5: 建立HTTPS连接】") + print("=" * 50) + http_client.setup_connection() + print("✅ HTTPS连接建立成功") + + # 发送GET请求 + print("\n" + "=" * 50) + print("【步骤6: 发送HTTPS GET请求】") + print("=" * 50) + + # 使用标准GET模式 (mode=0) + success, response = at_handler.send_at_command("AT+HTTPSACTION=0", timeout=5) + if not success: + raise Exception(f"发送HTTPS请求失败: {response}") + + # 等待响应数据 + print("等待HTTPS响应...") + recv_indicator = b"+HTTPSRECV:" + else: + # 初始化HTTP客户端 + http_client = HTTPATClient(at_handler) + + # ========== 开始计时:HTTP访问耗时 ========== + access_start_time = time.time() + + # 设置URL + print("\n" + "=" * 50) + print("【步骤3: 设置HTTP请求参数】") + print("=" * 50) + print(f"设置请求URL: {url}") + http_client.set_url(url) + + # 建立HTTP连接 + print("\n" + "=" * 50) + print("【步骤4: 建立HTTP连接】") + print("=" * 50) + http_client.setup_connection() + print("✅ HTTP连接建立成功") + + # 发送GET请求 + print("\n" + "=" * 50) + print("【步骤5: 发送HTTP GET请求】") + print("=" * 50) + + # 使用标准GET模式 (mode=0) + success, response = at_handler.send_at_command("AT+HTTPACTION=0", timeout=5) + if not success: + raise Exception(f"发送HTTP请求失败: {response}") + + # 等待响应数据 + print("等待HTTP响应...") + recv_indicator = b"+HTTPRECV:" + + # 重要:直接读取二进制数据,不使用wait_for_unsolicited(它会解码为文本,可能丢失二进制数据) + # 手动等待+HTTPRECV:/+HTTPSRECV:提示符,然后立即开始读取二进制数据 + response_data = b"" + start_time = time.time() + last_data_time = time.time() + header_received = False + content_length = None + body_start_pos = None + recv_found = False + + # 等待+HTTPRECV:/+HTTPSRECV:提示符 + indicator_name = protocol_name + "RECV" + print(f"等待+{indicator_name}:提示符...") + while time.time() - start_time < 60: + if at_handler.serial_conn.in_waiting > 0: + # 读取数据(二进制模式) + chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting) + if chunk: + response_data += chunk + last_data_time = time.time() + + # 检查是否包含+HTTPRECV:/+HTTPSRECV:(可能在字节串中) + if recv_indicator in response_data: + recv_found = True + print(f"✅ 收到+{indicator_name}:提示符") + # 继续读取,查找HTTP响应头 + break + time.sleep(0.1) + + if not recv_found: + # 尝试查找HTTP响应头(可能+HTTPRECV:/+HTTPSRECV:已经被读取) + if b"HTTP/" in response_data: + recv_found = True + print("✅ 检测到HTTP响应数据") + + if not recv_found: + raise Exception(f"未收到+{indicator_name}:提示符或HTTP响应") + + # 查找HTTP响应头开始位置(可能在+HTTPSRECV:之后) + http_start = response_data.find(b"HTTP/") + if http_start == -1: + # 如果没有找到HTTP/,继续读取直到找到 + print("继续读取数据以查找HTTP响应头...") + while time.time() - start_time < 10: + if at_handler.serial_conn.in_waiting > 0: + chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting) + if chunk: + response_data += chunk + http_start = response_data.find(b"HTTP/") + if http_start != -1: + break + time.sleep(0.1) + + if http_start == -1: + raise Exception("未找到HTTP响应头") + + # 从HTTP响应头开始位置处理数据 + # 跳过+HTTPSRECV:前缀(如果存在) + if http_start > 0: + print(f"跳过+HTTPSRECV:前缀 ({http_start} 字节)") + response_data = response_data[http_start:] + + # 查找HTTP响应头结束位置(\r\n\r\n) + print("读取响应头...") + header_timeout = 10 + header_start = time.time() + + while time.time() - header_start < header_timeout: + if at_handler.serial_conn.in_waiting > 0: + chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting) + if chunk: + response_data += chunk + + # 查找HTTP响应头结束位置(\r\n\r\n) + header_end = response_data.find(b"\r\n\r\n") + if header_end != -1: + header_received = True + body_start_pos = header_end + 4 + + # 解析响应头 + header_text = response_data[:header_end].decode('utf-8', errors='ignore') + print("\nHTTP响应头:") + print(header_text) + + # 提取Content-Length + content_length_match = re.search(r'Content-Length:\s*(\d+)', header_text, re.IGNORECASE) + if content_length_match: + content_length = int(content_length_match.group(1)) + print(f"\n文件大小: {content_length} 字节 ({content_length/1024/1024:.2f} MB)") + else: + print("⚠️ 未找到Content-Length,将接收所有数据直到连接关闭") + + # 检查状态码 + status_match = re.search(r'HTTP/\d\.\d\s+(\d+)', header_text) + if status_match: + status_code = int(status_match.group(1)) + if status_code != 200: + raise Exception(f"HTTP请求失败,状态码: {status_code}") + print(f"HTTP状态码: {status_code} ✅") + + # 检查文件数据开始位置的前几个字节(用于调试) + if len(response_data) > body_start_pos: + preview = response_data[body_start_pos:body_start_pos+20] + print(f"\n文件数据前20字节(hex): {preview.hex()}") + print(f"文件数据前20字节(repr): {repr(preview)}") + + break + time.sleep(0.1) + + if not header_received: + raise Exception("未收到完整的HTTP响应头") + + # 继续接收文件数据 + print("\n开始接收文件数据...") + last_progress_time = time.time() # 上次打印进度的时间 + progress_interval = 5 # 每5秒打印一次进度 + + while time.time() - start_time < 600: # 最多等待10分钟 + if at_handler.serial_conn.in_waiting > 0: + chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting) + if chunk: + response_data += chunk + last_data_time = time.time() + + # 如果知道文件大小,显示进度 + if content_length: + body_data = response_data[body_start_pos:] + received = len(body_data) + if received >= content_length: + print(f"\n✅ 文件接收完成: {received} / {content_length} 字节") + break + else: + # 每5秒打印一次进度 + current_time = time.time() + if current_time - last_progress_time >= progress_interval: + progress = (received / content_length) * 100 + print(f"接收进度: {progress:.1f}% ({received}/{content_length} 字节, {received/1024/1024:.2f} MB)") + last_progress_time = current_time + else: + # 不知道文件大小,每5秒显示已接收的数据量 + current_time = time.time() + if current_time - last_progress_time >= progress_interval: + body_data = response_data[body_start_pos:] + received = len(body_data) + print(f"已接收: {received} 字节 ({received/1024/1024:.2f} MB)") + last_progress_time = current_time + else: + # 如果超过10秒没有新数据,检查是否接收完成 + if time.time() - last_data_time > 10: + if content_length: + body_data = response_data[body_start_pos:] + received = len(body_data) + if received >= content_length: + print(f"\n✅ 文件接收完成: {received} / {content_length} 字节") + break + else: + print(f"\n⚠️ 接收超时,已接收: {received} / {content_length} 字节") + break + else: + # 没有Content-Length,如果10秒没有新数据,认为接收完成 + print("\n响应数据接收完成(超时)") + break + time.sleep(0.1) + + # 提取文件数据(跳过HTTP响应头) + file_data = response_data[body_start_pos:] + + # 验证文件数据(MP4文件格式) + # MP4文件结构:Box size (4字节) + Box type (4字节,通常是"ftyp") + # 所以文件应该以 0x00 0x00 0x00 0x1C 开头(或类似的大小值),然后是 "ftyp" + print(f"\n文件数据前16字节(hex): {file_data[:16].hex()}") + print(f"文件数据前16字节(repr): {repr(file_data[:16])}") + + # MP4文件应该以Box size开头(4字节大端整数),然后是"ftyp" + # 检查前4字节是否为有效的Box size(通常不会太大,比如小于100MB) + if len(file_data) >= 8: + box_size_bytes = file_data[:4] + box_type = file_data[4:8] + + # 计算Box size(大端序) + box_size = int.from_bytes(box_size_bytes, byteorder='big') + + print(f"检测到的Box size: {box_size} (hex: {box_size_bytes.hex()})") + print(f"检测到的Box type: {box_type} (hex: {box_type.hex()})") + + # 验证是否为有效的MP4文件 + if box_type == b"ftyp": + print("✅ 检测到有效的MP4文件格式(ftyp box)") + elif box_size > 0 and box_size < 100 * 1024 * 1024: # Box size在合理范围内 + print(f"⚠️ Box size看起来合理,但Box type不是'ftyp',可能是其他格式") + else: + # 如果前4字节不是有效的Box size,尝试查找ftyp + ftyp_pos = file_data.find(b"ftyp") + if ftyp_pos != -1 and ftyp_pos < 100: + print(f"⚠️ 检测到文件数据偏移,ftyp在位置 {ftyp_pos}") + print(f" 丢失了前 {ftyp_pos} 字节数据") + print(f" 建议检查数据接收过程,确保没有丢失数据") + # 不自动调整,保留原始数据 + # file_data = file_data[ftyp_pos:] + + # 保存文件 + print("\n" + "=" * 50) + print("【步骤7: 保存文件】") + print("=" * 50) + + # 创建输出目录 + os.makedirs(os.path.dirname(output_path), exist_ok=True) + + # 写入文件 + with open(output_path, 'wb') as f: + f.write(file_data) + + file_size = len(file_data) + print(f"✅ 文件保存成功: {output_path}") + print(f" 文件大小: {file_size} 字节 ({file_size/1024/1024:.2f} MB)") + + # 关闭连接 + print("\n" + "=" * 50) + print(f"【步骤8: 关闭{protocol_name}连接】") + print("=" * 50) + try: + http_client.close_connection() + print(f"✅ {protocol_name}连接已关闭") + except Exception as e: + print(f"警告: 关闭{protocol_name}连接时出错: {str(e)}") + + # ========== 结束计时:HTTP/HTTPS访问耗时 ========== + access_end_time = time.time() + access_duration = access_end_time - access_start_time + + print("\n" + "=" * 50) + print(f"【{protocol_name}访问耗时统计】") + print("=" * 50) + print(f"{protocol_name}访问总耗时: {access_duration:.3f} 秒 ({access_duration*1000:.1f} 毫秒)") + print(f"下载速度: {file_size/1024/1024/access_duration:.2f} MB/s") + print("\n耗时包含以下步骤:") + if is_https: + print(" - HTTPS配置和验证") + print(" - URL和端口设置") + print(f" - {protocol_name}连接建立") + print(" - HTTP请求发送") + print(" - 文件数据接收") + print(" - 连接关闭") + print("=" * 50) + + return True + + except Exception as e: + print("\n" + "=" * 50) + print("【错误处理】") + print("=" * 50) + print(f"❌ 下载异常: {str(e)}") + import traceback + traceback.print_exc() + return False + finally: + # 断开串口连接 + print("\n断开串口连接...") + at_handler.disconnect() + +# -------------------------- 3. 主程序 -------------------------- +if __name__ == "__main__": + print("=" * 50) + print("HTTPS MP4文件下载工具(AT指令版本)") + print("=" * 50) + print(f"目标URL: {MP4_URL}") + + # 构建输出路径 + output_path = os.path.join(OUTPUT_DIR, OUTPUT_FILENAME) + print(f"保存路径: {output_path}") + print("=" * 50) + + # 执行下载 + success = download_mp4_file( + url=MP4_URL, + output_path=output_path, + auth_mode=AUTH_MODE, + ca_cert_path=CA_CERT_PATH + ) + + if success: + print("\n✅ MP4文件下载完成!") + else: + print("\n❌ MP4文件下载失败!") + exit(1) + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/get_device_token_at.py b/code/sdk/zhihui/leiniao/leiniao_python_test/get_device_token_at.py new file mode 100644 index 000000000..667db3f92 --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/get_device_token_at.py @@ -0,0 +1,1320 @@ +import serial +import time +import hashlib +import hmac +import random +import string +import json +import re +import urllib.parse +import os + +# -------------------------- 1. 接口基础配置 -------------------------- +BASE_URL = "https://ambybox-api.test.leiniao.com/ambybox-device-api" +INTERFACE_PATH = "/v1/auth/getDeviceToken" +FULL_URL = BASE_URL + INTERFACE_PATH +SIGN_SECRET = "vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus=" + +# AT模块串口配置(根据实际硬件配置修改) +SERIAL_PORT = "COM19" # 修改为实际的串口设备 +SERIAL_BAUDRATE = 1000000 +SERIAL_TIMEOUT = 10 + +# -------------------------- 2. AT指令通信类 -------------------------- +class ATCommandHandler: + """AT指令处理器,用于与N706A模组通信""" + + def __init__(self, port=SERIAL_PORT, baudrate=SERIAL_BAUDRATE, timeout=SERIAL_TIMEOUT): + self.port = port + self.baudrate = baudrate + self.timeout = timeout + self.serial_conn = None + + def connect(self): + """打开串口连接""" + try: + self.serial_conn = serial.Serial( + port=self.port, + baudrate=self.baudrate, + timeout=self.timeout, + bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE + ) + print(f"串口连接成功: {self.port} @ {self.baudrate}") + return True + except Exception as e: + print(f"串口连接失败: {e}") + return False + + def disconnect(self): + """关闭串口连接""" + if self.serial_conn and self.serial_conn.is_open: + self.serial_conn.close() + print("串口连接已关闭") + + def send_at_command(self, command, expected_response="OK", timeout=5): + """ + 发送AT指令并等待响应 + + Args: + command: AT指令字符串(不需要包含\r\n) + expected_response: 期望的响应(可以是字符串或正则表达式) + timeout: 超时时间(秒) + + Returns: + tuple: (success: bool, response: str) + """ + if not self.serial_conn or not self.serial_conn.is_open: + return False, "串口未连接" + + # 清空输入缓冲区 + self.serial_conn.reset_input_buffer() + + # 发送AT指令(添加\r\n) + cmd_bytes = (command + "\r\n").encode('utf-8') + self.serial_conn.write(cmd_bytes) + print(f"发送: {command}") + + # 等待响应 + start_time = time.time() + response_lines = [] + + while time.time() - start_time < timeout: + if self.serial_conn.in_waiting > 0: + line = self.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if line: + response_lines.append(line) + print(f"接收: {line}") + + # 检查是否收到期望的响应 + if isinstance(expected_response, str): + if expected_response in line: + return True, "\n".join(response_lines) + else: # 正则表达式 + if expected_response.search(line): + return True, "\n".join(response_lines) + + # 检查错误响应 + if "ERROR" in line: + return False, "\n".join(response_lines) + + time.sleep(0.1) + + # 超时 + return False, f"超时: {timeout}秒内未收到期望响应\n" + "\n".join(response_lines) + + def wait_for_unsolicited(self, pattern, timeout=30): + """ + 等待主动上报消息(如+HTTPSRECV) + + Args: + pattern: 匹配模式(字符串或正则表达式) + timeout: 超时时间(秒) + + Returns: + tuple: (success: bool, response: str) + """ + if not self.serial_conn or not self.serial_conn.is_open: + return False, "串口未连接" + + start_time = time.time() + response_lines = [] + pattern_compiled = re.compile(pattern) if isinstance(pattern, str) else pattern + + while time.time() - start_time < timeout: + if self.serial_conn.in_waiting > 0: + line = self.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if line: + response_lines.append(line) + print(f"接收: {line}") + + if pattern_compiled.search(line): + return True, "\n".join(response_lines) + + time.sleep(0.1) + + return False, f"超时: {timeout}秒内未收到主动上报\n" + "\n".join(response_lines) + + def check_ppp_status(self): + """ + 检查PPP拨号状态 + + Returns: + tuple: (is_connected: bool, ip_address: str) + """ + success, response = self.send_at_command("AT+XIIC?", timeout=5) + if not success: + return False, None + + # 解析响应格式: +XIIC: , + # 注意:手册说明state前面可能有4个空格 + xiic_pattern = re.compile(r'\+XIIC:\s*(\d+),([0-9.]+)') + match = xiic_pattern.search(response) + + if match: + state = int(match.group(1)) + ip = match.group(2) + + # state=1表示已连接,且IP不是0.0.0.0 + if state == 1 and ip != "0.0.0.0": + return True, ip + else: + return False, ip + else: + print(f"警告: 无法解析PPP状态响应: {response}") + return False, None + + def check_network_registration(self): + """ + 检查网络注册状态 + + Returns: + bool: True表示已注册,False表示未注册 + """ + success, response = self.send_at_command("AT+CREG?", timeout=5) + if not success: + return False + + # 解析响应格式: +CREG: , + # stat=1或5表示已注册 + creg_pattern = re.compile(r'\+CREG:\s*\d+,([15])') + match = creg_pattern.search(response) + + if match: + return True + else: + print(f"警告: 网络未注册,响应: {response}") + return False + + def set_apn(self, apn_name, cid=1, pdp_type="IP"): + """ + 设置APN参数 + + Args: + apn_name: APN名称(如"CMNET") + cid: 上下文标识符,默认1 + pdp_type: PDP类型,默认"IP" + + Returns: + bool: 是否设置成功 + """ + command = f'AT+CGDCONT={cid},"{pdp_type}","{apn_name}"' + success, response = self.send_at_command(command, timeout=5) + return success + + def establish_ppp_connection(self, apn_name=None, max_retries=3): + """ + 建立PPP连接 + + Args: + apn_name: APN名称(可选,如果为None则不设置APN) + max_retries: 最大重试次数 + + Returns: + tuple: (success: bool, ip_address: str) + """ + # 检查网络注册状态 + print("检查网络注册状态...") + if not self.check_network_registration(): + print("❌ 网络未注册,无法建立PPP连接") + return False, None + + print("✅ 网络已注册") + + # 设置APN(如果提供) + if apn_name: + print(f"设置APN: {apn_name}") + if not self.set_apn(apn_name): + print(f"警告: APN设置失败,继续尝试拨号") + + # 尝试建立PPP连接 + for attempt in range(1, max_retries + 1): + print(f"\n尝试建立PPP连接 (第{attempt}次)...") + success, response = self.send_at_command("AT+XIIC=1", timeout=30) + + if not success: + print(f"❌ PPP连接失败: {response}") + if attempt < max_retries: + print(f"等待3秒后重试...") + time.sleep(3) + continue + + # 等待一下,然后查询状态确认 + time.sleep(2) + is_connected, ip = self.check_ppp_status() + + if is_connected: + print(f"✅ PPP连接成功,IP地址: {ip}") + return True, ip + else: + print(f"❌ PPP连接状态检查失败,IP: {ip}") + if attempt < max_retries: + print(f"等待3秒后重试...") + time.sleep(3) + + return False, None + +# -------------------------- 3. HTTPS AT指令封装 -------------------------- +class HTTPSATClient: + """基于AT指令的HTTPS客户端""" + + def __init__(self, at_handler): + self.at = at_handler + self.response_data = None + + def add_certificate(self, cert_name, cert_data): + """ + 添加SSL证书到模块(根据SSL/TLS应用指南) + + 注意:AT+CERTADD命令的响应格式特殊: + 1. 发送 AT+CERTADD=, + 2. 立即返回 CONNECT(不是OK) + 3. 发送证书数据 + 4. 返回 +CERTADD: ,OK 或 +CERTADD: ERROR + + Args: + cert_name: 证书文件名(如 "ca.crt") + cert_data: 证书内容(字节数据或字符串) + + Returns: + bool: 是否添加成功 + """ + # 如果是字符串,转换为字节 + if isinstance(cert_data, str): + cert_data = cert_data.encode('utf-8') + + cert_length = len(cert_data) + + # 清空输入缓冲区 + self.at.serial_conn.reset_input_buffer() + + # 直接发送AT+CERTADD命令(不使用send_at_command,因为响应是CONNECT而不是OK) + command = f'AT+CERTADD={cert_name},{cert_length}' + cmd_bytes = (command + "\r\n").encode('utf-8') + self.at.serial_conn.write(cmd_bytes) + print(f"发送: {command}") + + # 等待CONNECT提示符(AT+CERTADD会立即返回CONNECT,而不是OK) + print("等待'CONNECT'提示符...") + start_time = time.time() + connect_received = False + response_lines = [] + + while time.time() - start_time < 5: + if self.at.serial_conn.in_waiting > 0: + line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if line: + response_lines.append(line) + print(f"接收: {line}") + + # 检查是否收到CONNECT + if "CONNECT" in line.upper(): + connect_received = True + break + + # 检查错误响应 + if "ERROR" in line: + print(f"❌ 证书添加命令失败: {line}") + return False + time.sleep(0.1) + + if not connect_received: + newline = '\n' + print(f"❌ 错误: 未收到'CONNECT'提示符,响应: {newline.join(response_lines)}") + return False + + # 发送证书数据 + print(f"发送证书数据 ({cert_length} 字节)...") + self.at.serial_conn.write(cert_data) + # 确保数据发送完成 + self.at.serial_conn.flush() + + # 等待证书添加完成响应(+CERTADD: ,OK 或 +CERTADD: ERROR) + print("等待证书添加完成响应...") + start_time = time.time() + certadd_received = False + response_lines = [] + + while time.time() - start_time < 30: # 证书写入可能需要较长时间 + if self.at.serial_conn.in_waiting > 0: + line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if line: + response_lines.append(line) + print(f"接收: {line}") + + # 检查是否收到+CERTADD响应 + if "+CERTADD:" in line: + certadd_received = True + # 检查是否成功 + if f"{cert_length},OK" in line or ",OK" in line: + print(f"✅ 证书 {cert_name} 添加成功") + return True + elif "ERROR" in line: + print(f"❌ 证书添加失败: {line}") + return False + time.sleep(0.1) + + if not certadd_received: + newline = '\n' + print(f"❌ 错误: 证书添加超时,响应: {newline.join(response_lines)}") + return False + + return False + + def check_certificate(self, cert_name): + """ + 确认证书是否成功写入(根据SSL/TLS应用指南) + + Args: + cert_name: 证书文件名 + + Returns: + bool: 证书是否存在 + """ + success, response = self.at.send_at_command( + f'AT+CERTCHECK={cert_name}', + timeout=5 + ) + if not success: + return False + + # 检查响应格式: +CERTCHECK: ,OK + if f"{cert_name},OK" in response: + print(f"✅ 证书 {cert_name} 确认成功") + return True + else: + print(f"❌ 证书 {cert_name} 不存在或验证失败: {response}") + return False + + def list_certificates(self): + """ + 查询设备中所有已添加的证书列表 + + Returns: + list: 证书文件名列表,如果查询失败返回None + """ + success, response = self.at.send_at_command("AT+CERTCHECK?", timeout=5) + if not success: + print(f"警告: 证书列表查询失败: {response}") + return None + + # 解析响应格式: 每行一个证书文件名,最后是OK + # 例如: + # cacert.pem + # keycert.pem + # OK + cert_list = [] + for line in response.split('\n'): + line = line.strip() + if line and line not in ['OK', 'AT+CERTCHECK?']: + # 跳过响应前缀(如果有) + if line.startswith('+CERTCHECK:'): + continue + cert_list.append(line) + + return cert_list + + def certificate_exists(self, cert_name): + """ + 检查证书是否已存在于设备中(避免重复添加) + + Args: + cert_name: 证书文件名 + + Returns: + bool: 证书是否存在 + """ + success, response = self.at.send_at_command( + f'AT+CERTCHECK={cert_name}', + timeout=5 + ) + if not success: + return False + + # 检查响应格式: +CERTCHECK: ,OK 或 +CERTCHECK: ERROR + if f"{cert_name},OK" in response: + return True + else: + return False + + def configure_https(self, ssl_version=3, auth_mode=0, ca_cert=None, client_cert=None, client_key=None): + """ + 配置HTTPS SSL参数 + + Args: + ssl_version: SSL版本,0=SSL3.0, 1=TLS1.0, 2=TLS1.1, 3=TLS1.2 + auth_mode: 认证模式,0=不认证, 1=单向认证, 2=双向认证 + ca_cert: CA证书文件名(单向/双向认证需要) + client_cert: 客户端证书文件名(双向认证需要) + client_key: 客户端密钥文件名(双向认证需要) + """ + # 设置SSL版本 + success, response = self.at.send_at_command( + f'AT+HTTPSCFG="sslversion",{ssl_version}', + timeout=5 + ) + if not success: + print(f"警告: SSL版本配置失败: {response}") + return False + + # 设置认证模式 + success, response = self.at.send_at_command( + f'AT+HTTPSCFG="authmode",{auth_mode}', + timeout=5 + ) + if not success: + print(f"警告: 认证模式配置失败: {response}") + return False + + # 单向认证:配置CA证书 + if auth_mode == 1: + if not ca_cert: + print("错误: 单向认证需要CA证书") + return False + + success, response = self.at.send_at_command( + f'AT+HTTPSCFG="cacert",{ca_cert}', + timeout=5 + ) + if not success: + print(f"错误: CA证书配置失败: {response}") + return False + print(f"✅ 已配置CA证书: {ca_cert}") + + # 双向认证:配置CA证书、客户端证书和密钥 + elif auth_mode == 2: + if not ca_cert or not client_cert or not client_key: + print("错误: 双向认证需要CA证书、客户端证书和密钥") + return False + + # 配置CA证书 + success, response = self.at.send_at_command( + f'AT+HTTPSCFG="cacert",{ca_cert}', + timeout=5 + ) + if not success: + print(f"错误: CA证书配置失败: {response}") + return False + + # 配置客户端证书 + success, response = self.at.send_at_command( + f'AT+HTTPSCFG="clientcert",{client_cert}', + timeout=5 + ) + if not success: + print(f"错误: 客户端证书配置失败: {response}") + return False + + # 配置客户端密钥 + success, response = self.at.send_at_command( + f'AT+HTTPSCFG="clientkey",{client_key}', + timeout=5 + ) + if not success: + print(f"错误: 客户端密钥配置失败: {response}") + return False + + print(f"✅ 已配置双向认证证书: CA={ca_cert}, ClientCert={client_cert}, ClientKey={client_key}") + + return True + + def verify_https_config(self): + """ + 确认HTTPS配置(根据SSL/TLS应用指南要求) + + Returns: + dict: 包含配置信息的字典,如果查询失败返回None + """ + success, response = self.at.send_at_command("AT+HTTPSCFG?", timeout=5) + if not success: + print(f"警告: HTTPS配置查询失败: {response}") + return None + + # 解析响应格式: +HTTPSCFG: ,,,, + # 例如: +HTTPSCFG: 3,0,,, + httpcfg_pattern = re.compile(r'\+HTTPSCFG:\s*(\d+),(\d+),([^,]*),([^,]*),([^,\r\n]*)') + match = httpcfg_pattern.search(response) + + if match: + config = { + 'sslversion': int(match.group(1)), + 'authmode': int(match.group(2)), + 'cacert': match.group(3).strip() if match.group(3).strip() else None, + 'clientcert': match.group(4).strip() if match.group(4).strip() else None, + 'clientkey': match.group(5).strip() if match.group(5).strip() else None + } + + # 打印配置信息 + ssl_version_names = {0: "SSL3.0", 1: "TLS1.0", 2: "TLS1.1", 3: "TLS1.2"} + auth_mode_names = {0: "无需认证", 1: "单向认证", 2: "双向认证"} + + print(f" SSL版本: {config['sslversion']} ({ssl_version_names.get(config['sslversion'], '未知')})") + print(f" 认证模式: {config['authmode']} ({auth_mode_names.get(config['authmode'], '未知')})") + if config['cacert']: + print(f" CA证书: {config['cacert']}") + if config['clientcert']: + print(f" 客户端证书: {config['clientcert']}") + if config['clientkey']: + print(f" 客户端密钥: {config['clientkey']}") + + return config + else: + print(f"警告: 无法解析HTTPS配置响应: {response}") + return None + + def set_url(self, url, keepalive=0): + """ + 设置HTTPS请求URL + + Args: + url: 完整的URL(如 https://example.com/path) + keepalive: 是否启用长连接,0=关闭(默认),1=启用 + 根据SSL/TLS应用指南,启用后可以多次请求而无需重新建立连接 + """ + # 解析URL + parsed = urllib.parse.urlparse(url) + host = parsed.netloc + path = parsed.path + if parsed.query: + path += "?" + parsed.query + + # 设置URL(不包含协议和端口) + url_param = f"{host}{path}" + success, response = self.at.send_at_command( + f'AT+HTTPSPARA=url,"{url_param}"', + timeout=5 + ) + if not success: + raise Exception(f"设置URL失败: {response}") + + # 设置端口(HTTPS默认443) + port = parsed.port if parsed.port else 443 + success, response = self.at.send_at_command( + f'AT+HTTPSPARA=port,{port}', + timeout=5 + ) + if not success: + raise Exception(f"设置端口失败: {response}") + + # 设置keepalive(根据SSL/TLS应用指南,可选配置) + if keepalive in [0, 1]: + success, response = self.at.send_at_command( + f'AT+HTTPSPARA=keepalive,{keepalive}', + timeout=5 + ) + if not success: + print(f"警告: keepalive配置失败: {response}") + else: + if keepalive == 1: + print(" ✅ 已启用长连接(keepalive=1)") + else: + print(" ✅ 已关闭长连接(keepalive=0,默认)") + + return True + + def setup_connection(self): + """建立HTTPS连接""" + success, response = self.at.send_at_command("AT+HTTPSSETUP", timeout=15) + if not success or "ERROR" in response: + raise Exception(f"HTTPS连接建立失败: {response}") + return True + + def send_get_request(self, headers=None): + """ + 发送HTTPS GET请求 + + Args: + headers: 自定义HTTP头字典 + + Returns: + str: 响应数据 + """ + if headers: + # 使用OPEN_MODE (mode=99) 自定义HTTP报文 + # 根据手册要求:用户自定义报文时需遵循HTTP协议 + # 构建自定义HTTP请求 + parsed = urllib.parse.urlparse(FULL_URL) + path = parsed.path + if parsed.query: + path += "?" + parsed.query + + # 构建HTTP请求报文(严格按照HTTP协议格式) + # 请求行:GET /path HTTP/1.1 + http_request = f"GET {path} HTTP/1.1\r\n" + # Host头:Host: hostname + http_request += f"Host: {parsed.netloc}\r\n" + + # 添加自定义头(每行格式:Header-Name: value) + for key, value in headers.items(): + http_request += f"{key}: {value}\r\n" + + # Connection头 + http_request += "Connection: close\r\n" + # 请求头和请求体之间的空行(即使没有请求体也需要) + http_request += "\r\n" + + # 计算请求长度(字节数,用于AT+HTTPSACTION=99,) + request_length = len(http_request.encode('utf-8')) + + # 步骤1:直接发送AT+HTTPSACTION=99,命令 + # 注意:根据手册,AT+HTTPSACTION=99会先返回">"提示符,而不是OK + # 所以不能使用send_at_command(它等待OK),需要直接发送并等待">" + self.at.serial_conn.reset_input_buffer() + command = f"AT+HTTPSACTION=99,{request_length}" + cmd_bytes = (command + "\r\n").encode('utf-8') + self.at.serial_conn.write(cmd_bytes) + print(f"发送: {command}") + + # 步骤2:等待">"提示符(根据手册,发送AT+HTTPSACTION=99后会收到">"提示符) + print("等待'>'提示符...") + start_time = time.time() + prompt_received = False + response_lines = [] + + while time.time() - start_time < 5: + if self.at.serial_conn.in_waiting > 0: + line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if line: + response_lines.append(line) + print(f"接收: {line}") + + # 检查是否收到">"提示符 + if ">" in line: + prompt_received = True + break + + # 检查错误响应 + if "ERROR" in line: + newline = '\n' + raise Exception(f"HTTPS请求命令失败: {newline.join(response_lines)}") + time.sleep(0.1) + + if not prompt_received: + newline = '\n' + raise Exception(f"未收到'>'提示符,响应: {newline.join(response_lines)}") + + # 步骤3:发送完整的HTTP请求报文(根据手册,需要发送完整的HTTP协议报文) + # 注意:手册要求遵循HTTP协议,使用作为行结束符 + print(f"发送HTTP请求 ({request_length} 字节)...") + self.at.serial_conn.write(http_request.encode('utf-8')) + # 确保数据发送完成 + self.at.serial_conn.flush() + print(f"HTTP请求内容:\n{http_request}") + + # 步骤4:等待OK响应(发送数据后应该会收到OK) + print("等待OK响应...") + start_time = time.time() + ok_received = False + + while time.time() - start_time < 5: + if self.at.serial_conn.in_waiting > 0: + line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if line: + print(f"接收: {line}") + if "OK" in line: + ok_received = True + break + if "ERROR" in line: + raise Exception(f"HTTPS请求发送失败: {line}") + time.sleep(0.1) + + if not ok_received: + print("⚠️ 未收到OK响应,但继续等待HTTPS响应...") + else: + # 使用标准GET模式 (mode=0) + success, response = self.at.send_at_command("AT+HTTPSACTION=0", timeout=5) + if not success: + raise Exception(f"发送HTTPS请求失败: {response}") + + # 等待响应数据 + print("等待HTTPS响应...") + success, response = self.at.wait_for_unsolicited(r"\+HTTPSRECV:", timeout=30) + if not success: + raise Exception(f"等待HTTPS响应超时: {response}") + + # 继续读取响应数据 + response_data = response + start_time = time.time() + last_data_time = time.time() + + while time.time() - start_time < 30: # 最多等待30秒接收完整响应 + if self.at.serial_conn.in_waiting > 0: + line = self.at.serial_conn.readline().decode('utf-8', errors='ignore') + if line.strip(): + response_data += line + print(f"接收: {line.strip()[:100]}...") # 只打印前100个字符 + last_data_time = time.time() + + # 检查是否收到连接关闭消息 + if "+HTTPSCLOSED" in line or "+HTTPSCLOSE" in line: + print("收到连接关闭消息") + break + else: + # 如果超过2秒没有新数据,认为响应接收完成 + if time.time() - last_data_time > 2: + print("响应数据接收完成(超时)") + break + time.sleep(0.1) + + return response_data + + def close_connection(self): + """关闭HTTPS连接""" + success, response = self.at.send_at_command("AT+HTTPSCLOSE", timeout=5) + return success + +# -------------------------- 4. HTTP AT指令封装 -------------------------- +class HTTPATClient: + """基于AT指令的HTTP客户端""" + + def __init__(self, at_handler): + self.at = at_handler + self.response_data = None + + def set_url(self, url, keepalive=0): + """ + 设置HTTP请求URL + + Args: + url: 完整的URL(如 http://example.com/path) + keepalive: 是否启用长连接,0=关闭(默认),1=启用 + """ + # 解析URL + parsed = urllib.parse.urlparse(url) + host = parsed.netloc + path = parsed.path + if parsed.query: + path += "?" + parsed.query + + # 设置URL(不包含协议和端口) + url_param = f"{host}{path}" + success, response = self.at.send_at_command( + f'AT+HTTPPARA=url,"{url_param}"', + timeout=5 + ) + if not success: + raise Exception(f"设置URL失败: {response}") + + # 设置端口(如果URL中指定了端口) + if parsed.port: + port = parsed.port + else: + port = 80 # HTTP默认端口 + + success, response = self.at.send_at_command( + f'AT+HTTPPARA=port,{port}', + timeout=5 + ) + if not success: + raise Exception(f"设置端口失败: {response}") + + # 设置keepalive(可选) + if keepalive: + success, response = self.at.send_at_command( + f'AT+HTTPPARA=keepalive,{keepalive}', + timeout=5 + ) + if not success: + print(f"警告: 设置keepalive失败: {response}") + + def setup_connection(self): + """ + 建立HTTP连接 + + Returns: + bool: 是否建立成功 + """ + success, response = self.at.send_at_command("AT+HTTPSETUP", timeout=15) + if not success: + raise Exception(f"建立HTTP连接失败: {response}") + return True + + def send_get_request(self, timeout=60): + """ + 发送HTTP GET请求(使用标准GET模式) + + Args: + timeout: 超时时间(秒) + + Returns: + tuple: (success: bool, response: str) + """ + # 使用标准GET模式 (mode=0) + success, response = self.at.send_at_command("AT+HTTPACTION=0", timeout=5) + if not success: + raise Exception(f"发送HTTP请求失败: {response}") + + # 等待响应(+HTTPRECV:) + success, response = self.at.wait_for_unsolicited(r"\+HTTPRECV:", timeout=timeout) + return success, response + + def close_connection(self): + """ + 关闭HTTP连接 + + Returns: + bool: 是否关闭成功 + """ + success, response = self.at.send_at_command("AT+HTTPCLOSE", timeout=5) + return success + + def parse_http_response(self, raw_response): + """ + 解析HTTP响应 + + Returns: + dict: 包含status_code, headers, body的字典 + """ + # 查找HTTP响应开始位置(可能在+HTTPSRECV:之后) + http_start = raw_response.find("HTTP/") + if http_start == -1: + # 如果没有找到HTTP/,尝试查找状态码 + status_match = re.search(r'(\d{3})\s+', raw_response) + if status_match: + http_start = status_match.start() + else: + raise Exception("未找到HTTP响应") + + response_text = raw_response[http_start:] + + # 分离头部和正文 + header_end = response_text.find("\r\n\r\n") + if header_end == -1: + header_end = response_text.find("\n\n") + + if header_end == -1: + # 如果没有找到空行分隔符,尝试查找第一个{作为JSON开始 + json_start = response_text.find("{") + if json_start != -1: + headers_text = response_text[:json_start] + body_text = response_text[json_start:] + else: + raise Exception("无法解析HTTP响应格式") + else: + headers_text = response_text[:header_end] + body_text = response_text[header_end:].strip() + + # 解析状态行 + lines = headers_text.split("\n") + status_line = lines[0].strip() + status_match = re.search(r'HTTP/\d\.\d\s+(\d+)', status_line) + status_code = int(status_match.group(1)) if status_match else 0 + + # 解析头部 + headers = {} + for line in lines[1:]: + line = line.strip() + if ":" in line: + key, value = line.split(":", 1) + headers[key.strip()] = value.strip() + + # 提取JSON正文(去除+HTTPSRECV:前缀等) + # 查找JSON开始位置 + json_start = body_text.find("{") + json_end = body_text.rfind("}") + 1 + + if json_start != -1 and json_end > json_start: + body_json = body_text[json_start:json_end] + else: + # 如果没有找到JSON,返回原始正文 + body_json = body_text.strip() + # 清理可能的AT指令响应前缀 + for prefix in ["+HTTPSRECV:", "+HTTPSCLOSED:", "+HTTPSCLOSE:"]: + if body_json.startswith(prefix): + body_json = body_json[len(prefix):].strip() + + return { + "status_code": status_code, + "headers": headers, + "body": body_json + } + +# -------------------------- 4. 生成合法请求参数 -------------------------- +def generate_request_params(): + device_id = "DEVICE_PC_TEST_001" + device_iccid = "86123456789012345678" + + # 1. 时间戳格式修正:确保是字符串类型的毫秒级时间戳 + timestamp = str(int(time.time() * 1000)) + + # 2. 随机数生成:确保长度在8-16位之间(当前12位符合要求) + nonce_length = 12 + nonce = ''.join(random.choices(string.ascii_letters + string.digits, k=nonce_length)) + + # 3. 签名生成(保持不变,符合协议要求) + sign_data = f"{device_id}:{device_iccid}:{timestamp}:{nonce}" + hmac_obj = hmac.new(SIGN_SECRET.encode("utf-8"), sign_data.encode("utf-8"), hashlib.sha256) + signature = hmac_obj.hexdigest().lower() + + # 4. 返回参数(键名严格匹配文档,保持不变) + return { + "deviceId": device_id, + "deviceIccid": device_iccid, + "timestamp": timestamp, + "nonce": nonce, + "signature": signature, + "deviceModel": "AB101", + "deviceVersion": "1.0.0" + } + +# -------------------------- 5. 证书管理辅助函数 -------------------------- +def load_certificate_file(cert_path): + """ + 从文件加载证书内容 + + Args: + cert_path: 证书文件路径 + + Returns: + bytes: 证书内容(字节数据) + """ + try: + with open(cert_path, 'rb') as f: + cert_data = f.read() + print(f"✅ 成功加载证书文件: {cert_path} ({len(cert_data)} 字节)") + return cert_data + except FileNotFoundError: + print(f"❌ 证书文件不存在: {cert_path}") + return None + except Exception as e: + print(f"❌ 读取证书文件失败: {e}") + return None + +# -------------------------- 6. 发送请求并解析响应 -------------------------- +def send_get_device_token_request(auth_mode=0, ca_cert_path=None, client_cert_path=None, client_key_path=None): + """使用AT指令发送HTTPS GET请求""" + + # 初始化AT指令处理器 + at_handler = ATCommandHandler() + + try: + # 连接串口 + if not at_handler.connect(): + print("❌ 无法连接串口,请检查串口配置") + return + + # 测试AT连接 + success, response = at_handler.send_at_command("AT", timeout=3) + if not success: + print("❌ AT模块无响应,请检查连接") + return + print("✅ AT模块连接正常") + + # 查询版本号 + print("\n查询模块版本号...") + success, response = at_handler.send_at_command("AT+GMR", timeout=5) + if success: + print("✅ 版本信息:") + # 打印响应中的所有行(去除OK等状态行) + for line in response.split('\n'): + line = line.strip() + if line and line not in ['OK', 'AT+GMR']: + print(f" {line}") + else: + print(f"⚠️ 版本查询失败: {response}") + + # 检查PPP拨号状态 + print("\n" + "=" * 50) + print("【步骤1: 检查PPP拨号状态】") + print("=" * 50) + is_ppp_connected, ppp_ip = at_handler.check_ppp_status() + + if is_ppp_connected: + print(f"✅ PPP已连接,IP地址: {ppp_ip}") + else: + print(f"❌ PPP未连接,IP地址: {ppp_ip or '未知'}") + print("\n尝试建立PPP连接...") + + # 建立PPP连接(可以根据实际情况设置APN) + # 如果不需要设置APN,可以传入None + apn_name = None # 可以根据实际情况修改,如 "CMNET"、"3GNET" 等 + success, ip = at_handler.establish_ppp_connection(apn_name=apn_name) + + if not success: + print("❌ PPP连接建立失败,无法继续HTTPS访问") + return + + ppp_ip = ip + + # 初始化HTTPS客户端 + https_client = HTTPSATClient(at_handler) + + # 查询设备中已有的证书列表(可选,用于调试) + if auth_mode in [1, 2]: + print("\n" + "=" * 50) + print("【步骤2: 查询设备证书列表】") + print("=" * 50) + cert_list = https_client.list_certificates() + if cert_list: + print(f"设备中已有 {len(cert_list)} 个证书:") + for cert in cert_list: + print(f" - {cert}") + else: + print("设备中暂无证书") + + # 配置HTTPS(根据认证模式) + print("\n" + "=" * 50) + print("【步骤3: 配置HTTPS参数】") + print("=" * 50) + + # 根据认证模式处理证书 + ca_cert_name = None + client_cert_name = None + client_key_name = None + + if auth_mode == 1: # 单向认证 + print("配置模式: 单向认证") + if not ca_cert_path: + print("❌ 错误: 单向认证需要提供CA证书路径") + return + + # 提取证书文件名 + ca_cert_name = os.path.basename(ca_cert_path) + + # 检查证书是否已存在(避免重复添加) + print(f"\n检查证书是否已存在: {ca_cert_name}") + if https_client.certificate_exists(ca_cert_name): + print(f"✅ 证书 {ca_cert_name} 已存在于设备中,跳过添加步骤") + else: + print(f"证书 {ca_cert_name} 不存在,开始添加...") + + # 加载CA证书 + ca_cert_data = load_certificate_file(ca_cert_path) + if not ca_cert_data: + print("❌ 无法加载CA证书,终止流程") + return + + # 添加CA证书到模块 + print(f"\n添加CA证书: {ca_cert_name}") + if not https_client.add_certificate(ca_cert_name, ca_cert_data): + print("❌ CA证书添加失败,终止流程") + return + + # 确认证书 + if not https_client.check_certificate(ca_cert_name): + print("❌ CA证书确认失败,终止流程") + return + + elif auth_mode == 2: # 双向认证 + print("配置模式: 双向认证") + if not ca_cert_path or not client_cert_path or not client_key_path: + print("❌ 错误: 双向认证需要提供CA证书、客户端证书和密钥路径") + return + + ca_cert_name = os.path.basename(ca_cert_path) + client_cert_name = os.path.basename(client_cert_path) + client_key_name = os.path.basename(client_key_path) + + # 检查并添加CA证书 + print(f"\n检查CA证书: {ca_cert_name}") + if https_client.certificate_exists(ca_cert_name): + print(f"✅ 证书 {ca_cert_name} 已存在,跳过添加") + else: + ca_cert_data = load_certificate_file(ca_cert_path) + if not ca_cert_data: + return + if not https_client.add_certificate(ca_cert_name, ca_cert_data): + return + if not https_client.check_certificate(ca_cert_name): + return + + # 检查并添加客户端证书 + print(f"\n检查客户端证书: {client_cert_name}") + if https_client.certificate_exists(client_cert_name): + print(f"✅ 证书 {client_cert_name} 已存在,跳过添加") + else: + client_cert_data = load_certificate_file(client_cert_path) + if not client_cert_data: + return + if not https_client.add_certificate(client_cert_name, client_cert_data): + return + if not https_client.check_certificate(client_cert_name): + return + + # 检查并添加客户端密钥 + print(f"\n检查客户端密钥: {client_key_name}") + if https_client.certificate_exists(client_key_name): + print(f"✅ 密钥 {client_key_name} 已存在,跳过添加") + else: + client_key_data = load_certificate_file(client_key_path) + if not client_key_data: + return + if not https_client.add_certificate(client_key_name, client_key_data): + return + if not https_client.check_certificate(client_key_name): + return + else: + print("配置模式: 无需认证") + + # ========== 开始计时:HTTPS访问耗时 ========== + https_start_time = time.time() + + # 配置HTTPS参数 + print("\n" + "=" * 50) + print("【步骤4: 配置HTTPS参数】") + print("=" * 50) + if not https_client.configure_https( + ssl_version=3, # TLS1.2 + auth_mode=auth_mode, + ca_cert=ca_cert_name, + client_cert=client_cert_name, + client_key=client_key_name + ): + print("❌ HTTPS配置失败,终止流程") + return + + # 确认HTTPS配置(根据SSL/TLS应用指南要求) + print("\n确认HTTPS配置...") + config = https_client.verify_https_config() + if config: + print("✅ HTTPS配置确认成功") + else: + print("⚠️ HTTPS配置确认失败,但继续执行") + + # 设置URL + print("\n" + "=" * 50) + print("【步骤5: 设置HTTPS请求参数】") + print("=" * 50) + print(f"设置请求URL: {FULL_URL}") + https_client.set_url(FULL_URL) + + # 建立HTTPS连接 + print("\n" + "=" * 50) + print("【步骤6: 建立HTTPS连接】") + print("=" * 50) + https_client.setup_connection() + print("✅ HTTPS连接建立成功") + + # 生成请求参数 + params = generate_request_params() + + # 构建HTTP头(根据API文档要求,参数需要作为HTTP头传递,而不是URL查询参数) + # 参考:设备API接口文档.md - 请求头参数部分 + # 注意:为了与get_device_token_1.py保持一致,先添加参数,最后添加Content-Type + headers = {} + # 将参数添加到HTTP头(deviceId, deviceIccid, timestamp, nonce, signature等) + for key, value in params.items(): + headers[key] = str(value) + # Content-Type放在最后(与requests库的行为保持一致) + headers["Content-Type"] = "application/json" + + print("\n" + "=" * 50) + print("【步骤7: 发送HTTPS GET请求】") + print("=" * 50) + print("【请求信息】") + print(f"请求 URL: {FULL_URL}") + print(f"请求头: {headers}") + print("=" * 50) + + # 发送请求 + try: + raw_response = https_client.send_get_request(headers=headers) + except Exception as e: + print(f"❌ HTTPS请求发送失败: {str(e)}") + raise + + # 解析响应 + print("\n" + "=" * 50) + print("【步骤8: 解析HTTPS响应】") + print("=" * 50) + try: + parsed_response = https_client.parse_http_response(raw_response) + except Exception as e: + print(f"❌ HTTP响应解析失败: {str(e)}") + print(f"原始响应: {raw_response[:500]}...") # 只打印前500字符 + raise + + print(f"HTTP状态码: {parsed_response['status_code']}") + print(f"响应头: {parsed_response['headers']}") + print(f"响应体: {parsed_response['body']}") + + # 解析JSON响应 + print("\n" + "=" * 50) + print("【步骤9: 解析JSON响应】") + print("=" * 50) + try: + response_data = json.loads(parsed_response['body']) + print("\n【JSON响应】") + print(json.dumps(response_data, ensure_ascii=False, indent=2)) + + # 响应判断逻辑 + if response_data.get("errorCode") is None: + print("\n✅ 请求成功!") + if "data" in response_data: + print(f"设备 Token: {response_data['data'].get('token')}") + print(f"Token 过期时间: {response_data['data'].get('expireTime')} 秒({response_data['data'].get('expireTime', 0)//86400} 天)") + else: + print(f"\n❌ 请求失败!错误码: {response_data['errorCode']}, 原因: {response_data.get('message', '未知错误')}") + except json.JSONDecodeError as e: + print(f"\n❌ JSON解析失败: {e}") + print(f"原始响应体: {parsed_response['body']}") + raise + + # 关闭连接 + print("\n" + "=" * 50) + print("【步骤10: 关闭HTTPS连接】") + print("=" * 50) + try: + https_client.close_connection() + print("✅ HTTPS连接已关闭") + except Exception as e: + print(f"警告: 关闭HTTPS连接时出错: {str(e)}") + + # ========== 结束计时:HTTPS访问耗时 ========== + https_end_time = time.time() + https_duration = https_end_time - https_start_time + + print("\n" + "=" * 50) + print("【HTTPS访问耗时统计】") + print("=" * 50) + print(f"HTTPS访问总耗时: {https_duration:.3f} 秒 ({https_duration*1000:.1f} 毫秒)") + print("\n耗时包含以下步骤:") + print(" - HTTPS配置和验证") + print(" - URL和端口设置") + print(" - HTTPS连接建立") + print(" - HTTP请求发送") + print(" - HTTP响应接收") + print(" - 响应解析") + print(" - 连接关闭") + print("=" * 50) + + except Exception as e: + print("\n" + "=" * 50) + print("【错误处理】") + print("=" * 50) + print(f"❌ 请求异常: {str(e)}") + import traceback + traceback.print_exc() + finally: + # 断开串口连接 + print("\n断开串口连接...") + at_handler.disconnect() + +# -------------------------- 7. 执行请求 -------------------------- +if __name__ == "__main__": + # 认证模式配置 + # auth_mode: 0=无需认证, 1=单向认证, 2=双向认证 + # AUTH_MODE = 0 # 默认无需认证 + + # 证书文件路径(单向/双向认证时需要) + # CA_CERT_PATH = None # 例如: "certs/ca.crt" + CLIENT_CERT_PATH = None # 例如: "certs/client.crt" + CLIENT_KEY_PATH = None # 例如: "certs/client.key" + + # 如果使用单向认证,取消下面的注释并设置证书路径 + AUTH_MODE = 1 + CA_CERT_PATH = "certs/ca_chain.pem" + + # 如果使用双向认证,取消下面的注释并设置所有证书路径 + # AUTH_MODE = 2 + # CA_CERT_PATH = "certs/ca.crt" + # CLIENT_CERT_PATH = "certs/client.crt" + # CLIENT_KEY_PATH = "certs/client.key" + + send_get_device_token_request( + auth_mode=AUTH_MODE, + ca_cert_path=CA_CERT_PATH, + client_cert_path=CLIENT_CERT_PATH, + client_key_path=CLIENT_KEY_PATH + ) diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_at_handler.py b/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_at_handler.py new file mode 100644 index 000000000..3aa037b8a --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_at_handler.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +N706A AT指令处理器 +提供基础的AT指令通信功能,包括串口通信、PPP拨号等 +""" + +import serial +import time +import re + + +class ATCommandHandler: + """AT指令处理器,用于与N706A模组通信""" + + def __init__(self, port="COM19", baudrate=1000000, timeout=10): + """ + 初始化AT指令处理器 + + Args: + port: 串口设备路径(如 "COM19" 或 "/dev/ttyUSB0") + baudrate: 波特率,默认1000000 + timeout: 超时时间(秒),默认10 + """ + self.port = port + self.baudrate = baudrate + self.timeout = timeout + self.serial_conn = None + + def connect(self): + """ + 打开串口连接 + + Returns: + bool: 连接是否成功 + """ + try: + self.serial_conn = serial.Serial( + port=self.port, + baudrate=self.baudrate, + timeout=self.timeout, + bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE + ) + print(f"串口连接成功: {self.port} @ {self.baudrate}") + return True + except Exception as e: + print(f"串口连接失败: {e}") + return False + + def disconnect(self): + """关闭串口连接""" + if self.serial_conn and self.serial_conn.is_open: + self.serial_conn.close() + print("串口连接已关闭") + + def send_at_command(self, command, expected_response="OK", timeout=5): + """ + 发送AT指令并等待响应 + + Args: + command: AT指令字符串(不需要包含\r\n) + expected_response: 期望的响应(可以是字符串或正则表达式) + timeout: 超时时间(秒) + + Returns: + tuple: (success: bool, response: str) + """ + if not self.serial_conn or not self.serial_conn.is_open: + return False, "串口未连接" + + # 清空输入缓冲区 + self.serial_conn.reset_input_buffer() + + # 发送AT指令(添加\r\n) + cmd_bytes = (command + "\r\n").encode('utf-8') + self.serial_conn.write(cmd_bytes) + print(f"发送: {command}") + + # 等待响应 + start_time = time.time() + response_lines = [] + + while time.time() - start_time < timeout: + if self.serial_conn.in_waiting > 0: + line = self.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if line: + response_lines.append(line) + print(f"接收: {line}") + + # 检查是否收到期望的响应 + if isinstance(expected_response, str): + if expected_response in line: + return True, "\n".join(response_lines) + else: # 正则表达式 + if expected_response.search(line): + return True, "\n".join(response_lines) + + # 检查错误响应 + if "ERROR" in line: + return False, "\n".join(response_lines) + + time.sleep(0.1) + + # 超时 + return False, f"超时: {timeout}秒内未收到期望响应\n" + "\n".join(response_lines) + + def wait_for_unsolicited(self, pattern, timeout=30): + """ + 等待主动上报消息(如+HTTPSRECV、+HTTPRECV等) + + Args: + pattern: 匹配模式(字符串或正则表达式) + timeout: 超时时间(秒) + + Returns: + tuple: (success: bool, response: str) + """ + if not self.serial_conn or not self.serial_conn.is_open: + return False, "串口未连接" + + start_time = time.time() + response_lines = [] + pattern_compiled = re.compile(pattern) if isinstance(pattern, str) else pattern + + while time.time() - start_time < timeout: + if self.serial_conn.in_waiting > 0: + line = self.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if line: + response_lines.append(line) + print(f"接收: {line}") + + if pattern_compiled.search(line): + return True, "\n".join(response_lines) + + time.sleep(0.1) + + return False, f"超时: {timeout}秒内未收到主动上报\n" + "\n".join(response_lines) + + def read_binary_data(self, max_bytes=None, timeout=30): + """ + 读取二进制数据(用于接收文件等场景) + + Args: + max_bytes: 最大读取字节数,None表示读取所有可用数据 + timeout: 超时时间(秒) + + Returns: + bytes: 读取的二进制数据 + """ + if not self.serial_conn or not self.serial_conn.is_open: + return b"" + + start_time = time.time() + data = b"" + + while time.time() - start_time < timeout: + if self.serial_conn.in_waiting > 0: + if max_bytes: + remaining = max_bytes - len(data) + if remaining <= 0: + break + chunk = self.serial_conn.read(min(remaining, self.serial_conn.in_waiting)) + else: + chunk = self.serial_conn.read(self.serial_conn.in_waiting) + + if chunk: + data += chunk + start_time = time.time() # 重置超时计时器 + else: + # 如果没有更多数据,等待一小段时间 + time.sleep(0.1) + + return data + + def check_ppp_status(self): + """ + 检查PPP拨号状态 + + Returns: + tuple: (is_connected: bool, ip_address: str) + """ + success, response = self.send_at_command("AT+XIIC?", timeout=5) + if not success: + return False, None + + # 解析响应格式: +XIIC: , + # 注意:手册说明state前面可能有4个空格 + xiic_pattern = re.compile(r'\+XIIC:\s*(\d+),([0-9.]+)') + match = xiic_pattern.search(response) + + if match: + state = int(match.group(1)) + ip = match.group(2) + + # state=1表示已连接,且IP不是0.0.0.0 + if state == 1 and ip != "0.0.0.0": + return True, ip + else: + return False, ip + else: + print(f"警告: 无法解析PPP状态响应: {response}") + return False, None + + def check_network_registration(self): + """ + 检查网络注册状态 + + Returns: + bool: True表示已注册,False表示未注册 + """ + success, response = self.send_at_command("AT+CREG?", timeout=5) + if not success: + return False + + # 解析响应格式: +CREG: , + # stat=1或5表示已注册 + creg_pattern = re.compile(r'\+CREG:\s*\d+,([15])') + match = creg_pattern.search(response) + + if match: + return True + else: + print(f"警告: 网络未注册,响应: {response}") + return False + + def set_apn(self, apn_name, cid=1, pdp_type="IP"): + """ + 设置APN参数 + + Args: + apn_name: APN名称(如"CMNET") + cid: 上下文标识符,默认1 + pdp_type: PDP类型,默认"IP" + + Returns: + bool: 是否设置成功 + """ + command = f'AT+CGDCONT={cid},"{pdp_type}","{apn_name}"' + success, response = self.send_at_command(command, timeout=5) + return success + + def establish_ppp_connection(self, apn_name=None, max_retries=3): + """ + 建立PPP连接 + + Args: + apn_name: APN名称(可选,如果为None则不设置APN) + max_retries: 最大重试次数 + + Returns: + tuple: (success: bool, ip_address: str) + """ + # 检查网络注册状态 + print("检查网络注册状态...") + if not self.check_network_registration(): + print("❌ 网络未注册,无法建立PPP连接") + return False, None + + print("✅ 网络已注册") + + # 设置APN(如果提供) + if apn_name: + print(f"设置APN: {apn_name}") + if not self.set_apn(apn_name): + print(f"警告: APN设置失败,继续尝试拨号") + + # 尝试建立PPP连接 + for attempt in range(1, max_retries + 1): + print(f"\n尝试建立PPP连接 (第{attempt}次)...") + success, response = self.send_at_command("AT+XIIC=1", timeout=30) + + if not success: + print(f"❌ PPP连接失败: {response}") + if attempt < max_retries: + print(f"等待3秒后重试...") + time.sleep(3) + continue + + # 等待一下,然后查询状态确认 + time.sleep(2) + is_connected, ip = self.check_ppp_status() + + if is_connected: + print(f"✅ PPP连接成功,IP地址: {ip}") + return True, ip + else: + print(f"❌ PPP连接状态检查失败,IP: {ip}") + if attempt < max_retries: + print(f"等待3秒后重试...") + time.sleep(3) + + return False, None + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_http_client.py b/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_http_client.py new file mode 100644 index 000000000..4d68bf220 --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_http_client.py @@ -0,0 +1,910 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +N706A HTTP/HTTPS客户端 +支持HTTP和HTTPS协议,通过URL自动判断协议类型 +""" + +import urllib.parse +import re +import time +import os +from n706a_at_handler import ATCommandHandler + + +class N706AHTTPClient: + """基于AT指令的HTTP/HTTPS客户端,支持通过URL自动判断协议类型""" + + def __init__(self, at_handler): + """ + 初始化HTTP/HTTPS客户端 + + Args: + at_handler: ATCommandHandler实例 + """ + self.at = at_handler + self.is_https = False + self.current_url = None + + def add_certificate(self, cert_name, cert_data): + """ + 添加SSL证书到模块(仅HTTPS需要) + + Args: + cert_name: 证书文件名(如 "ca.crt") + cert_data: 证书内容(字节数据或字符串) + + Returns: + bool: 是否添加成功 + """ + # 如果是字符串,转换为字节 + if isinstance(cert_data, str): + cert_data = cert_data.encode('utf-8') + + cert_length = len(cert_data) + + # 清空输入缓冲区 + self.at.serial_conn.reset_input_buffer() + + # 直接发送AT+CERTADD命令(不使用send_at_command,因为响应是CONNECT而不是OK) + command = f'AT+CERTADD={cert_name},{cert_length}' + cmd_bytes = (command + "\r\n").encode('utf-8') + self.at.serial_conn.write(cmd_bytes) + print(f"发送: {command}") + + # 等待CONNECT提示符 + print("等待'CONNECT'提示符...") + start_time = time.time() + connect_received = False + response_lines = [] + + while time.time() - start_time < 5: + if self.at.serial_conn.in_waiting > 0: + line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if line: + response_lines.append(line) + print(f"接收: {line}") + + if "CONNECT" in line.upper(): + connect_received = True + break + + if "ERROR" in line: + print(f"❌ 证书添加命令失败: {line}") + return False + time.sleep(0.1) + + if not connect_received: + newline = '\n' + print(f"❌ 错误: 未收到'CONNECT'提示符,响应: {newline.join(response_lines)}") + return False + + # 发送证书数据 + print(f"发送证书数据 ({cert_length} 字节)...") + self.at.serial_conn.write(cert_data) + self.at.serial_conn.flush() + + # 等待证书添加完成响应 + print("等待证书添加完成响应...") + start_time = time.time() + certadd_received = False + response_lines = [] + + while time.time() - start_time < 30: + if self.at.serial_conn.in_waiting > 0: + line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if line: + response_lines.append(line) + print(f"接收: {line}") + + if "+CERTADD:" in line: + certadd_received = True + if f"{cert_length},OK" in line or ",OK" in line: + print(f"✅ 证书 {cert_name} 添加成功") + return True + elif "ERROR" in line: + print(f"❌ 证书添加失败: {line}") + return False + time.sleep(0.1) + + if not certadd_received: + newline = '\n' + print(f"❌ 错误: 证书添加超时,响应: {newline.join(response_lines)}") + return False + + return False + + def check_certificate(self, cert_name): + """ + 确认证书是否成功写入 + + Args: + cert_name: 证书文件名 + + Returns: + bool: 证书是否存在 + """ + success, response = self.at.send_at_command( + f'AT+CERTCHECK={cert_name}', + timeout=5 + ) + if not success: + return False + + if f"{cert_name},OK" in response: + print(f"✅ 证书 {cert_name} 确认成功") + return True + else: + print(f"❌ 证书 {cert_name} 不存在或验证失败: {response}") + return False + + def list_certificates(self): + """ + 查询设备中所有已添加的证书列表 + + Returns: + list: 证书文件名列表,如果查询失败返回None + """ + success, response = self.at.send_at_command("AT+CERTCHECK?", timeout=5) + if not success: + print(f"警告: 证书列表查询失败: {response}") + return None + + cert_list = [] + for line in response.split('\n'): + line = line.strip() + if line and line not in ['OK', 'AT+CERTCHECK?']: + if line.startswith('+CERTCHECK:'): + continue + cert_list.append(line) + + return cert_list + + def certificate_exists(self, cert_name): + """ + 检查证书是否已存在于设备中 + + Args: + cert_name: 证书文件名 + + Returns: + bool: 证书是否存在 + """ + success, response = self.at.send_at_command( + f'AT+CERTCHECK={cert_name}', + timeout=5 + ) + if not success: + return False + + if f"{cert_name},OK" in response: + return True + else: + return False + + def delete_certificate(self, cert_name): + """ + 删除SSL证书 + + Args: + cert_name: 证书文件名 + + Returns: + bool: 是否删除成功 + """ + success, response = self.at.send_at_command( + f'AT+CERTDEL={cert_name}', + timeout=5 + ) + if not success: + print(f"❌ 证书删除命令失败: {response}") + return False + + if "OK" in response: + print(f"✅ 证书 {cert_name} 已删除") + return True + else: + print(f"⚠️ 证书删除响应异常: {response}") + # 即使响应异常,也可能删除成功(如果证书不存在) + return "ERROR" not in response.upper() + + def configure_https(self, ssl_version=3, auth_mode=0, ca_cert=None, client_cert=None, client_key=None): + """ + 配置HTTPS SSL参数(仅HTTPS需要) + + Args: + ssl_version: SSL版本,0=SSL3.0, 1=TLS1.0, 2=TLS1.1, 3=TLS1.2 + auth_mode: 认证模式,0=不认证, 1=单向认证, 2=双向认证 + ca_cert: CA证书文件名(单向/双向认证需要) + client_cert: 客户端证书文件名(双向认证需要) + client_key: 客户端密钥文件名(双向认证需要) + """ + if not self.is_https: + print("警告: 当前URL不是HTTPS,无需配置SSL参数") + return True + + # 设置SSL版本 + success, response = self.at.send_at_command( + f'AT+HTTPSCFG="sslversion",{ssl_version}', + timeout=5 + ) + if not success: + print(f"警告: SSL版本配置失败: {response}") + return False + + # 设置认证模式 + success, response = self.at.send_at_command( + f'AT+HTTPSCFG="authmode",{auth_mode}', + timeout=5 + ) + if not success: + print(f"警告: 认证模式配置失败: {response}") + return False + + # 单向认证:配置CA证书 + if auth_mode == 1: + if not ca_cert: + print("错误: 单向认证需要CA证书") + return False + + success, response = self.at.send_at_command( + f'AT+HTTPSCFG="cacert",{ca_cert}', + timeout=5 + ) + if not success: + print(f"错误: CA证书配置失败: {response}") + return False + print(f"✅ 已配置CA证书: {ca_cert}") + + # 双向认证:配置CA证书、客户端证书和密钥 + elif auth_mode == 2: + if not ca_cert or not client_cert or not client_key: + print("错误: 双向认证需要CA证书、客户端证书和密钥") + return False + + # 配置CA证书 + success, response = self.at.send_at_command( + f'AT+HTTPSCFG="cacert",{ca_cert}', + timeout=5 + ) + if not success: + print(f"错误: CA证书配置失败: {response}") + return False + + # 配置客户端证书 + success, response = self.at.send_at_command( + f'AT+HTTPSCFG="clientcert",{client_cert}', + timeout=5 + ) + if not success: + print(f"错误: 客户端证书配置失败: {response}") + return False + + # 配置客户端密钥 + success, response = self.at.send_at_command( + f'AT+HTTPSCFG="clientkey",{client_key}', + timeout=5 + ) + if not success: + print(f"错误: 客户端密钥配置失败: {response}") + return False + + print(f"✅ 已配置双向认证证书: CA={ca_cert}, ClientCert={client_cert}, ClientKey={client_key}") + + return True + + def verify_https_config(self): + """ + 确认HTTPS配置(仅HTTPS需要) + + Returns: + dict: 包含配置信息的字典,如果查询失败返回None + """ + if not self.is_https: + return None + + success, response = self.at.send_at_command("AT+HTTPSCFG?", timeout=5) + if not success: + print(f"警告: HTTPS配置查询失败: {response}") + return None + + # 解析响应格式: +HTTPSCFG: ,,,, + httpcfg_pattern = re.compile(r'\+HTTPSCFG:\s*(\d+),(\d+),([^,]*),([^,]*),([^,\r\n]*)') + match = httpcfg_pattern.search(response) + + if match: + config = { + 'sslversion': int(match.group(1)), + 'authmode': int(match.group(2)), + 'cacert': match.group(3).strip() if match.group(3).strip() else None, + 'clientcert': match.group(4).strip() if match.group(4).strip() else None, + 'clientkey': match.group(5).strip() if match.group(5).strip() else None + } + + # 打印配置信息 + ssl_version_names = {0: "SSL3.0", 1: "TLS1.0", 2: "TLS1.1", 3: "TLS1.2"} + auth_mode_names = {0: "无需认证", 1: "单向认证", 2: "双向认证"} + + print(f" SSL版本: {config['sslversion']} ({ssl_version_names.get(config['sslversion'], '未知')})") + print(f" 认证模式: {config['authmode']} ({auth_mode_names.get(config['authmode'], '未知')})") + if config['cacert']: + print(f" CA证书: {config['cacert']}") + if config['clientcert']: + print(f" 客户端证书: {config['clientcert']}") + if config['clientkey']: + print(f" 客户端密钥: {config['clientkey']}") + + return config + else: + print(f"警告: 无法解析HTTPS配置响应: {response}") + return None + + def set_url(self, url, keepalive=0): + """ + 设置HTTP/HTTPS请求URL(自动判断协议类型) + + Args: + url: 完整的URL(如 http://example.com/path 或 https://example.com/path) + keepalive: 是否启用长连接,0=关闭(默认),1=启用 + """ + # 解析URL,判断协议类型 + parsed = urllib.parse.urlparse(url) + self.is_https = parsed.scheme.lower() == 'https' + self.current_url = url + + if not (parsed.scheme.lower() in ['http', 'https']): + raise ValueError(f"不支持的URL协议: {parsed.scheme},仅支持 http:// 或 https://") + + protocol_name = "HTTPS" if self.is_https else "HTTP" + print(f"检测到协议类型: {protocol_name}") + + host = parsed.netloc + path = parsed.path + if parsed.query: + path += "?" + parsed.query + + # 根据协议类型设置URL + if self.is_https: + # HTTPS: 使用AT+HTTPSPARA + url_param = f"{host}{path}" + success, response = self.at.send_at_command( + f'AT+HTTPSPARA=url,"{url_param}"', + timeout=5 + ) + if not success: + raise Exception(f"设置HTTPS URL失败: {response}") + + # 设置端口(HTTPS默认443) + port = parsed.port if parsed.port else 443 + success, response = self.at.send_at_command( + f'AT+HTTPSPARA=port,{port}', + timeout=5 + ) + if not success: + raise Exception(f"设置HTTPS端口失败: {response}") + + # 设置keepalive + if keepalive in [0, 1]: + success, response = self.at.send_at_command( + f'AT+HTTPSPARA=keepalive,{keepalive}', + timeout=5 + ) + if not success: + print(f"警告: keepalive配置失败: {response}") + else: + # HTTP: 使用AT+HTTPPARA + url_param = f"{host}{path}" + success, response = self.at.send_at_command( + f'AT+HTTPPARA=url,"{url_param}"', + timeout=5 + ) + if not success: + raise Exception(f"设置HTTP URL失败: {response}") + + # 设置端口(HTTP默认80) + port = parsed.port if parsed.port else 80 + success, response = self.at.send_at_command( + f'AT+HTTPPARA=port,{port}', + timeout=5 + ) + if not success: + raise Exception(f"设置HTTP端口失败: {response}") + + # 设置keepalive + if keepalive: + success, response = self.at.send_at_command( + f'AT+HTTPPARA=keepalive,{keepalive}', + timeout=5 + ) + if not success: + print(f"警告: 设置keepalive失败: {response}") + + return True + + def setup_connection(self): + """ + 建立HTTP/HTTPS连接(根据URL自动选择) + + Returns: + bool: 是否建立成功 + """ + if self.is_https: + success, response = self.at.send_at_command("AT+HTTPSSETUP", timeout=15) + if not success or "ERROR" in response: + raise Exception(f"HTTPS连接建立失败: {response}") + else: + success, response = self.at.send_at_command("AT+HTTPSETUP", timeout=15) + if not success: + raise Exception(f"HTTP连接建立失败: {response}") + + return True + + def send_get_request(self, headers=None, timeout=60, return_full_response=False): + """ + 发送HTTP/HTTPS GET请求 + + Args: + headers: 自定义HTTP头字典(可选) + timeout: 超时时间(秒) + return_full_response: 是否返回完整响应(包括响应头),默认False只返回响应体 + + Returns: + bytes: 响应数据(二进制),如果return_full_response=True,返回完整响应 + """ + if self.is_https: + full_response, header_text, body_data = self._send_https_get_request(headers, timeout) + else: + full_response, header_text, body_data = self._send_http_get_request(headers, timeout) + + if return_full_response: + return full_response + else: + return body_data + + def _send_https_get_request(self, headers=None, timeout=60): + """发送HTTPS GET请求""" + if headers: + # 使用OPEN_MODE (mode=99) 自定义HTTP报文 + parsed = urllib.parse.urlparse(self.current_url) + path = parsed.path + if parsed.query: + path += "?" + parsed.query + + # 构建HTTP请求报文 + http_request = f"GET {path} HTTP/1.1\r\n" + http_request += f"Host: {parsed.netloc}\r\n" + + for key, value in headers.items(): + http_request += f"{key}: {value}\r\n" + + http_request += "Connection: close\r\n" + http_request += "\r\n" + + request_length = len(http_request.encode('utf-8')) + + # 发送AT+HTTPSACTION=99,命令 + self.at.serial_conn.reset_input_buffer() + command = f"AT+HTTPSACTION=99,{request_length}" + cmd_bytes = (command + "\r\n").encode('utf-8') + self.at.serial_conn.write(cmd_bytes) + print(f"发送: {command}") + + # 等待">"提示符 + print("等待'>'提示符...") + start_time = time.time() + prompt_received = False + + while time.time() - start_time < 5: + if self.at.serial_conn.in_waiting > 0: + line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if line: + print(f"接收: {line}") + if ">" in line: + prompt_received = True + break + if "ERROR" in line: + raise Exception(f"HTTPS请求命令失败: {line}") + time.sleep(0.1) + + if not prompt_received: + raise Exception("未收到'>'提示符") + + # 发送HTTP请求报文 + print(f"发送HTTP请求 ({request_length} 字节)...") + self.at.serial_conn.write(http_request.encode('utf-8')) + self.at.serial_conn.flush() + + # 等待OK响应 + print("等待OK响应...") + start_time = time.time() + ok_received = False + + while time.time() - start_time < 5: + if self.at.serial_conn.in_waiting > 0: + line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if line: + print(f"接收: {line}") + if "OK" in line: + ok_received = True + break + if "ERROR" in line: + raise Exception(f"HTTPS请求发送失败: {line}") + time.sleep(0.1) + else: + # 使用标准GET模式 (mode=0) + success, response = self.at.send_at_command("AT+HTTPSACTION=0", timeout=5) + if not success: + raise Exception(f"发送HTTPS请求失败: {response}") + + # 等待响应数据(二进制模式) + return self._receive_http_response(b"+HTTPSRECV:", timeout) + + def _send_http_get_request(self, headers=None, timeout=60): + """发送HTTP GET请求""" + # 使用标准GET模式 (mode=0) + success, response = self.at.send_at_command("AT+HTTPACTION=0", timeout=5) + if not success: + raise Exception(f"发送HTTP请求失败: {response}") + + # 等待响应数据(二进制模式) + return self._receive_http_response(b"+HTTPRECV:", timeout) + + def _receive_http_response(self, recv_indicator, timeout=60): + """ + 接收HTTP/HTTPS响应数据(二进制模式) + + Args: + recv_indicator: 接收指示符(b"+HTTPSRECV:" 或 b"+HTTPRECV:") + timeout: 超时时间(秒) + + Returns: + tuple: (完整响应数据bytes, 响应头文本str, 响应体bytes) + """ + response_data = b"" + start_time = time.time() + last_data_time = time.time() + recv_found = False + + # 等待接收指示符 + indicator_name = recv_indicator.decode('utf-8', errors='ignore').replace(':', '') + print(f"等待+{indicator_name}:提示符...") + while time.time() - start_time < timeout: + if self.at.serial_conn.in_waiting > 0: + chunk = self.at.serial_conn.read(self.at.serial_conn.in_waiting) + if chunk: + response_data += chunk + last_data_time = time.time() + + if recv_indicator in response_data: + recv_found = True + print(f"✅ 收到+{indicator_name}:提示符") + break + time.sleep(0.1) + + if not recv_found: + if b"HTTP/" in response_data: + recv_found = True + print("✅ 检测到HTTP响应数据") + + if not recv_found: + raise Exception(f"未收到+{indicator_name}:提示符或HTTP响应") + + # 查找HTTP响应头开始位置 + http_start = response_data.find(b"HTTP/") + if http_start == -1: + # 继续读取直到找到HTTP响应头 + print("继续读取数据以查找HTTP响应头...") + while time.time() - start_time < 10: + if self.at.serial_conn.in_waiting > 0: + chunk = self.at.serial_conn.read(self.at.serial_conn.in_waiting) + if chunk: + response_data += chunk + http_start = response_data.find(b"HTTP/") + if http_start != -1: + break + time.sleep(0.1) + + if http_start == -1: + raise Exception("未找到HTTP响应头") + + # 跳过接收指示符前缀 + if http_start > 0: + print(f"跳过+{indicator_name}:前缀 ({http_start} 字节)") + response_data = response_data[http_start:] + + # 查找HTTP响应头结束位置(\r\n\r\n) + print("读取响应头...") + header_timeout = 10 + header_start = time.time() + header_received = False + body_start_pos = None + content_length = None + transfer_encoding_chunked = False + header_text = "" + + while time.time() - header_start < header_timeout: + if self.at.serial_conn.in_waiting > 0: + chunk = self.at.serial_conn.read(self.at.serial_conn.in_waiting) + if chunk: + response_data += chunk + + header_end = response_data.find(b"\r\n\r\n") + if header_end != -1: + header_received = True + body_start_pos = header_end + 4 + + # 解析响应头 + header_text = response_data[:header_end].decode('utf-8', errors='ignore') + print("\nHTTP响应头:") + print(header_text) + + # 检查Transfer-Encoding + if re.search(r'Transfer-Encoding:\s*chunked', header_text, re.IGNORECASE): + transfer_encoding_chunked = True + print("检测到Transfer-Encoding: chunked") + + # 提取Content-Length + content_length_match = re.search(r'Content-Length:\s*(\d+)', header_text, re.IGNORECASE) + if content_length_match: + content_length = int(content_length_match.group(1)) + print(f"\n文件大小: {content_length} 字节 ({content_length/1024/1024:.2f} MB)") + + # 检查状态码 + status_match = re.search(r'HTTP/\d\.\d\s+(\d+)', header_text) + if status_match: + status_code = int(status_match.group(1)) + if status_code != 200: + raise Exception(f"HTTP请求失败,状态码: {status_code}") + print(f"HTTP状态码: {status_code} ✅") + + break + time.sleep(0.1) + + if not header_received: + raise Exception("未收到完整的HTTP响应头") + + # 继续接收响应体数据 + print("\n开始接收响应数据...") + last_progress_time = time.time() + progress_interval = 5 + last_chunked_check_size = 0 # 上次检查chunked编码时的数据大小 + chunked_check_interval = 1024 # 每1KB检查一次chunked编码结束标记 + + while time.time() - start_time < 600: # 最多等待10分钟 + if self.at.serial_conn.in_waiting > 0: + chunk = self.at.serial_conn.read(self.at.serial_conn.in_waiting) + if chunk: + response_data += chunk + last_data_time = time.time() + + # 对于chunked编码,减少检查频率(每1KB检查一次) + if transfer_encoding_chunked: + current_size = len(response_data) + if current_size - last_chunked_check_size >= chunked_check_interval: + if response_data.endswith(b"0\r\n\r\n") or b"\r\n0\r\n\r\n" in response_data: + print("\n✅ Chunked编码响应接收完成") + break + last_chunked_check_size = current_size + # 如果知道文件大小,使用长度计算避免频繁切片 + elif content_length: + # 使用长度计算,避免每次循环都切片 + received = len(response_data) - body_start_pos + if received >= content_length: + print(f"\n✅ 文件接收完成: {received} / {content_length} 字节") + break + else: + # 只在需要显示进度时才切片 + current_time = time.time() + if current_time - last_progress_time >= progress_interval: + body_data = response_data[body_start_pos:] + progress = (received / content_length) * 100 + print(f"接收进度: {progress:.1f}% ({received}/{content_length} 字节, {received/1024/1024:.2f} MB)") + last_progress_time = current_time + else: + # 不知道文件大小,只在需要显示进度时才切片 + current_time = time.time() + if current_time - last_progress_time >= progress_interval: + body_data = response_data[body_start_pos:] + received = len(body_data) + print(f"已接收: {received} 字节 ({received/1024/1024:.2f} MB)") + last_progress_time = current_time + else: + # 如果超过10秒没有新数据,检查是否接收完成 + if time.time() - last_data_time > 10: + if transfer_encoding_chunked: + # 检查是否有结束标记 + if b"0\r\n\r\n" in response_data or response_data.endswith(b"\r\n0\r\n\r\n"): + print("\n响应数据接收完成(chunked编码)") + break + else: + print("\n⚠️ Chunked编码响应接收超时") + break + elif content_length: + received = len(response_data) - body_start_pos + if received >= content_length: + print(f"\n✅ 文件接收完成: {received} / {content_length} 字节") + break + else: + print(f"\n⚠️ 接收超时,已接收: {received} / {content_length} 字节") + break + else: + print("\n响应数据接收完成(超时)") + break + time.sleep(0.1) + + # 提取响应体数据(跳过HTTP响应头) + body_data = response_data[body_start_pos:] + + # 如果是chunked编码,解码chunked格式 + if transfer_encoding_chunked: + body_data = self._decode_chunked_response(body_data) + + # 返回完整响应数据、响应头文本和响应体 + return response_data, header_text, body_data + + def _decode_chunked_response(self, chunked_data): + """ + 解码Transfer-Encoding: chunked格式的响应体 + + Args: + chunked_data: chunked编码的响应体数据(bytes) + + Returns: + bytes: 解码后的响应体数据 + """ + decoded_data = b"" + data_str = chunked_data.decode('utf-8', errors='ignore') + lines = data_str.split('\r\n') + + i = 0 + while i < len(lines): + # 读取chunk size(十六进制) + chunk_size_line = lines[i].strip() + if not chunk_size_line: + i += 1 + continue + + try: + # 解析chunk size(可能包含分号后的扩展信息,忽略) + chunk_size_str = chunk_size_line.split(';')[0].strip() + chunk_size = int(chunk_size_str, 16) + except ValueError: + # 如果解析失败,尝试查找下一个有效的chunk size + i += 1 + continue + + if chunk_size == 0: + # 最后一个chunk + break + + # 读取chunk数据 + i += 1 + if i < len(lines): + chunk_data = lines[i] + decoded_data += chunk_data.encode('utf-8')[:chunk_size] + i += 1 # 跳过chunk数据后的空行 + + return decoded_data + + def parse_http_response(self, raw_response, header_text=None, body_data=None): + """ + 解析HTTP响应(文本模式,用于JSON等文本响应) + + Args: + raw_response: 原始响应字符串或bytes(完整响应或仅响应体) + header_text: 响应头文本(可选,如果提供则直接使用) + body_data: 响应体数据bytes(可选,如果提供则直接使用) + + Returns: + dict: 包含status_code, headers, body的字典 + """ + # 如果提供了header_text和body_data,直接使用 + if header_text and body_data is not None: + headers_text = header_text + body_text = body_data.decode('utf-8', errors='ignore') if isinstance(body_data, bytes) else body_data + else: + # 尝试从raw_response解析 + if isinstance(raw_response, bytes): + raw_response = raw_response.decode('utf-8', errors='ignore') + + # 查找HTTP响应开始位置 + http_start = raw_response.find("HTTP/") + if http_start == -1: + # 如果没有找到HTTP/,可能是只有响应体,尝试直接解析为JSON + json_start = raw_response.find("{") + if json_start != -1: + # 只有响应体,没有响应头 + return { + "status_code": 200, # 假设成功 + "headers": {}, + "body": raw_response[json_start:].strip() + } + else: + raise Exception("未找到HTTP响应或JSON数据") + + response_text = raw_response[http_start:] + + # 分离头部和正文 + header_end = response_text.find("\r\n\r\n") + if header_end == -1: + header_end = response_text.find("\n\n") + + if header_end == -1: + json_start = response_text.find("{") + if json_start != -1: + headers_text = response_text[:json_start] + body_text = response_text[json_start:] + else: + raise Exception("无法解析HTTP响应格式") + else: + headers_text = response_text[:header_end] + body_text = response_text[header_end:].strip() + + # 解析状态行 + lines = headers_text.split("\n") + status_line = lines[0].strip() + status_match = re.search(r'HTTP/\d\.\d\s+(\d+)', status_line) + status_code = int(status_match.group(1)) if status_match else 0 + + # 解析头部 + headers = {} + for line in lines[1:]: + line = line.strip() + if ":" in line: + key, value = line.split(":", 1) + headers[key.strip()] = value.strip() + + # 提取JSON正文 + json_start = body_text.find("{") + json_end = body_text.rfind("}") + 1 + + if json_start != -1 and json_end > json_start: + body_json = body_text[json_start:json_end] + else: + body_json = body_text.strip() + for prefix in ["+HTTPSRECV:", "+HTTPRECV:", "+HTTPSCLOSED:", "+HTTPSCLOSE:"]: + if body_json.startswith(prefix): + body_json = body_json[len(prefix):].strip() + + return { + "status_code": status_code, + "headers": headers, + "body": body_json + } + + def close_connection(self): + """ + 关闭HTTP/HTTPS连接 + + Returns: + bool: 是否关闭成功 + """ + if self.is_https: + success, response = self.at.send_at_command("AT+HTTPSCLOSE", timeout=5) + else: + success, response = self.at.send_at_command("AT+HTTPCLOSE", timeout=5) + return success + + +def load_certificate_file(cert_path): + """ + 从文件加载证书内容(辅助函数) + + Args: + cert_path: 证书文件路径 + + Returns: + bytes: 证书内容(字节数据) + """ + try: + with open(cert_path, 'rb') as f: + cert_data = f.read() + print(f"✅ 成功加载证书文件: {cert_path} ({len(cert_data)} 字节)") + return cert_data + except FileNotFoundError: + print(f"❌ 证书文件不存在: {cert_path}") + return None + except Exception as e: + print(f"❌ 读取证书文件失败: {e}") + return None + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_tcp_client.py b/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_tcp_client.py new file mode 100644 index 000000000..9ece1e6b1 --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_tcp_client.py @@ -0,0 +1,521 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +N706A TCP/TCP+SSL客户端 +支持TCP和TCP+SSL连接,通过参数选择连接类型 +""" + +import re +import time +from n706a_at_handler import ATCommandHandler + + +class N706ATCPClient: + """基于AT指令的TCP/TCP+SSL客户端""" + + def __init__(self, at_handler, socket_id=0): + """ + 初始化TCP客户端 + + Args: + at_handler: ATCommandHandler实例 + socket_id: Socket ID(0-5),默认0 + """ + self.at = at_handler + self.socket_id = socket_id + self.is_ssl = False + self.is_connected = False + self.server_ip = None + self.server_port = None + self.transparent_mode = False # 透传模式标志 + + def configure_ssl(self, ssl_version=3, auth_mode=0, ca_cert=None, client_cert=None, client_key=None): + """ + 配置SSL TCP参数(仅TCP+SSL需要) + + Args: + ssl_version: SSL版本,0=SSL3.0, 1=TLS1.0, 2=TLS1.1, 3=TLS1.2, 4=TLS1.3 + auth_mode: 认证模式,0=不认证, 1=单向认证, 2=双向认证 + ca_cert: CA证书文件名(单向/双向认证需要) + client_cert: 客户端证书文件名(双向认证需要) + client_key: 客户端密钥文件名(双向认证需要) + + Returns: + bool: 是否配置成功 + """ + # 设置SSL版本 + success, response = self.at.send_at_command( + f'AT+SSLTCPCFG="sslversion",{ssl_version}', + timeout=5 + ) + if not success: + print(f"警告: SSL版本配置失败: {response}") + return False + + # 设置认证模式 + success, response = self.at.send_at_command( + f'AT+SSLTCPCFG="authmode",{auth_mode}', + timeout=5 + ) + if not success: + print(f"警告: 认证模式配置失败: {response}") + return False + + # 单向认证:配置CA证书 + if auth_mode == 1: + if not ca_cert: + print("错误: 单向认证需要CA证书") + return False + + success, response = self.at.send_at_command( + f'AT+SSLTCPCFG="cacert",{ca_cert}', + timeout=5 + ) + if not success: + print(f"错误: CA证书配置失败: {response}") + return False + print(f"✅ 已配置CA证书: {ca_cert}") + + # 双向认证:配置CA证书、客户端证书和密钥 + elif auth_mode == 2: + if not ca_cert or not client_cert or not client_key: + print("错误: 双向认证需要CA证书、客户端证书和密钥") + return False + + # 配置CA证书 + success, response = self.at.send_at_command( + f'AT+SSLTCPCFG="cacert",{ca_cert}', + timeout=5 + ) + if not success: + print(f"错误: CA证书配置失败: {response}") + return False + + # 配置客户端证书 + success, response = self.at.send_at_command( + f'AT+SSLTCPCFG="clientcert",{client_cert}', + timeout=5 + ) + if not success: + print(f"错误: 客户端证书配置失败: {response}") + return False + + # 配置客户端密钥 + success, response = self.at.send_at_command( + f'AT+SSLTCPCFG="clientkey",{client_key}', + timeout=5 + ) + if not success: + print(f"错误: 客户端密钥配置失败: {response}") + return False + + print(f"✅ 已配置双向认证证书: CA={ca_cert}, ClientCert={client_cert}, ClientKey={client_key}") + + return True + + def connect(self, server_ip, server_port, use_ssl=False, transparent_mode=False): + """ + 建立TCP或TCP+SSL连接 + + Args: + server_ip: 服务器IP地址或域名 + server_port: 服务器端口 + use_ssl: 是否使用SSL/TLS加密,默认False(普通TCP) + transparent_mode: 是否使用透传模式,默认False(非透传) + + Returns: + bool: 是否连接成功 + """ + self.server_ip = server_ip + self.server_port = server_port + self.is_ssl = use_ssl + self.transparent_mode = transparent_mode + + if use_ssl: + # SSL TCP连接 + mode = 1 if transparent_mode else 0 + command = f'AT+SSLTCPSETUP={self.socket_id},{server_ip},{server_port},{mode}' + success, response = self.at.send_at_command(command, timeout=60) + + if not success: + print(f"❌ SSL TCP连接失败: {response}") + return False + + # 等待连接结果(+SSLTCPSETUP: , 或 CONNECT) + if transparent_mode: + # 透传模式会返回CONNECT + if "CONNECT" in response: + self.is_connected = True + print(f"✅ SSL TCP连接成功(透传模式): {server_ip}:{server_port}") + return True + else: + # 非透传模式会返回+SSLTCPSETUP: ,OK + sslsetup_pattern = re.compile(r'\+SSLTCPSETUP:\s*(\d+),(OK|FAIL|ERROR|AUTHFAIL)') + match = sslsetup_pattern.search(response) + if match: + status = match.group(2) + if status == "OK": + self.is_connected = True + print(f"✅ SSL TCP连接成功: {server_ip}:{server_port}") + return True + else: + print(f"❌ SSL TCP连接失败: {status}") + return False + + # 如果没有匹配到预期响应,尝试等待主动上报 + print("等待SSL TCP连接结果...") + success, response = self.at.wait_for_unsolicited(r"\+SSLTCPSETUP:", timeout=30) + if success: + sslsetup_pattern = re.compile(r'\+SSLTCPSETUP:\s*(\d+),(OK|FAIL|ERROR|AUTHFAIL)') + match = sslsetup_pattern.search(response) + if match: + status = match.group(2) + if status == "OK": + self.is_connected = True + print(f"✅ SSL TCP连接成功: {server_ip}:{server_port}") + return True + + print(f"❌ SSL TCP连接失败: {response}") + return False + else: + # 普通TCP连接 + command = f'AT+TCPSETUP={self.socket_id},{server_ip},{server_port}' + success, response = self.at.send_at_command(command, timeout=60) + + if not success: + print(f"❌ TCP连接失败: {response}") + return False + + # 等待连接结果(+TCPSETUP: ,OK 或 +TCPSETUP: ,FAIL) + tcpsetup_pattern = re.compile(r'\+TCPSETUP:\s*(\d+),(OK|FAIL|ERROR)') + match = tcpsetup_pattern.search(response) + if match: + status = match.group(2) + if status == "OK": + self.is_connected = True + print(f"✅ TCP连接成功: {server_ip}:{server_port}") + return True + else: + print(f"❌ TCP连接失败: {status}") + return False + + # 如果没有匹配到预期响应,尝试等待主动上报 + print("等待TCP连接结果...") + success, response = self.at.wait_for_unsolicited(r"\+TCPSETUP:", timeout=30) + if success: + tcpsetup_pattern = re.compile(r'\+TCPSETUP:\s*(\d+),(OK|FAIL|ERROR)') + match = tcpsetup_pattern.search(response) + if match: + status = match.group(2) + if status == "OK": + self.is_connected = True + print(f"✅ TCP连接成功: {server_ip}:{server_port}") + return True + + print(f"❌ TCP连接失败: {response}") + return False + + def send(self, data): + """ + 发送TCP/TCP+SSL数据 + + Args: + data: 要发送的数据(bytes或str) + + Returns: + bool: 是否发送成功 + """ + if not self.is_connected: + print("❌ 连接未建立,无法发送数据") + return False + + # 如果是字符串,转换为字节 + if isinstance(data, str): + data = data.encode('utf-8') + + data_length = len(data) + + if self.is_ssl: + # SSL TCP发送 + return self._send_ssl_tcp_data(data, data_length) + else: + # 普通TCP发送 + return self._send_tcp_data(data, data_length) + + def _send_tcp_data(self, data, data_length): + """发送普通TCP数据""" + # 使用buffer模式发送(不包含content参数) + command = f'AT+TCPSEND={self.socket_id},{data_length}' + self.at.serial_conn.reset_input_buffer() + cmd_bytes = (command + "\r\n").encode('utf-8') + self.at.serial_conn.write(cmd_bytes) + print(f"发送: {command}") + + # 等待">"提示符 + print("等待'>'提示符...") + start_time = time.time() + prompt_received = False + + while time.time() - start_time < 5: + if self.at.serial_conn.in_waiting > 0: + line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if line: + print(f"接收: {line}") + if ">" in line: + prompt_received = True + break + if "ERROR" in line: + print(f"❌ TCP发送命令失败: {line}") + return False + time.sleep(0.1) + + if not prompt_received: + print("❌ 未收到'>'提示符") + return False + + # 延迟50-100ms(根据手册建议) + time.sleep(0.1) + + # 发送数据 + print(f"发送TCP数据 ({data_length} 字节)...") + self.at.serial_conn.write(data) + self.at.serial_conn.flush() + + # 等待发送完成响应(+TCPSEND: ,) + print("等待发送完成响应...") + start_time = time.time() + send_complete = False + + while time.time() - start_time < 30: + if self.at.serial_conn.in_waiting > 0: + line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if line: + print(f"接收: {line}") + if f"+TCPSEND: {self.socket_id},{data_length}" in line: + send_complete = True + print(f"✅ TCP数据发送成功: {data_length} 字节") + break + if "ERROR" in line or "FAIL" in line: + print(f"❌ TCP数据发送失败: {line}") + return False + time.sleep(0.1) + + if not send_complete: + print("❌ TCP数据发送超时") + return False + + return True + + def _send_ssl_tcp_data(self, data, data_length): + """发送SSL TCP数据""" + # SSL TCP发送命令格式: AT+SSLTCPSEND=, + command = f'AT+SSLTCPSEND={self.socket_id},{data_length}' + self.at.serial_conn.reset_input_buffer() + cmd_bytes = (command + "\r\n").encode('utf-8') + self.at.serial_conn.write(cmd_bytes) + print(f"发送: {command}") + + # 等待">"提示符 + print("等待'>'提示符...") + start_time = time.time() + prompt_received = False + + while time.time() - start_time < 5: + if self.at.serial_conn.in_waiting > 0: + line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if line: + print(f"接收: {line}") + if ">" in line: + prompt_received = True + break + if "ERROR" in line: + print(f"❌ SSL TCP发送命令失败: {line}") + return False + time.sleep(0.1) + + if not prompt_received: + print("❌ 未收到'>'提示符") + return False + + # 发送数据 + print(f"发送SSL TCP数据 ({data_length} 字节)...") + self.at.serial_conn.write(data) + self.at.serial_conn.flush() + + # 等待发送完成响应(+SSLTCPSEND: ,OK 或 +SSLTCPSEND: ,FAIL) + print("等待发送完成响应...") + start_time = time.time() + send_complete = False + + while time.time() - start_time < 30: + if self.at.serial_conn.in_waiting > 0: + line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if line: + print(f"接收: {line}") + if f"+SSLTCPSEND: {self.socket_id},OK" in line: + send_complete = True + print(f"✅ SSL TCP数据发送成功: {data_length} 字节") + break + if "FAIL" in line or "ERROR" in line: + print(f"❌ SSL TCP数据发送失败: {line}") + return False + time.sleep(0.1) + + if not send_complete: + print("❌ SSL TCP数据发送超时") + return False + + return True + + def receive(self, max_length=2048, timeout=30): + """ + 接收TCP/TCP+SSL数据(非透传模式) + + Args: + max_length: 最大接收长度(1-2048字节),默认2048 + timeout: 超时时间(秒),默认30 + + Returns: + bytes: 接收到的数据,如果失败返回None + """ + if not self.is_connected: + print("❌ 连接未建立,无法接收数据") + return None + + if self.transparent_mode: + print("⚠️ 透传模式下,请使用被动接收方式(等待+TCPRECV或+SSLTCPRECV)") + return None + + if self.is_ssl: + # SSL TCP读取 + command = f'AT+SSLTCPREAD={self.socket_id},{max_length}' + else: + # 普通TCP读取 + command = f'AT+TCPREAD={self.socket_id},{max_length}' + + success, response = self.at.send_at_command(command, timeout=timeout) + if not success: + print(f"❌ 读取数据失败: {response}") + return None + + # 解析响应 + if self.is_ssl: + # +SSLTCPREAD: ,, + pattern = re.compile(r'\+SSLTCPREAD:\s*(\d+),(\d+),(.+)') + else: + # +TCPREAD: ,, + pattern = re.compile(r'\+TCPREAD:\s*(\d+),(\d+),(.+)') + + match = pattern.search(response) + if match: + length = int(match.group(2)) + data_str = match.group(3).strip() + + # 尝试解析数据(可能是ASCII或HEX) + try: + # 先尝试作为ASCII字符串 + data = data_str.encode('utf-8') + if len(data) == length: + return data + # 如果长度不匹配,尝试作为HEX字符串 + data = bytes.fromhex(data_str) + if len(data) == length: + return data + except: + pass + + # 如果解析失败,返回原始字符串的字节 + return data_str.encode('utf-8') + + return None + + def wait_for_data(self, timeout=30): + """ + 被动等待接收数据(透传或非透传模式都支持) + + Args: + timeout: 超时时间(秒),默认30 + + Returns: + bytes: 接收到的数据,如果超时返回None + """ + if not self.is_connected: + print("❌ 连接未建立,无法接收数据") + return None + + if self.is_ssl: + # 等待+SSLTCPRECV:提示符 + pattern = r"\+SSLTCPRECV:" + else: + # 等待+TCPRECV:提示符 + pattern = r"\+TCPRECV:" + + success, response = self.at.wait_for_unsolicited(pattern, timeout=timeout) + if not success: + print(f"等待数据超时: {timeout}秒") + return None + + # 解析接收到的数据 + if self.is_ssl: + # +SSLTCPRECV: ,, + recv_pattern = re.compile(r'\+SSLTCPRECV:\s*(\d+),(\d+),(.+)') + else: + # +TCPRECV: ,, 或 +TCPRECV: + recv_pattern = re.compile(r'\+TCPRECV:\s*(\d+)(?:,(\d+),(.+))?') + + match = recv_pattern.search(response) + if match: + if self.is_ssl: + length = int(match.group(2)) + data_str = match.group(3).strip() + else: + if match.group(2): # 有数据 + length = int(match.group(2)) + data_str = match.group(3).strip() + else: # 只有socket ID,需要主动读取 + print("收到+TCPRECV提示,需要主动读取数据") + return self.receive() + + # 尝试解析数据 + try: + # 先尝试作为ASCII字符串 + data = data_str.encode('utf-8') + if len(data) == length: + return data + # 如果长度不匹配,尝试作为HEX字符串 + data = bytes.fromhex(data_str) + if len(data) == length: + return data + except: + pass + + # 如果解析失败,返回原始字符串的字节 + return data_str.encode('utf-8') + + return None + + def close(self): + """ + 关闭TCP/TCP+SSL连接 + + Returns: + bool: 是否关闭成功 + """ + if not self.is_connected: + print("⚠️ 连接未建立,无需关闭") + return True + + if self.is_ssl: + command = f'AT+SSLTCPCLOSE={self.socket_id}' + else: + command = f'AT+TCPCLOSE={self.socket_id}' + + success, response = self.at.send_at_command(command, timeout=30) + if success: + self.is_connected = False + print(f"✅ 连接已关闭") + return True + else: + print(f"❌ 关闭连接失败: {response}") + return False + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/performance_analysis.md b/code/sdk/zhihui/leiniao/leiniao_python_test/performance_analysis.md new file mode 100644 index 000000000..8171d85fd --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/performance_analysis.md @@ -0,0 +1,139 @@ +# 下载速度性能分析 + +## 问题描述 +- `download_mp4_at.py`: 下载速度 0.02 MB/s +- `at_test_download_file.py`: 下载速度 0.01 MB/s(慢了一倍) + +## 关键差异分析 + +### 1. 数据接收循环中的性能差异 + +#### `download_mp4_at.py` (lines 328-373) +```python +while time.time() - start_time < 600: + if at_handler.serial_conn.in_waiting > 0: + chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting) + if chunk: + response_data += chunk + last_data_time = time.time() + + # 如果知道文件大小,显示进度 + if content_length: + body_data = response_data[body_start_pos:] # 只在需要时切片 + received = len(body_data) + if received >= content_length: + break + else: + # 每5秒打印一次进度 + current_time = time.time() + if current_time - last_progress_time >= progress_interval: + # 进度计算和打印 + last_progress_time = current_time +``` + +**特点:** +- 切片操作 `response_data[body_start_pos:]` 只在需要显示进度时执行(每5秒一次) +- 逻辑简单,条件判断少 +- 没有额外的字符串搜索操作 + +#### `n706a_http_client.py` (lines 645-700) +```python +while time.time() - start_time < 600: + if self.at.serial_conn.in_waiting > 0: + chunk = self.at.serial_conn.read(self.at.serial_conn.in_waiting) + if chunk: + response_data += chunk + last_data_time = time.time() + + # 对于chunked编码,检查是否收到结束标记 + if transfer_encoding_chunked: + if response_data.endswith(b"0\r\n\r\n") or b"\r\n0\r\n\r\n" in response_data: + break + # 如果知道文件大小,显示进度 + elif content_length: + body_data = response_data[body_start_pos:] # 每次循环都切片! + received = len(body_data) + if received >= content_length: + break + else: + current_time = time.time() + if current_time - last_progress_time >= progress_interval: + # 进度计算和打印 + last_progress_time = current_time +``` + +**问题:** +1. **每次循环都执行切片操作**:`body_data = response_data[body_start_pos:]` 在每次有数据时都会执行,即使不需要显示进度 +2. **额外的字符串搜索**:对于chunked编码,每次循环都检查 `response_data.endswith()` 和 `in` 操作 +3. **条件判断更复杂**:有 `if transfer_encoding_chunked` 分支 + +### 2. 性能影响分析 + +#### 切片操作的开销 +- `response_data[body_start_pos:]` 会创建一个新的字节对象 +- 如果 `response_data` 很大(比如几MB),这个操作会复制大量数据 +- 在 `n706a_http_client.py` 中,这个操作在**每次接收到数据时**都执行 +- 在 `download_mp4_at.py` 中,这个操作**只在需要显示进度时**执行(每5秒一次) + +#### 计算频率差异 +假设每秒接收 20KB 数据: +- `download_mp4_at.py`: 切片操作约 0.2 次/秒(每5秒一次) +- `n706a_http_client.py`: 切片操作约 10-100 次/秒(取决于串口缓冲区大小和读取频率) + +如果串口每次读取 1KB,那么每秒会读取约 20 次,每次都会执行切片操作。 + +### 3. 优化建议 + +#### 方案1:延迟切片操作(推荐) +只在需要检查进度或完成条件时才执行切片: + +```python +# 如果知道文件大小,显示进度 +elif content_length: + # 只在需要时计算已接收的数据量 + current_time = time.time() + need_check = (current_time - last_progress_time >= progress_interval) or True # 简化:总是检查完成条件 + + if need_check: + body_data = response_data[body_start_pos:] # 只在需要时切片 + received = len(body_data) + if received >= content_length: + break + + if current_time - last_progress_time >= progress_interval: + # 显示进度 + last_progress_time = current_time +``` + +#### 方案2:使用已接收长度缓存 +避免频繁切片,使用长度计算: + +```python +# 在循环外初始化 +initial_body_length = len(response_data) - body_start_pos if len(response_data) > body_start_pos else 0 + +# 在循环中 +elif content_length: + # 使用增量计算,避免切片 + received = len(response_data) - body_start_pos + if received >= content_length: + break +``` + +#### 方案3:减少chunked编码检查频率 +对于chunked编码的检查,也应该减少频率: + +```python +if transfer_encoding_chunked: + # 只在特定条件下检查(比如每接收一定数据量后) + if len(response_data) % 1024 == 0: # 每1KB检查一次 + if response_data.endswith(b"0\r\n\r\n") or b"\r\n0\r\n\r\n" in response_data: + break +``` + +## 结论 + +**主要原因:** `n706a_http_client.py` 在每次接收到数据时都执行 `response_data[body_start_pos:]` 切片操作,而 `download_mp4_at.py` 只在需要显示进度时(每5秒)才执行。对于大文件,频繁的切片操作会显著影响性能。 + +**建议修复:** 优化 `_receive_http_response` 方法,减少不必要的切片操作和字符串搜索操作。 + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/performance_optimization_summary.md b/code/sdk/zhihui/leiniao/leiniao_python_test/performance_optimization_summary.md new file mode 100644 index 000000000..299724100 --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/performance_optimization_summary.md @@ -0,0 +1,91 @@ +# 性能优化总结 + +## 问题 +`at_test_download_file.py` 的下载速度(0.01 MB/s)比 `download_mp4_at.py`(0.02 MB/s)慢了一倍。 + +## 根本原因 +在 `n706a_http_client.py` 的 `_receive_http_response` 方法中,数据接收循环存在性能瓶颈: + +1. **频繁的切片操作**:每次接收到数据时都执行 `response_data[body_start_pos:]`,这会创建新的字节对象并复制大量数据 +2. **频繁的字符串搜索**:对于chunked编码,每次循环都检查结束标记 +3. **不必要的计算**:即使不需要显示进度,也在每次循环中计算已接收的数据量 + +## 优化方案 + +### 1. 使用长度计算替代频繁切片 +**优化前:** +```python +elif content_length: + body_data = response_data[body_start_pos:] # 每次循环都切片 + received = len(body_data) + if received >= content_length: + break +``` + +**优化后:** +```python +elif content_length: + # 使用长度计算,避免每次循环都切片 + received = len(response_data) - body_start_pos + if received >= content_length: + break + else: + # 只在需要显示进度时才切片 + current_time = time.time() + if current_time - last_progress_time >= progress_interval: + body_data = response_data[body_start_pos:] # 每5秒才切片一次 + progress = (received / content_length) * 100 + print(f"接收进度: {progress:.1f}% ...") + last_progress_time = current_time +``` + +### 2. 减少chunked编码检查频率 +**优化前:** +```python +if transfer_encoding_chunked: + if response_data.endswith(b"0\r\n\r\n") or b"\r\n0\r\n\r\n" in response_data: + break +``` + +**优化后:** +```python +last_chunked_check_size = 0 +chunked_check_interval = 1024 # 每1KB检查一次 + +if transfer_encoding_chunked: + current_size = len(response_data) + if current_size - last_chunked_check_size >= chunked_check_interval: + if response_data.endswith(b"0\r\n\r\n") or b"\r\n0\r\n\r\n" in response_data: + break + last_chunked_check_size = current_size +``` + +### 3. 优化超时检查逻辑 +在超时检查分支中也使用长度计算,避免不必要的切片。 + +## 性能提升预期 + +### 优化前 +- 假设每秒接收 20KB 数据,串口每次读取 1KB +- 每秒执行约 20 次切片操作 +- 对于 10MB 文件,需要约 500 秒,执行约 10,000 次切片操作 + +### 优化后 +- 切片操作减少到每 5 秒一次(显示进度时) +- 对于 10MB 文件,约执行 100 次切片操作(减少 99%) +- 使用长度计算替代大部分切片操作,性能提升显著 + +## 预期效果 +- **下载速度提升**:预计从 0.01 MB/s 提升到接近 0.02 MB/s(与 `download_mp4_at.py` 相当) +- **CPU 使用率降低**:减少不必要的内存复制操作 +- **内存使用优化**:减少临时对象的创建 + +## 测试建议 +1. 使用相同的测试文件(MP4文件)进行对比测试 +2. 记录下载时间和速度 +3. 监控CPU和内存使用情况 +4. 验证下载的文件完整性 + +## 修改文件 +- `n706a_http_client.py`: 优化 `_receive_http_response` 方法(lines 640-707) + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/request_headers_comparison.md b/code/sdk/zhihui/leiniao/leiniao_python_test/request_headers_comparison.md new file mode 100644 index 000000000..cdfb699b9 --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/request_headers_comparison.md @@ -0,0 +1,118 @@ +# 请求头信息对比分析 + +## 两个请求头对比 + +### 请求1 +``` +请求 URL: https://ambybox-api.test.leiniao.com/ambybox-device-api/v1/auth/getDeviceToken +请求头: { + 'deviceId': 'DEVICE_PC_TEST_001', + 'deviceIccid': '86123456789012345678', + 'timestamp': '1762845627028', + 'nonce': 'DSAgsyFYxaOR', + 'signature': '0652010b2d574b034086a0efd707c94fef8250eac3fb02948c049b43cfb1f977', + 'deviceModel': 'AB101', + 'deviceVersion': '1.0.0', + 'Content-Type': 'application/json' +} +``` + +### 请求2 +``` +请求 URL: https://ambybox-api.test.leiniao.com/ambybox-device-api/v1/auth/getDeviceToken +请求头: { + 'deviceId': 'DEVICE_PC_TEST_001', + 'deviceIccid': '86123456789012345678', + 'timestamp': '1762845599887', + 'nonce': 'ogHL53iSPoaU', + 'signature': '1457b817ddeffa108341b750fa32063febc9dee9a24eaca1918fb1748b5e7987', + 'deviceModel': 'AB101', + 'deviceVersion': '1.0.0', + 'Content-Type': 'application/json' +} +``` + +## 详细对比 + +| 字段 | 请求1 | 请求2 | 状态 | 说明 | +|------|-------|-------|------|------| +| **URL** | `https://ambybox-api.test.leiniao.com/ambybox-device-api/v1/auth/getDeviceToken` | `https://ambybox-api.test.leiniao.com/ambybox-device-api/v1/auth/getDeviceToken` | ✅ 相同 | 两个请求使用相同的URL | +| **deviceId** | `DEVICE_PC_TEST_001` | `DEVICE_PC_TEST_001` | ✅ 相同 | 设备ID一致 | +| **deviceIccid** | `86123456789012345678` | `86123456789012345678` | ✅ 相同 | 设备ICCID一致 | +| **deviceModel** | `AB101` | `AB101` | ✅ 相同 | 设备型号一致 | +| **deviceVersion** | `1.0.0` | `1.0.0` | ✅ 相同 | 设备版本一致 | +| **Content-Type** | `application/json` | `application/json` | ✅ 相同 | 内容类型一致 | +| **timestamp** | `1762845627028` | `1762845599887` | ⚠️ 不同 | **正常差异**:时间戳不同(相差约27秒) | +| **nonce** | `DSAgsyFYxaOR` | `ogHL53iSPoaU` | ⚠️ 不同 | **正常差异**:随机数不同(每次请求都会生成新的) | +| **signature** | `0652010b2d574b034086a0efd707c94fef8250eac3fb02948c049b43cfb1f977` | `1457b817ddeffa108341b750fa32063febc9dee9a24eaca1918fb1748b5e7987` | ⚠️ 不同 | **正常差异**:签名不同(因为基于timestamp和nonce计算) | + +## 时间戳分析 + +- **请求1时间戳**: `1762845627028` (毫秒) +- **请求2时间戳**: `1762845599887` (毫秒) +- **时间差**: `27141` 毫秒 ≈ **27.14秒** + +转换为可读时间: +- 请求1: 约 2025-11-11 的某个时间点 +- 请求2: 约 2025-11-11 的某个时间点(比请求1早约27秒) + +## 签名验证 + +签名计算公式:`HMAC-SHA256(signSecret, "deviceId:deviceIccid:timestamp:nonce")` + +### 请求1的签名数据 +``` +signData = "DEVICE_PC_TEST_001:86123456789012345678:1762845627028:DSAgsyFYxaOR" +signature = "0652010b2d574b034086a0efd707c94fef8250eac3fb02948c049b43cfb1f977" +``` + +### 请求2的签名数据 +``` +signData = "DEVICE_PC_TEST_001:86123456789012345678:1762845599887:ogHL53iSPoaU" +signature = "1457b817ddeffa108341b750fa32063febc9dee9a24eaca1918fb1748b5e7987" +``` + +## 结论 + +### ✅ 正常情况 +两个请求头信息**符合预期**,差异是正常的: + +1. **静态字段一致**:deviceId, deviceIccid, deviceModel, deviceVersion, Content-Type 都相同 +2. **动态字段不同**:timestamp, nonce, signature 不同是**正常的**,因为: + - `timestamp` 是当前时间戳,每次请求都会变化 + - `nonce` 是随机生成的,每次请求都不同(用于防重放攻击) + - `signature` 基于 timestamp 和 nonce 计算,因此也会不同 + +### 📊 请求头结构 +两个请求的请求头结构完全一致,都包含: +- 设备标识信息(deviceId, deviceIccid) +- 设备信息(deviceModel, deviceVersion) +- 认证信息(timestamp, nonce, signature) +- 内容类型(Content-Type) + +### 🔍 验证建议 +如果需要验证签名是否正确,可以使用以下Python代码: + +```python +import hmac +import hashlib + +SIGN_SECRET = "vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus=" + +# 验证请求1 +sign_data_1 = "DEVICE_PC_TEST_001:86123456789012345678:1762845627028:DSAgsyFYxaOR" +hmac_obj_1 = hmac.new(SIGN_SECRET.encode("utf-8"), sign_data_1.encode("utf-8"), hashlib.sha256) +signature_1 = hmac_obj_1.hexdigest().lower() +print(f"请求1签名: {signature_1}") +print(f"期望签名: 0652010b2d574b034086a0efd707c94fef8250eac3fb02948c049b43cfb1f977") +print(f"匹配: {signature_1 == '0652010b2d574b034086a0efd707c94fef8250eac3fb02948c049b43cfb1f977'}") + +# 验证请求2 +sign_data_2 = "DEVICE_PC_TEST_001:86123456789012345678:1762845599887:ogHL53iSPoaU" +hmac_obj_2 = hmac.new(SIGN_SECRET.encode("utf-8"), sign_data_2.encode("utf-8"), hashlib.sha256) +signature_2 = hmac_obj_2.hexdigest().lower() +print(f"请求2签名: {signature_2}") +print(f"期望签名: 1457b817ddeffa108341b750fa32063febc9dee9a24eaca1918fb1748b5e7987") +print(f"匹配: {signature_2 == '1457b817ddeffa108341b750fa32063febc9dee9a24eaca1918fb1748b5e7987'}") +``` + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/single_auth_usage.md b/code/sdk/zhihui/leiniao/leiniao_python_test/single_auth_usage.md new file mode 100644 index 000000000..552253786 --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/single_auth_usage.md @@ -0,0 +1,145 @@ +# 单向认证使用说明 + +## 概述 + +单向认证(服务器认证)是HTTPS最常用的认证方式,客户端会验证服务器的身份。根据SSL/TLS应用指南,需要以下步骤: + +1. 配置认证模式为单向认证(auth_mode=1) +2. 添加CA证书到模块 +3. 确认证书 +4. 配置CA证书到HTTPS + +## 代码修改步骤 + +### 1. 准备CA证书文件 + +将CA证书文件放在可访问的路径,例如: +``` +certs/ + └── ca.crt +``` + +### 2. 修改主程序配置 + +在 `get_device_token_at.py` 文件末尾,修改以下配置: + +```python +# 认证模式配置 +AUTH_MODE = 1 # 改为1(单向认证) + +# 证书文件路径 +CA_CERT_PATH = "certs/ca.crt" # 设置CA证书路径 +``` + +### 3. 运行程序 + +```bash +python3 get_device_token_at.py +``` + +## 完整流程说明 + +### 步骤1: 模块初始化 +- AT+GMR 查询版本号 +- 检查PPP拨号状态 + +### 步骤2: 配置HTTPS参数(单向认证) +1. **加载CA证书文件** + ```python + ca_cert_data = load_certificate_file("certs/ca.crt") + ``` + +2. **添加CA证书到模块** + ``` + AT+CERTADD=ca.crt, + CONNECT + <证书内容> + +CERTADD: ,OK + ``` + +3. **确认证书** + ``` + AT+CERTCHECK=ca.crt + +CERTCHECK: ca.crt,OK + ``` + +4. **配置SSL/TLS参数** + ``` + AT+HTTPSCFG="sslversion",3 + AT+HTTPSCFG="authmode",1 + AT+HTTPSCFG="cacert",ca.crt + ``` + +5. **确认配置** + ``` + AT+HTTPSCFG? + +HTTPSCFG: 3,1,ca.crt,, + ``` + +### 步骤3: 设置HTTPS连接参数 +- 设置URL +- 设置端口(默认443) +- 可选:设置keepalive + +### 步骤4: 建立HTTPS连接 +``` +AT+HTTPSSETUP +``` + +### 步骤5: 发送HTTPS请求 +``` +AT+HTTPSACTION=99, +> +``` + +### 步骤6: 关闭连接 +``` +AT+HTTPSCLOSE +``` + +## 代码示例 + +### 使用单向认证的完整示例 + +```python +# 在文件末尾修改 +if __name__ == "__main__": + # 单向认证配置 + AUTH_MODE = 1 + CA_CERT_PATH = "certs/ca.crt" # 根据实际路径修改 + + send_get_device_token_request( + auth_mode=AUTH_MODE, + ca_cert_path=CA_CERT_PATH + ) +``` + +## 注意事项 + +1. **证书文件格式**:支持PEM格式的证书文件 +2. **证书长度**:确保 `AT+CERTADD` 中的长度参数与实际证书文件大小(字节数)一致 +3. **证书路径**:使用相对路径或绝对路径都可以 +4. **证书文件名**:模块中存储的证书名称就是文件名(如 `ca.crt`) + +## 与无需认证的对比 + +| 项目 | 无需认证 | 单向认证 | +|------|---------|---------| +| auth_mode | 0 | 1 | +| 需要证书 | ❌ 否 | ✅ CA证书 | +| 安全性 | ⚠️ 较低 | ✅ 较高 | +| 配置步骤 | 简单 | 需要添加证书 | + +## 错误处理 + +如果证书添加失败,程序会: +1. 打印错误信息 +2. 终止流程 +3. 不会继续执行HTTPS请求 + +常见错误: +- 证书文件不存在 +- 证书长度不匹配 +- 证书格式错误 +- 模块存储空间不足 + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/tcp_test_server.py b/code/sdk/zhihui/leiniao/leiniao_python_test/tcp_test_server.py new file mode 100644 index 000000000..c4c30ca06 --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/tcp_test_server.py @@ -0,0 +1,491 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +TCP测试服务器 +监听端口8910,支持多客户端连接 +可以查看连接状态、接收设备数据、发送数据给设备 +""" + +import socket +import threading +import time +import sys +from datetime import datetime +from collections import defaultdict + +class TCPTestServer: + def __init__(self, host='0.0.0.0', port=8910): + """ + 初始化TCP测试服务器 + + Args: + host: 监听地址,默认0.0.0.0(所有网络接口) + port: 监听端口,默认8910 + """ + self.host = host + self.port = port + self.server_socket = None + self.clients = {} # {client_id: {'socket': socket, 'address': (ip, port), 'connect_time': datetime}} + self.client_counter = 0 + self.running = False + self.lock = threading.Lock() + + def start(self): + """启动服务器""" + try: + self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.server_socket.bind((self.host, self.port)) + self.server_socket.listen(10) # 最多10个待连接 + self.server_socket.settimeout(1.0) # 设置超时以便定期检查running状态 + + self.running = True + print("=" * 60) + print(f"TCP测试服务器已启动") + print(f"监听地址: {self.host}:{self.port}") + print(f"服务器地址: snicker.com.cn:{self.port}") + print("=" * 60) + print("\n命令说明:") + print(" list - 显示所有连接的客户端") + print(" send - 发送数据给指定客户端(会提示输入数据)") + print(" sendall - 发送数据给所有客户端(会提示输入数据)") + print(" close - 关闭指定客户端连接") + print(" closeall - 关闭所有客户端连接") + print(" help - 显示帮助信息") + print(" quit/exit - 退出服务器") + print("=" * 60) + print() + + # 启动接受连接的线程 + accept_thread = threading.Thread(target=self._accept_connections, daemon=True) + accept_thread.start() + + # 启动命令行输入处理线程 + input_thread = threading.Thread(target=self._handle_commands, daemon=True) + input_thread.start() + + # 主线程等待 + accept_thread.join() + + except Exception as e: + print(f"❌ 服务器启动失败: {e}") + self.running = False + + def _accept_connections(self): + """接受客户端连接""" + while self.running: + try: + client_socket, address = self.server_socket.accept() + client_socket.settimeout(1.0) + + with self.lock: + self.client_counter += 1 + client_id = self.client_counter + self.clients[client_id] = { + 'socket': client_socket, + 'address': address, + 'connect_time': datetime.now() + } + + # 显示连接信息 + print(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] ✅ 客户端连接") + print(f" 客户端ID: {client_id}") + print(f" 地址: {address[0]}:{address[1]}") + print(f" 当前连接数: {len(self.clients)}") + print() + + # 启动接收数据的线程 + recv_thread = threading.Thread( + target=self._handle_client, + args=(client_id, client_socket, address), + daemon=True + ) + recv_thread.start() + + except socket.timeout: + continue + except Exception as e: + if self.running: + print(f"❌ 接受连接时出错: {e}") + + def _handle_client(self, client_id, client_socket, address): + """处理客户端数据接收""" + buffer = b"" + try: + while self.running: + try: + data = client_socket.recv(4096) + if not data: + # 客户端关闭连接 + break + + buffer += data + + # 显示接收到的数据 + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + print(f"\n[{timestamp}] 📥 收到数据 [客户端ID: {client_id}, {address[0]}:{address[1]}]") + print(f" 数据长度: {len(data)} 字节") + + # 尝试以文本和十六进制显示 + try: + text = data.decode('utf-8', errors='ignore') + if text.isprintable() or '\n' in text or '\r' in text: + print(f" 文本内容:") + for line in text.splitlines(): + print(f" {line}") + else: + print(f" 十六进制: {data.hex()}") + except: + print(f" 十六进制: {data.hex()}") + + # 显示前64字节的ASCII表示 + if len(data) <= 64: + ascii_repr = ''.join(chr(b) if 32 <= b < 127 else '.' for b in data) + print(f" ASCII: {ascii_repr}") + else: + ascii_repr = ''.join(chr(b) if 32 <= b < 127 else '.' for b in data[:64]) + print(f" ASCII (前64字节): {ascii_repr}...") + + print() + + except socket.timeout: + continue + except Exception as e: + if self.running: + print(f"❌ 接收数据时出错 [客户端ID: {client_id}]: {e}") + break + + except Exception as e: + print(f"❌ 处理客户端时出错 [客户端ID: {client_id}]: {e}") + finally: + # 客户端断开连接 + self._disconnect_client(client_id, address) + + def _disconnect_client(self, client_id, address): + """断开客户端连接""" + with self.lock: + if client_id in self.clients: + try: + self.clients[client_id]['socket'].close() + except: + pass + del self.clients[client_id] + + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + print(f"\n[{timestamp}] ❌ 客户端断开连接") + print(f" 客户端ID: {client_id}") + print(f" 地址: {address[0]}:{address[1]}") + print(f" 当前连接数: {len(self.clients)}") + print() + + def _handle_commands(self): + """处理命令行输入""" + while self.running: + try: + command = input().strip() + if not command: + continue + + parts = command.split(None, 1) + cmd = parts[0].lower() + args = parts[1] if len(parts) > 1 else "" + + if cmd in ['quit', 'exit', 'q']: + print("\n正在关闭服务器...") + self.stop() + break + elif cmd == 'list': + self._list_clients() + elif cmd == 'send': + if args: + try: + client_id = int(args) + self._send_to_client(client_id) + except ValueError: + print("❌ 无效的客户端ID,请输入数字") + else: + print("❌ 用法: send <客户端ID>") + elif cmd == 'sendall': + self._send_to_all_clients() + elif cmd == 'close': + if args: + try: + client_id = int(args) + self._close_client(client_id) + except ValueError: + print("❌ 无效的客户端ID,请输入数字") + else: + print("❌ 用法: close <客户端ID>") + elif cmd == 'closeall': + self._close_all_clients() + elif cmd in ['help', 'h']: + self._show_help() + else: + print(f"❌ 未知命令: {cmd},输入 help 查看帮助") + + except EOFError: + # 输入流关闭(Ctrl+D) + break + except KeyboardInterrupt: + print("\n正在关闭服务器...") + self.stop() + break + except Exception as e: + print(f"❌ 处理命令时出错: {e}") + + def _list_clients(self): + """列出所有连接的客户端""" + with self.lock: + if not self.clients: + print("\n当前没有连接的客户端") + return + + print("\n" + "=" * 60) + print("连接的客户端列表:") + print("=" * 60) + print(f"{'ID':<6} {'地址':<25} {'连接时间':<20}") + print("-" * 60) + + for client_id, client_info in self.clients.items(): + address = f"{client_info['address'][0]}:{client_info['address'][1]}" + connect_time = client_info['connect_time'].strftime('%Y-%m-%d %H:%M:%S') + print(f"{client_id:<6} {address:<25} {connect_time:<20}") + + print("=" * 60) + print(f"总计: {len(self.clients)} 个客户端") + print() + + def _send_to_client(self, client_id): + """发送数据给指定客户端""" + with self.lock: + if client_id not in self.clients: + print(f"❌ 客户端ID {client_id} 不存在") + return + + client_info = self.clients[client_id] + client_socket = client_info['socket'] + address = client_info['address'] + + try: + print(f"\n发送数据给客户端 {client_id} ({address[0]}:{address[1]})") + print("输入数据(支持文本或十六进制,十六进制以0x或hex:开头):") + data_input = input("> ").strip() + + if not data_input: + print("❌ 数据为空,取消发送") + return + + # 解析输入数据 + data = self._parse_input_data(data_input) + if data is None: + print("❌ 数据解析失败") + return + + # 发送数据 + client_socket.sendall(data) + + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + print(f"\n[{timestamp}] 📤 已发送数据 [客户端ID: {client_id}]") + print(f" 数据长度: {len(data)} 字节") + print(f" 十六进制: {data.hex()}") + if len(data) <= 64: + ascii_repr = ''.join(chr(b) if 32 <= b < 127 else '.' for b in data) + print(f" ASCII: {ascii_repr}") + print() + + except Exception as e: + print(f"❌ 发送数据失败: {e}") + + def _send_to_all_clients(self): + """发送数据给所有客户端""" + with self.lock: + if not self.clients: + print("❌ 当前没有连接的客户端") + return + + client_ids = list(self.clients.keys()) + + try: + print(f"\n发送数据给所有客户端(共 {len(client_ids)} 个)") + print("输入数据(支持文本或十六进制,十六进制以0x或hex:开头):") + data_input = input("> ").strip() + + if not data_input: + print("❌ 数据为空,取消发送") + return + + # 解析输入数据 + data = self._parse_input_data(data_input) + if data is None: + print("❌ 数据解析失败") + return + + # 发送给所有客户端 + success_count = 0 + failed_clients = [] + + with self.lock: + for client_id in client_ids: + if client_id in self.clients: + try: + self.clients[client_id]['socket'].sendall(data) + success_count += 1 + except Exception as e: + failed_clients.append((client_id, str(e))) + + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + print(f"\n[{timestamp}] 📤 已发送数据给 {success_count} 个客户端") + print(f" 数据长度: {len(data)} 字节") + if failed_clients: + print(f" 失败: {len(failed_clients)} 个客户端") + for client_id, error in failed_clients: + print(f" 客户端 {client_id}: {error}") + print() + + except Exception as e: + print(f"❌ 发送数据失败: {e}") + + def _parse_input_data(self, data_input): + """ + 解析输入数据 + 支持文本、十六进制(0x开头或hex:开头) + + Args: + data_input: 输入字符串 + + Returns: + bytes: 解析后的数据,失败返回None + """ + data_input = data_input.strip() + + # 十六进制格式: 0x... 或 hex:... + if data_input.lower().startswith('0x'): + hex_str = data_input[2:].strip() + # 移除空格 + hex_str = hex_str.replace(' ', '').replace('\n', '').replace('\r', '') + try: + return bytes.fromhex(hex_str) + except ValueError: + print(f"❌ 无效的十六进制数据: {hex_str}") + return None + elif data_input.lower().startswith('hex:'): + hex_str = data_input[4:].strip() + # 移除空格 + hex_str = hex_str.replace(' ', '').replace('\n', '').replace('\r', '') + try: + return bytes.fromhex(hex_str) + except ValueError: + print(f"❌ 无效的十六进制数据: {hex_str}") + return None + else: + # 文本格式 + return data_input.encode('utf-8') + + def _close_client(self, client_id): + """关闭指定客户端连接""" + with self.lock: + if client_id not in self.clients: + print(f"❌ 客户端ID {client_id} 不存在") + return + + client_info = self.clients[client_id] + address = client_info['address'] + + try: + client_info['socket'].close() + print(f"✅ 已关闭客户端 {client_id} 的连接") + except Exception as e: + print(f"❌ 关闭客户端连接失败: {e}") + + def _close_all_clients(self): + """关闭所有客户端连接""" + with self.lock: + if not self.clients: + print("当前没有连接的客户端") + return + + client_ids = list(self.clients.keys()) + count = len(client_ids) + + closed_count = 0 + for client_id in client_ids: + try: + with self.lock: + if client_id in self.clients: + self.clients[client_id]['socket'].close() + closed_count += 1 + except: + pass + + print(f"✅ 已关闭 {closed_count}/{count} 个客户端连接") + + def _show_help(self): + """显示帮助信息""" + print("\n" + "=" * 60) + print("命令帮助:") + print("=" * 60) + print(" list - 显示所有连接的客户端") + print(" send - 发送数据给指定客户端") + print(" 例如: send 1") + print(" sendall - 发送数据给所有客户端") + print(" close - 关闭指定客户端连接") + print(" 例如: close 1") + print(" closeall - 关闭所有客户端连接") + print(" help - 显示此帮助信息") + print(" quit/exit/q - 退出服务器") + print() + print("数据输入格式:") + print(" - 文本: 直接输入文本,例如: Hello World") + print(" - 十六进制: 以0x开头,例如: 0x48656c6c6f") + print(" - 十六进制: 以hex:开头,例如: hex:48656c6c6f") + print("=" * 60) + print() + + def stop(self): + """停止服务器""" + self.running = False + + # 关闭所有客户端连接 + with self.lock: + for client_id, client_info in list(self.clients.items()): + try: + client_info['socket'].close() + except: + pass + self.clients.clear() + + # 关闭服务器socket + if self.server_socket: + try: + self.server_socket.close() + except: + pass + + print("✅ 服务器已关闭") + + +def main(): + """主函数""" + import argparse + + parser = argparse.ArgumentParser(description='TCP测试服务器') + parser.add_argument('--host', default='0.0.0.0', help='监听地址(默认: 0.0.0.0)') + parser.add_argument('--port', type=int, default=8910, help='监听端口(默认: 8910)') + + args = parser.parse_args() + + server = TCPTestServer(host=args.host, port=args.port) + + try: + server.start() + except KeyboardInterrupt: + print("\n正在关闭服务器...") + server.stop() + except Exception as e: + print(f"❌ 服务器运行出错: {e}") + server.stop() + + +if __name__ == "__main__": + main() + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/verify_signatures.py b/code/sdk/zhihui/leiniao/leiniao_python_test/verify_signatures.py new file mode 100644 index 000000000..4dd8841d1 --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/verify_signatures.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""验证两个请求的签名是否正确""" + +import hmac +import hashlib + +SIGN_SECRET = "vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus=" + +# 请求1的签名验证 +sign_data_1 = "DEVICE_PC_TEST_001:86123456789012345678:1762845627028:DSAgsyFYxaOR" +hmac_obj_1 = hmac.new(SIGN_SECRET.encode("utf-8"), sign_data_1.encode("utf-8"), hashlib.sha256) +signature_1 = hmac_obj_1.hexdigest().lower() +expected_signature_1 = "0652010b2d574b034086a0efd707c94fef8250eac3fb02948c049b43cfb1f977" + +print("=" * 60) +print("请求1签名验证:") +print(f" 签名数据: {sign_data_1}") +print(f" 计算签名: {signature_1}") +print(f" 期望签名: {expected_signature_1}") +print(f" 匹配结果: {'✅ 正确' if signature_1 == expected_signature_1 else '❌ 错误'}") +print("=" * 60) + +# 请求2的签名验证 +sign_data_2 = "DEVICE_PC_TEST_001:86123456789012345678:1762845599887:ogHL53iSPoaU" +hmac_obj_2 = hmac.new(SIGN_SECRET.encode("utf-8"), sign_data_2.encode("utf-8"), hashlib.sha256) +signature_2 = hmac_obj_2.hexdigest().lower() +expected_signature_2 = "1457b817ddeffa108341b750fa32063febc9dee9a24eaca1918fb1748b5e7987" + +print("请求2签名验证:") +print(f" 签名数据: {sign_data_2}") +print(f" 计算签名: {signature_2}") +print(f" 期望签名: {expected_signature_2}") +print(f" 匹配结果: {'✅ 正确' if signature_2 == expected_signature_2 else '❌ 错误'}") +print("=" * 60) + +# 时间戳差异分析 +timestamp_1 = 1762845627028 +timestamp_2 = 1762845599887 +time_diff = timestamp_1 - timestamp_2 +print(f"\n时间戳差异分析:") +print(f" 请求1时间戳: {timestamp_1}") +print(f" 请求2时间戳: {timestamp_2}") +print(f" 时间差: {time_diff} 毫秒 ({time_diff/1000:.2f} 秒)") +print("=" * 60) + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/websocket_feasibility_analysis.md b/code/sdk/zhihui/leiniao/leiniao_python_test/websocket_feasibility_analysis.md new file mode 100644 index 000000000..980173a56 --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/websocket_feasibility_analysis.md @@ -0,0 +1,394 @@ +# WebSocket实现可行性分析 + +## 问题 +AT命令手册中的TCP指令和HTTP指令是否足够实现WebSocket(ws和wss)接口,要求支持二进制数据和文本数据传输? + +## 简短回答 + +**✅ 是的,AT指令集中的TCP和SSL TCP指令足够实现WebSocket接口。** + +- **ws(非加密)**:使用 `AT+TCPSETUP` 建立TCP连接 +- **wss(加密)**:使用 `AT+SSLTCPSETUP` 建立SSL TCP连接 +- **二进制和文本数据**:都支持,通过HEX模式发送二进制,ASCII模式发送文本 + +**⚠️ 但需要手动实现WebSocket协议层**(帧封装、握手处理等) + +## WebSocket协议要求 + +### 1. WebSocket连接建立流程 +``` +1. 建立TCP/SSL TCP连接 +2. 发送HTTP/HTTPS握手请求(包含Upgrade: websocket头) +3. 接收服务器握手响应(HTTP 101 Switching Protocols) +4. 连接升级为WebSocket协议 +5. 在TCP连接上传输WebSocket帧(文本/二进制) +``` + +### 2. WebSocket帧格式 +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-------+-+-------------+-------------------------------+ +|F|R|R|R| opcode|M| Payload len | Extended payload length | +|I|S|S|S| (4) |A| (7) | (16/64) | +|N|V|V|V| |S| | (if payload len==126/127) | +| |1|2|3| |K| | | ++-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + +| Extended payload length continued, if payload len == 127 | ++ - - - - - - - - - - - - - - - +-------------------------------+ +| |Masking-key, if MASK set to 1 | ++-------------------------------+-------------------------------+ +| Masking-key (continued) | Payload Data | ++-------------------------------- - - - - - - - - - - - - - - - + +: Payload Data continued ... : ++ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +| Payload Data continued ... | ++---------------------------------------------------------------+ +``` + +关键点: +- 需要按照WebSocket帧格式封装数据 +- 支持文本帧(opcode=0x1)和二进制帧(opcode=0x2) +- 客户端发送的数据需要mask(MASK=1) +- 服务器发送的数据不需要mask(MASK=0) + +## AT命令手册能力分析 + +### ✅ 可用的指令 + +#### 1. TCP连接管理 +- **AT+TCPSETUP** - 建立TCP连接(支持ws) + - 支持域名解析 + - 支持0-5路连接 + - 连接建立后返回OK + +- **AT+SSLTCPSETUP** - 建立SSL TCP连接(支持wss) + - 支持TLS/SSL加密 + - 支持证书配置 + - 支持0-5路连接 + +#### 2. 数据发送能力 +- **AT+TCPSEND** - 发送TCP数据 + - **Buffer模式**: + - ASCII模式:最大4096字节 + - HEX模式:最大2048字节(二进制数据) + - **命令模式**: + - ASCII/HEX模式:最大512字节 + - 支持二进制数据传输(HEX模式) + +- **AT+SSLTCPSEND** - 发送SSL TCP数据 + - 最大4096字节 + - 支持二进制数据传输 + +#### 3. 数据接收能力 +- **+TCPRECV** - 接收TCP数据主动上报 +- **AT+TCPREAD** - 读取TCP数据 +- **+SSLTCPRECV** - 接收SSL TCP数据主动上报 +- **AT+SSLTCPREAD** - 读取SSL TCP数据 +- **AT+RECVMODE** - 设置接收模式(ASCII/HEX) + +#### 4. HTTP指令(用于握手) +- **AT+HTTPSETUP/AT+HTTPSSETUP** - 建立HTTP/HTTPS连接 +- **AT+HTTPACTION/AT+HTTPSACTION** - 发送HTTP请求 +- 或者使用TCP指令手动发送HTTP握手请求 + +### ⚠️ 限制和挑战 + +#### 1. 数据长度限制 +- TCP buffer模式HEX:最大2048字节 +- SSL TCP:最大4096字节 +- WebSocket帧可能超过这些限制(需要分片) + +#### 2. WebSocket帧封装 +- **需要手动实现**WebSocket帧的编码/解码 +- 需要处理: + - 帧头构造(FIN, opcode, MASK, payload length) + - Masking-key生成和应用 + - 扩展payload length(126/127字节) + - 分片处理(FIN=0的连续帧) + +#### 3. 握手处理 +- 需要生成WebSocket握手请求 +- 需要解析服务器握手响应 +- 需要验证Sec-WebSocket-Accept + +#### 4. 实时性要求 +- WebSocket需要及时处理接收到的数据 +- 需要轮询或使用主动上报机制 + +## 实现方案 + +### 方案1:使用TCP/SSL TCP指令(推荐) + +#### 优点 +- ✅ 完全控制数据传输 +- ✅ 支持二进制数据(HEX模式) +- ✅ 支持长连接 +- ✅ 可以处理任意大小的数据(通过分片) + +#### 实现步骤 + +**1. 建立连接(ws)** +``` +AT+TCPSETUP=0,websocket.example.com,80 +``` + +**2. 发送WebSocket握手请求** +``` +AT+TCPSEND=0, +> GET /path HTTP/1.1\r\n + Host: websocket.example.com\r\n + Upgrade: websocket\r\n + Connection: Upgrade\r\n + Sec-WebSocket-Key: \r\n + Sec-WebSocket-Version: 13\r\n + \r\n +``` + +**3. 接收握手响应** +``` ++TCPRECV: 0, +HTTP/1.1 101 Switching Protocols +Upgrade: websocket +Connection: Upgrade +Sec-WebSocket-Accept: +``` + +**4. 发送WebSocket帧(文本)** +``` +// 构造WebSocket帧 +// FIN=1, opcode=0x1(文本), MASK=1, payload_len, masking_key, masked_payload +AT+TCPSEND=0,,1 // HEX模式 +> +``` + +**5. 发送WebSocket帧(二进制)** +``` +// 构造WebSocket帧 +// FIN=1, opcode=0x2(二进制), MASK=1, payload_len, masking_key, masked_payload +AT+TCPSEND=0,,1 // HEX模式 +> +``` + +**6. 接收WebSocket帧** +``` ++TCPRECV: 0, +// 解析帧头,提取payload +``` + +### 方案2:使用HTTP指令(不推荐) + +#### 缺点 +- ❌ HTTP指令主要用于请求-响应模式 +- ❌ 不支持长连接的双向通信 +- ❌ 难以处理WebSocket帧 + +## 关键实现点 + +### 1. WebSocket帧编码(发送) + +```python +def encode_websocket_frame(payload, opcode=0x1, fin=1): + """ + 编码WebSocket帧 + opcode: 0x1=文本, 0x2=二进制 + """ + # 1. 构造帧头 + frame = bytearray() + + # 第一个字节:FIN(1) + RSV(000) + opcode(4) + first_byte = (fin << 7) | opcode + frame.append(first_byte) + + # 2. 生成masking key(客户端必须mask) + masking_key = os.urandom(4) + + # 3. 第二个字节:MASK(1) + payload length + payload_len = len(payload) + if payload_len < 126: + second_byte = 0x80 | payload_len # MASK=1 + frame.append(second_byte) + elif payload_len < 65536: + frame.append(0x80 | 126) # MASK=1, len=126 + frame.extend(payload_len.to_bytes(2, 'big')) + else: + frame.append(0x80 | 127) # MASK=1, len=127 + frame.extend(payload_len.to_bytes(8, 'big')) + + # 4. 添加masking key + frame.extend(masking_key) + + # 5. 对payload进行mask + masked_payload = bytearray() + for i, byte in enumerate(payload): + masked_payload.append(byte ^ masking_key[i % 4]) + + frame.extend(masked_payload) + return bytes(frame) +``` + +### 2. WebSocket帧解码(接收) + +```python +def decode_websocket_frame(data): + """ + 解码WebSocket帧 + """ + if len(data) < 2: + return None, data + + # 解析第一个字节 + first_byte = data[0] + fin = (first_byte >> 7) & 0x1 + opcode = first_byte & 0x0F + + # 解析第二个字节 + second_byte = data[1] + mask = (second_byte >> 7) & 0x1 + payload_len = second_byte & 0x7F + + # 解析扩展payload length + offset = 2 + if payload_len == 126: + if len(data) < 4: + return None, data + payload_len = int.from_bytes(data[2:4], 'big') + offset = 4 + elif payload_len == 127: + if len(data) < 10: + return None, data + payload_len = int.from_bytes(data[2:10], 'big') + offset = 10 + + # 解析masking key(服务器发送的不mask) + masking_key = None + if mask: + if len(data) < offset + 4: + return None, data + masking_key = data[offset:offset+4] + offset += 4 + + # 读取payload + if len(data) < offset + payload_len: + return None, data + + payload = data[offset:offset+payload_len] + + # 如果masked,进行unmask + if mask and masking_key: + payload = bytearray(payload) + for i in range(len(payload)): + payload[i] ^= masking_key[i % 4] + payload = bytes(payload) + + # 返回帧信息和剩余数据 + frame_info = { + 'fin': fin, + 'opcode': opcode, + 'payload': payload, + 'is_text': opcode == 0x1, + 'is_binary': opcode == 0x2 + } + + remaining = data[offset+payload_len:] + return frame_info, remaining +``` + +### 3. 握手请求生成 + +```python +import base64 +import hashlib +import secrets + +def generate_websocket_handshake(host, path, port=80): + """ + 生成WebSocket握手请求 + """ + # 生成Sec-WebSocket-Key + key = base64.b64encode(secrets.token_bytes(16)).decode('utf-8') + + handshake = ( + f"GET {path} HTTP/1.1\r\n" + f"Host: {host}:{port}\r\n" + f"Upgrade: websocket\r\n" + f"Connection: Upgrade\r\n" + f"Sec-WebSocket-Key: {key}\r\n" + f"Sec-WebSocket-Version: 13\r\n" + f"\r\n" + ) + + return handshake, key +``` + +## 结论 + +### ✅ **可以实现WebSocket(ws和wss)** + +**理由:** +1. ✅ TCP/SSL TCP指令支持建立连接 +2. ✅ 支持二进制数据传输(HEX模式) +3. ✅ 支持文本数据传输(ASCII模式) +4. ✅ 支持长连接 +5. ✅ 可以手动发送HTTP握手请求 + +### ⚠️ **需要额外实现** + +1. **WebSocket帧编码/解码** - 需要手动实现 +2. **握手处理** - 需要生成和解析HTTP握手 +3. **分片处理** - 处理大于2048/4096字节的数据 +4. **心跳/Ping-Pong** - 保持连接活跃 +5. **错误处理** - 处理连接断开、超时等 + +### 📊 **数据长度限制** + +| 传输类型 | 最大长度 | 解决方案 | +|---------|---------|---------| +| TCP HEX模式 | 2048字节 | 分片发送(FIN=0) | +| SSL TCP | 4096字节 | 分片发送(FIN=0) | +| 文本数据 | 无限制 | 分片处理 | + +### 🎯 **推荐实现方式** + +**使用TCP/SSL TCP指令 + 手动实现WebSocket协议层** + +1. 使用 `AT+TCPSETUP` / `AT+SSLTCPSETUP` 建立连接 +2. 使用 `AT+TCPSEND` / `AT+SSLTCPSEND` 发送握手和帧数据(HEX模式) +3. 使用 `+TCPRECV` / `+SSLTCPRECV` 接收数据 +4. 在应用层实现WebSocket帧的编码/解码 + +### 📝 **注意事项** + +1. **数据分片**:超过2048/4096字节的数据需要分片发送 +2. **实时性**:需要及时处理接收到的数据,避免buffer溢出 +3. **错误恢复**:实现重连机制 +4. **性能优化**:合理使用buffer模式和命令模式 + +## 总结 + +### ✅ **结论:AT指令集中的TCP和SSL TCP指令足够实现WebSocket接口** + +**核心能力:** +1. ✅ **连接建立**:`AT+TCPSETUP`(ws)和 `AT+SSLTCPSETUP`(wss) +2. ✅ **数据发送**:`AT+TCPSEND` / `AT+SSLTCPSEND` 支持二进制(HEX模式)和文本(ASCII模式) +3. ✅ **数据接收**:`+TCPRECV` / `+SSLTCPRECV` 主动上报,支持二进制和文本 +4. ✅ **长连接**:TCP/SSL TCP连接支持保持连接,满足WebSocket要求 + +**需要手动实现的部分:** +1. ⚠️ **WebSocket握手**:生成HTTP握手请求,解析服务器响应 +2. ⚠️ **帧封装/解析**:按照RFC 6455标准实现WebSocket帧的编码和解码 +3. ⚠️ **数据分片**:处理超过2048/4096字节的数据(使用FIN=0分片) +4. ⚠️ **心跳机制**:实现Ping/Pong帧保持连接活跃 + +**推荐实现方式:** +- 使用 `AT+TCPSETUP` / `AT+SSLTCPSETUP` 建立连接 +- 使用 `AT+TCPSEND` / `AT+SSLTCPSEND` 发送握手和WebSocket帧(HEX模式) +- 使用 `+TCPRECV` / `+SSLTCPRECV` 接收数据 +- 在应用层实现WebSocket协议栈(帧封装、握手、心跳等) + +**数据长度限制:** +- TCP HEX模式:最大2048字节/帧(可通过分片解决) +- SSL TCP:最大4096字节/帧(可通过分片解决) + +**总结:AT指令集提供了足够的底层传输能力,但需要在应用层实现WebSocket协议层。** + diff --git "a/code/sdk/zhihui/ppp/doc/Neoway_\345\212\237\350\203\275\346\250\241\345\235\227_SSL_TLS_\345\272\224\347\224\250\346\214\207\345\215\227_V1.0.md" "b/code/sdk/zhihui/ppp/doc/Neoway_\345\212\237\350\203\275\346\250\241\345\235\227_SSL_TLS_\345\272\224\347\224\250\346\214\207\345\215\227_V1.0.md" new file mode 100644 index 000000000..f6710361e --- /dev/null +++ "b/code/sdk/zhihui/ppp/doc/Neoway_\345\212\237\350\203\275\346\250\241\345\235\227_SSL_TLS_\345\272\224\347\224\250\346\214\207\345\215\227_V1.0.md" @@ -0,0 +1,3378 @@ +# SSL/TLS应用指南 + +### SSL/TLS 应用指南 + +###### 版本 1.0 日期 2021-08-30 + +Neoway SSL/TLS 应用指南 + +版权声明 + +深圳市有方科技股份有限公司版权所有 +ii + +###### 版权声明 + +版权所有 © 深圳市有方科技股份有限公司 2021。深圳市有方科技股份有限公司保留所有权利。 + +未经深圳市有方科技股份有限公司书面同意,任何单位和个人不得擅自摘抄、复制本文档内容的部分或全部, +并不得以任何形式传播。 + +是深圳市有方科技股份有限公司所有商标。 + +本文档中出现的其他商标,由商标所有者所有。 + +###### 说明 + +本文档对应产品为Neoway 模块。 + +本文档的使用对象为系统工程师,开发工程师及测试工程师。 + +本设计指南为客户产品设计提供支持,客户须按照本文中的规范和参数进行产品设计和调试。如因客户操作 +不当造成的人身伤害和财产损失,有方概不承担责任。 + +由于产品版本升级或其它原因,本文档内容会在不预先通知的情况下进行必要的更新。 + +除非另有约定,本文档中的所有陈述、信息和建议不构成任何明示或暗示的担保。 + +深圳市有方科技股份有限公司为客户提供全方位的技术支持,任何垂询请直接联系您的客户经理或发送邮件 +至以下邮箱: + +Sales@neoway.com + +Support@neoway.com + +公司网址:http://www.neoway.com + +Neoway SSL/TLS 应用指南 + +目录 + +深圳市有方科技股份有限公司版权所有 +iii + +#### 目 录 + +###### 关于本文档 .................................................................................................... v + +范围 ..................................................................................................................................................... v + +读者对象 .............................................................................................................................................. v + +修订记录 .............................................................................................................................................. v + +符号约定 .............................................................................................................................................. v + +相关文档 .............................................................................................................................................. v + +###### 1 概述 ........................................................................................................... 6 + +1.1 SSL/TLS 简介 ............................................................................................................................... 6 + +1.2 SSL/TLS 使用流程 ........................................................................................................................ 7 + +1.3 注意事项 ...................................................................................................................................... 8 + +###### 2 HTTPS 应用流程........................................................................................ 9 + +2.1 无需认证 ...................................................................................................................................... 9 + +2.2 单向认证 ..................................................................................................................................... 11 + +2.3 双向认证 .................................................................................................................................... 12 + +2.4 示例 ........................................................................................................................................... 14 + +2.4.1 无需认证 ........................................................................................................................... 14 + +2.4.2 单向认证 ........................................................................................................................... 15 + +2.4.3 双向认证 ........................................................................................................................... 15 + +###### 3 MQTTS 应用流程 ..................................................................................... 17 + +3.1 不加密 ........................................................................................................................................ 17 + +3.2 加密 ........................................................................................................................................... 18 + +3.2.1 单向认证 ........................................................................................................................... 18 + +3.2.2 双向认证 ........................................................................................................................... 19 + +3.3 示例 ........................................................................................................................................... 20 + +3.3.1 单向认证 ........................................................................................................................... 20 + +3.3.2 双向认证 ........................................................................................................................... 21 + +###### 4 TCPS 应用流程 ........................................................................................ 23 + +4.1 无需认证 .................................................................................................................................... 23 + +4.2 单向认证 .................................................................................................................................... 23 + +4.3 双向认证 .................................................................................................................................... 24 + +4.4 示例 ........................................................................................................................................... 25 + +4.4.1 无需认证 ........................................................................................................................... 25 + +4.4.2 单向认证 ........................................................................................................................... 26 + +4.4.3 双向认证 ........................................................................................................................... 26 + +Neoway SSL/TLS 应用指南 + +目录 + +深圳市有方科技股份有限公司版权所有 +iv + +###### A 相关AT 指令 ........................................................................................... 28 + +A.1 HTTPS 相关指令 ........................................................................................................................ 28 + +A.1.1 AT+CERTADD–添加SSL 证书 ......................................................................................... 28 + +A.1.2 AT+CERTCHECK–SSL 证书确认 ................................................................................... 28 + +A.1.3 AT+HTTPSCFG–配置HTTPS 参数 ................................................................................. 29 + +A.1.4 AT+HTTPSPARA–设置HTTPS 参数 ............................................................................... 30 + +A.1.5 AT+HTTPSSETUP–建立HTTPS 链路 ............................................................................ 31 + +A.1.6 AT+HTTPSACTION–发送HTTPS 请求 ........................................................................... 32 + +A.1.7 AT+HTTPSCLOSE–主动关闭HTTPS 链路 ..................................................................... 32 + +A.2 MQTTS 相关指令 ....................................................................................................................... 33 + +A.2.1 AT+CERTADD–添加SSL 证书 ......................................................................................... 33 + +A.2.2 AT+CERTCHECK–SSL 证书确认 ................................................................................... 34 + +A.2.3 AT+MQTTTLS - TLS 参数配置 .......................................................................................... 34 + +A.2.4 AT+MQTTCONNPARAM–用户参数设置 ......................................................................... 35 + +A.2.5 AT+MQTTWILLPARAM–遗嘱设置 ................................................................................... 36 + +A.2.6 AT+MQTTCONN–连接命令 ............................................................................................. 37 + +A.2.7 AT+MQTTSUB–订阅命令 ................................................................................................ 37 + +A.2.8 AT+MQTTPUB–发布主题 ................................................................................................ 38 + +A.2.9 AT+MQTTDISCONN–关闭MQTT 连接 ........................................................................... 39 + +A.3 TCPS 相关指令 .......................................................................................................................... 39 + +A.3.1 AT+CERTADD–添加SSL 证书 ......................................................................................... 39 + +A.3.2 AT+CERTCHECK–SSL 证书确认 ................................................................................... 40 + +A.3.3 AT+SSLTCPCFG–SSL TCP 配置参数 ............................................................................. 41 + +A.3.4 AT+SSLTCPSETUP–建立SSL TCP 连接 ....................................................................... 42 + +A.3.5 AT+SSLTCPSEND–SSL TCP 数据发送 ............................................................................ 43 + +A.3.6 AT+SSLTCPCLOSE–SSL TCP 关闭连接指令 ................................................................. 44 + +###### B 初始化流程 ............................................................................................. 45 + +Neoway SSL/TLS 应用指南 + +关于本文档 + +深圳市有方科技股份有限公司版权所有 +v + +#### 关于本文档 + +##### 范围 + +本文档对应产品为Neoway 模块,主要介绍了有方模块是如何使用AT 指令实现SSL/TLS 加密传 + +输功能的。适用的模块有:N58、N77、N716、N720、N720V5 等,其他模块是否适用请参考具体AT + +指令集或者寻问对应的技术支持。 + +##### 读者对象 + +本文档的使用对象为系统工程师,开发工程师及测试工程师。 + +##### 修订记录 + +版本 +日期 +变更 +作者 + +1.0 +2021-05 +初始版本 +Thea Chen + +##### 符号约定 + +符号 +含义 + +危险或警告,用户必须遵从的规则,否则会造成模块或客户设备不可逆的故障损坏, + +甚至可能造成人员身体伤害。 + +注意,警示用户使用模块时应该特别注意的地方,如不遵从,模块或客户设备可能 + +出现故障。 + +说明或提示,提供模块使用的意见或建议。 + +##### 相关文档 + +《Neoway_功能模块_HTTP 应用指南》 + +《Neoway_功能模块_MQTT 应用指南》 + +Neoway SSL/TLS 应用指南 + +第1 章概述 + +深圳市有方科技股份有限公司版权所有 +6 + +#### 1 概述 + +本应用文档主要描述有方功能模块使用AT 指令实现SSL/TLS 加密传输功能的应用流程。在进行服 + +务器-客户端之间的数据交互时,经常会有确保数据安全传输的需要,用户只需要使用有方AT 指令集提 + +供的相应指令即可实现安全的数据交互,如:HTTPS、MQTTS、TCPS。 + +##### 1.1 SSL/TLS 简介 + +SSL(Secure Sockets Layer)安全套接字层及其继任者TLS(Transport Layer Security)传输层 + +安全协议是为网络通信提供安全及数据完整性的一种安全协议。SSL 作用在端系统应用层和运输层之 + +间,在TCP 之上建立起一个安全通道,为通过TCP 传输的应用层数据提供安全保障,如下图。 + +图 1-1 SSL/TLS 所处TCP/IP 层次 + +Application layer protocol + +TCP + +IP + +SSL/TLS + +SSL 协议是Netscape 公司在1994 年开发的安全协议,广泛应用于基于万维网的各种网络应用(但 + +不限于万维网应用)。1995 å¹´ Netscape 公司把SSL 转交给IETF,希望能够把SSL 进行标准化。于是 + +IETF 在 SSL3.0 的基础上设计了TLS 协议,目前为了应对网络安全的变化,IETF 及时地对TLS 的版 + +本进行升级,如2008 å¹´8 月的互联网建议标准TLS1.2。 + +目前有方模块支持的SSL/TLS 协议版本如下表所示: + +表 1-1 SSL/TLS 版本 + +参数值 +SSL/TLS 版本 + +0 +SSL3.0 + +1 +TLS1.0 + +2 +TLS1.1 + +3 +TLS1.2 + +Neoway SSL/TLS 应用指南 + +第1 章概述 + +深圳市有方科技股份有限公司版权所有 +7 + +SSL/TLS 协议分两层,第一层是记录协议,主要用于传输数据的加密压缩;第二层是握手协议,它 + +建立在第一层协议之上,主要用于数据传输前的双方身份认证、协商加密算法、交换密钥,以及警告协 + +议,用于在协商出错时发送警告消息。SSL 验证过程具体分为单向认证和双向认证。 + +图 1-2 SSL/TLS 协议 + +SSL handshake protocol + +SSL alert protocol + +SSL change cipher spec + +protocol + +Application layer protocol + +SSL record protocol + +IP + +TCP + +单向认证和双向认证对客户端和服务器端都有不同的要求,如下表: + +表 1-2 认证模式 + +参数值 +认证模式 +客户端要求 +服务器端要求 + +0 +无需认证 +无 +无 + +1 +单向认证 +ca.crt +server.crt、server.key + +2 +双向认证 +ca.crt、client.crt、client.key +ca.crt、server.crt、server.key + +##### 1.2 SSL/TLS 使用流程 + +SSL/TLS 功能在Neoway 模块上的具体应用有HTTPS、MQTTS 和TCPS,以下是如何使用 + +SSL/TLS 功能的具体步骤: + +步骤 1:模块进行初始化操作,具体流程请参考附录B 。 + +步骤 2:模块进行拨号上网。 + +步骤 3:配置加密参数。 + +1. +选择SSL/TLS 协议版本。 + +2. +选择安全认证模式。 + +3. +配置相应证书文件,以便在握手过程中使用。 + +Neoway SSL/TLS 应用指南 + +第1 章概述 + +深圳市有方科技股份有限公司版权所有 +8 + +根据所选安全认证模式不同添加证书内容也不同。 + +当安全认证模式设置为无需认证时不需要此步骤。 + +4. +确认各加密参数成功设置。 + +步骤 4:设置服务器连接参数,进行相应的HTTPS 连接、MQTTS 连接或者TCPS 连接。 + +##### 1.3 注意事项 + +建立连接过程可能会失败,以下几点需要注意: + +⚫ +确保模块已经成功激活PDP,获取到IP 并且可以成功上网,一般是使用指令 + +“AT+PING=”进行测试。 + +⚫ +确保证书等文件已成功导入: + +指令“AT+CERTADD=,”中,“length”字段一定要和导入的证书长度一 + +致,单位为字节(Byte)。可以右击证书文件,在“属性”查看证书文件的大小是多少字节。 + +⚫ +确保证书文件已正确配置: + +无需认证不需要配置证书,单向认证需要配置CA 证书,双向认证需要配置CA 证书、客户端证 + +书、客户端密钥。 + +⚫ +确保加密参数是否正确配置: + +− +所配置的sslversion 服务器是否支持; + +− +所配置的authmode 是否和配置的证书文件匹配。 + +⚫ +确保连接参数已正确配置: + +− +确认目标路径是否正确无误,目标路径可以是IP 地址或者域名。 + +− +确认目标端口号是否与服务器所用端口号一致。 + +⚫ +模块使用POST 方式进行HTTPS 请求时: + +− +确保POST 请求方式中所配置的请求数据类型与实际请求数据一致。例如,若请求为xml æ ¼ + +式,请求参数配置时就需要配置为3,若请求为html 格式则需要将请求参数配置为4。 + +− +此外,还需要确认所配置的请求长度与实际请求长度一致。 + +⚫ +模块进行MQTTS 请求时: + +需要确认连接参数clientdID、username、password 是否正确。 + +Neoway SSL/TLS 应用指南 + +HTTPS 应用流程 + +深圳市有方科技股份有限公司版权所有 +9 + +#### 2 HTTPS 应用流程 + +HTTPS (Secure Hypertext Transfer Protocol)安全超文本传输协议,HTTPS 是在HTTP 上建立 + +SSL 加密层,并对传输数据进行加密,是HTTP 协议的安全版,简而言之就是: + +HTTPS = HTTP + 加密 + 身份认证 + 完整性保护 + +图 2-1 HTTPS 协议 + +###### HTTP + +###### TCP + +###### IP + +###### SSL/TLS + +###### HTTP + +###### TCP + +###### IP + +通常,HTTP 直接和TCP 通信,当使用SSL 时,则先和SSL 通信,再由SSL 和TCP 通信。在采 + +用SSL 后,HTTP 就拥有了HTTPS 的加密、认证和完整性保护这些功能。 + +在实际应用中,按照身份认证的不同需求,又可以分为无需认证,单向认证和双向认证。 + +##### 2.1 无需认证 + +配置无需认证时,HTTPS 的客户端或者服务器端就会接受不受信的证书,相当于不认证传输数据 + +前通信双方的身份,而是直接进行之后的协商加密算法、交换密钥过程。虽然数据是经过加密进行传输, + +但是由于未进行身份认证,仍有风险,需谨慎使用。 + +步骤 1:模块进行初始化操作,具体流程请参考附录B 。 + +步骤 2:模块进行拨号上网。 + +步骤 3:配置HTTPS 加密参数。 + +1. +配置SSL/TLS 协议版本。 + +例如,指令“AT+HTTPSCFG=sslversion,3”,表示配置SSL/TLS 协议版本为TLS1.2。 + +2. +执行“AT+HTTPSCFG=authmode,0”指令,将SSL/TLS 认证方式配置为无需认证。 + +Neoway SSL/TLS 应用指南 + +HTTPS 应用流程 + +深圳市有方科技股份有限公司版权所有 +10 + +当SSL/TLS 认证方式配置为无需认证时,不需要配置证书。 + +步骤 4:配置HTTPS 连接参数。 + +1. +配置目标路径。 + +例如,“AT+HTTPSPARA=url, www.ip138.com”,表示目标路径为IP 地址查询网。 + +2. +配置目标端口。 + +例如,“AT+HTTPSPARA=port,443”,表示目标端口号为443(默认端口),若是自己搭建的 + +HTTPS 服务器则需根据所设端口进行更改。 + +3. +执行“AT+HTTPSPARA=keepalive,1”指令配置长连接(可选)。 + +HTTPS 中长连接是默认关闭的,即每次发送完一次请求之后HTTPS 连接就会关闭,再次请求的 + +话就需要重新建立连接。如果希望建立一次连接后可以多次请求,则可执行该操作。 + +步骤 5:执行“AT+HTTPSSETUP”指令建立HTTPS 连接。 + +步骤 6:发送HTTPS 请求。 + +用户根据自身需求选择不同请求方式,当服务器收到请求并进行处理后,会返回响应的数据,客 + +户端收到后会主动上报收到的内容。 + +1. +使用指令“AT+HTTPSACTION=0”,表示使用GET 请求方式,读取步骤4 中所设URL 中的资源 + +内容。 + +2. +使用指令“AT+HTTPSACTION=2,446,3”,表示使用POST 请求方式,用于向指定URL 资源提交 + +一大小为446 字节(需与之后POST 内容大小一致)的xml 格式数据进行处理请求。使用此指令 + +后会出现一个尖括号“>”,这时上传POST 内容,上传完毕后服务器即会返回处理后的内容。 + +Neoway 模块支持的HTTPS 请求方式有4 种,用户可根据自身需要进行选择: + +表 2-1 HTTPS 请求方式 + +参数值 +SSL/TLS 请求方式 +意义 + +0 +GET +请求读取由URL 所标志的信息 + +1 +HEAD +请求读取由URL 所标志的信息首部 + +2 +POST +向指定资源提交数据进行处理请求 + +99 +OPEN_MODE +用户自己定义报文模式 + +POST 请求的数据类型有5 种,用户根据自身请求内容类型进行选择: + +Neoway SSL/TLS 应用指南 + +HTTPS 应用流程 + +深圳市有方科技股份有限公司版权所有 +11 + +表 2-2 POST 请求的数据类型 + +参数值 +POST 请求的数据类型 + +0 +x-www-form-urlencoded + +1 +text + +2 +json + +3 +xml + +4 +html + +步骤 7:使用“AT+HTTPSCLOSE”指令关闭HTTPS 连接。 + +##### 2.2 单向认证 + +配置单向认证时,HTTPS 的客户端将会验证服务器的身份,验证通过后才会进行之后的协商加密 + +算法以及交换密钥过程。此认证方式下,客户端需要CA 证书,服务器端需要服务器证书和服务器密钥。 + +目前这一认证方式是HTTPS 使用最广泛的一种。 + +步骤 1:模块进行初始化操作,具体流程请参考附录B 。 + +步骤 2:模块进行拨号上网。 + +步骤 3:配置HTTPS 加密参数。 + +1. +配置SSL/TLS 协议版本。 + +例如,指令“AT+HTTPSCFG=sslversion,3”,表示配置SSL/TLS 协议版本为TLS1.2。 + +2. +执行“AT+HTTPSCFG=authmode,1”指令将SSL/TLS 认证方式配置为单向认证。 + +3. +向模块写入CA 证书。 + +例如,使用指令“AT+CERTADD=ca.crt,1196”,表示向模块写入一份名为ca.crt、长度为1196 + +字节的CA 证书文件。 + +当SSL/TLS 认证方式配置为单向认证时,需要向客户端(模块)配置CA 证书。 + +4. +执行“AT+CERTCHECK=ca.crt”指令,确认证书是否成功写入。 + +5. +执行“AT+HTTPSCFG=cacert,ca.crt”指令,将写入的CA 证书配置到客户端(模块端)。 + +6. +执行“AT+HTTPSCFG?”指令,确认当前SSL/TLS 加密参数配置内容是否正确。 + +步骤 4:配置HTTPS 连接参数。 + +Neoway SSL/TLS 应用指南 + +HTTPS 应用流程 + +深圳市有方科技股份有限公司版权所有 +12 + +1. +配置目标路径。 + +例如,“AT+HTTPSPARA=url, www.ip138.com”,表示目标路径为IP 地址查询网。 + +2. +配置目标端口。 + +例如,“AT+HTTPSPARA=port,443”,表示将目标端口号设置为443(默认端口),若是自己搭建 + +的HTTPS 服务器则需根据所设端口进行更改。 + +3. +执行“AT+HTTPSPARA=keepalive,1”指令配置长连接(可选)。 + +HTTPS 中长连接是默认关闭的,即每次发送完一次请求之后HTTPS 连接就会关闭,再次请求的 + +话就需要重新建立连接。如果希望建立一次连接后可以多次请求,则可执行该操作。 + +步骤 5:执行“AT+HTTPSSETUP”指令建立HTTPS 连接。 + +步骤 6:发送HTTPS 请求。 + +用户根据自身需求选择不同请求方式,当服务器收到请求并进行处理后,会返回响应的数据,客 + +户端收到后会主动上报收到的内容。 + +1. +使用指令“AT+HTTPSACTION=0”,表示使用GET 请求方式,读取步骤4 中所设URL 中的资源 + +内容。 + +2. +使用指令“AT+HTTPSACTION=2,446,3”,表示使用POST 请求方式,用于向指定URL 资源提交 + +一大小为446 字节(需与之后POST 内容大小一致)的xml 格式数据进行处理请求。使用此指令 + +后会出现一个尖括号“>”,这时上传POST 内容,上传完毕后服务器即会返回处理后的内容。 + +步骤 7:使用“AT+HTTPSCLOSE” 指令关闭HTTPS 连接。 + +##### 2.3 双向认证 + +双向认证和单向认证原理基本差不多,只是除了客户端需要认证服务端以外,增加了服务端对客户 + +端的认证。在此认证模式下,客户端需要CA 证书、客户端证书与客户端密钥,服务器端需要CA 证书、 + +服务器证书与服务器密钥。 + +步骤 1:模块进行初始化操作,具体流程请参考附录B 。 + +步骤 2:模块进行拨号上网。 + +步骤 3:配置HTTPS 加密参数。 + +1. +配置SSL/TLS 协议版本。 + +例如,指令“AT+HTTPSCFG=sslversion,3”,表示配置SSL/TLS 协议版本为TLS1.2。 + +2. +执行“AT+HTTPSCFG=authmode,2”指令将SSL/TLS 认证方式配置为双向认证。 + +3. +向模块写入CA 证书。 + +Neoway SSL/TLS 应用指南 + +HTTPS 应用流程 + +深圳市有方科技股份有限公司版权所有 +13 + +使用指令“AT+CERTADD=ca.crt,1196”,表示向模块写入一份名为ca.crt、长度为1196 字节的 + +CA 证书文件。 + +当认证方式配置为双向认证时,需要向客户端(模块)写入CA 证书、客户端证书与客户端密钥。 + +4. +使用指令“AT+CERTCHECK=ca.crt”确认证书是否成功写入。 + +5. +使用指令“AT+HTTPSCFG=cacert,ca.crt”,表示将写入的CA 证书配置到客户端(模块端)。 + +6. +同理向客户端证书和客户端密钥,并进行确认。 + +使用指令“AT+CERTADD=client.crt,1204”、“AT+CERTADD=client.key,1743”,表示向模块写入 + +一份名为client.crt、长度为1204 字节的客户端证书文件和一份名为client.key、长度为1743 字 + +节的客户端密钥文件。 + +7. +使用指令“AT+CERTCHECK=client.crt”、“AT+CERTCHECK=client.key”确认证书和密钥是否成 + +功写入。 + +8. +使用指令“AT+HTTPSCFG=clientcacert,client.crt”、“AT+HTTPSCFG=clientkey,client.key”,表示 + +将写入的客户端证书和客户端密钥配置到客户端(模块端)。 + +9. +执行“AT+HTTPSCFG?”指令,确认当前SSL/TLS 加密参数配置内容是否正确。 + +步骤 4:配置HTTPS 连接参数。 + +1. +配置目标路径。 + +例如,"AT+HTTPSPARA=url, www.ip138.com”,表示目标路径为IP 地址查询网。 + +2. +配置端口。 + +例如,“AT+HTTPSPARA=port,443”,表示目标端口号为443(默认端口),若是自己搭建的 + +HTTPS 服务器则需根据所设端口进行更改。 + +3. +执行“AT+HTTPSPARA=keepalive,1”指令配置长连接(可选)。 + +HTTPS 中长连接是默认关闭的,即每次发送完一次请求之后HTTPS 连接就会关闭,再次请求的 + +话就需要重新建立连接。如果希望建立一次连接后可以多次请求,则可执行该操作。 + +步骤 5:执行“AT+HTTPSSETUP”指令建立HTTPS 连接。 + +步骤 6:发送HTTPS 请求。 + +用户根据自身需求选择不同请求方式,当服务器收到请求并进行处理后,会返回响应的数据,客 + +户端收到后会主动上报收到的内容。 + +⚫ +使用指令“AT+HTTPSACTION=0”,表示使用GET 请求方式,读取步骤4 中所设URL 中的资源 + +内容。 + +Neoway SSL/TLS 应用指南 + +HTTPS 应用流程 + +深圳市有方科技股份有限公司版权所有 +14 + +⚫ +使用指令“AT+HTTPSACTION=2,446,3”,表示使用POST 请求方式,用于向指定URL 资源提 + +交一大小为446 字节(需与之后POST 内容大小一致)的xml 格式数据进行处理请求。使用此指 + +令后会出现一个尖括号“>”,这时上传POST 内容,上传完毕后服务器即会返回处理后的内容。 + +步骤 7:使用“AT+HTTPSCLOSE” 指令关闭HTTPS 连接。 + +##### 2.4 示例 + +以下流程示例在执行的过程中需首先对模块进行初始化后,才能进行。初始化流程,详见附录B 。 + +###### 2.4.1 无需认证 + +``` +AT+XIIC=1 +``` + +OK + +``` +AT+XIIC? +``` + ++XIIC: 1,100.77.55.140 + +OK + +``` +AT+HTTPSCFG=sslversion,3 +``` + +OK + +``` +AT+HTTPSCFG=authmode,0 +``` + +OK + +``` +AT+HTTPSCFG? +``` + ++HTTPSCFG: 3,0,,, + +OK + +``` +AT+HTTPSPARA=url,www.ip138.com +``` + +OK + +``` +AT+HTTPSPARA=port,443 +``` + +OK + +``` +AT+HTTPSPARA=keepalive,1 +``` + +OK + +``` +AT+HTTPSSETUP +``` + +OK + +``` +AT+HTTPSACTION=1 +``` + +OK + ++HTTPSRECV: + +HTTP/1.1 200 OK + +Date: Thu, 18 Mar 2021 03:48:38 GMT + +Content-Type: text/html + +Content-Length: 18544 + +Connection: close + +Content-Location: http://www.ip138.com/index.htm + +Last-Modified: Thu, 11 Mar 2021 09:04:14 GMT + +Accept-Ranges: bytes + +ETag: "2ab018825516d71:218a" + +Server: Microsoft-IIS/6.0 + +X-Powered-By: ASP.NET + +Age: 54728 + +X-Via: 1.1 PSfjfzsx3ky59:9 (Cdn Cache Server V2.0), 1.1 PSjsczsxlm170:5 (Cdn Cache Server + +V2.0), 1.1 PS-TYN-011yu167:20 (Cdn Cache Server V2.0) + +X-Ws-Request-Id: 6052cd96_PS-TYN-011yu167_48200-28314 + +``` +AT+HTTPSCLOSE +``` + +Neoway SSL/TLS 应用指南 + +HTTPS 应用流程 + +深圳市有方科技股份有限公司版权所有 +15 + +OK + ++HTTPSCLOSE: HTTPS Link Closed + +###### 2.4.2 单向认证 + +``` +AT+XIIC=1 +``` + +OK + +``` +AT+XIIC? +``` + ++XIIC: 1,100.77.55.140 + +OK + +``` +AT+HTTPSCFG=sslversion,3 +``` + +OK + +``` +AT+HTTPSCFG=authmode,1 +``` + +OK + +``` +AT+CERTADD=ca.crt,1196 +``` + +CONNECT + ++CERTADD: 1196,OK + +``` +AT+HTTPSCFG=cacert,ca.crt +``` + +OK + +``` +AT+HTTPSCFG? +``` + ++HTTPSCFG: 3,1,ca.crt,, + +OK + +``` +AT+HTTPSPARA=url,183.239.240.40 +``` + +OK + +``` +AT+HTTPSPARA=port,12011 +``` + +OK + +``` +AT+HTTPSPARA=keepalive,1 +``` + +OK + +``` +AT+HTTPSSETUP +``` + +OK + +``` +AT+HTTPSACTION=1 +``` + +OK + ++HTTPSRECV: + +HTTP/1.1 200 OK + +Server: Apache-Coyote/1.1 + +Content-Type: text/html;charset=UTF-8 + +Content-Length: 0 + +Date: Thu, 18 Mar 2021 07:05:47 GMT + +``` +AT+HTTPSCLOSE +``` + +OK + ++HTTPSCLOSE: HTTPS Link Closed + +###### 2.4.3 双向认证 + +``` +AT+XIIC=1 +``` + +OK + +``` +AT+XIIC? +``` + ++XIIC: 1,100.77.55.140 + +OK + +``` +AT+HTTPSCFG=sslversion,3 +``` + +OK + +``` +AT+HTTPSCFG=authmode,2 +``` + +Neoway SSL/TLS 应用指南 + +HTTPS 应用流程 + +深圳市有方科技股份有限公司版权所有 +16 + +OK + +``` +AT+CERTADD=ca.crt,1196 +``` + +CONNECT + ++CERTADD: 1196,OK + +``` +AT+CERTADD=client.crt,1204 +``` + +CONNECT + ++CERTADD: 1204,OK + +``` +AT+CERTADD=client.key,1743 +``` + +CONNECT + ++CERTADD: 1743,OK + +``` +AT+HTTPSCFG=cacert,ca.crt +``` + +OK + +``` +AT+HTTPSCFG=clientcert,client.crt +``` + +OK + +``` +AT+HTTPSCFG=clientkey,client.key +``` + +OK + +``` +AT+HTTPSCFG? +``` + ++HTTPSCFG: 3,2,ca.crt,client.crt,client.key + +OK + +``` +AT+HTTPSPARA=url,183.239.240.40 +``` + +OK + +``` +AT+HTTPSPARA=port,12012 +``` + +OK + +``` +AT+HTTPSPARA=keepalive,1 +``` + +OK + +``` +AT+HTTPSSETUP +``` + +OK + +``` +AT+HTTPSACTION=1 +``` + +OK + ++HTTPSRECV: + +HTTP/1.1 200 OK + +Server: Apache-Coyote/1.1 + +Content-Type: text/html;charset=UTF-8 + +Content-Length: 0 + +Date: Thu, 18 Mar 2021 07:19:09 GMT + +``` +AT+HTTPSCLOSE +``` + +OK + ++HTTPSCLOSE: HTTPS Link Closed + +Neoway SSL/TLS 应用指南 + +MQTTS 应用流程 + +深圳市有方科技股份有限公司版权所有 +17 + +#### 3 MQTTS 应用流程 + +Neoway 模块MQTT 也开发了SSL/TLS 加密功能,称为MQTTS。与HTTPS 略有不同的是, + +MQTTS 参数配置有些差异:MQTT 底层SSL/TLS 协议默认支持TLS1.2,所以在进行安全加密参数设 + +置时只需要设置为加密和不加密。 + +表 3-1 MQTTS 安全加密模式 + +sslmode +安全加密模式 + +0 +不加密 + +1 +加密 + +安全认证模式则分为两类:单向认证和双向认证,但是参数设置略有不同。 + +表 3-2 MQTTS 安全认证模式 + +authmode +认证模式 +客户端要求 +服务器端要求 + +0 +单向认证 +ca.crt +server.crt、server.key + +1 +双向认证 +ca.crt、client.crt、client.key +ca.crt、server.crt、server.key + +##### 3.1 不加密 + +配置不加密时,MQTTS 流程与MQTT 一致,最终的MQTT 数据通信不会进行加密。 + +步骤 1:模块进行初始化操作,具体流程请参考附录B 。 + +步骤 2:模块进行拨号上网。 + +步骤 3:执行“AT+MQTTTLS=sslmode,0” 指令,将MQTTS 连接配置为不加密。 + +连接配置为不加密时不需配置认证证书。 + +步骤 4:配置MQTTS 连接参数。 + +1. +执行“AT+MQTTCONNPARAM=,, ”指令对用户设备ID、用 + +户名和密码进行设置。 + +2. +根据需要使用指令“AT+MQTTWILLPARAM=,, ,”设置 + +遗嘱主题(最大长度128Bytes)与遗嘱消息(最大长度1024Bytes)。 + +Neoway SSL/TLS 应用指南 + +MQTTS 应用流程 + +深圳市有方科技股份有限公司版权所有 +18 + +步骤 5:执行“AT+MQTTCONN=<"host">,,”指令建立MQTTS 连接。 + +“host”为服务器地址(url:port);“clean”为是否清除session,默认为0,表示不清除; + +keep_alive 时间取值范围为[20,180],单位为s,默认为60s。 + +步骤 6:执行“AT+MQTTPUB=,,<"topicname">,<"message">”指令 发布主题。 + +其中,“retained”为保留标志,“qos”为服务质量,“topicname”为发布主题(最大长度 + +128Bytes),“message”为发布消息(最大长度为1024Bytes)。 + +步骤 7:执行“AT+MQTTSUB=<"topicname">,”指令进行主题的订阅。 + +步骤 8:执行“AT+MQTTDISCONN”指令,使模块主动断开和服务端的连接,然后释放 MQTT 资源, + +参数设置也会释放,如果需要PUB 消息,需要重新发起参数设置再连接设备。 + +##### 3.2 加密 + +###### 3.2.1 单向认证 + +配置单向认证时,MQTTS 的客户端将会验证服务器的身份,验证身份后进行之后的协商加密算法 + +以及交换密钥过程,协商出通信密钥后进行加密通信。 + +步骤 1:模块进行初始化操作,具体流程请参考附录B 。 + +步骤 2:模块进行拨号上网。 + +步骤 3:配置MQTTS 加密参数。 + +1. +执行“AT+MQTTTLS=sslmode,1”指令,将MQTT 连接配置为安全加密模式。 + +2. +执行“AT+MQTTTLS=authmode,0”指令,将SSL/TLS 认证方式配置为单向认证。 + +3. +向模块写入CA 证书文件: + +例如,使用指令“AT+CERTADD= MQTTS-certificate.pem,1180 ”,表示向模块写入一份名为 + +MQTTS-certificate.pem、长度为1180 字节的CA 证书文件。 + +4. +使用指令“AT+CERTCHECK= MQTTS-certificate.pem”确认证书是否成功写入。 + +5. +使用指令“AT+MQTTTLS=rootca, MQTTS-certificate.pem”,将写入的CA 证书配置到客户端(模 + +块端)。 + +CA 证书的参数配置与HTTPS 有所不同: + +⚫ +MQTTS 中指令AT+MQTTTLS=,中,字段为rootca; + +⚫ +而HTTPS 中指令AT+HTTPSCFG=, 中,字段为cacert。 + +Neoway SSL/TLS 应用指南 + +MQTTS 应用流程 + +深圳市有方科技股份有限公司版权所有 +19 + +6. +执行“AT+MQTTTLS?”指令,确认当前SSL/TLS 加密参数配置内容是否正确。 + +步骤 4:配置MQTTS 连接参数。 + +1. +使用指令“AT+MQTTCONNPARAM=,, ”对用户设备ID、用 + +户名和密码进行设置。 + +2. +根据需要使用指令“AT+MQTTWILLPARAM=,, ,”设置 + +遗嘱主题(最大长度128Bytes)与遗嘱消息(最大长度1024Bytes)。 + +步骤 5:使用指令“AT+MQTTCONN=<"host">,,”建立MQTTS 连接。 + +其中,“host”为服务器地址(url:port);“clean”为是否清除session,默认为0,表示不清除; + +keep_alive 时间取值范围为[20,180],单位为s,默认为60s。 + +步骤 6:执行“AT+MQTTPUB=,,<"topicname">,<"message">”指令发布主题。其中, + +“retained”为保留标志,“qos”为服务质量,“topicname”为发布主题(最大长度 + +128Bytes),“message”为发布消息(最大长度为1024 Bytes)。 + +步骤 7:执行“AT+MQTTSUB=<"topicname">,”指令进行主题的订阅。 + +步骤 8:执行“AT+MQTTDISCONN”指令,使模块主动断开和服务端的连接,然后释放 MQTT 资源, + +参数设置也会释放。 + +###### 3.2.2 双向认证 + +双向认证和单向认证原理基本相差不大,只是除了客户端需要认证服务端以外,增加了服务端对客 + +户端的认证。在此认证模式下,客户端需要CA 证书、客户端证书与客户端密钥,服务器端需要CA 证 + +书、服务器证书与服务器密钥。 + +步骤 1:模块进行初始化操作,具体流程请参考附录B 。 + +步骤 2:模块进行拨号上网。 + +步骤 3:配置MQTTS 加密参数。 + +1. +执行“AT+MQTTTLS=sslmode,1”指令,将MQTT 连接配置为安全加密模式。 + +2. +执行“AT+MQTTTLS=authmode,1”指令,再向模块写入CA 证书、客户端证书和客户端密钥。 + +3. +向模块写入CA 文件: + +使用指令“AT+CERTADD= rootca.pem,1758”,表示向模块写入一份名为rootca.pem、长度为 + +1758 字节的CA 证书文件。指令发送后会出现“CONNECT”字样,这时将CA 证书文件导入, + +导入成功后会有回码“+CERTADD:1758,OK”,表示证书已成功写入模块。 + +当认证方式配置为双向认证时,需要向客户端(模块)写入CA 证书、客户端证书与客户端密钥。 + +Neoway SSL/TLS 应用指南 + +MQTTS 应用流程 + +深圳市有方科技股份有限公司版权所有 +20 + +4. +使用指令“AT+CERTCHECK= rootca.pem”确认证书是否成功写入。 + +5. +使用指令“AT+MQTTTLS=rootca, rootca.pem”,表示将写入的CA 证书配置到客户端(模块端)。 + +6. +同理向客户端证书和客户端密钥,并进行确认。 + +使用指令“AT+CERTADD= nwy_all.cert.pem,1220”、“AT+CERTADD= + +nwy_all.private.key,1675”,表示向模块写入一份名为nwy_all.cert.pem、长度为1220 字节的客 + +户端证书文件和一份名为nwy_all.private.key、长度为1675 字节的客户端密钥文件。 + +7. +使用指令“AT+CERTCHECK= nwy_all.cert.pem”、“AT+CERTCHECK= nwy_all.private.key”确 + +认证书和密钥是否成功写入。 + +8. +使用指令“ AT+MQTTTLS=clientcacert, nwy_all.cert.pem ”、“ AT+MQTTTLS=clientkey, + +nwy_all.private.key”,表示将写入的客户端证书和客户端密钥配置到客户端(模块端)。 + +9. +执行“AT+MQTTTLS?”指令,确认当前SSL/TLS 加密参数配置内容是否正确。 + +步骤 4:配置MQTTS 连接参数。 + +1. +使用指令“AT+MQTTCONNPARAM=,, ”对用户设备ID、用 + +户名和密码进行设置。 + +2. +根据需要使用指令“AT+MQTTWILLPARAM=,, ,”设置 + +遗嘱主题(最大长度128Bytes)与遗嘱消息(最大长度1024Bytes)。 + +步骤 5:使用指令“AT+MQTTCONN=<"host">,,”建立MQTTS 连接。 + +其中,“host”为服务器地址(url:port);“clean”为是否清除session,默认为0,表示不清除; + +keep_alive 时间取值范围为[20,180],单位为s,默认为60s。 + +步骤 6:执行“AT+MQTTPUB=,,<"topicname">,<"message">”指令发布主题。其中, + +“retained”为保留标志,“qos”为服务质量,“topicname”为发布主题(最大长度 + +128Bytes),“message”为发布消息(最大长度为1024Bytes)。 + +步骤 7:执行“AT+MQTTSUB=<"topicname">,”指令进行主题的订阅。 + +步骤 8:执行“AT+MQTTDISCONN”指令,使模块主动断开和服务端的连接,然后释放 MQTT 资源, + +参数设置也会释放。 + +##### 3.3 示例 + +以下流程示例在执行的过程中需首先对模块进行初始化后,才能进行。初始化流程,详见附录B 。 + +以下示例中,单向认证使用中国移动的OneNET 平台,双向认证使用亚马逊平台。 + +###### 3.3.1 单向认证 + +``` +AT+XIIC=1 +``` + +Neoway SSL/TLS 应用指南 + +MQTTS 应用流程 + +深圳市有方科技股份有限公司版权所有 +21 + +OK + +``` +AT+XIIC? +``` + ++XIIC: 1,10.193.121.203 + +``` +AT+MQTTTLS=sslmode,1 +``` + +OK + +``` +AT+MQTTTLS=authmode,0 +``` + +OK + +``` +AT+CERTADD=MQTTS-certificate.pem,1180 +``` + +CONNECT + ++CERTADD: 1180,OK + +``` +AT+CERTCHECK=MQTTS-certificate.pem +``` + ++CERTCHECK: MQTTS-certificate.pem,OK + +``` +AT+MQTTTLS=rootca,MQTTS-certificate.pem +``` + +OK + +``` +AT+MQTTTLS? +``` + ++MQTTTLS: 1,0,MQTTS-certificate.pem,, + +OK + +``` +AT+MQTTCONNPARAM="N58","421872","version=2018-10- +``` + +31&res=products%2F421872%2Fdevices%2FN58&et=1650974923&method=md5&sign=WKGlErACNTMV5n4XIAxwtQ%3 + +D%3D" OK + +``` +AT+MQTTCONN="183.230.40.16:8883",1,60 +``` + +OK + +###### 3.3.2 双向认证 + +``` +AT+XIIC=1 +``` + +OK + +``` +AT+XIIC? +``` + ++XIIC: 1,10.100.55.5 + +OK + +``` +AT+MQTTTLS=sslmode,1 +``` + +OK + +``` +AT+MQTTTLS=authmode,1 +``` + +OK + +``` +AT+CERTADD=rootca.pem,1758 +``` + +CONNECT + ++CERTADD: 1758,OK + +``` +AT+CERTADD=nwy_all.cert.pem,1220 +``` + +CONNECT + ++CERTADD: 1220,OK + +``` +AT+CERTADD=nwy_all.private.key,1675 +``` + +CONNECT + ++CERTADD: 1675,OK + +``` +AT+SSLTCPCFG=cacert,rootca.pem +``` + +OK + +``` +AT+SSLTCPCFG=clientcert,nwy_all.cert.pem +``` + +OK + +``` +AT+SSLTCPCFG=clientkey,nwy_all.private.key +``` + +OK + +``` +AT+MQTTTLS=rootca,rootca.pem +``` + +OK + +``` +AT+MQTTTLS=clientcert,nwy_all.cert.pem +``` + +OK + +Neoway SSL/TLS 应用指南 + +MQTTS 应用流程 + +深圳市有方科技股份有限公司版权所有 +22 + +``` +AT+MQTTTLS=clientkey,nwy_all.private.key +``` + +OK + +``` +AT+MQTTTLS? +``` + ++MQTTTLS: 1,1,rootca.pem,nwy_all.cert.pem,nwy_all.private.key + +OK + +``` +AT+MQTTCONNPARAM="a1epg1vh6w7hlk","","" +``` + +OK + +AT+MQTTCONN="a1epg1vh6w7hlk.iot.us-east-2.amazonaws.com:8883",1,60 +OK + +Neoway SSL/TLS 应用指南 + +TCPS 应用流程 + +深圳市有方科技股份有限公司版权所有 +23 + +#### 4 TCPS 应用流程 + +TCPS 与HTTPS 基本原理相差不大。TCP 协议加上SSL/TLS 协议,使得普通的TCP 连接变成了 + +可以安全传输数据的TCPS 连接。 + +和HTTPS 一样,按照身份认证可以分为无需认证,单向认证和双向认证。 + +##### 4.1 无需认证 + +配置无需认证时,TCPS 的客户端或者服务器端就会接受不受信的证书,相当于不认证传输数据前 + +通信双方的身份,而是直接进行之后的协商加密算法、交换密钥过程。 + +步骤 1:模块进行初始化操作,具体流程请参考附录B 。 + +步骤 2:模块进行拨号上网。 + +步骤 3:配置TCPS 加密参数。 + +1. +配置SSL/TLS 协议版本。 + +例如,指令“AT+SSLTCPCFG=sslversion,3”,表示配置SSL/TLS 协议版本为TLS1.2。 + +2. +执行“AT+ SSLTCPCFG =authmode,0”指令将SSL/TLS 认证方式配置为无需认证。 + +当SSL/TLS 认证方式配置无需认证时,不需要配置证书。 + +步骤 4:执行“AT+SSLTCPSETUP= ,,,”建立TCPS 连接。 + +步骤 5:执行“AT+SSLTCPSEND=,”向服务器发送数据。 + +当接收到服务器数据时,会主动上报收到的数据内容。 + +步骤 6:当需要主动关闭连接时可执行“AT+SSLTCPCLOSE=”关闭TCPS 连接。 + +##### 4.2 单向认证 + +配置单向认证时,TCPS 的客户端将会验证服务器的身份,验证通过后才会进行之后的协商加密算 + +法以及交换密钥过程。此认证方式下,客户端需要CA 证书,服务器端需要服务器证书和服务器密钥。 + +步骤 1:模块进行初始化操作,具体流程请参考附录B 。 + +Neoway SSL/TLS 应用指南 + +TCPS 应用流程 + +深圳市有方科技股份有限公司版权所有 +24 + +步骤 2:模块进行拨号上网。 + +步骤 3:配置TCPS 加密参数。 + +1. +配置SSL/TLS 协议版本。 + +例如,指令“AT+SSLTCPCFG=sslversion,3”,表示配置SSL/TLS 协议版本为TLS1.2。 + +2. +执行“AT+SSLTCPCFG =authmode,1”指令将SSL/TLS 认证方式配置为单向认证。 + +3. +向模块写入CA 证书。 + +例如,使用指令“AT+CERTADD=ca.crt, 1196”,表示向模块写入一份名为ca.crt、长度为1196 + +字节的CA 证书文件。 + +当SSL/TLS 认证方设置为单向认证时,需要向客户端(模块)配置CA 证书。 + +4. +执行“AT+CERTCHECK=ca.crt”指令,确认证书是否成功写入。 + +5. +执行“AT+SSLTCPCFG =cacert,ca.crt”指令将写入的CA 证书配置到客户端(模块端)。 + +6. +执行“AT+SSLTCPCFG?”指令查询当前SSL/TLS 参数配置,确认为所配置内容。 + +步骤 4:执行“AT+SSLTCPETUP= ,,,”建立TCPS 连接。 + +步骤 5:执行“AT+SSLTCPSEND=,”向服务器发送数据。 + +当接收到服务器数据时,会主动上报收到的数据内容。 + +步骤 6:需要关闭连接时可执行“AT+SSLTCPCLOSE=”关闭TCPS 连接。 + +##### 4.3 双向认证 + +双向认证和单向认证原理基本差不多,只是除了客户端需要认证服务端以外,增加了服务端对客户 + +端的认证。在此认证模式下,客户端需要CA 证书、客户端证书与客户端密钥,服务器端需要CA 证书、 + +服务器证书与服务器密钥。 + +步骤 1:模块进行初始化操作,具体流程请参考附录B 。 + +步骤 2:模块进行拨号上网。 + +步骤 3:配置TCPS 加密参数。 + +1. +配置SSL/TLS 协议版本。 + +例如,指令“AT+ SSLTCPCFG =sslversion,3”,表示配置SSL/TLS 协议版本为TLS1.2。 + +2. +使用指令“AT+ SSLTCPCFG =authmode,2”,表示配置的SSL/TLS 认证方式为双向认证。 + +Neoway SSL/TLS 应用指南 + +TCPS 应用流程 + +深圳市有方科技股份有限公司版权所有 +25 + +3. +向模块写入CA 证书。 + +使用指令“AT+CERTADD=ca.crt, 1196”,表示向模块写入一份名为ca.crt、长度为1196 字节的 + +CA 证书文件。 + +当SSL/TLS 认证方设置为双向认证时,所以需要向客户端(模块)写入CA 证书、客户端证书与客 + +户端密钥。 + +4. +使用指令“AT+CERTCHECK=ca.crt”确认证书是否成功写入。 + +5. +使用指令“AT+SSLTCPCFG=cacert,ca.crt”,将写入的CA 证书配置到客户端(模块端)。 + +6. +同理写入客户端证书和客户端密钥。 + +使用指令“AT+CERTADD=client.crt,1204”、“AT+CERTADD=client.key,1743”,表示向模块写入 + +一份名为client.crt、长度为1204 字节的客户端证书文件和一份名为client.key、长度为1743 字 + +节的客户端密钥文件。 + +7. +使用指令“AT+CERTCHECK=client.crt”、“AT+CERTCHECK=client.key”确认证书和密钥是否成 + +功写入。 + +8. +使用指令“AT+SSLTCPCFG=clientcacert,client.crt”、“AT+SSLTCPCFG=clientkey,client.key”,表 + +示将写入的客户端证书和客户端密钥配置到客户端(模块端)。 + +9. +执行“AT+TCPSCFG?”指令,确认当前SSL/TLS 加密参数配置内容是否正确。 + +步骤 4:执行“AT+SSLTCPSETUP=,,,”建立TCPS 连接。 + +步骤 5:执行“AT+SSLTCPSEND=,”向服务器发送数据。 + +当接收到服务器数据时,会主动上报收到的数据内容。 + +步骤 6:关闭TCPS 连接。 + +需要主动关闭连接时可执行“AT+SSLTCPCLOSE=”关闭TCPS 连接。 + +##### 4.4 示例 + +以下流程示例是使用我司自己搭建的SSLTCP 服务器,证书也是自己制作的,用户在使用前需自行 + +搭建服务器及制作证书。且在执行的过程中需首先对模块进行初始化后,才能进行。初始化流程详见附 + +录B 。 + +###### 4.4.1 无需认证 + +``` +AT+XIIC=1 +``` + +OK + +``` +AT+XIIC? +``` + ++XIIC: 1,10.100.101.229 + +Neoway SSL/TLS 应用指南 + +TCPS 应用流程 + +深圳市有方科技股份有限公司版权所有 +26 + +OK + +``` +AT+SSLTCPCFG=sslversion,3 +``` + +OK + +``` +AT+SSLTCPCFG=authmode,0 +``` + +OK + +``` +AT+SSLTCPCFG? +``` + ++SSLTCPCFG:3,0,,, + +OK + +``` +AT+SSLTCPSETUP=1,183.239.240.40,12013,0 +``` + +OK + ++SSLTCPSETUP: 1,OK + +``` +AT+SSLTCPSEND=1,10 +``` + +> + ++SSLTCPSEND: 1,OK + ++SSLTCPRECV: 1,10,1234567890 + +``` +AT+SSLTCPCLOSE=1 +``` + ++SSLTCPCLOSE: 1,OK + +###### 4.4.2 单向认证 + +``` +AT+XIIC=1 +``` + +OK + +``` +AT+XIIC? +``` + ++XIIC: 1,10.100.101.229 + +OK + +``` +AT+SSLTCPCFG=sslversion,3 +``` + +OK + +``` +AT+SSLTCPCFG=authmode,1 +``` + +OK + +``` +AT+CERTADD=ca.crt,1196 +``` + +CONNECT + ++CERTADD: 1196,OK + +``` +AT+CERTCHECK=ca.crt +``` + ++CERTCHECK: ca.crt,OK + +``` +AT+SSLTCPCFG=cacert,ca.crt +``` + +OK + +``` +AT+SSLTCPCFG? +``` + ++SSLTCPCFG:3,1,ca.crt,, + +OK + +``` +AT+SSLTCPSETUP=1,183.239.240.40,12013,0 +``` + +OK + ++SSLTCPSETUP: 1,OK + +``` +AT+SSLTCPSEND=1,10 +``` + +> + ++SSLTCPSEND: 1,OK + ++SSLTCPRECV: 1,10,1234567890 + +``` +AT+SSLTCPCLOSE=1 +``` + ++SSLTCPCLOSE: 1,OK + +###### 4.4.3 双向认证 + +``` +AT+XIIC=1 +``` + +OK + +Neoway SSL/TLS 应用指南 + +TCPS 应用流程 + +深圳市有方科技股份有限公司版权所有 +27 + +``` +AT+XIIC? +``` + ++XIIC: 1,10.100.104.208 + +OK + +``` +AT+SSLTCPCFG=sslversion,3 +``` + +OK + +``` +AT+SSLTCPCFG=authmode,2 +``` + +OK + +``` +AT+CERTADD=ca.crt,1196 +``` + +CONNECT + ++CERTADD: 1196,OK + +``` +AT+CERTADD=client.crt,1204 +``` + +CONNECT + ++CERTADD: 1204,OK + +``` +AT+CERTADD=client.key,1743 +``` + +CONNECT + ++CERTADD: 1743,OK + +``` +AT+CERTCHECK? +``` + +ca.crt + +client.crt + +client.key + +OK + +``` +AT+SSLTCPCFG=cacert,ca.crt +``` + +OK + +``` +AT+SSLTCPCFG=clientcert,client.crt +``` + +OK + +``` +AT+SSLTCPCFG=clientkey,client.key +``` + +OK + +``` +AT+SSLTCPCFG? +``` + ++SSLTCPCFG:3,2,ca.crt,client.crt,client.key + +OK + +``` +AT+SSLTCPSETUP=1,183.239.240.40,12013,0 +``` + +OK + ++SSLTCPSETUP: 1,OK + +``` +AT+SSLTCPSEND=1,10 +``` + +> + ++SSLTCPSEND: 1,OK + ++SSLTCPRECV: 1,10,1234567890 + +``` +AT+SSLTCPCLOSE=1 +``` + ++SSLTCPCLOSE: 1,OK + +Neoway SSL/TLS 应用指南 + +A 相关AT 指令 + +深圳市有方科技股份有限公司版权所有 +28 + +#### A 相关AT 指令 + +##### A.1 HTTPS 相关指令 + +###### A.1.1 AT+CERTADD–添加SSL 证书 + +向模块写入证书。 + +写入过程可以通过“+++”中断。 + +###### 命令格式 + +###### 类型 命令 响应格式 + +执行 +AT+CERTADD=, + +CONNECT ++CERTADD: ,OK +Or ++CERTADD: ERROR + +###### 响应时间 + +最大响应时间300ms。 + +###### 参数 + + +写入模块的证书名称 + + +写入的长度 + +###### A.1.2 AT+CERTCHECK–SSL 证书确认 + +检查证书。 + +Neoway SSL/TLS 应用指南 + +A 相关AT 指令 + +深圳市有方科技股份有限公司版权所有 +29 + +###### 命令格式 + +###### 类型 命令 响应格式 + +执行 +AT+CERTCHECK= + ++CERTCHECK: ,OK +Or ++CERTCHECK: ERROR + +查询 +AT+CERTCHECK? + + +[] +OK + +###### 响应时间 + +最大响应时间300ms。 + +###### 参数 + + +要确认的证书名称。 + +###### A.1.3 AT+HTTPSCFG–配置HTTPS 参数 + +配置HTTPS 加密选项。 + +###### 命令格式 + +###### 类型 命令 响应格式 + +设置 +AT+HTTPSCFG=, + +OK +Or +ERROR + +查询 +AT+HTTPSCFG? + ++HTTPSCFG: ,,,,, +OK + +测试 +AT+HTTPSCFG=? ++HTTPSCFG: , +OK + +Neoway SSL/TLS 应用指南 + +A 相关AT 指令 + +深圳市有方科技股份有限公司版权所有 +30 + +###### 响应时间 + +最大响应时间300ms。 + +###### 参数 + + +配置SSL 选项。 + +sslversion: SSL 协议版本 + +authmode: 安全认证模式 + +ciphersuite: 加密控件 + +cacert: 根证书 + +clientcert: 客户端证书 + +clientkey: 客户端密匙 + + +和参数的取值,对应关系如下 + +sslversion +0:SSL3.0 + +1:TLS1.0 + +2:TLS1.1 + +3:TLS1.2 + +authmode + +0:No authentication + +1:Manage server authentication + +2:Manage server and client authentication if requested by the remote server + +ciphersuite: reserved. +cacert 根证书文件名 + +clientcert 客户端证书文件名 + +clientkey 客户端密匙文件名 + +###### A.1.4 AT+HTTPSPARA–设置HTTPS 参数 + +设置HTTPS 命令参数。 + +新的HTTPS 请求需要设置新的HTTPS PARAMETER。若执行+HTTPSCLOSE,链路关闭的同时, + +HTTPS 参数会被清空。 + +###### 命令格式 + +###### 类型 命令 响应格式 + +设置 +AT+HTTPSPARA=, +OK +Or + +Neoway SSL/TLS 应用指南 + +A 相关AT 指令 + +深圳市有方科技股份有限公司版权所有 +31 + +ERROR + +###### 响应时间 + +最大响应时间300ms。 + +###### 参数 + + +HTTPS 参数,支持两个参数设置 + +url:目标路径 + +port:目标端口号(未设置缺省值) + +keepalive:设置长连接,http 协议的长连接,默认值para_value=0,para_value + +为1 时打开长连接 + +recvmode:接收模式,para_value=0 默认接收模式,一个HTTP 响应只包含一个 + ++HTTPRECV:头表示;para_value=1,数据以+HTTPRECV: ,å½¢ + +式呈现 + + +对应的值,其中url 参数值最大为1022 个字节,url 支持域名解析,url 需加 + +双引号 + +###### A.1.5 AT+HTTPSSETUP–建立HTTPS 链路 + +建立HTTPS 链接。正确设置目标地址和端口号才能链接成功。 + +###### 命令格式 + +###### 类型 命令 响应格式 + +执行 +AT+HTTPSSETUP + +OK +Or +ERROR + +###### 响应时间 + +最大响应时间60s。 + +###### 参数 + +无 + +Neoway SSL/TLS 应用指南 + +A 相关AT 指令 + +深圳市有方科技股份有限公司版权所有 +32 + +###### A.1.6 AT+HTTPSACTION–发送HTTPS 请求 + +执行HTTPS 请求。 + +用户自定义报文时需遵循HTTPS 协议;用户在自定义报文的时候注意使用的工具里头是否会自带 + +回车和换行这些字符。 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 + +AT+HTTPSACTION=[, +[,] +AT+HTTPSACTION=[,, +] + +OK +Or +> +OK + +###### 响应时间 + +响应超时时间为60 秒。 + +###### 参数 + + +HTTP 请求方式,可取值为0,1,2,99 + +0:GET + +1:HEAD + +2:POST + +99:OPEN_MODE,用户自己定义报文模式 + + +POST 内容长度或自定义报文长度,在为POST 和OPEN_MODE 时必 + +须设置,最大长度为2048 + + +POST 请求数据类型 + +1:text + +2:json + +3:xml + +4:html + + +偏移量,通过GET 方式下载文件时,可以指定下载起始位置 + + +下载长度,通过GET 方式下载文件时,可以指定下载长度 + + +HTTPSPOST 发送的数据 + +###### A.1.7 AT+HTTPSCLOSE–主动关闭HTTPS 链路 + +关闭HTTPS 链接。 + +Neoway SSL/TLS 应用指南 + +A 相关AT 指令 + +深圳市有方科技股份有限公司版权所有 +33 + +执行+HTTPSCLOSE 命令,在关闭HTTPS 链路的同时,会清除+HTTPSPARA 命令设置的参数。 + +###### 命令格式 + +###### 类型 命令 响应格式 + +执行 +AT+HTTPCLOSE +OK ++HTTPSCLOSE: HTTPS Link Closed + +###### 响应时间 + +最大响应时间60s。 + +###### 参数 + +无 + +##### A.2 MQTTS 相关指令 + +###### A.2.1 AT+CERTADD–添加SSL 证书 + +向模块写入证书 + +写入过程可以通过“+++”中断。 + +###### 命令格式 + +###### 类型 命令 响应格式 + +执行 +AT+CERTADD=, + +CONNECT ++CERTADD: ,OK +Or ++CERTADD: ERROR + +###### 响应时间 + +最大响应时间300ms。 + +Neoway SSL/TLS 应用指南 + +A 相关AT 指令 + +深圳市有方科技股份有限公司版权所有 +34 + +###### 参数 + + +写入模块的证书名称 + + +写入的长度 + +###### A.2.2 AT+CERTCHECK–SSL 证书确认 + +检查证书。 + +###### 命令格式 + +###### 类型 命令 响应格式 + +执行 +AT+CERTCHECK= + ++CERTCHECK: ,OK +Or ++CERTCHECK: ERROR + +查询 +AT+CERTCHECK? + + +[] +OK + +###### 响应时间 + +最大响应时间300ms。 + +###### 参数 + + +要确认的证书名称。 + +###### A.2.3 AT+MQTTTLS - TLS 参数配置 + +MQTT TLS 参数配置。 + +通过AT+CERTADD 导入证书。 + +###### 命令格式 + +###### 类型 命令 响应格式 + +设置 +AT+MQTTTLS=, + +OK +Or +ERROR + +Neoway SSL/TLS 应用指南 + +A 相关AT 指令 + +深圳市有方科技股份有限公司版权所有 +35 + +查询 +AT+MQTTTLS? + ++MQTTTLS: +,, + +,, +OK + +测试 +AT+MQTTTLS=? ++MQTTTLS: , +OK + +###### 响应时间 + +最大响应时间300ms。 + +###### 参数 + + +配置参数类型 + + +配置参数值 + + +安全加密模式 + +0:不加密 + +1:加密 + + +安全认证模式 + +0:verify optional + +1:verify required + + +string,CA 证书 + + +string,客户端证书 + + +string,客户端密钥 + +###### A.2.4 AT+MQTTCONNPARAM–用户参数设置 + +设置ID、用户名、密码参数。 + +###### 命令格式 + +###### 类型 命令 响应格式 + +设置 +AT+MQTTCONNPARAM=, +, + +OK +Or +ERROR + +查询 +AT+MQTTCONNPARAM? + ++MQTTCONNPARAM:<"clientID" +>,<"username">,<"password"> +OK + +测试 +AT+MQTTCONNPARAM=? +OK + +Neoway SSL/TLS 应用指南 + +A 相关AT 指令 + +深圳市有方科技股份有限公司版权所有 +36 + +###### 响应时间 + +最大响应时间300ms。 + +###### 参数 + + +设备ID,最大长度256。 + + +用户名,最大长度512。 + + +密码,最大长度256。 + + +建立连接的适配参数 + +###### A.2.5 AT+MQTTWILLPARAM–遗嘱设置 + +设置遗嘱信息。 + +在MQTT 已经连接的情况下设置参数无效,掉电后参数需重新设置。 + +###### 命令格式 + +###### 类型 命令 响应格式 + +设置 +AT+MQTTWILLPARAM=,,, + ++GNSSSTATE: +Or +OK + +查询 +AT+MQTTWILLPARAM? + ++MQTTWILLPARAM:,,<"topicname">,<"message"> +OK + +测试 +AT+MQTTWILLPARAM=? +OK + +###### 响应时间 + +最大响应时间300ms。 + +###### 参数 + + +保留标志,数字类型。 + + +服务质量,目前仅支持Qos=0 和1。 + + +遗嘱主题,最大长度128。 + +Neoway SSL/TLS 应用指南 + +A 相关AT 指令 + +深圳市有方科技股份有限公司版权所有 +37 + + +遗嘱消息,最大长度1024。 + +###### A.2.6 AT+MQTTCONN–连接命令 + +连接MQTT 服务器。 + +在连接的过程中等待连接返回值,没有返回值的情况下,不能再次进行连接操作。 + +当连接成功后,没有主动断开情况下如果模块主动上报+MQTTDISCONNED: Link Closed,需手动 + +进行连接。 + +###### 命令格式 + +###### 类型 命令 响应格式 + +执行 +AT+MQTTCONN=<"host">,, + +OK +Or +ERROR + +查询 +AT+MQTTCONN? + ++MQTTCONN:<"ip:port">,, + +OK + +测试 +AT+MQTTCONN=? +OK + +###### 响应时间 + +最大响应时间60s。 + +###### 参数 + +<"host"> +服务器地址(url:port)。 + + +是否清除session,数字类型 + +0-不清除(默认) + +1-清除 + + +keepAlive 时间设置,取值范围[20,180],单位s。 + +###### A.2.7 AT+MQTTSUB–订阅命令 + +订阅主题。 + +当SUB 失败后,查询MQTT 和网络状态再进行操作。当网络不佳的情况下,返回值较慢。 + +Neoway SSL/TLS 应用指南 + +A 相关AT 指令 + +深圳市有方科技股份有限公司版权所有 +38 + +查询指令只有连接情况下才能查询到,只能查询到当前最后一次订阅的qos 和topic。 + +###### 命令格式 + +###### 类型 命令 响应格式 + +执行 +AT+MQTTSUB=<"topicname">, + +OK +Or +ERROR + +查询 +AT+MQTTSUB? + ++MQTTSUB:<"topicname">, + +OK + +测试 +AT+MQTTSUB=? +OK + +###### 响应时间 + +最大响应时间30s。 + +###### 参数 + +<"topicname"> 订阅的主题,最大长度128。 + + +服务质量,目前支持Qos=0,1 和2 + +###### A.2.8 AT+MQTTPUB–发布主题 + +主题发布。 + +当网络不佳的情况下,返回值较慢。 + +###### 命令格式 + +###### 类型 命令 响应格式 + +执行 +AT+MQTTPUB=,,<"topic +name">,<"message"> + +OK +Or +ERROR + +Neoway SSL/TLS 应用指南 + +A 相关AT 指令 + +深圳市有方科技股份有限公司版权所有 +39 + +###### 响应时间 + +最大响应时间30s。 + +###### 参数 + + +保留标志,数字类型,0 和1。 + + +服务质量,目前仅支持Qos=0,1 和2。 + +<"topicname"> +发布的主题,最大长度128。 + +<"message"> +发布的消息,最大长度1024。 + +###### A.2.9 AT+MQTTDISCONN–关闭MQTT 连接 + +关闭MQTT 连接。 + +终端主动断开和服务端的连接,然后释放MQTT 资源,参数设置也会释放,如果需要pub 消息, + +需要重新发起参数设置再连接设备。 + +###### 命令格式 + +###### 类型 命令 响应格式 + +执行 +AT+MQTTDISCONN +OK + +###### 响应时间 + +最大响应时间30s。 + +###### 参数 + +无 + +##### A.3 TCPS 相关指令 + +###### A.3.1 AT+CERTADD–添加SSL 证书 + +向模块写入证书 + +写入过程可以通过“+++”中断。 + +Neoway SSL/TLS 应用指南 + +A 相关AT 指令 + +深圳市有方科技股份有限公司版权所有 +40 + +###### 命令格式 + +###### 类型 命令 响应格式 + +执行 +AT+CERTADD=, + +CONNECT ++CERTADD: ,OK +Or ++CERTADD: ERROR + +###### 响应时间 + +最大响应时间300ms。 + +###### 参数 + + +写入模块的证书名称 + + +写入的长度 + +###### A.3.2 AT+CERTCHECK–SSL 证书确认 + +检查证书。 + +###### 命令格式 + +###### 类型 命令 响应格式 + +执行 +AT+CERTCHECK= + ++CERTCHECK: ,OK +Or ++CERTCHECK: ERROR + +查询 +AT+CERTCHECK? + + +[] +OK + +###### 响应时间 + +最大响应时间300ms。 + +###### 参数 + + +要确认的证书名称。 + +Neoway SSL/TLS 应用指南 + +A 相关AT 指令 + +深圳市有方科技股份有限公司版权所有 +41 + +###### A.3.3 AT+SSLTCPCFG–SSL TCP 配置参数 + +配置SSL 加密选项。 + +###### 命令格式 + +###### 类型 命令 响应格式 + +设置 +AT+SSLTCPCFG=, + +OK +Or +ERROR + +查询 +AT+SSLTCPCFG? + ++SSLTCPCFG: ,,,,, +OK + +测试 +AT+SSLTCPCFG=? ++SSLTCPCFG: , +OK + +###### 响应时间 + +最大响应时间300ms。 + +###### 参数 + + +配置SSL 选项。 + +sslversion: SSL 协议版本 + +authmode: 安全认证模式 + +ciphersuite: 加密控件 + +cacert: 根证书 + +clientcert: 客户端证书 + +clientkey: 客户端密匙 + + +和参数的取值,对应关系如下: + +⚫ +sslversion +0:SSL3.0 + +1:TLS1.0 + +2:TLS1.1 + +3:TLS1.2 + +⚫ +authmode + +Neoway SSL/TLS 应用指南 + +A 相关AT 指令 + +深圳市有方科技股份有限公司版权所有 +42 + +0:No authentication,不需要设置cacert、clientcert、clientkey 等内容。 + +1:Manage server authentication + +2:Manage server and client authentication if requested by the remote server + +⚫ +ciphersuite: reserved. + +⚫ +cacert 根证书文件名 + +⚫ +clientcert 客户端证书文件名 + +⚫ +clientkey 客户端密匙文件名 + +###### A.3.4 AT+SSLTCPSETUP–建立SSL TCP 连接 + +建立SSL TCP 连接。 + +建立SSL TCP 连接之前,需要先激活PPP,申请到IP 地址。 + +###### 命令格式 + +###### 类型 命令 响应格式 + +执行 +AT+SSLTCPSETUP=,,, + +OK ++SSLTCPSETUP: , +Or +CONNECT +Or ++SSLTCPSETUP: ERROR +Or ++SSLTCPSETUP: GPRS +DISCONNECTION + +查询 +AT+SSLTCPSETUP? + ++SSLTCPSETUP: ,, +, +[+SSLTCPSETUP: ,, +,]… +OK + +测试 +AT+SSLTCPSETUP=? ++SSLTCPSETUP: ,, +, + +###### 响应时间 + +最大响应时间60s。 + +###### 参数 + + +链路号,范围0-5,用来区分与服务器的连接 + +Neoway SSL/TLS 应用指南 + +A 相关AT 指令 + +深圳市有方科技股份有限公司版权所有 +43 + + +服务器的IP 地址/域名 + + +服务器的端口 + + +传输模式 + +0:非透传 + +1:透传 + + +OK +ERROR1 +AUTHFAIL +FAIL + +###### A.3.5 AT+SSLTCPSEND–SSL TCP 数据发送 + +SSL TCP 数据发送。 + +###### 命令格式 + +###### 类型 命令 响应格式 + +执行 +AT+SSLTCPSEND=, + +> ++SSLTCPSEND: +, +Or ++SSLTCPSEND: Data length +error + +测试 +AT+SSLTCPSEND=? + ++SSLTCPSEND: (value range +of),(value range +of) + +###### 响应时间 + +最大响应时间30s。 + +###### 参数 + + +链路号 0~5 + + 要发送的数据长度,取值范围 1-4096 + + +OK +FAIL + +Neoway SSL/TLS 应用指南 + +A 相关AT 指令 + +深圳市有方科技股份有限公司版权所有 +44 + +###### A.3.6 AT+SSLTCPCLOSE–SSL TCP 关闭连接指令 + +关闭SSL TCP 连接。 + +###### 命令格式 + +###### 类型 命令 响应格式 + +执行 +AT+SSLTCPCLOSE= + ++SSLTCPCLOSE: +, +Or ++SSLTCPCLOSE: ERROR + +主动上报 ++SSLTCPCLOSE: ,Link Closed + +###### 响应时间 + +最大响应时间30s。 + +###### 参数 + + +链路号 0~5 + + +OK +ERROR +Link Closed + +Neoway SSL/TLS 应用指南 + +B 初始化流程 + +深圳市有方科技股份有限公司版权所有 +45 + +#### B 初始化流程 + +Yes + +No + +Yes + +Yes + +Yes + +Yes + +1. 上电开机 + +延时 + +OK? + +2. AT + +Yes + +return code? + +3. AT+CGSN + +Yes + +return code? + +4. AT+CGMR + +Yes + +5. AT+CPIN? + ++CPIN: READY ? + +6. AT+CIMI + +OK? + +7. AT+CSQ + ++CSQ: , + and 12≤signal≤31 ? + +8. AT+CREG=2 + +return code? + +9. AT+CREG? + ++CREG: 2,1 or + ++CREG: 2,5? + +12. AT+CGATT? + ++CGATT: 1? + +Yes + +Yes + +10. AT+CREG=0 + +return code? + +11. AT+CGATT=1 + +Yes + +No + +No + +AT$MYSYSINFO + +return code ? + +Yes + +``` +AT+XGAUTH=1,1,"gsm","1234" +``` + +OK? + +``` +AT+CGDCONT=1,"IP","CMNET" +``` + +OK? + +Yes + +Yes + +Yes + +查询<40次? + +查询<40次? + +Yes + +Yes + +No + +查询<15次? + +查询<30次? + +查询<3次? + +手动附着 + +<30次? + +设置<3次? + +查询<3次? + +模块初始化完成 + +No + +No + +No + +Yes + +Yes + +No + +No + +No + +No + +No + +No + +No + +No + +2G模块延时3s,4G模块延 +时10s。该过程中模块处于 +初始化阶段,可能出现未响 +应或响应出错情况。 + +返回“+CGATT:1”则表示 +已注册上PS域业务,如果30 +s内仍未返回,建议手动附 +着CGATT流程。 + +不设置主动上报的情况下, +回码判断包含“+CREG:0,1 +或者+CREG:0,5”即可进入 +下一步,表示已经注册上CS +域业务。 +60s内未注册上,建议执行 +重启模块动作。 + +设置错误的APN可能会导致 +后续拨号动作失败,具体A +PN信息需要和卡商确认。 + +部分SIM卡识卡超过30s, +建议出厂前对SIM卡作充分 +兼容性测试。 + +Yes + +模块重启 + +重启模块请参照 +《Neoway_标准模块_开关机流程操 +作指南》进行。24小时内过多的重 +启可能会导致FLASH及SIM卡不可 +逆的损坏。 + +设置PPP拨号过程中鉴权类 +型和身份认证参数,设置错 +误的参数同样可能导致PPP +拨号失败,CID要和设置A +PN的参数保持一致。 diff --git "a/code/sdk/zhihui/ppp/doc/Neoway_\345\212\237\350\203\275\346\250\241\345\235\227_SSL_TLS_\345\272\224\347\224\250\346\214\207\345\215\227_V1.0.pdf" "b/code/sdk/zhihui/ppp/doc/Neoway_\345\212\237\350\203\275\346\250\241\345\235\227_SSL_TLS_\345\272\224\347\224\250\346\214\207\345\215\227_V1.0.pdf" new file mode 100644 index 000000000..b6f955a99 Binary files /dev/null and "b/code/sdk/zhihui/ppp/doc/Neoway_\345\212\237\350\203\275\346\250\241\345\235\227_SSL_TLS_\345\272\224\347\224\250\346\214\207\345\215\227_V1.0.pdf" differ diff --git a/code/sdk/zhihui/ppp/doc/pdf_to_md.py b/code/sdk/zhihui/ppp/doc/pdf_to_md.py new file mode 100644 index 000000000..a829b4f00 --- /dev/null +++ b/code/sdk/zhihui/ppp/doc/pdf_to_md.py @@ -0,0 +1,614 @@ +#!/usr/bin/env python3.9 +# -*- coding: utf-8 -*- +""" +PDF 转 Markdown 脚本 +功能:将 PDF 文件转换为 Markdown 格式 + +支持两种模式: +1. 结构化模式(默认):智能识别文档结构,优化层级和格式,适合大模型理解 +2. 原始模式(--raw):忠实还原 PDF 原始格式和布局,保留原始文本顺序 + +特性: +- 结构化模式:智能识别标题、段落、列表、表格、代码块 +- 原始模式:保留 PDF 原始布局和文本顺序 +- 可选的目录生成 +- 自动清理格式问题 + +使用方法: + python3.9 pdf_to_md.py # 转换当前目录所有 PDF(结构化) + python3.9 pdf_to_md.py input.pdf # 转换指定 PDF(结构化) + python3.9 pdf_to_md.py --raw input.pdf # 原始格式转换 + python3.9 pdf_to_md.py --toc input.pdf # 转换并生成目录 +""" + +import os +import sys +import re +import argparse +from pathlib import Path +from collections import defaultdict + +try: + import fitz # PyMuPDF +except ImportError: + print("错误: 需要安装 PyMuPDF 库") + print("请运行: pip3 install PyMuPDF") + sys.exit(1) + + +class PDFStructureAnalyzer: + """PDF 结构分析器:分析文档的字体、大小等属性,识别结构""" + + def __init__(self, doc): + self.doc = doc + self.font_sizes = defaultdict(int) # 统计字体大小出现频率 + self.common_font_size = None # 正文字体大小 + self.heading_sizes = [] # 标题字体大小列表 + + def analyze(self): + """分析整个文档的结构""" + # 统计字体大小 + for page in self.doc: + blocks = page.get_text("dict")["blocks"] + for block in blocks: + if block.get("type") == 0: # 文本块 + for line in block.get("lines", []): + for span in line.get("spans", []): + size = round(span.get("size", 12)) + self.font_sizes[size] += len(span.get("text", "")) + + # 确定正文字体大小(出现最频繁的) + if self.font_sizes: + self.common_font_size = max(self.font_sizes.items(), key=lambda x: x[1])[0] + + # 确定标题字体大小(大于正文的) + self.heading_sizes = sorted([s for s in self.font_sizes.keys() + if s > self.common_font_size], reverse=True) + + return self + + def get_heading_level(self, font_size): + """根据字体大小确定标题级别""" + font_size = round(font_size) + + if font_size <= self.common_font_size: + return 0 # 不是标题 + + # 根据字体大小确定标题级别 (H2-H6) + for i, size in enumerate(self.heading_sizes): + if font_size >= size: + return min(i + 2, 6) # H2 到 H6 + + return 2 # 默认 H2 + + +class MarkdownConverter: + """Markdown 转换器:将 PDF 内容转换为结构化的 Markdown""" + + def __init__(self, doc, generate_toc=False): + self.doc = doc + self.generate_toc = generate_toc + self.analyzer = PDFStructureAnalyzer(doc).analyze() + self.toc = [] # 目录 + self.content = [] # 内容 + + def convert(self): + """执行转换""" + # 添加文档标题 + title = self.doc.metadata.get('title', '') or Path(self.doc.name).stem + self.content.append(f"# {title}\n") + + # 处理每一页 + for page_num in range(len(self.doc)): + self._process_page(page_num) + + # 生成最终 Markdown + markdown_parts = [] + + # 添加目录(如果需要) + if self.generate_toc and self.toc: + markdown_parts.append("## 目录\n") + for level, text in self.toc: + indent = " " * (level - 2) + anchor = self._text_to_anchor(text) + markdown_parts.append(f"{indent}- [{text}](#{anchor})") + markdown_parts.append("\n---\n") + + # 添加内容 + markdown_parts.extend(self.content) + + # 清理多余空行 + markdown_text = '\n'.join(markdown_parts) + markdown_text = re.sub(r'\n{3,}', '\n\n', markdown_text) + + return markdown_text + + def _process_page(self, page_num): + """处理单个页面""" + page = self.doc[page_num] + blocks = page.get_text("dict")["blocks"] + + # 添加页面注释(可选) + # self.content.append(f"\n\n") + + for block in blocks: + if block.get("type") == 0: # 文本块 + self._process_text_block(block) + + def _process_text_block(self, block): + """处理文本块""" + lines = block.get("lines", []) + if not lines: + return + + # 获取块的主要字体大小 + all_spans = [] + for line in lines: + all_spans.extend(line.get("spans", [])) + + if not all_spans: + return + + # 按字符数统计主要字体大小 + size_chars = defaultdict(int) + for span in all_spans: + size = round(span.get("size", 12)) + size_chars[size] += len(span.get("text", "")) + + main_size = max(size_chars.items(), key=lambda x: x[1])[0] if size_chars else 12 + + # 提取文本 + text = "" + for line in lines: + line_text = "" + for span in line.get("spans", []): + line_text += span.get("text", "") + text += line_text + "\n" + + text = text.strip() + if not text: + return + + # 判断文本类型并格式化 + self._format_text(text, main_size) + + def _format_text(self, text, font_size): + """格式化文本为 Markdown""" + # 检查是否是标题 + heading_level = self.analyzer.get_heading_level(font_size) + + if heading_level > 0: + # 是标题 + self._add_heading(text, heading_level) + elif self._is_at_command(text): + # AT 命令 + self._add_code_block(text, "") + elif self._is_table(text): + # 表格 + self._add_table(text) + elif self._is_list(text): + # 列表 + self._add_list(text) + else: + # 普通段落 + self._add_paragraph(text) + + def _add_heading(self, text, level): + """添加标题""" + # 清理标题文本 + text = re.sub(r'\s+', ' ', text).strip() + + # 记录到目录 + if self.generate_toc: + self.toc.append((level, text)) + + # 添加标题 + self.content.append(f"\n{'#' * level} {text}\n") + + def _add_paragraph(self, text): + """添加段落""" + # 清理文本 + text = self._clean_text(text) + if text: + self.content.append(f"{text}\n") + + def _add_code_block(self, text, lang=""): + """添加代码块""" + self.content.append(f"```{lang}\n{text}\n```\n") + + def _add_table(self, text): + """添加表格""" + # 简单的表格处理:将制表符转换为 Markdown 表格 + lines = text.split('\n') + table_lines = [] + + for i, line in enumerate(lines): + if '\t' in line or '|' in line: + # 处理表格行 + if '\t' in line: + cells = [cell.strip() for cell in line.split('\t') if cell.strip()] + line = '| ' + ' | '.join(cells) + ' |' + table_lines.append(line) + + # 添加表头分隔符(第一行后) + if i == 0 and '\t' in lines[0]: + num_cols = len(cells) + separator = '| ' + ' | '.join(['---'] * num_cols) + ' |' + table_lines.append(separator) + + if table_lines: + self.content.append('\n'.join(table_lines) + '\n') + else: + self._add_paragraph(text) + + def _add_list(self, text): + """添加列表""" + lines = text.split('\n') + for line in lines: + line = line.strip() + if line: + # 检查是否已经是列表格式 + if re.match(r'^[\d]+\.?\s', line) or re.match(r'^[•\-\*]\s', line): + # 规范化列表格式 + if re.match(r'^[\d]+\.?\s', line): + # 数字列表 + self.content.append(line) + else: + # 无序列表 + line = re.sub(r'^[•\-\*]\s', '- ', line) + self.content.append(line) + else: + self.content.append(f"- {line}") + self.content.append("") + + def _is_at_command(self, text): + """判断是否是 AT 命令""" + lines = text.split('\n') + at_lines = sum(1 for line in lines if line.strip().startswith(('AT+', 'AT^', 'AT*'))) + return at_lines > 0 and at_lines / len(lines) > 0.5 + + def _is_table(self, text): + """判断是否是表格""" + lines = text.split('\n') + tab_lines = sum(1 for line in lines if '\t' in line or line.count('|') > 2) + return tab_lines > 0 and tab_lines / len(lines) > 0.5 + + def _is_list(self, text): + """判断是否是列表""" + lines = [line.strip() for line in text.split('\n') if line.strip()] + if len(lines) < 2: + return False + + list_lines = sum(1 for line in lines + if re.match(r'^[\d]+\.?\s', line) or re.match(r'^[•\-\*]\s', line)) + return list_lines / len(lines) > 0.6 + + def _clean_text(self, text): + """清理文本""" + # 移除软连字符 + text = text.replace('\u00ad', '') + + # 修复单词断行 + text = re.sub(r'(\w)-\s*\n\s*(\w)', r'\1\2', text) + + # 规范化空白字符 + text = re.sub(r'[ \t]+', ' ', text) + text = re.sub(r'\n{2,}', '\n\n', text) + + return text.strip() + + def _text_to_anchor(self, text): + """将文本转换为 Markdown 锚点""" + # 简化为小写,移除特殊字符 + anchor = text.lower() + anchor = re.sub(r'[^\w\s\u4e00-\u9fff-]', '', anchor) + anchor = re.sub(r'\s+', '-', anchor) + return anchor + + +class RawMarkdownConverter: + """原始 Markdown 转换器:忠实还原 PDF 原始格式""" + + def __init__(self, doc, preserve_pages=True): + self.doc = doc + self.preserve_pages = preserve_pages + self.content = [] + + def convert(self): + """执行转换""" + # 添加文档标题 + title = self.doc.metadata.get('title', '') or Path(self.doc.name).stem + self.content.append(f"# {title}\n") + + # 处理每一页 + total_pages = len(self.doc) + for page_num in range(total_pages): + self._process_page(page_num) + + # 添加页面分隔 + if self.preserve_pages and page_num < total_pages - 1: + self.content.append(f"\n---\n") + self.content.append(f"\n") + + # 生成最终 Markdown + markdown_text = '\n'.join(self.content) + + # 基本清理:移除3个以上的连续空行 + markdown_text = re.sub(r'\n{4,}', '\n\n\n', markdown_text) + + return markdown_text + + def _process_page(self, page_num): + """处理单个页面 - 保留原始格式""" + page = self.doc[page_num] + + # 添加页面标记 + if self.preserve_pages: + self.content.append(f"\n\n") + + # 获取原始文本块,按位置排序 + blocks = page.get_text("dict")["blocks"] + + # 按垂直位置排序(从上到下) + text_blocks = [b for b in blocks if b.get("type") == 0] + text_blocks.sort(key=lambda b: (b["bbox"][1], b["bbox"][0])) + + for block in text_blocks: + text = self._extract_block_text(block) + if text.strip(): + # 检测是否是 AT 命令行 + if self._is_at_command_line(text): + self.content.append(f"```") + self.content.append(text.strip()) + self.content.append(f"```\n") + else: + # 保留原始文本,只做基本清理 + self.content.append(text) + self.content.append("") # 块之间空行 + + def _extract_block_text(self, block): + """提取文本块内容,保留原始格式""" + lines = block.get("lines", []) + text_lines = [] + + for line in lines: + line_text = "" + spans = line.get("spans", []) + + # 按水平位置排序 + spans.sort(key=lambda s: s["bbox"][0]) + + for span in spans: + span_text = span.get("text", "") + line_text += span_text + + if line_text.strip(): + text_lines.append(line_text) + + # 保留行内换行 + return '\n'.join(text_lines) + + def _is_at_command_line(self, text): + """判断是否是 AT 命令(单行)""" + text = text.strip() + # 单行且以 AT 开头 + return text.startswith(('AT+', 'AT^', 'AT*', 'AT')) and '\n' not in text + + +def pdf_to_markdown(pdf_path, output_path=None, generate_toc=False, raw_mode=False, preserve_pages=True): + """ + 将 PDF 文件转换为 Markdown + + Args: + pdf_path: PDF 文件路径 + output_path: 输出 Markdown 文件路径(可选) + generate_toc: 是否生成目录(默认 False,仅结构化模式) + raw_mode: 是否使用原始模式(默认 False,使用结构化模式) + preserve_pages: 是否保留页面标记(默认 True,仅原始模式) + + Returns: + 转换是否成功 + """ + try: + mode_str = "原始格式" if raw_mode else "结构化" + print(f"正在处理: {pdf_path} [{mode_str}模式]") + + # 打开 PDF + doc = fitz.open(pdf_path) + total_pages = len(doc) + + print(f" 文档信息: {total_pages} 页") + + # 根据模式选择转换器 + if raw_mode: + # 原始格式转换 + converter = RawMarkdownConverter(doc, preserve_pages=preserve_pages) + markdown_text = converter.convert() + heading_count = 0 + else: + # 结构化转换 + converter = MarkdownConverter(doc, generate_toc=generate_toc) + markdown_text = converter.convert() + heading_count = len(converter.toc) if generate_toc else markdown_text.count('\n## ') + + doc.close() + + print(f" 转换完成") + + # 确定输出路径 + if output_path is None: + output_path = pdf_path.rsplit('.', 1)[0] + '.md' + + # 保存 Markdown 文件 + with open(output_path, 'w', encoding='utf-8') as f: + f.write(markdown_text) + + # 统计信息 + file_size_kb = os.path.getsize(output_path) / 1024 + line_count = markdown_text.count('\n') + + print(f"✓ 转换成功: {output_path}") + print(f" 文件大小: {file_size_kb:.2f} KB") + print(f" 行数: {line_count}") + if not raw_mode: + print(f" 标题数: {heading_count}") + + return True + + except Exception as e: + print(f"✗ 转换失败: {e}") + import traceback + traceback.print_exc() + return False + + +def process_directory(directory, generate_toc=False, raw_mode=False, preserve_pages=True): + """ + 处理目录下的所有 PDF 文件 + + Args: + directory: 目录路径 + generate_toc: 是否生成目录 + raw_mode: 是否使用原始模式 + preserve_pages: 是否保留页面标记 + """ + pdf_files = list(Path(directory).glob('*.pdf')) + + if not pdf_files: + print(f"在 {directory} 目录下没有找到 PDF 文件") + return + + print(f"找到 {len(pdf_files)} 个 PDF 文件\n") + + success_count = 0 + for pdf_file in pdf_files: + if pdf_to_markdown(str(pdf_file), generate_toc=generate_toc, + raw_mode=raw_mode, preserve_pages=preserve_pages): + success_count += 1 + print() + + print(f"\n总结: {success_count}/{len(pdf_files)} 个文件转换成功") + + +def main(): + """主函数""" + parser = argparse.ArgumentParser( + description='将 PDF 文件转换为 Markdown 格式', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +示例: + %(prog)s # 转换当前目录所有 PDF(结构化) + %(prog)s input.pdf # 转换指定 PDF 文件(结构化) + %(prog)s --raw input.pdf # 原始格式转换 + %(prog)s --toc input.pdf # 转换并生成目录(结构化) + %(prog)s -o output.md input.pdf # 指定输出文件名 + %(prog)s /path/to/docs/ # 转换指定目录下的所有 PDF + +模式说明: + 结构化模式(默认):智能识别文档结构,优化层级和格式,适合大模型理解 + 原始模式(--raw):忠实还原 PDF 原始格式和布局,保留原始文本顺序 + +注意: + - 请确保输入的 PDF 文件已经去除了水印 + - 使用 pdf_watermark_remover.py 可以预先清理 PDF 水印 + """ + ) + + parser.add_argument( + 'input', + nargs='?', + help='输入 PDF 文件或目录路径(默认为脚本所在目录)' + ) + + parser.add_argument( + '-o', '--output', + help='输出 Markdown 文件路径(仅当输入为单个文件时有效)' + ) + + parser.add_argument( + '--toc', + action='store_true', + help='生成文档目录(仅结构化模式有效)' + ) + + parser.add_argument( + '--raw', + action='store_true', + help='使用原始格式模式,忠实还原 PDF 布局' + ) + + parser.add_argument( + '--no-pages', + action='store_true', + help='不添加页面标记(仅原始模式有效)' + ) + + args = parser.parse_args() + + # 确定输入路径 + if args.input: + input_path = args.input + else: + # 默认为脚本所在目录 + input_path = os.path.dirname(os.path.abspath(__file__)) + + # 检查路径是否存在 + if not os.path.exists(input_path): + print(f"错误: 路径 '{input_path}' 不存在") + sys.exit(1) + + # 确定模式和参数 + mode_name = "原始格式模式" if args.raw else "结构化模式" + preserve_pages = not args.no_pages + + # 打印工具信息 + print("=" * 70) + print(f"PDF 转 Markdown 工具 - {mode_name}".center(70)) + print("=" * 70) + + # 判断是文件还是目录 + if os.path.isfile(input_path): + # 处理单个文件 + if not input_path.lower().endswith('.pdf'): + print(f"错误: '{input_path}' 不是 PDF 文件") + sys.exit(1) + + print(f"输入文件: {input_path}") + if args.output: + print(f"输出文件: {args.output}") + print(f"转换模式: {mode_name}") + if args.toc and not args.raw: + print("生成目录: 是") + if args.no_pages and args.raw: + print("页面标记: 否") + print("=" * 70) + print() + + success = pdf_to_markdown(input_path, args.output, args.toc, + args.raw, preserve_pages) + sys.exit(0 if success else 1) + + elif os.path.isdir(input_path): + # 处理目录 + print(f"处理目录: {input_path}") + print(f"转换模式: {mode_name}") + if args.toc and not args.raw: + print("生成目录: 是") + if args.no_pages and args.raw: + print("页面标记: 否") + if args.output: + print("警告: 处理目录时 -o/--output 参数将被忽略") + print("=" * 70) + print() + + process_directory(input_path, args.toc, args.raw, preserve_pages) + sys.exit(0) + + else: + print(f"错误: '{input_path}' 既不是文件也不是目录") + sys.exit(1) + + +if __name__ == '__main__': + main() + diff --git a/code/sdk/zhihui/ppp/doc/pdf_watermark_remover.py b/code/sdk/zhihui/ppp/doc/pdf_watermark_remover.py new file mode 100644 index 000000000..35cb884bd --- /dev/null +++ b/code/sdk/zhihui/ppp/doc/pdf_watermark_remover.py @@ -0,0 +1,451 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +PDF 水印去除工具 - 真正工作的版本 + +核心理念: +- 旋转的文本 = 水印(默认:旋转>0.5度即判定为水印) +- 正常文本都是水平的(角度=0) + +核心实现: +1. 使用PyMuPDF识别旋转文本(水印) +2. 使用pikepdf解析和修改PDF内容流 +3. 检测Tm和cm矩阵的旋转角度 +4. 删除包含旋转矩阵的整个BT...ET文本块 +5. 重建内容流 + +依赖: + pip3 install pikepdf PyMuPDF + +使用方法: + # 分析水印 + python3 pdf_watermark_remover_working.py --mode analyze --input file.pdf + + # 删除水印(默认:旋转>0.5度) + python3 pdf_watermark_remover_working.py --mode remove --input file.pdf --output clean.pdf + + # 删除所有旋转文本(角度阈值=0) + python3 pdf_watermark_remover_working.py --mode remove --input file.pdf --output clean.pdf -a 0 +""" + +import os +import sys +import argparse +import math +import re +from collections import defaultdict + +try: + import pikepdf +except ImportError: + print("错误: 需要安装 pikepdf 库") + print("请运行: pip3 install pikepdf") + sys.exit(1) + +try: + import fitz # PyMuPDF +except ImportError: + print("错误: 需要安装 PyMuPDF 库") + print("请运行: pip3 install PyMuPDF") + sys.exit(1) + + +class PDFContentStreamParser: + """简化的PDF内容流解析器""" + + def __init__(self, angle_threshold=0.5): + self.angle_threshold = angle_threshold # 默认0.5度,任何旋转都会被检测 + + def parse_content_stream(self, stream_data): + """ + 解析PDF内容流,识别并删除旋转文本 + + Returns: + 修改后的内容流数据 + """ + try: + content = stream_data.decode('latin-1') + except: + # 如果解码失败,尝试其他编码 + try: + content = stream_data.decode('utf-8') + except: + return stream_data # 无法解码,返回原始数据 + + # 按行分割 + lines = content.split('\n') + + # 过滤后的行 + filtered_lines = [] + + # 状态追踪 + in_text_block = False + skip_until_et = False + bt_start_line = -1 + has_rotated_cm = False # 追踪是否有旋转的cm + + i = 0 + while i < len(lines): + line = lines[i].strip() + + # 检查cm(坐标变换矩阵)- Draft使用的是cm而不是Tm! + if ' cm' in line and not in_text_block: + parts = line.split() + try: + # cm也是6个参数:a b c d e f cm + cm_idx = -1 + for idx, part in enumerate(parts): + if part == 'cm': + cm_idx = idx + break + + if cm_idx >= 6: + a = float(parts[cm_idx-6]) + b = float(parts[cm_idx-5]) + c = float(parts[cm_idx-4]) + d = float(parts[cm_idx-3]) + + angle = math.degrees(math.atan2(b, a)) + + if abs(angle) > self.angle_threshold: + # 有旋转的cm,标记状态 + has_rotated_cm = True + # 不添加这行 + i += 1 + continue + except (ValueError, IndexError): + pass + + # 检查BT(Begin Text) + if line.endswith('BT') or line == 'BT': + in_text_block = True + # 如果之前有旋转的cm,标记跳过整个文本块 + if has_rotated_cm: + skip_until_et = True + else: + skip_until_et = False + bt_start_line = len(filtered_lines) + + if not skip_until_et: + filtered_lines.append(lines[i]) + i += 1 + continue + + # 检查ET(End Text) + if line.endswith('ET') or line == 'ET': + if skip_until_et: + # 删除从BT到ET的所有行 + filtered_lines = filtered_lines[:bt_start_line] + else: + filtered_lines.append(lines[i]) + + in_text_block = False + skip_until_et = False + bt_start_line = -1 + has_rotated_cm = False # 重置cm状态 + i += 1 + continue + + # 检查Q(恢复图形状态)- 重置cm状态 + if line == 'Q': + has_rotated_cm = False + + # 在文本块内,检查Tm操作符 + if in_text_block and 'Tm' in line: + # 尝试解析Tm矩阵:a b c d e f Tm + # 可能在一行中,也可能与其他操作符混合 + tm_match = line + + # 查找最后的6个数字 + Tm + parts = tm_match.split() + + # 尝试找到Tm操作符的位置 + try: + tm_idx = -1 + for idx, part in enumerate(parts): + if part == 'Tm' or part.endswith('Tm'): + tm_idx = idx + break + + if tm_idx >= 6: + # 获取Tm前的6个参数 + a = float(parts[tm_idx-6]) + b = float(parts[tm_idx-5]) + c = float(parts[tm_idx-4]) + d = float(parts[tm_idx-3]) + e = float(parts[tm_idx-2]) + f = float(parts[tm_idx-1]) + + # 计算旋转角度 + angle = math.degrees(math.atan2(b, a)) + + if abs(angle) > self.angle_threshold: + # 这是旋转文本(水印),标记跳过整个BT...ET块 + skip_until_et = True + i += 1 + continue + except (ValueError, IndexError): + pass # 解析失败,保留这行 + + # 如果不在跳过状态,保留这行 + if not skip_until_et: + filtered_lines.append(lines[i]) + + i += 1 + + # 重建内容流 + modified_content = '\n'.join(filtered_lines) + + try: + return modified_content.encode('latin-1') + except: + return modified_content.encode('utf-8') + + +class WorkingWatermarkRemover: + """真正工作的水印去除器""" + + def __init__(self, angle_threshold=0.5): + self.angle_threshold = angle_threshold # 默认0.5度,任何旋转都会被检测 + self.parser = PDFContentStreamParser(angle_threshold) + + def _is_rotated_text(self, line_dir): + """判断文本是否旋转""" + angle = math.degrees(math.atan2(line_dir[1], line_dir[0])) + is_rotated = abs(angle) > self.angle_threshold + return is_rotated, angle + + def analyze(self, pdf_path): + """分析PDF中的水印""" + print(f"\n正在分析 PDF: {pdf_path}") + print("=" * 80) + + doc = fitz.open(pdf_path) + watermark_info = {} + total_rotated = 0 + + for page_num in range(len(doc)): + print(f" 扫描第 {page_num + 1}/{len(doc)} 页...", end='\r') + page = doc[page_num] + blocks = page.get_text("dict")["blocks"] + + page_watermarks = [] + + for block in blocks: + if "lines" not in block: + continue + + for line in block["lines"]: + line_dir = line.get("dir", (1, 0)) + is_rotated, angle = self._is_rotated_text(line_dir) + + if is_rotated: + for span in line["spans"]: + text = span["text"].strip() + if text: + page_watermarks.append({ + 'text': text, + 'font': span["font"], + 'angle': angle + }) + total_rotated += 1 + + if page_watermarks: + watermark_info[page_num] = page_watermarks + + doc.close() + + print(f"\n\n分析完成!") + print(f"\n发现的水印:") + print(f" 总数: {total_rotated}") + print(f" 分布页数: {len(watermark_info)}") + + # 显示示例 + if watermark_info: + print(f"\n水印示例(前10个):") + sample_count = 0 + for page_num, watermarks in list(watermark_info.items())[:5]: + for wm in watermarks[:3]: + if sample_count >= 10: + break + sample_count += 1 + print(f" {sample_count}. 第{page_num + 1}页: \"{wm['text'][:40]}\"") + print(f" 字体: {wm['font']}, 角度: {wm['angle']:+.1f}°") + + return watermark_info + + def remove(self, input_pdf, output_pdf): + """删除水印(真正的实现)""" + print(f"\n正在删除水印: {input_pdf}") + print(f"输出文件: {output_pdf}") + print(f"方法: 内容流操作(删除旋转文本对象)") + print("=" * 80) + + # 先分析 + watermark_info = self.analyze(input_pdf) + + if not watermark_info: + print("\n未发现水印,直接复制文件") + import shutil + shutil.copy2(input_pdf, output_pdf) + return 0 + + print(f"\n开始处理内容流...") + print("-" * 80) + + # 打开PDF + pdf = pikepdf.open(input_pdf) + + modified_pages = 0 + processed_blocks = 0 + + for page_num, page in enumerate(pdf.pages): + if page_num not in watermark_info: + continue + + try: + # 获取页面内容流 + if '/Contents' not in page: + continue + + contents = page.Contents + + # 处理单个或多个内容流 + if isinstance(contents, pikepdf.Array): + # 多个内容流对象 + for i, content_obj in enumerate(contents): + if hasattr(content_obj, 'read_bytes'): + stream_data = content_obj.read_bytes() + + # 修改内容流 + modified_data = self.parser.parse_content_stream(stream_data) + + # 更新内容流 + content_obj.write(modified_data) + processed_blocks += 1 + else: + # 单个内容流对象 + if hasattr(contents, 'read_bytes'): + stream_data = contents.read_bytes() + + # 修改内容流 + modified_data = self.parser.parse_content_stream(stream_data) + + # 更新内容流 + contents.write(modified_data) + processed_blocks += 1 + + modified_pages += 1 + print(f" ✓ 第{page_num + 1}页: 处理了 {len(watermark_info[page_num])} 个水印") + + except Exception as e: + print(f" ✗ 第{page_num + 1}页处理失败: {e}") + continue + + print(f"\n保存修改后的PDF...") + print("-" * 80) + + # 保存 + pdf.save(output_pdf) + pdf.close() + + print(f"\n完成!") + print(f" 修改页数: {modified_pages}") + print(f" 处理的内容流块: {processed_blocks}") + print(f" 标识的水印数: {sum(len(wms) for wms in watermark_info.values())}") + + # 文件大小对比 + original_size = os.path.getsize(input_pdf) + final_size = os.path.getsize(output_pdf) + print(f"\n文件大小:") + print(f" 原始: {original_size / 1024 / 1024:.2f} MB") + print(f" 清理后: {final_size / 1024 / 1024:.2f} MB") + + return modified_pages + + +def main(): + """主函数""" + parser = argparse.ArgumentParser( + description='PDF 水印去除工具 - 真正工作的版本', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +特点: + - 真正从内容流中删除旋转文本对象 + - 不是覆盖,是删除操作符 + - 基于Tm和cm矩阵识别旋转文本 + - 任何旋转的文本都会被识别为水印 + - 删除整个BT...ET文本块 + +工作原理: + 1. 解析PDF内容流 + 2. 识别包含旋转矩阵的Tm或cm操作符 + 3. 删除从BT到ET的整个文本块 + 4. 重建内容流 + +示例: + # 分析 + %(prog)s --mode analyze --input file.pdf + + # 删除(默认:旋转>0.5度即删除) + %(prog)s --mode remove --input file.pdf --output clean.pdf + + # 只删除旋转超过10度的(更保守) + %(prog)s --mode remove --input file.pdf --output clean.pdf --angle-threshold 10.0 + + # 删除所有旋转文本(设为0) + %(prog)s --mode remove --input file.pdf --output clean.pdf --angle-threshold 0 + """ + ) + + parser.add_argument('--mode', '-m', + choices=['analyze', 'remove'], + required=True, + help='操作模式') + + parser.add_argument('--input', '-i', + required=True, + help='输入 PDF 文件路径') + + parser.add_argument('--output', '-o', + help='输出 PDF 文件路径(remove模式必需)') + + parser.add_argument('--angle-threshold', '-a', + type=float, + default=0.5, + help='旋转角度阈值(度),默认0.5,任何旋转都会被检测为水印') + + args = parser.parse_args() + + if not os.path.exists(args.input): + print(f"错误: 文件不存在: {args.input}") + sys.exit(1) + + if args.mode == 'remove' and not args.output: + print("错误: remove 模式需要指定 --output 参数") + sys.exit(1) + + print("=" * 80) + print("PDF 水印去除工具 - 真正工作的版本") + print("=" * 80) + + remover = WorkingWatermarkRemover(angle_threshold=args.angle_threshold) + + if args.mode == 'analyze': + remover.analyze(args.input) + + elif args.mode == 'remove': + if os.path.exists(args.output): + response = input(f"\n输出文件已存在: {args.output}\n是否覆盖? [y/N]: ").strip().lower() + if response not in ['y', 'yes']: + print("操作已取消") + sys.exit(0) + + remover.remove(args.input, args.output) + print(f"\n✓ 完成! 输出文件: {args.output}") + print("\n💡 建议:打开PDF检查效果,确认正常内容是否完整") + + +if __name__ == '__main__': + main() +