--- /dev/null
+#!/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)
+
--- /dev/null
+#!/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)
+
--- /dev/null
+#!/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
+ )
--- /dev/null
+at+clac\r
+\r
+AT+CFUN\r
+\r
+AT+CSQ\r
+\r
+AT+COPS\r
+\r
+AT+CGATT\r
+\r
+AT+CTZR\r
+\r
+AT+CCLK\r
+\r
+AT+CEREG\r
+\r
+AT+CPLS\r
+\r
+AT+CPOL\r
+\r
+AT+COPN\r
+\r
+AT+NPSMR\r
+\r
+AT+NPSMSET\r
+\r
+AT+NEARFCN\r
+\r
+AT+CPSMS\r
+\r
+AT+NBAND\r
+\r
+AT+CNEC\r
+\r
+AT+CTZU\r
+\r
+AT+NDRX\r
+\r
+AT+CSCON\r
+\r
+AT+NCSEARFCN\r
+\r
+AT+NUESTATS\r
+\r
+AT+NPOWD\r
+\r
+AT+CREG\r
+\r
+AT+CGREG\r
+\r
+AT+NSQ\r
+\r
+AT+NNMPSD\r
+\r
+AT+NGSBLACKCELL\r
+\r
+AT+NCSBLACKCELL\r
+\r
+AT+NBACKOFF\r
+\r
+AT+NROAMING\r
+\r
+AT+NRESELCFG\r
+\r
+AT+NSIMAC\r
+\r
+AT+NDRXS\r
+\r
+AT+CGTFT\r
+\r
+AT+CGPADDR\r
+\r
+AT+CGDCONT\r
+\r
+AT+CGACT\r
+\r
+AT+CGCMOD\r
+\r
+AT+CGDSCONT\r
+\r
+AT+CGTFTRDP\r
+\r
+AT+CGEQOSRDP\r
+\r
+AT+CGAUTH\r
+\r
+AT+CGEQOS\r
+\r
+AT+CEER\r
+\r
+AT+CGCONTRDP\r
+\r
+AT+NCONFIG\r
+\r
+AT+CIMI\r
+\r
+AT+CGLA\r
+\r
+AT+CRSM\r
+\r
+AT+CSIM\r
+\r
+AT+CCHC\r
+\r
+AT+CCHO\r
+\r
+AT+USIMHOTPLUG\r
+\r
+AT+NUICC\r
+\r
+AT+ICCID\r
+\r
+AT+PHONENUM\r
+\r
+AT+NSIMSRS\r
+\r
+AT+CLEARFPLMN\r
+\r
+AT+USIMRETRYTIMES\r
+\r
+AT+CPIN\r
+\r
+AT+CLCK\r
+\r
+AT+CPWD\r
+\r
+AT+NPIN\r
+\r
+AT+SETHWLOCK\r
+\r
+AT+CARDLOCK\r
+\r
+AT+SIMLOCK\r
+\r
+AT+CPINR\r
+\r
+AT+CMEE\r
+\r
+AT+CGMI\r
+\r
+AT+CGSN\r
+\r
+AT+CLAC\r
+\r
+AT+RESET\r
+\r
+AT+NLOGLEVEL\r
+\r
+AT+NATSPEED\r
+\r
+AT+NFWUPD\r
+\r
+AT+NDSDELAY\r
+\r
+AT+NCRASHINFO\r
+\r
+AT+NTSETID\r
+\r
+AT+NTPERMID\r
+\r
+AT+NCHIPTYPE\r
+\r
+AT+NQSTATE\r
+\r
+AT+USIMSEL\r
+\r
+AT+NVETOSUPERDS\r
+\r
+AT+NBANKD\r
+\r
+AT+NVER\r
+\r
+AT+MODEMTEMP\r
+\r
+AT+NUSBPSM\r
+\r
+AT+NRATEMODE\r
+\r
+AT+NFLIST\r
+\r
+AT+RECONNDEL\r
+\r
+AT+NRCUPD\r
+\r
+AT+NLOGMODFT\r
+\r
+AT+NRDCTRL\r
+\r
+AT+NRDSET\r
+\r
+AT+NRDEXEC\r
+\r
+AT+NGOLD\r
+\r
+AT+CSCA\r
+\r
+AT+CNMI\r
+\r
+AT+CMGS\r
+\r
+AT+CMMS\r
+\r
+AT+CMGF\r
+\r
+AT+CSMP\r
+\r
+AT+CSDH\r
+\r
+AT+CMGR\r
+\r
+AT+CMGL\r
+\r
+AT+CMGD\r
+\r
+AT+CMGW\r
+\r
+AT+CPMS\r
+\r
+AT+CMSS\r
+\r
+AT+CSCS\r
+\r
+AT+NDISDUP\r
+\r
+AT+NPING\r
+\r
+AT+NDNS\r
+\r
+AT+NSOCR\r
+\r
+AT+NSOST\r
+\r
+AT+NSOCO\r
+\r
+AT+NSOSD\r
+\r
+AT+NQSOS\r
+\r
+AT+NSORF\r
+\r
+AT+NSOCL\r
+\r
+AT+NSOSTATUS\r
+\r
+AT+NCIDSTATUS\r
+\r
+AT+NDATATRAN\r
+\r
+AT+NIPINFO\r
+\r
+AT+NKEEPALIVE\r
+\r
+AT+NISEND\r
+\r
+AT+NDATARATE\r
+\r
+AT+NFCMEMBYTE\r
+\r
+AT+TTSBATLV\r
+\r
+AT+TTSAMOUNT\r
+\r
+AT+TTSPRESET\r
+\r
+AT+NRDTEST\r
+\r
+AT+NWIFISCAN\r
+\r
+AT+NWIFISCANSTOP\r
+\r
+AT+NTXPOWER\r
+\r
+AT+DEMO\r
+\r
+AT+IVSN\r
+\r
+AT+GMR\r
+\r
+AT+FTCAL\r
+\r
+AT+FTRF\r
+\r
+AT+IFC\r
+\r
+AT+ICF\r
+\r
+AT+IPR\r
+\r
+AT+NWIDCFG\r
+\r
+AT+DUMP\r
+\r
+AT+CERTADD\r
+\r
+AT+CERTCHECK\r
+\r
+AT+CERTDEL\r
+\r
+AT+IPINFO\r
+\r
+AT+IPSTATUS\r
+\r
+AT+NCUSTSWITCH\r
+\r
+AT+PDPKEEPALIVE\r
+\r
+AT+PDPSTATUS\r
+\r
+AT+RECVMODE\r
+\r
+AT+TCPACK\r
+\r
+AT+TCPCLOSE\r
+\r
+AT+TCPKEEPALIVE\r
+\r
+AT+TCPREAD\r
+\r
+AT+TCPSEND\r
+\r
+AT+TCPSETUP\r
+\r
+AT+TCPTRANS\r
+\r
+AT+TRANSCLOSE\r
+\r
+AT+UDPCLOSE\r
+\r
+AT+UDPREAD\r
+\r
+AT+UDPSEND\r
+\r
+AT+UDPSETUP\r
+\r
+AT+UDPTRANS\r
+\r
+AT+CESQ\r
+\r
+AT+CPSERVICE\r
+\r
+AT+ENPWRSAVE\r
+\r
+AT+IMEI\r
+\r
+AT+NFTCHIPTYPE\r
+\r
+AT+NFTFBACKUP\r
+\r
+AT+NFTIMEI\r
+\r
+AT+NFTRCAL\r
+\r
+AT+NFTRCREG\r
+\r
+AT+NFTRCSQ\r
+\r
+AT+NFTRFBAK\r
+\r
+AT+NFTRFIT\r
+\r
+AT+NWENPWRSAVE\r
+\r
+AT+NWPWROFF\r
+\r
+AT+QCNVERSION\r
+\r
+AT+READADC\r
+\r
+AT+CCID\r
+\r
+AT+CNUM\r
+\r
+AT+NWCCID\r
+\r
+AT+NWSIMCFG\r
+\r
+AT+NWSIMDET\r
+\r
+AT+NWSIMWRCNT\r
+\r
+AT+NWSIMWRCNTCTL\r
+\r
+AT+SIMCOUNTCTL\r
+\r
+AT+SIMCOUNTREAD\r
+\r
+AT+SIMCROSS\r
+\r
+AT+SIMHOTSWAP\r
+\r
+AT+NETSHAREACT\r
+\r
+AT+XIIC\r
+\r
+AT+CGMM\r
+\r
+AT+CGMR\r
+\r
+AT+GMM\r
+\r
+AT+GSN\r
+\r
+AT+HEAPINFO\r
+\r
+AT+NETSHAREMODE\r
+\r
+AT+RSPACK\r
+\r
+AT+SIGNAL\r
+\r
+AT+BANDLOCK\r
+\r
+AT+MYCELLINFO\r
+\r
+AT+MYCGED\r
+\r
+AT+MYLACID\r
+\r
+AT+MYNETINFO\r
+\r
+AT+MYSYSINFO\r
+\r
+AT+MYSYSINFOURC\r
+\r
+AT+MYURCSYSINFO\r
+\r
+AT+NBANDLOCK\r
+\r
+AT+NETDMSG\r
+\r
+AT+NETMSG\r
+\r
+AT+NFREQLOCK\r
+\r
+AT+NRSP\r
+\r
+AT+NSTGETRSSI\r
+\r
+AT+NWENG\r
+\r
+AT+NWRFTEST\r
+\r
+AT+NWRRCRELEASE\r
+\r
+AT+NWUEDRX\r
+\r
+AT+MQTTCONN\r
+\r
+AT+MQTTCONNPARAM\r
+\r
+AT+MQTTDISCONN\r
+\r
+AT+MQTTMUX\r
+\r
+AT+MQTTPUB\r
+\r
+AT+MQTTPUBS\r
+\r
+AT+MQTTSTATE\r
+\r
+AT+MQTTSUB\r
+\r
+AT+MQTTTLS\r
+\r
+AT+MQTTUNSUB\r
+\r
+AT+MQTTWILLMSG\r
+\r
+AT+MQTTWILLPARAM\r
+\r
+AT+SSLCIPHERSET\r
+\r
+AT+SSLTCPCFG\r
+\r
+AT+SSLTCPCFGA\r
+\r
+AT+SSLTCPCLOSE\r
+\r
+AT+SSLTCPREAD\r
+\r
+AT+SSLTCPSEND\r
+\r
+AT+SSLTCPSETUP\r
+\r
+AT+FILEHTTPACTION\r
+\r
+AT+FILEHTTPSACTION\r
+\r
+AT+HTTPACTION\r
+\r
+AT+HTTPCLOSE\r
+\r
+AT+HTTPGET\r
+\r
+AT+HTTPGETSTAT\r
+\r
+AT+HTTPPARA\r
+\r
+AT+HTTPSACTION\r
+\r
+AT+HTTPSCFG\r
+\r
+AT+HTTPSCLOSE\r
+\r
+AT+HTTPSETUP\r
+\r
+AT+HTTPSGET\r
+\r
+AT+HTTPSGETSTAT\r
+\r
+AT+HTTPSPARA\r
+\r
+AT+HTTPSSETUP\r
+\r
+AT+FSDF\r
+\r
+AT+FSFAT\r
+\r
+AT+FSFS\r
+\r
+AT+FSLIST\r
+\r
+AT+FSLS\r
+\r
+AT+FSRF\r
+\r
+AT+FSRFEX\r
+\r
+AT+FSRN\r
+\r
+AT+FSWF\r
+\r
+AT+NWATFOTA\r
+\r
+AT+NWFOTA\r
+\r
+AT+NWFOTASCFG\r
+\r
+AT+NWFTPFOTA\r
+\r
+AT+FILEFTPGET\r
+\r
+AT+FTPGET\r
+\r
+AT+FTPLOGI\r
+N\r
+\r
+AT+FTPLOGOUT\r
+\r
+AT+FTPPUT\r
+\r
+AT+FTPSIZE\r
+\r
+AT+FTPSTATUS\r
+\r
+AT+DNSSERVER\r
+\r
+AT+NWDNS\r
+\r
+AT+PING\r
+\r
+AT+UPDATETIME\r
+\r
+AT+CIPGSMLOC\r
+\r
+AT+NWURCCFG\r
+\r
+AT&F\r
+\r
+AT&W\r
+\r
+AT&V\r
+\r
+ATD\r
+\r
+ATH\r
+\r
+ATS0\r
+\r
+ATE\r
+\r
+ATI\r
+\r
+ATO\r
+\r
+OK
\ No newline at end of file
--- /dev/null
+# 证书存储和使用机制说明\r
+\r
+## 证书存储方式\r
+\r
+### 1. 存储机制\r
+- **存储方式**:通过 `AT+CERTADD=<file_name>,<length>` 添加证书\r
+- **存储位置**:证书存储在模组的非易失性存储器中\r
+- **存储格式**:以**文件名**作为唯一标识符存储证书内容\r
+- **持久化**:证书会持久保存,即使模组重启也不会丢失\r
+\r
+### 2. 证书数量\r
+- 模组可以存储**多个证书**(具体数量限制取决于模组型号和存储空间)\r
+- 每个证书通过**唯一的文件名**区分\r
+- 可以通过 `AT+CERTCHECK?` 查询所有已存储的证书列表\r
+\r
+### 3. 证书管理\r
+- **添加证书**:`AT+CERTADD=<file_name>,<length>`\r
+- **检查证书**:`AT+CERTCHECK=<file_name>` 或 `AT+CERTCHECK?`\r
+- **删除证书**:`AT+CERTDEL=<file_name>`\r
+\r
+## HTTPS下载时证书的指定方式\r
+\r
+### 关键机制:每次连接前明确指定证书\r
+\r
+**重要**:HTTPS下载时,**不会混用证书**,因为每次连接前都会**明确指定**使用哪个证书。\r
+\r
+### 工作流程\r
+\r
+1. **添加证书到模组**(如果不存在)\r
+ ```python\r
+ http_client.add_certificate(ca_cert_name, ca_cert_data)\r
+ ```\r
+ - 执行:`AT+CERTADD=<cert_name>,<length>`\r
+ - 作用:将证书存储到模组中\r
+\r
+2. **配置HTTPS使用指定证书**(每次连接前)\r
+ ```python\r
+ http_client.configure_https(\r
+ ssl_version=3,\r
+ auth_mode=1, # 单向认证\r
+ ca_cert=ca_cert_name # 明确指定证书名称\r
+ )\r
+ ```\r
+ - 执行:`AT+HTTPSCFG="cacert",<cert_name>`\r
+ - 作用:**明确指定**本次HTTPS连接使用哪个证书\r
+\r
+3. **建立HTTPS连接**\r
+ - 模组会使用步骤2中配置的证书来验证服务器证书\r
+ - **不会使用其他证书**\r
+\r
+### 证书配置的作用范围\r
+\r
+- **会话级别**:`AT+HTTPSCFG="cacert"` 的配置是**会话级别的**\r
+- **每次连接都需要重新配置**:每次建立新的HTTPS连接前,都需要重新调用 `configure_https()`\r
+- **不会影响其他证书**:配置只影响当前HTTPS会话,不会影响模组中存储的其他证书\r
+\r
+## 为什么不会混用证书?\r
+\r
+### 原因1:明确指定机制\r
+```python\r
+# 每次HTTPS连接前都会执行\r
+AT+HTTPSCFG="cacert",<cert_name> # 明确指定使用哪个证书\r
+```\r
+\r
+### 原因2:会话隔离\r
+- 每个HTTPS连接都是独立的会话\r
+- 每个会话都会明确指定使用的证书\r
+- 不同会话之间不会互相影响\r
+\r
+### 原因3:配置覆盖\r
+- 如果之前配置过其他证书,新的配置会**覆盖**之前的配置\r
+- 但只影响**当前会话**,不会影响模组中存储的证书\r
+\r
+## 实际代码流程\r
+\r
+```python\r
+# 1. 添加证书(如果不存在)\r
+if not http_client.certificate_exists(ca_cert_name):\r
+ http_client.add_certificate(ca_cert_name, ca_cert_data)\r
+\r
+# 2. 配置HTTPS使用指定证书(每次连接前都会执行)\r
+http_client.configure_https(\r
+ ssl_version=3,\r
+ auth_mode=1,\r
+ ca_cert=ca_cert_name # 明确指定:使用这个证书\r
+)\r
+\r
+# 3. 建立HTTPS连接\r
+# 模组会使用步骤2中指定的证书来验证服务器\r
+http_client.setup_connection()\r
+http_client.send_get_request()\r
+```\r
+\r
+## 关于删除证书的必要性\r
+\r
+### 如果证书名称不同\r
+- **不需要删除**:因为每个证书都有唯一的名称\r
+- **不会混用**:因为每次都会明确指定使用哪个证书\r
+\r
+### 如果证书名称相同\r
+- **理论上不需要删除**:如果保证同一个名称的证书内容相同\r
+- **但建议删除**:如果证书内容可能不同,建议先删除再添加,确保使用最新证书\r
+\r
+### 当前代码的处理方式\r
+\r
+```python\r
+# 当前代码:如果证书已存在,先删除再添加\r
+if http_client.certificate_exists(ca_cert_name):\r
+ http_client.delete_certificate(ca_cert_name)\r
+\r
+http_client.add_certificate(ca_cert_name, ca_cert_data)\r
+```\r
+\r
+**对于您的场景**(每个证书名称都不同,且保证同名证书内容相同):\r
+- **可以优化**:如果证书已存在且内容相同,可以跳过添加步骤\r
+- **不会混用**:因为每次都会通过 `configure_https(ca_cert=ca_cert_name)` 明确指定\r
+\r
+## 建议的优化方案\r
+\r
+### 方案1:检查证书是否存在,存在则跳过(推荐)\r
+\r
+```python\r
+# 如果证书已存在,跳过添加(假设同名证书内容相同)\r
+if http_client.certificate_exists(ca_cert_name):\r
+ print(f"✅ 证书 {ca_cert_name} 已存在,跳过添加")\r
+else:\r
+ # 只有不存在时才添加\r
+ http_client.add_certificate(ca_cert_name, ca_cert_data)\r
+ http_client.check_certificate(ca_cert_name)\r
+\r
+# 无论证书是否已存在,都需要配置HTTPS使用该证书\r
+http_client.configure_https(\r
+ ssl_version=3,\r
+ auth_mode=1,\r
+ ca_cert=ca_cert_name # 明确指定使用这个证书\r
+)\r
+```\r
+\r
+### 方案2:验证证书内容(如果模组支持)\r
+\r
+如果模组支持验证证书内容,可以比较证书内容是否相同,但通常不需要这么复杂。\r
+\r
+## 总结\r
+\r
+1. **证书存储**:模组可以存储多个证书,每个证书通过文件名唯一标识\r
+2. **证书指定**:每次HTTPS连接前,通过 `AT+HTTPSCFG="cacert",<cert_name>` 明确指定使用哪个证书\r
+3. **不会混用**:因为每次都会明确指定,不会使用其他证书\r
+4. **删除必要性**:如果证书名称不同,不需要删除;如果名称相同但内容可能不同,建议先删除再添加\r
+5. **优化建议**:如果保证同名证书内容相同,可以检查存在后跳过添加,但**必须**每次都调用 `configure_https()` 明确指定证书\r
+\r
--- /dev/null
+# AT指令HTTP/HTTPS证书使用指南\r
+\r
+## 证书管理AT指令\r
+\r
+### 1. 添加证书:`AT+CERTADD`\r
+```\r
+AT+CERTADD=<file_name>,<length>\r
+```\r
+- 发送后会返回 `CONNECT`\r
+- 然后发送证书数据(二进制)\r
+- 最后返回 `+CERTADD: <length>,OK` 或 `+CERTADD: ERROR`\r
+\r
+### 2. 检查证书:`AT+CERTCHECK`\r
+```\r
+AT+CERTCHECK=<file_name> # 检查指定证书是否存在\r
+AT+CERTCHECK? # 列出所有证书\r
+```\r
+\r
+### 3. 删除证书:`AT+CERTDEL`\r
+```\r
+AT+CERTDEL=<file_name> # 删除指定证书\r
+```\r
+\r
+## 证书使用流程\r
+\r
+### 当前实现方式\r
+\r
+在 `at_test_download_file.py` 中,证书添加流程如下:\r
+\r
+1. **检查证书是否存在**(可选)\r
+ ```python\r
+ if http_client.certificate_exists(ca_cert_name):\r
+ print(f"✅ 证书 {ca_cert_name} 已存在,跳过添加")\r
+ ```\r
+\r
+2. **添加证书**\r
+ ```python\r
+ http_client.add_certificate(ca_cert_name, ca_cert_data)\r
+ ```\r
+\r
+3. **确认证书**\r
+ ```python\r
+ http_client.check_certificate(ca_cert_name)\r
+ ```\r
+\r
+4. **配置HTTPS使用证书**\r
+ ```python\r
+ http_client.configure_https(\r
+ ssl_version=3,\r
+ auth_mode=1, # 单向认证\r
+ ca_cert=ca_cert_name\r
+ )\r
+ ```\r
+\r
+## 关于证书覆盖的问题\r
+\r
+### 问题分析\r
+\r
+**如果使用相同的证书名称添加证书:**\r
+- 模组可能会**覆盖**旧证书\r
+- 但为了**确保验证的是当前证书**,建议先删除旧证书\r
+\r
+### 为什么需要删除旧证书?\r
+\r
+1. **确保证书一致性**:删除旧证书可以确保模组中只有最新的证书\r
+2. **避免证书冲突**:如果证书内容不同但名称相同,可能导致验证失败\r
+3. **清理不需要的证书**:删除不再使用的证书,释放存储空间\r
+\r
+## 推荐的最佳实践\r
+\r
+### 方案1:先删除再添加(推荐)\r
+\r
+```python\r
+# 1. 检查证书是否存在\r
+if http_client.certificate_exists(ca_cert_name):\r
+ print(f"证书 {ca_cert_name} 已存在,先删除旧证书...")\r
+ http_client.delete_certificate(ca_cert_name)\r
+\r
+# 2. 添加新证书\r
+http_client.add_certificate(ca_cert_name, ca_cert_data)\r
+\r
+# 3. 确认证书\r
+http_client.check_certificate(ca_cert_name)\r
+```\r
+\r
+### 方案2:使用唯一证书名称\r
+\r
+```python\r
+# 使用带时间戳或版本的证书名称\r
+import time\r
+ca_cert_name = f"ca_{int(time.time())}.crt"\r
+# 或\r
+ca_cert_name = f"ca_v1.crt"\r
+```\r
+\r
+### 方案3:列出所有证书并清理\r
+\r
+```python\r
+# 列出所有证书\r
+cert_list = http_client.list_certificates()\r
+print(f"当前证书列表: {cert_list}")\r
+\r
+# 删除不需要的证书\r
+for old_cert in old_cert_list:\r
+ http_client.delete_certificate(old_cert)\r
+\r
+# 添加新证书\r
+http_client.add_certificate(ca_cert_name, ca_cert_data)\r
+```\r
+\r
+## 证书验证机制\r
+\r
+### HTTPS单向认证流程\r
+\r
+1. **客户端(模组)** 使用 `AT+HTTPSCFG="cacert",<cert_name>` 配置CA证书\r
+2. **建立HTTPS连接** 时,模组会:\r
+ - 接收服务器的证书\r
+ - 使用配置的CA证书验证服务器证书\r
+ - 如果验证通过,建立连接\r
+ - 如果验证失败,返回错误\r
+\r
+### 证书选择\r
+\r
+- 模组会使用 `AT+HTTPSCFG="cacert"` 中指定的证书名称\r
+- 如果该证书不存在,HTTPS连接会失败\r
+- **如果模组中有多个证书,只有配置的证书会被使用**\r
+\r
+## 注意事项\r
+\r
+1. **证书名称唯一性**:如果使用相同的证书名称,新证书会覆盖旧证书\r
+2. **证书存储限制**:模组可能有证书存储数量限制\r
+3. **证书大小限制**:单个证书可能有大小限制\r
+4. **证书格式**:必须是PEM格式的证书\r
+\r
+## 建议的代码改进\r
+\r
+在 `n706a_http_client.py` 中添加删除证书的方法:\r
+\r
+```python\r
+def delete_certificate(self, cert_name):\r
+ """\r
+ 删除SSL证书\r
+ \r
+ Args:\r
+ cert_name: 证书文件名\r
+ \r
+ Returns:\r
+ bool: 是否删除成功\r
+ """\r
+ success, response = self.at.send_at_command(\r
+ f'AT+CERTDEL={cert_name}',\r
+ timeout=5\r
+ )\r
+ if not success:\r
+ return False\r
+ \r
+ if "OK" in response:\r
+ print(f"✅ 证书 {cert_name} 已删除")\r
+ return True\r
+ else:\r
+ print(f"❌ 证书删除失败: {response}")\r
+ return False\r
+```\r
+\r
+然后在添加证书前先删除旧证书:\r
+\r
+```python\r
+# 如果证书已存在,先删除\r
+if http_client.certificate_exists(ca_cert_name):\r
+ print(f"证书 {ca_cert_name} 已存在,先删除...")\r
+ http_client.delete_certificate(ca_cert_name)\r
+\r
+# 添加新证书\r
+http_client.add_certificate(ca_cert_name, ca_cert_data)\r
+```\r
+\r
--- /dev/null
+# 证书文件(包含敏感信息,不应提交到版本控制)\r
+*.crt\r
+*.key\r
+# *.pem\r
+*.der\r
+*.csr\r
+*.srl\r
+\r
+# 但保留示例和说明文件\r
+!README.md\r
+!certificate_preparation_guide.md\r
+!generate_test_certs.sh\r
+!download_server_ca.py\r
+!check_certificate.py\r
+!extract_ca_from_chain.py\r
+!verify_ca_for_https.py\r
+!ca_cert_usage_explanation.md\r
+\r
--- /dev/null
+# CA证书使用说明:服务器证书 vs CA证书\r
+\r
+## 问题:直接拿服务器的ca.crt可以用到client上吗?\r
+\r
+**简短回答**:**可以,但需要确认证书类型**。\r
+\r
+## 详细说明\r
+\r
+### 1. 证书类型区分\r
+\r
+#### CA证书(Certificate Authority Certificate)\r
+- **用途**:用于验证其他证书的有效性\r
+- **特点**:是根证书或中间证书,用于验证服务器证书是否由可信CA签发\r
+- **客户端使用**:✅ **可以直接使用**,用于单向认证验证服务器身份\r
+\r
+#### 服务器证书(Server Certificate)\r
+- **用途**:服务器自己的身份证书\r
+- **特点**:由CA签发的终端实体证书\r
+- **客户端使用**:❌ **不能直接使用**,但可以从中提取CA证书链\r
+\r
+### 2. 如何判断证书类型\r
+\r
+#### 方法1:查看证书主题(Subject)\r
+\r
+```bash\r
+openssl x509 -in ca.crt -noout -subject -issuer\r
+```\r
+\r
+**CA证书特征**:\r
+- Subject 和 Issuer **相同**(自签名)\r
+- 通常包含 "CA"、"Certificate Authority" 等字样\r
+- CN(Common Name)通常是 "Root CA"、"Intermediate CA" 等\r
+\r
+**服务器证书特征**:\r
+- Subject 和 Issuer **不同**\r
+- Subject 通常是服务器域名(如 `ambybox-api.test.leiniao.com`)\r
+- Issuer 是CA的名称\r
+\r
+#### 方法2:查看证书扩展\r
+\r
+```bash\r
+openssl x509 -in ca.crt -noout -text | grep -A 5 "X509v3 extensions"\r
+```\r
+\r
+**CA证书**:通常包含 `Basic Constraints: CA:TRUE`\r
+**服务器证书**:通常包含 `Basic Constraints: CA:FALSE`\r
+\r
+### 3. 使用场景\r
+\r
+#### 场景A:服务器提供的是CA证书 ✅\r
+\r
+如果服务器提供的 `ca.crt` 是真正的CA证书(根证书或中间证书),那么:\r
+\r
+```python\r
+# 可以直接使用\r
+AUTH_MODE = 1\r
+CA_CERT_PATH = "certs/ca.crt" # 直接使用\r
+```\r
+\r
+**这是最常见的情况**,因为:\r
+- 客户端需要CA证书来验证服务器证书\r
+- 服务器通常会提供其证书链中的CA证书\r
+\r
+#### 场景B:服务器提供的是服务器证书 ⚠️\r
+\r
+如果服务器提供的是服务器自己的证书(不是CA证书),需要:\r
+\r
+**选项1:提取CA证书链**\r
+\r
+```bash\r
+# 使用OpenSSL获取完整证书链(包括CA证书)\r
+openssl s_client -showcerts -connect ambybox-api.test.leiniao.com:443 </dev/null 2>/dev/null | \\r
+ openssl x509 -outform PEM > server_chain.pem\r
+\r
+# 从证书链中提取CA证书(通常是第二个或最后一个证书)\r
+openssl x509 -in server_chain.pem -outform PEM -out ca.crt\r
+```\r
+\r
+**选项2:从服务器管理员获取CA证书**\r
+\r
+联系服务器管理员,获取用于验证服务器证书的CA证书。\r
+\r
+### 4. 实际使用建议\r
+\r
+#### 推荐方法:从服务器下载完整证书链\r
+\r
+使用提供的脚本:\r
+\r
+```bash\r
+cd leiniao/leiniao_python_test/certs\r
+python3 download_server_ca.py https://ambybox-api.test.leiniao.com -o ca.crt\r
+```\r
+\r
+这个脚本会:\r
+1. 连接到服务器\r
+2. 获取服务器证书\r
+3. 提取CA证书链\r
+4. 保存为 `ca.crt`\r
+\r
+#### 验证证书是否可用\r
+\r
+```bash\r
+# 检查证书类型\r
+openssl x509 -in ca.crt -noout -subject -issuer\r
+\r
+# 验证证书格式\r
+python3 check_certificate.py ca.crt\r
+```\r
+\r
+### 5. 常见问题\r
+\r
+#### Q1: 服务器给的ca.crt是服务器证书怎么办?\r
+\r
+**A**: 需要提取CA证书链,或联系服务器管理员获取真正的CA证书。\r
+\r
+#### Q2: 如何确认证书是否可用?\r
+\r
+**A**: \r
+1. 检查证书的 Subject 和 Issuer\r
+2. 如果 Subject == Issuer,通常是CA证书,可以直接使用\r
+3. 如果 Subject != Issuer,可能是服务器证书,需要提取CA证书\r
+\r
+#### Q3: 证书链中有多个证书怎么办?\r
+\r
+**A**: \r
+- 对于单向认证,通常只需要**根CA证书**(最后一个证书)\r
+- 某些情况下可能需要**中间CA证书**\r
+- 可以尝试使用根CA证书,如果验证失败,再添加中间CA证书\r
+\r
+### 6. 代码中的使用\r
+\r
+```python\r
+# 在 get_device_token_at.py 中\r
+if __name__ == "__main__":\r
+ # 单向认证配置\r
+ AUTH_MODE = 1\r
+ CA_CERT_PATH = "certs/ca.crt" # 使用从服务器获取的CA证书\r
+ \r
+ send_get_device_token_request(\r
+ auth_mode=AUTH_MODE,\r
+ ca_cert_path=CA_CERT_PATH\r
+ )\r
+```\r
+\r
+### 7. 总结\r
+\r
+| 证书类型 | 能否直接用于客户端 | 说明 |\r
+|---------|------------------|------|\r
+| CA证书(根证书/中间证书) | ✅ **可以** | 直接用于单向认证验证服务器 |\r
+| 服务器证书 | ❌ **不能** | 需要提取CA证书链 |\r
+| 证书链文件 | ⚠️ **需要提取** | 从中提取CA证书部分 |\r
+\r
+**最佳实践**:\r
+1. 使用 `download_server_ca.py` 脚本从服务器下载CA证书\r
+2. 验证证书类型(Subject == Issuer)\r
+3. 如果验证失败,联系服务器管理员获取正确的CA证书\r
+\r
--- /dev/null
+-----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-----
--- /dev/null
+-----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-----
--- /dev/null
+-----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-----
--- /dev/null
+#!/usr/bin/env python3\r
+# -*- coding: utf-8 -*-\r
+"""\r
+检查证书文件信息\r
+用于验证证书文件是否正确\r
+"""\r
+\r
+import sys\r
+import os\r
+import ssl\r
+import socket\r
+from datetime import datetime\r
+\r
+def check_certificate_file(cert_path):\r
+ """\r
+ 检查证书文件\r
+ \r
+ Args:\r
+ cert_path: 证书文件路径\r
+ """\r
+ if not os.path.exists(cert_path):\r
+ print(f"❌ 证书文件不存在: {cert_path}")\r
+ return False\r
+ \r
+ # 读取文件大小\r
+ file_size = os.path.getsize(cert_path)\r
+ print(f"文件: {cert_path}")\r
+ print(f"大小: {file_size} 字节")\r
+ print()\r
+ \r
+ try:\r
+ # 读取证书内容\r
+ with open(cert_path, 'rb') as f:\r
+ cert_data = f.read()\r
+ \r
+ # 尝试解析证书\r
+ try:\r
+ cert = ssl.PEM_cert_to_DER_cert(cert_data.decode('utf-8'))\r
+ cert_info = ssl.DER_cert_to_PEM_cert(cert)\r
+ \r
+ # 使用OpenSSL解析(如果可用)\r
+ import subprocess\r
+ result = subprocess.run(\r
+ ['openssl', 'x509', '-in', cert_path, '-noout', '-text'],\r
+ capture_output=True,\r
+ text=True,\r
+ timeout=5\r
+ )\r
+ \r
+ if result.returncode == 0:\r
+ print("✅ 证书格式正确")\r
+ print()\r
+ print("证书详细信息:")\r
+ print("-" * 50)\r
+ print(result.stdout)\r
+ print("-" * 50)\r
+ \r
+ # 提取关键信息\r
+ subject_result = subprocess.run(\r
+ ['openssl', 'x509', '-in', cert_path, '-noout', '-subject'],\r
+ capture_output=True,\r
+ text=True,\r
+ timeout=5\r
+ )\r
+ issuer_result = subprocess.run(\r
+ ['openssl', 'x509', '-in', cert_path, '-noout', '-issuer'],\r
+ capture_output=True,\r
+ text=True,\r
+ timeout=5\r
+ )\r
+ dates_result = subprocess.run(\r
+ ['openssl', 'x509', '-in', cert_path, '-noout', '-dates'],\r
+ capture_output=True,\r
+ text=True,\r
+ timeout=5\r
+ )\r
+ \r
+ if subject_result.returncode == 0:\r
+ print(subject_result.stdout.strip())\r
+ if issuer_result.returncode == 0:\r
+ print(issuer_result.stdout.strip())\r
+ if dates_result.returncode == 0:\r
+ print(dates_result.stdout.strip())\r
+ \r
+ return True\r
+ else:\r
+ print("⚠️ 无法使用OpenSSL解析证书,但文件格式可能正确")\r
+ print(f"错误: {result.stderr}")\r
+ return False\r
+ \r
+ except Exception as e:\r
+ print(f"❌ 证书解析失败: {e}")\r
+ return False\r
+ \r
+ except Exception as e:\r
+ print(f"❌ 读取证书文件失败: {e}")\r
+ return False\r
+\r
+def main():\r
+ if len(sys.argv) < 2:\r
+ print("用法: python3 check_certificate.py <证书文件路径>")\r
+ print("示例: python3 check_certificate.py ca.crt")\r
+ sys.exit(1)\r
+ \r
+ cert_path = sys.argv[1]\r
+ \r
+ print("=" * 50)\r
+ print("证书文件检查")\r
+ print("=" * 50)\r
+ print()\r
+ \r
+ success = check_certificate_file(cert_path)\r
+ \r
+ if success:\r
+ print()\r
+ print("=" * 50)\r
+ print("✅ 证书检查完成")\r
+ print("=" * 50)\r
+ print()\r
+ print("在 get_device_token_at.py 中使用:")\r
+ print(f" CA_CERT_PATH = \"{cert_path}\"")\r
+ else:\r
+ print()\r
+ print("=" * 50)\r
+ print("❌ 证书检查失败")\r
+ print("=" * 50)\r
+ sys.exit(1)\r
+\r
+if __name__ == "__main__":\r
+ main()\r
+\r
--- /dev/null
+#!/usr/bin/env python3\r
+# -*- coding: utf-8 -*-\r
+"""\r
+从HTTPS服务器下载CA证书\r
+用于单向认证场景\r
+"""\r
+\r
+import ssl\r
+import socket\r
+import sys\r
+import argparse\r
+from urllib.parse import urlparse\r
+\r
+def download_server_certificate(hostname, port=443, output_file="ca.crt", extract_ca=True):\r
+ """\r
+ 从HTTPS服务器下载证书\r
+ \r
+ Args:\r
+ hostname: 服务器主机名或IP\r
+ port: 端口号(默认443)\r
+ output_file: 输出文件名\r
+ extract_ca: 是否提取CA证书(True=提取CA证书,False=保存服务器证书)\r
+ """\r
+ try:\r
+ # 创建SSL上下文\r
+ context = ssl.create_default_context()\r
+ \r
+ # 连接到服务器\r
+ print(f"正在连接到 {hostname}:{port}...")\r
+ with socket.create_connection((hostname, port), timeout=10) as sock:\r
+ with context.wrap_socket(sock, server_hostname=hostname) as ssock:\r
+ # 获取证书链(Python的getpeercert只返回服务器证书,需要从SSL连接获取完整链)\r
+ cert_der = ssock.getpeercert(binary_form=True)\r
+ cert_pem = ssl.DER_cert_to_PEM_cert(cert_der)\r
+ \r
+ # 获取证书信息\r
+ cert_info = ssock.getpeercert()\r
+ \r
+ print(f"\n服务器证书信息:")\r
+ print(f" 主题: {cert_info.get('subject', 'N/A')}")\r
+ print(f" 颁发者: {cert_info.get('issuer', 'N/A')}")\r
+ \r
+ # 判断是服务器证书还是CA证书\r
+ subject = str(cert_info.get('subject', ''))\r
+ issuer = str(cert_info.get('issuer', ''))\r
+ is_ca_cert = (subject == issuer)\r
+ \r
+ if extract_ca and not is_ca_cert:\r
+ # 如果提取CA证书,但当前是服务器证书,提示用户\r
+ print(f"\n⚠️ 注意: 下载的是服务器证书,不是CA证书")\r
+ print(f" 建议使用OpenSSL获取完整证书链:")\r
+ print(f" openssl s_client -showcerts -connect {hostname}:{port} </dev/null 2>/dev/null | \\")\r
+ print(f" openssl x509 -outform PEM > {output_file}")\r
+ print(f"\n 或者联系服务器管理员获取CA证书")\r
+ print(f"\n 当前保存服务器证书到: {output_file}")\r
+ print(f" (可以尝试使用,但可能无法验证服务器身份)")\r
+ elif extract_ca and is_ca_cert:\r
+ print(f"\n✅ 这是CA证书(Subject == Issuer),可以直接使用")\r
+ else:\r
+ print(f"\n✅ 保存服务器证书到: {output_file}")\r
+ \r
+ # 保存证书\r
+ with open(output_file, 'w') as f:\r
+ f.write(cert_pem)\r
+ \r
+ print(f" 文件大小: {len(cert_pem)} 字节")\r
+ print(f" 有效期: {cert_info.get('notBefore', 'N/A')} 至 {cert_info.get('notAfter', 'N/A')}")\r
+ \r
+ return True\r
+ \r
+ except socket.gaierror as e:\r
+ print(f"❌ DNS解析失败: {e}")\r
+ return False\r
+ except socket.timeout:\r
+ print(f"❌ 连接超时")\r
+ return False\r
+ except ssl.SSLError as e:\r
+ print(f"❌ SSL错误: {e}")\r
+ return False\r
+ except Exception as e:\r
+ print(f"❌ 错误: {e}")\r
+ import traceback\r
+ traceback.print_exc()\r
+ return False\r
+\r
+def main():\r
+ parser = argparse.ArgumentParser(description='从HTTPS服务器下载证书')\r
+ parser.add_argument('url', help='HTTPS URL (例如: https://example.com)')\r
+ parser.add_argument('-o', '--output', default='ca.crt', help='输出文件名 (默认: ca.crt)')\r
+ parser.add_argument('-p', '--port', type=int, help='端口号 (默认从URL解析或443)')\r
+ parser.add_argument('--server-cert', action='store_true', help='下载服务器证书而不是CA证书')\r
+ \r
+ args = parser.parse_args()\r
+ \r
+ # 解析URL\r
+ parsed = urlparse(args.url)\r
+ hostname = parsed.hostname\r
+ port = args.port or parsed.port or 443\r
+ \r
+ if not hostname:\r
+ print("❌ 错误: 无法从URL解析主机名")\r
+ sys.exit(1)\r
+ \r
+ print("=" * 50)\r
+ if args.server_cert:\r
+ print("从HTTPS服务器下载服务器证书")\r
+ else:\r
+ print("从HTTPS服务器下载证书(尝试提取CA证书)")\r
+ print("=" * 50)\r
+ print(f"服务器: {hostname}:{port}")\r
+ print(f"输出文件: {args.output}")\r
+ print("=" * 50)\r
+ \r
+ success = download_server_certificate(hostname, port, args.output, extract_ca=not args.server_cert)\r
+ \r
+ if success:\r
+ print("\n" + "=" * 50)\r
+ print("✅ 证书下载成功!")\r
+ print("=" * 50)\r
+ print(f"\n使用方法:")\r
+ print(f" 在 get_device_token_at.py 中设置:")\r
+ print(f" AUTH_MODE = 1")\r
+ print(f" CA_CERT_PATH = \"{args.output}\"")\r
+ print(f"\n注意:")\r
+ print(f" - 如果下载的是服务器证书,可能需要提取CA证书链")\r
+ print(f" - 使用以下命令获取完整证书链:")\r
+ print(f" openssl s_client -showcerts -connect {hostname}:{port} </dev/null 2>/dev/null | \\")\r
+ print(f" openssl x509 -outform PEM > {args.output}")\r
+ else:\r
+ print("\n❌ 证书下载失败!")\r
+ sys.exit(1)\r
+\r
+if __name__ == "__main__":\r
+ main()\r
+\r
--- /dev/null
+#!/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证书文件路径 (默认: <chain_file>_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()
+
--- /dev/null
+#!/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 "=========================================="
+
--- /dev/null
+-----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-----
--- /dev/null
+-----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-----
--- /dev/null
+#!/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()
+
--- /dev/null
+#!/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()
+
+
+
+
--- /dev/null
+#!/usr/bin/env python3\r
+# -*- coding: utf-8 -*-\r
+"""\r
+使用AT指令通过HTTPS下载MP4文件\r
+参考 get_device_token_at.py 实现\r
+"""\r
+\r
+import serial\r
+import time\r
+import re\r
+import urllib.parse\r
+import os\r
+from get_device_token_at import ATCommandHandler, HTTPSATClient, HTTPATClient\r
+\r
+# -------------------------- 1. 配置参数 --------------------------\r
+# 目标文件URL\r
+MP4_URL = "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/RoundEyes/Blue/Angry.mp4"\r
+# MP4_URL = "http://snicker.com.cn:8089/LEINIAO/blue/angry.zip"\r
+\r
+# 保存路径\r
+OUTPUT_DIR = "downloads"\r
+OUTPUT_FILENAME = "Angry.mp4"\r
+\r
+# AT模块串口配置(根据实际硬件配置修改)\r
+SERIAL_PORT = "COM19" # 修改为实际的串口设备\r
+SERIAL_BAUDRATE = 2000000\r
+SERIAL_TIMEOUT = 10\r
+\r
+# 认证模式配置\r
+AUTH_MODE = 1 # 0=无需认证, 1=单向认证, 2=双向认证\r
+# CA_CERT_PATH = None # 例如: "certs/ca.crt"\r
+CA_CERT_PATH = "certs/ca_chain.pem"\r
+\r
+# -------------------------- 2. 下载MP4文件函数 --------------------------\r
+def download_mp4_file(url, output_path, auth_mode=0, ca_cert_path=None):\r
+ """\r
+ 使用AT指令通过HTTP/HTTPS下载MP4文件\r
+ \r
+ Args:\r
+ url: MP4文件的HTTP/HTTPS URL\r
+ output_path: 保存路径\r
+ auth_mode: 认证模式,0=无需认证, 1=单向认证, 2=双向认证(仅HTTPS需要)\r
+ ca_cert_path: CA证书路径(HTTPS单向认证时需要)\r
+ """\r
+ # 解析URL,判断协议类型\r
+ parsed_url = urllib.parse.urlparse(url)\r
+ is_https = parsed_url.scheme.lower() == 'https'\r
+ is_http = parsed_url.scheme.lower() == 'http'\r
+ \r
+ if not (is_http or is_https):\r
+ raise ValueError(f"不支持的URL协议: {parsed_url.scheme},仅支持 http:// 或 https://")\r
+ \r
+ protocol_name = "HTTPS" if is_https else "HTTP"\r
+ print(f"检测到协议类型: {protocol_name}")\r
+ # 初始化AT指令处理器\r
+ at_handler = ATCommandHandler(SERIAL_PORT, SERIAL_BAUDRATE, SERIAL_TIMEOUT)\r
+ \r
+ try:\r
+ # 连接串口\r
+ if not at_handler.connect():\r
+ print("❌ 无法连接串口,请检查串口配置")\r
+ return False\r
+ \r
+ # 测试AT连接\r
+ success, response = at_handler.send_at_command("AT", timeout=3)\r
+ if not success:\r
+ print("❌ AT模块无响应,请检查连接")\r
+ return False\r
+ print("✅ AT模块连接正常")\r
+ \r
+ # 检查PPP拨号状态\r
+ print("\n" + "=" * 50)\r
+ print("【步骤1: 检查PPP拨号状态】")\r
+ print("=" * 50)\r
+ is_ppp_connected, ppp_ip = at_handler.check_ppp_status()\r
+ \r
+ if is_ppp_connected:\r
+ print(f"✅ PPP已连接,IP地址: {ppp_ip}")\r
+ else:\r
+ print(f"❌ PPP未连接,IP地址: {ppp_ip or '未知'}")\r
+ print("\n尝试建立PPP连接...")\r
+ \r
+ apn_name = None\r
+ success, ip = at_handler.establish_ppp_connection(apn_name=apn_name)\r
+ \r
+ if not success:\r
+ print(f"❌ PPP连接建立失败,无法继续{protocol_name}访问")\r
+ return False\r
+ \r
+ ppp_ip = ip\r
+ \r
+ # 根据协议类型初始化客户端\r
+ if is_https:\r
+ # 初始化HTTPS客户端\r
+ http_client = HTTPSATClient(at_handler)\r
+ \r
+ # 处理证书(如果需要)\r
+ ca_cert_name = None\r
+ if auth_mode == 1 and ca_cert_path:\r
+ print("\n" + "=" * 50)\r
+ print("【步骤2: 处理证书】")\r
+ print("=" * 50)\r
+ \r
+ ca_cert_name = os.path.basename(ca_cert_path)\r
+ \r
+ # 检查证书是否已存在\r
+ if http_client.certificate_exists(ca_cert_name):\r
+ print(f"✅ 证书 {ca_cert_name} 已存在,跳过添加")\r
+ else:\r
+ print(f"证书 {ca_cert_name} 不存在,开始添加...")\r
+ from get_device_token_at import load_certificate_file\r
+ \r
+ ca_cert_data = load_certificate_file(ca_cert_path)\r
+ if not ca_cert_data:\r
+ print("❌ 无法加载CA证书,终止流程")\r
+ return False\r
+ \r
+ if not http_client.add_certificate(ca_cert_name, ca_cert_data):\r
+ print("❌ CA证书添加失败,终止流程")\r
+ return False\r
+ \r
+ if not http_client.check_certificate(ca_cert_name):\r
+ print("❌ CA证书确认失败,终止流程")\r
+ return False\r
+ \r
+ # ========== 开始计时:HTTPS访问耗时 ==========\r
+ access_start_time = time.time()\r
+ \r
+ # 配置HTTPS参数\r
+ print("\n" + "=" * 50)\r
+ print("【步骤3: 配置HTTPS参数】")\r
+ print("=" * 50)\r
+ if not http_client.configure_https(\r
+ ssl_version=3, # TLS1.2\r
+ auth_mode=auth_mode,\r
+ ca_cert=ca_cert_name\r
+ ):\r
+ print("❌ HTTPS配置失败,终止流程")\r
+ return False\r
+ \r
+ # 确认HTTPS配置\r
+ print("\n确认HTTPS配置...")\r
+ config = http_client.verify_https_config()\r
+ if config:\r
+ print("✅ HTTPS配置确认成功")\r
+ else:\r
+ print("⚠️ HTTPS配置确认失败,但继续执行")\r
+ \r
+ # 设置URL\r
+ print("\n" + "=" * 50)\r
+ print("【步骤4: 设置HTTPS请求参数】")\r
+ print("=" * 50)\r
+ print(f"设置请求URL: {url}")\r
+ http_client.set_url(url)\r
+ \r
+ # 建立HTTPS连接\r
+ print("\n" + "=" * 50)\r
+ print("【步骤5: 建立HTTPS连接】")\r
+ print("=" * 50)\r
+ http_client.setup_connection()\r
+ print("✅ HTTPS连接建立成功")\r
+ \r
+ # 发送GET请求\r
+ print("\n" + "=" * 50)\r
+ print("【步骤6: 发送HTTPS GET请求】")\r
+ print("=" * 50)\r
+ \r
+ # 使用标准GET模式 (mode=0)\r
+ success, response = at_handler.send_at_command("AT+HTTPSACTION=0", timeout=5)\r
+ if not success:\r
+ raise Exception(f"发送HTTPS请求失败: {response}")\r
+ \r
+ # 等待响应数据\r
+ print("等待HTTPS响应...")\r
+ recv_indicator = b"+HTTPSRECV:"\r
+ else:\r
+ # 初始化HTTP客户端\r
+ http_client = HTTPATClient(at_handler)\r
+ \r
+ # ========== 开始计时:HTTP访问耗时 ==========\r
+ access_start_time = time.time()\r
+ \r
+ # 设置URL\r
+ print("\n" + "=" * 50)\r
+ print("【步骤3: 设置HTTP请求参数】")\r
+ print("=" * 50)\r
+ print(f"设置请求URL: {url}")\r
+ http_client.set_url(url)\r
+ \r
+ # 建立HTTP连接\r
+ print("\n" + "=" * 50)\r
+ print("【步骤4: 建立HTTP连接】")\r
+ print("=" * 50)\r
+ http_client.setup_connection()\r
+ print("✅ HTTP连接建立成功")\r
+ \r
+ # 发送GET请求\r
+ print("\n" + "=" * 50)\r
+ print("【步骤5: 发送HTTP GET请求】")\r
+ print("=" * 50)\r
+ \r
+ # 使用标准GET模式 (mode=0)\r
+ success, response = at_handler.send_at_command("AT+HTTPACTION=0", timeout=5)\r
+ if not success:\r
+ raise Exception(f"发送HTTP请求失败: {response}")\r
+ \r
+ # 等待响应数据\r
+ print("等待HTTP响应...")\r
+ recv_indicator = b"+HTTPRECV:"\r
+ \r
+ # 重要:直接读取二进制数据,不使用wait_for_unsolicited(它会解码为文本,可能丢失二进制数据)\r
+ # 手动等待+HTTPRECV:/+HTTPSRECV:提示符,然后立即开始读取二进制数据\r
+ response_data = b""\r
+ start_time = time.time()\r
+ last_data_time = time.time()\r
+ header_received = False\r
+ content_length = None\r
+ body_start_pos = None\r
+ recv_found = False\r
+ \r
+ # 等待+HTTPRECV:/+HTTPSRECV:提示符\r
+ indicator_name = protocol_name + "RECV"\r
+ print(f"等待+{indicator_name}:提示符...")\r
+ while time.time() - start_time < 60:\r
+ if at_handler.serial_conn.in_waiting > 0:\r
+ # 读取数据(二进制模式)\r
+ chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting)\r
+ if chunk:\r
+ response_data += chunk\r
+ last_data_time = time.time()\r
+ \r
+ # 检查是否包含+HTTPRECV:/+HTTPSRECV:(可能在字节串中)\r
+ if recv_indicator in response_data:\r
+ recv_found = True\r
+ print(f"✅ 收到+{indicator_name}:提示符")\r
+ # 继续读取,查找HTTP响应头\r
+ break\r
+ time.sleep(0.1)\r
+ \r
+ if not recv_found:\r
+ # 尝试查找HTTP响应头(可能+HTTPRECV:/+HTTPSRECV:已经被读取)\r
+ if b"HTTP/" in response_data:\r
+ recv_found = True\r
+ print("✅ 检测到HTTP响应数据")\r
+ \r
+ if not recv_found:\r
+ raise Exception(f"未收到+{indicator_name}:提示符或HTTP响应")\r
+ \r
+ # 查找HTTP响应头开始位置(可能在+HTTPSRECV:之后)\r
+ http_start = response_data.find(b"HTTP/")\r
+ if http_start == -1:\r
+ # 如果没有找到HTTP/,继续读取直到找到\r
+ print("继续读取数据以查找HTTP响应头...")\r
+ while time.time() - start_time < 10:\r
+ if at_handler.serial_conn.in_waiting > 0:\r
+ chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting)\r
+ if chunk:\r
+ response_data += chunk\r
+ http_start = response_data.find(b"HTTP/")\r
+ if http_start != -1:\r
+ break\r
+ time.sleep(0.1)\r
+ \r
+ if http_start == -1:\r
+ raise Exception("未找到HTTP响应头")\r
+ \r
+ # 从HTTP响应头开始位置处理数据\r
+ # 跳过+HTTPSRECV:前缀(如果存在)\r
+ if http_start > 0:\r
+ print(f"跳过+HTTPSRECV:前缀 ({http_start} 字节)")\r
+ response_data = response_data[http_start:]\r
+ \r
+ # 查找HTTP响应头结束位置(\r\n\r\n)\r
+ print("读取响应头...")\r
+ header_timeout = 10\r
+ header_start = time.time()\r
+ \r
+ while time.time() - header_start < header_timeout:\r
+ if at_handler.serial_conn.in_waiting > 0:\r
+ chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting)\r
+ if chunk:\r
+ response_data += chunk\r
+ \r
+ # 查找HTTP响应头结束位置(\r\n\r\n)\r
+ header_end = response_data.find(b"\r\n\r\n")\r
+ if header_end != -1:\r
+ header_received = True\r
+ body_start_pos = header_end + 4\r
+ \r
+ # 解析响应头\r
+ header_text = response_data[:header_end].decode('utf-8', errors='ignore')\r
+ print("\nHTTP响应头:")\r
+ print(header_text)\r
+ \r
+ # 提取Content-Length\r
+ content_length_match = re.search(r'Content-Length:\s*(\d+)', header_text, re.IGNORECASE)\r
+ if content_length_match:\r
+ content_length = int(content_length_match.group(1))\r
+ print(f"\n文件大小: {content_length} 字节 ({content_length/1024/1024:.2f} MB)")\r
+ else:\r
+ print("⚠️ 未找到Content-Length,将接收所有数据直到连接关闭")\r
+ \r
+ # 检查状态码\r
+ status_match = re.search(r'HTTP/\d\.\d\s+(\d+)', header_text)\r
+ if status_match:\r
+ status_code = int(status_match.group(1))\r
+ if status_code != 200:\r
+ raise Exception(f"HTTP请求失败,状态码: {status_code}")\r
+ print(f"HTTP状态码: {status_code} ✅")\r
+ \r
+ # 检查文件数据开始位置的前几个字节(用于调试)\r
+ if len(response_data) > body_start_pos:\r
+ preview = response_data[body_start_pos:body_start_pos+20]\r
+ print(f"\n文件数据前20字节(hex): {preview.hex()}")\r
+ print(f"文件数据前20字节(repr): {repr(preview)}")\r
+ \r
+ break\r
+ time.sleep(0.1)\r
+ \r
+ if not header_received:\r
+ raise Exception("未收到完整的HTTP响应头")\r
+ \r
+ # 继续接收文件数据\r
+ print("\n开始接收文件数据...")\r
+ last_progress_time = time.time() # 上次打印进度的时间\r
+ progress_interval = 5 # 每5秒打印一次进度\r
+ \r
+ while time.time() - start_time < 600: # 最多等待10分钟\r
+ if at_handler.serial_conn.in_waiting > 0:\r
+ chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting)\r
+ if chunk:\r
+ response_data += chunk\r
+ last_data_time = time.time()\r
+ \r
+ # 如果知道文件大小,显示进度\r
+ if content_length:\r
+ body_data = response_data[body_start_pos:]\r
+ received = len(body_data)\r
+ if received >= content_length:\r
+ print(f"\n✅ 文件接收完成: {received} / {content_length} 字节")\r
+ break\r
+ else:\r
+ # 每5秒打印一次进度\r
+ current_time = time.time()\r
+ if current_time - last_progress_time >= progress_interval:\r
+ progress = (received / content_length) * 100\r
+ print(f"接收进度: {progress:.1f}% ({received}/{content_length} 字节, {received/1024/1024:.2f} MB)")\r
+ last_progress_time = current_time\r
+ else:\r
+ # 不知道文件大小,每5秒显示已接收的数据量\r
+ current_time = time.time()\r
+ if current_time - last_progress_time >= progress_interval:\r
+ body_data = response_data[body_start_pos:]\r
+ received = len(body_data)\r
+ print(f"已接收: {received} 字节 ({received/1024/1024:.2f} MB)")\r
+ last_progress_time = current_time\r
+ else:\r
+ # 如果超过10秒没有新数据,检查是否接收完成\r
+ if time.time() - last_data_time > 10:\r
+ if content_length:\r
+ body_data = response_data[body_start_pos:]\r
+ received = len(body_data)\r
+ if received >= content_length:\r
+ print(f"\n✅ 文件接收完成: {received} / {content_length} 字节")\r
+ break\r
+ else:\r
+ print(f"\n⚠️ 接收超时,已接收: {received} / {content_length} 字节")\r
+ break\r
+ else:\r
+ # 没有Content-Length,如果10秒没有新数据,认为接收完成\r
+ print("\n响应数据接收完成(超时)")\r
+ break\r
+ time.sleep(0.1)\r
+ \r
+ # 提取文件数据(跳过HTTP响应头)\r
+ file_data = response_data[body_start_pos:]\r
+ \r
+ # 验证文件数据(MP4文件格式)\r
+ # MP4文件结构:Box size (4字节) + Box type (4字节,通常是"ftyp")\r
+ # 所以文件应该以 0x00 0x00 0x00 0x1C 开头(或类似的大小值),然后是 "ftyp"\r
+ print(f"\n文件数据前16字节(hex): {file_data[:16].hex()}")\r
+ print(f"文件数据前16字节(repr): {repr(file_data[:16])}")\r
+ \r
+ # MP4文件应该以Box size开头(4字节大端整数),然后是"ftyp"\r
+ # 检查前4字节是否为有效的Box size(通常不会太大,比如小于100MB)\r
+ if len(file_data) >= 8:\r
+ box_size_bytes = file_data[:4]\r
+ box_type = file_data[4:8]\r
+ \r
+ # 计算Box size(大端序)\r
+ box_size = int.from_bytes(box_size_bytes, byteorder='big')\r
+ \r
+ print(f"检测到的Box size: {box_size} (hex: {box_size_bytes.hex()})")\r
+ print(f"检测到的Box type: {box_type} (hex: {box_type.hex()})")\r
+ \r
+ # 验证是否为有效的MP4文件\r
+ if box_type == b"ftyp":\r
+ print("✅ 检测到有效的MP4文件格式(ftyp box)")\r
+ elif box_size > 0 and box_size < 100 * 1024 * 1024: # Box size在合理范围内\r
+ print(f"⚠️ Box size看起来合理,但Box type不是'ftyp',可能是其他格式")\r
+ else:\r
+ # 如果前4字节不是有效的Box size,尝试查找ftyp\r
+ ftyp_pos = file_data.find(b"ftyp")\r
+ if ftyp_pos != -1 and ftyp_pos < 100:\r
+ print(f"⚠️ 检测到文件数据偏移,ftyp在位置 {ftyp_pos}")\r
+ print(f" 丢失了前 {ftyp_pos} 字节数据")\r
+ print(f" 建议检查数据接收过程,确保没有丢失数据")\r
+ # 不自动调整,保留原始数据\r
+ # file_data = file_data[ftyp_pos:]\r
+ \r
+ # 保存文件\r
+ print("\n" + "=" * 50)\r
+ print("【步骤7: 保存文件】")\r
+ print("=" * 50)\r
+ \r
+ # 创建输出目录\r
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)\r
+ \r
+ # 写入文件\r
+ with open(output_path, 'wb') as f:\r
+ f.write(file_data)\r
+ \r
+ file_size = len(file_data)\r
+ print(f"✅ 文件保存成功: {output_path}")\r
+ print(f" 文件大小: {file_size} 字节 ({file_size/1024/1024:.2f} MB)")\r
+ \r
+ # 关闭连接\r
+ print("\n" + "=" * 50)\r
+ print(f"【步骤8: 关闭{protocol_name}连接】")\r
+ print("=" * 50)\r
+ try:\r
+ http_client.close_connection()\r
+ print(f"✅ {protocol_name}连接已关闭")\r
+ except Exception as e:\r
+ print(f"警告: 关闭{protocol_name}连接时出错: {str(e)}")\r
+ \r
+ # ========== 结束计时:HTTP/HTTPS访问耗时 ==========\r
+ access_end_time = time.time()\r
+ access_duration = access_end_time - access_start_time\r
+ \r
+ print("\n" + "=" * 50)\r
+ print(f"【{protocol_name}访问耗时统计】")\r
+ print("=" * 50)\r
+ print(f"{protocol_name}访问总耗时: {access_duration:.3f} 秒 ({access_duration*1000:.1f} 毫秒)")\r
+ print(f"下载速度: {file_size/1024/1024/access_duration:.2f} MB/s")\r
+ print("\n耗时包含以下步骤:")\r
+ if is_https:\r
+ print(" - HTTPS配置和验证")\r
+ print(" - URL和端口设置")\r
+ print(f" - {protocol_name}连接建立")\r
+ print(" - HTTP请求发送")\r
+ print(" - 文件数据接收")\r
+ print(" - 连接关闭")\r
+ print("=" * 50)\r
+ \r
+ return True\r
+ \r
+ except Exception as e:\r
+ print("\n" + "=" * 50)\r
+ print("【错误处理】")\r
+ print("=" * 50)\r
+ print(f"❌ 下载异常: {str(e)}")\r
+ import traceback\r
+ traceback.print_exc()\r
+ return False\r
+ finally:\r
+ # 断开串口连接\r
+ print("\n断开串口连接...")\r
+ at_handler.disconnect()\r
+\r
+# -------------------------- 3. 主程序 --------------------------\r
+if __name__ == "__main__":\r
+ print("=" * 50)\r
+ print("HTTPS MP4文件下载工具(AT指令版本)")\r
+ print("=" * 50)\r
+ print(f"目标URL: {MP4_URL}")\r
+ \r
+ # 构建输出路径\r
+ output_path = os.path.join(OUTPUT_DIR, OUTPUT_FILENAME)\r
+ print(f"保存路径: {output_path}")\r
+ print("=" * 50)\r
+ \r
+ # 执行下载\r
+ success = download_mp4_file(\r
+ url=MP4_URL,\r
+ output_path=output_path,\r
+ auth_mode=AUTH_MODE,\r
+ ca_cert_path=CA_CERT_PATH\r
+ )\r
+ \r
+ if success:\r
+ print("\n✅ MP4文件下载完成!")\r
+ else:\r
+ print("\n❌ MP4文件下载失败!")\r
+ exit(1)\r
+\r
--- /dev/null
+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>,<ip>
+ # 注意:手册说明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: <n>,<stat>
+ # 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=<file_name>,<length>
+ 2. 立即返回 CONNECT(不是OK)
+ 3. 发送证书数据
+ 4. 返回 +CERTADD: <length>,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: <length>,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: <file_name>,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: <file_name>,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: <sslversion>,<authmode>,<cacert>,<clientcert>,<clientkey>
+ # 例如: +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<CRLF>
+ http_request = f"GET {path} HTTP/1.1\r\n"
+ # Host头:Host: hostname<CRLF>
+ http_request += f"Host: {parsed.netloc}\r\n"
+
+ # 添加自定义头(每行格式:Header-Name: value<CRLF>)
+ for key, value in headers.items():
+ http_request += f"{key}: {value}\r\n"
+
+ # Connection头
+ http_request += "Connection: close\r\n"
+ # 请求头和请求体之间的空行(即使没有请求体也需要)<CRLF><CRLF>
+ http_request += "\r\n"
+
+ # 计算请求长度(字节数,用于AT+HTTPSACTION=99,<length>)
+ request_length = len(http_request.encode('utf-8'))
+
+ # 步骤1:直接发送AT+HTTPSACTION=99,<length>命令
+ # 注意:根据手册,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协议,使用<CRLF>作为行结束符
+ 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
+ )
--- /dev/null
+#!/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>,<ip>
+ # 注意:手册说明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: <n>,<stat>
+ # 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
+
--- /dev/null
+#!/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: <sslversion>,<authmode>,<cacert>,<clientcert>,<clientkey>
+ 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,<length>命令
+ 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
+
--- /dev/null
+#!/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: <n>,<status> 或 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: <n>,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: <n>,OK 或 +TCPSETUP: <n>,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: <n>,<length>)
+ 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=<n>,<data_length>
+ 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: <n>,OK 或 +SSLTCPSEND: <n>,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: <n>,<len>,<data>
+ pattern = re.compile(r'\+SSLTCPREAD:\s*(\d+),(\d+),(.+)')
+ else:
+ # +TCPREAD: <n>,<len>,<data>
+ 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: <n>,<len>,<data>
+ recv_pattern = re.compile(r'\+SSLTCPRECV:\s*(\d+),(\d+),(.+)')
+ else:
+ # +TCPRECV: <n>,<len>,<data> 或 +TCPRECV: <n>
+ 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
+
--- /dev/null
+# 下载速度性能分析\r
+\r
+## 问题描述\r
+- `download_mp4_at.py`: 下载速度 0.02 MB/s\r
+- `at_test_download_file.py`: 下载速度 0.01 MB/s(慢了一倍)\r
+\r
+## 关键差异分析\r
+\r
+### 1. 数据接收循环中的性能差异\r
+\r
+#### `download_mp4_at.py` (lines 328-373)\r
+```python\r
+while time.time() - start_time < 600:\r
+ if at_handler.serial_conn.in_waiting > 0:\r
+ chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting)\r
+ if chunk:\r
+ response_data += chunk\r
+ last_data_time = time.time()\r
+ \r
+ # 如果知道文件大小,显示进度\r
+ if content_length:\r
+ body_data = response_data[body_start_pos:] # 只在需要时切片\r
+ received = len(body_data)\r
+ if received >= content_length:\r
+ break\r
+ else:\r
+ # 每5秒打印一次进度\r
+ current_time = time.time()\r
+ if current_time - last_progress_time >= progress_interval:\r
+ # 进度计算和打印\r
+ last_progress_time = current_time\r
+```\r
+\r
+**特点:**\r
+- 切片操作 `response_data[body_start_pos:]` 只在需要显示进度时执行(每5秒一次)\r
+- 逻辑简单,条件判断少\r
+- 没有额外的字符串搜索操作\r
+\r
+#### `n706a_http_client.py` (lines 645-700)\r
+```python\r
+while time.time() - start_time < 600:\r
+ if self.at.serial_conn.in_waiting > 0:\r
+ chunk = self.at.serial_conn.read(self.at.serial_conn.in_waiting)\r
+ if chunk:\r
+ response_data += chunk\r
+ last_data_time = time.time()\r
+ \r
+ # 对于chunked编码,检查是否收到结束标记\r
+ if transfer_encoding_chunked:\r
+ if response_data.endswith(b"0\r\n\r\n") or b"\r\n0\r\n\r\n" in response_data:\r
+ break\r
+ # 如果知道文件大小,显示进度\r
+ elif content_length:\r
+ body_data = response_data[body_start_pos:] # 每次循环都切片!\r
+ received = len(body_data)\r
+ if received >= content_length:\r
+ break\r
+ else:\r
+ current_time = time.time()\r
+ if current_time - last_progress_time >= progress_interval:\r
+ # 进度计算和打印\r
+ last_progress_time = current_time\r
+```\r
+\r
+**问题:**\r
+1. **每次循环都执行切片操作**:`body_data = response_data[body_start_pos:]` 在每次有数据时都会执行,即使不需要显示进度\r
+2. **额外的字符串搜索**:对于chunked编码,每次循环都检查 `response_data.endswith()` 和 `in` 操作\r
+3. **条件判断更复杂**:有 `if transfer_encoding_chunked` 分支\r
+\r
+### 2. 性能影响分析\r
+\r
+#### 切片操作的开销\r
+- `response_data[body_start_pos:]` 会创建一个新的字节对象\r
+- 如果 `response_data` 很大(比如几MB),这个操作会复制大量数据\r
+- 在 `n706a_http_client.py` 中,这个操作在**每次接收到数据时**都执行\r
+- 在 `download_mp4_at.py` 中,这个操作**只在需要显示进度时**执行(每5秒一次)\r
+\r
+#### 计算频率差异\r
+假设每秒接收 20KB 数据:\r
+- `download_mp4_at.py`: 切片操作约 0.2 次/秒(每5秒一次)\r
+- `n706a_http_client.py`: 切片操作约 10-100 次/秒(取决于串口缓冲区大小和读取频率)\r
+\r
+如果串口每次读取 1KB,那么每秒会读取约 20 次,每次都会执行切片操作。\r
+\r
+### 3. 优化建议\r
+\r
+#### 方案1:延迟切片操作(推荐)\r
+只在需要检查进度或完成条件时才执行切片:\r
+\r
+```python\r
+# 如果知道文件大小,显示进度\r
+elif content_length:\r
+ # 只在需要时计算已接收的数据量\r
+ current_time = time.time()\r
+ need_check = (current_time - last_progress_time >= progress_interval) or True # 简化:总是检查完成条件\r
+ \r
+ if need_check:\r
+ body_data = response_data[body_start_pos:] # 只在需要时切片\r
+ received = len(body_data)\r
+ if received >= content_length:\r
+ break\r
+ \r
+ if current_time - last_progress_time >= progress_interval:\r
+ # 显示进度\r
+ last_progress_time = current_time\r
+```\r
+\r
+#### 方案2:使用已接收长度缓存\r
+避免频繁切片,使用长度计算:\r
+\r
+```python\r
+# 在循环外初始化\r
+initial_body_length = len(response_data) - body_start_pos if len(response_data) > body_start_pos else 0\r
+\r
+# 在循环中\r
+elif content_length:\r
+ # 使用增量计算,避免切片\r
+ received = len(response_data) - body_start_pos\r
+ if received >= content_length:\r
+ break\r
+```\r
+\r
+#### 方案3:减少chunked编码检查频率\r
+对于chunked编码的检查,也应该减少频率:\r
+\r
+```python\r
+if transfer_encoding_chunked:\r
+ # 只在特定条件下检查(比如每接收一定数据量后)\r
+ if len(response_data) % 1024 == 0: # 每1KB检查一次\r
+ if response_data.endswith(b"0\r\n\r\n") or b"\r\n0\r\n\r\n" in response_data:\r
+ break\r
+```\r
+\r
+## 结论\r
+\r
+**主要原因:** `n706a_http_client.py` 在每次接收到数据时都执行 `response_data[body_start_pos:]` 切片操作,而 `download_mp4_at.py` 只在需要显示进度时(每5秒)才执行。对于大文件,频繁的切片操作会显著影响性能。\r
+\r
+**建议修复:** 优化 `_receive_http_response` 方法,减少不必要的切片操作和字符串搜索操作。\r
+\r
--- /dev/null
+# 性能优化总结\r
+\r
+## 问题\r
+`at_test_download_file.py` 的下载速度(0.01 MB/s)比 `download_mp4_at.py`(0.02 MB/s)慢了一倍。\r
+\r
+## 根本原因\r
+在 `n706a_http_client.py` 的 `_receive_http_response` 方法中,数据接收循环存在性能瓶颈:\r
+\r
+1. **频繁的切片操作**:每次接收到数据时都执行 `response_data[body_start_pos:]`,这会创建新的字节对象并复制大量数据\r
+2. **频繁的字符串搜索**:对于chunked编码,每次循环都检查结束标记\r
+3. **不必要的计算**:即使不需要显示进度,也在每次循环中计算已接收的数据量\r
+\r
+## 优化方案\r
+\r
+### 1. 使用长度计算替代频繁切片\r
+**优化前:**\r
+```python\r
+elif content_length:\r
+ body_data = response_data[body_start_pos:] # 每次循环都切片\r
+ received = len(body_data)\r
+ if received >= content_length:\r
+ break\r
+```\r
+\r
+**优化后:**\r
+```python\r
+elif content_length:\r
+ # 使用长度计算,避免每次循环都切片\r
+ received = len(response_data) - body_start_pos\r
+ if received >= content_length:\r
+ break\r
+ else:\r
+ # 只在需要显示进度时才切片\r
+ current_time = time.time()\r
+ if current_time - last_progress_time >= progress_interval:\r
+ body_data = response_data[body_start_pos:] # 每5秒才切片一次\r
+ progress = (received / content_length) * 100\r
+ print(f"接收进度: {progress:.1f}% ...")\r
+ last_progress_time = current_time\r
+```\r
+\r
+### 2. 减少chunked编码检查频率\r
+**优化前:**\r
+```python\r
+if transfer_encoding_chunked:\r
+ if response_data.endswith(b"0\r\n\r\n") or b"\r\n0\r\n\r\n" in response_data:\r
+ break\r
+```\r
+\r
+**优化后:**\r
+```python\r
+last_chunked_check_size = 0\r
+chunked_check_interval = 1024 # 每1KB检查一次\r
+\r
+if transfer_encoding_chunked:\r
+ current_size = len(response_data)\r
+ if current_size - last_chunked_check_size >= chunked_check_interval:\r
+ if response_data.endswith(b"0\r\n\r\n") or b"\r\n0\r\n\r\n" in response_data:\r
+ break\r
+ last_chunked_check_size = current_size\r
+```\r
+\r
+### 3. 优化超时检查逻辑\r
+在超时检查分支中也使用长度计算,避免不必要的切片。\r
+\r
+## 性能提升预期\r
+\r
+### 优化前\r
+- 假设每秒接收 20KB 数据,串口每次读取 1KB\r
+- 每秒执行约 20 次切片操作\r
+- 对于 10MB 文件,需要约 500 秒,执行约 10,000 次切片操作\r
+\r
+### 优化后\r
+- 切片操作减少到每 5 秒一次(显示进度时)\r
+- 对于 10MB 文件,约执行 100 次切片操作(减少 99%)\r
+- 使用长度计算替代大部分切片操作,性能提升显著\r
+\r
+## 预期效果\r
+- **下载速度提升**:预计从 0.01 MB/s 提升到接近 0.02 MB/s(与 `download_mp4_at.py` 相当)\r
+- **CPU 使用率降低**:减少不必要的内存复制操作\r
+- **内存使用优化**:减少临时对象的创建\r
+\r
+## 测试建议\r
+1. 使用相同的测试文件(MP4文件)进行对比测试\r
+2. 记录下载时间和速度\r
+3. 监控CPU和内存使用情况\r
+4. 验证下载的文件完整性\r
+\r
+## 修改文件\r
+- `n706a_http_client.py`: 优化 `_receive_http_response` 方法(lines 640-707)\r
+\r
--- /dev/null
+# 请求头信息对比分析\r
+\r
+## 两个请求头对比\r
+\r
+### 请求1\r
+```\r
+请求 URL: https://ambybox-api.test.leiniao.com/ambybox-device-api/v1/auth/getDeviceToken\r
+请求头: {\r
+ 'deviceId': 'DEVICE_PC_TEST_001', \r
+ 'deviceIccid': '86123456789012345678', \r
+ 'timestamp': '1762845627028', \r
+ 'nonce': 'DSAgsyFYxaOR', \r
+ 'signature': '0652010b2d574b034086a0efd707c94fef8250eac3fb02948c049b43cfb1f977', \r
+ 'deviceModel': 'AB101', \r
+ 'deviceVersion': '1.0.0', \r
+ 'Content-Type': 'application/json'\r
+}\r
+```\r
+\r
+### 请求2\r
+```\r
+请求 URL: https://ambybox-api.test.leiniao.com/ambybox-device-api/v1/auth/getDeviceToken\r
+请求头: {\r
+ 'deviceId': 'DEVICE_PC_TEST_001', \r
+ 'deviceIccid': '86123456789012345678', \r
+ 'timestamp': '1762845599887', \r
+ 'nonce': 'ogHL53iSPoaU', \r
+ 'signature': '1457b817ddeffa108341b750fa32063febc9dee9a24eaca1918fb1748b5e7987', \r
+ 'deviceModel': 'AB101', \r
+ 'deviceVersion': '1.0.0', \r
+ 'Content-Type': 'application/json'\r
+}\r
+```\r
+\r
+## 详细对比\r
+\r
+| 字段 | 请求1 | 请求2 | 状态 | 说明 |\r
+|------|-------|-------|------|------|\r
+| **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 |\r
+| **deviceId** | `DEVICE_PC_TEST_001` | `DEVICE_PC_TEST_001` | ✅ 相同 | 设备ID一致 |\r
+| **deviceIccid** | `86123456789012345678` | `86123456789012345678` | ✅ 相同 | 设备ICCID一致 |\r
+| **deviceModel** | `AB101` | `AB101` | ✅ 相同 | 设备型号一致 |\r
+| **deviceVersion** | `1.0.0` | `1.0.0` | ✅ 相同 | 设备版本一致 |\r
+| **Content-Type** | `application/json` | `application/json` | ✅ 相同 | 内容类型一致 |\r
+| **timestamp** | `1762845627028` | `1762845599887` | ⚠️ 不同 | **正常差异**:时间戳不同(相差约27秒) |\r
+| **nonce** | `DSAgsyFYxaOR` | `ogHL53iSPoaU` | ⚠️ 不同 | **正常差异**:随机数不同(每次请求都会生成新的) |\r
+| **signature** | `0652010b2d574b034086a0efd707c94fef8250eac3fb02948c049b43cfb1f977` | `1457b817ddeffa108341b750fa32063febc9dee9a24eaca1918fb1748b5e7987` | ⚠️ 不同 | **正常差异**:签名不同(因为基于timestamp和nonce计算) |\r
+\r
+## 时间戳分析\r
+\r
+- **请求1时间戳**: `1762845627028` (毫秒)\r
+- **请求2时间戳**: `1762845599887` (毫秒)\r
+- **时间差**: `27141` 毫秒 ≈ **27.14秒**\r
+\r
+转换为可读时间:\r
+- 请求1: 约 2025-11-11 的某个时间点\r
+- 请求2: 约 2025-11-11 的某个时间点(比请求1早约27秒)\r
+\r
+## 签名验证\r
+\r
+签名计算公式:`HMAC-SHA256(signSecret, "deviceId:deviceIccid:timestamp:nonce")`\r
+\r
+### 请求1的签名数据\r
+```\r
+signData = "DEVICE_PC_TEST_001:86123456789012345678:1762845627028:DSAgsyFYxaOR"\r
+signature = "0652010b2d574b034086a0efd707c94fef8250eac3fb02948c049b43cfb1f977"\r
+```\r
+\r
+### 请求2的签名数据\r
+```\r
+signData = "DEVICE_PC_TEST_001:86123456789012345678:1762845599887:ogHL53iSPoaU"\r
+signature = "1457b817ddeffa108341b750fa32063febc9dee9a24eaca1918fb1748b5e7987"\r
+```\r
+\r
+## 结论\r
+\r
+### ✅ 正常情况\r
+两个请求头信息**符合预期**,差异是正常的:\r
+\r
+1. **静态字段一致**:deviceId, deviceIccid, deviceModel, deviceVersion, Content-Type 都相同\r
+2. **动态字段不同**:timestamp, nonce, signature 不同是**正常的**,因为:\r
+ - `timestamp` 是当前时间戳,每次请求都会变化\r
+ - `nonce` 是随机生成的,每次请求都不同(用于防重放攻击)\r
+ - `signature` 基于 timestamp 和 nonce 计算,因此也会不同\r
+\r
+### 📊 请求头结构\r
+两个请求的请求头结构完全一致,都包含:\r
+- 设备标识信息(deviceId, deviceIccid)\r
+- 设备信息(deviceModel, deviceVersion)\r
+- 认证信息(timestamp, nonce, signature)\r
+- 内容类型(Content-Type)\r
+\r
+### 🔍 验证建议\r
+如果需要验证签名是否正确,可以使用以下Python代码:\r
+\r
+```python\r
+import hmac\r
+import hashlib\r
+\r
+SIGN_SECRET = "vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus="\r
+\r
+# 验证请求1\r
+sign_data_1 = "DEVICE_PC_TEST_001:86123456789012345678:1762845627028:DSAgsyFYxaOR"\r
+hmac_obj_1 = hmac.new(SIGN_SECRET.encode("utf-8"), sign_data_1.encode("utf-8"), hashlib.sha256)\r
+signature_1 = hmac_obj_1.hexdigest().lower()\r
+print(f"请求1签名: {signature_1}")\r
+print(f"期望签名: 0652010b2d574b034086a0efd707c94fef8250eac3fb02948c049b43cfb1f977")\r
+print(f"匹配: {signature_1 == '0652010b2d574b034086a0efd707c94fef8250eac3fb02948c049b43cfb1f977'}")\r
+\r
+# 验证请求2\r
+sign_data_2 = "DEVICE_PC_TEST_001:86123456789012345678:1762845599887:ogHL53iSPoaU"\r
+hmac_obj_2 = hmac.new(SIGN_SECRET.encode("utf-8"), sign_data_2.encode("utf-8"), hashlib.sha256)\r
+signature_2 = hmac_obj_2.hexdigest().lower()\r
+print(f"请求2签名: {signature_2}")\r
+print(f"期望签名: 1457b817ddeffa108341b750fa32063febc9dee9a24eaca1918fb1748b5e7987")\r
+print(f"匹配: {signature_2 == '1457b817ddeffa108341b750fa32063febc9dee9a24eaca1918fb1748b5e7987'}")\r
+```\r
+\r
--- /dev/null
+# 单向认证使用说明\r
+\r
+## 概述\r
+\r
+单向认证(服务器认证)是HTTPS最常用的认证方式,客户端会验证服务器的身份。根据SSL/TLS应用指南,需要以下步骤:\r
+\r
+1. 配置认证模式为单向认证(auth_mode=1)\r
+2. 添加CA证书到模块\r
+3. 确认证书\r
+4. 配置CA证书到HTTPS\r
+\r
+## 代码修改步骤\r
+\r
+### 1. 准备CA证书文件\r
+\r
+将CA证书文件放在可访问的路径,例如:\r
+```\r
+certs/\r
+ └── ca.crt\r
+```\r
+\r
+### 2. 修改主程序配置\r
+\r
+在 `get_device_token_at.py` 文件末尾,修改以下配置:\r
+\r
+```python\r
+# 认证模式配置\r
+AUTH_MODE = 1 # 改为1(单向认证)\r
+\r
+# 证书文件路径\r
+CA_CERT_PATH = "certs/ca.crt" # 设置CA证书路径\r
+```\r
+\r
+### 3. 运行程序\r
+\r
+```bash\r
+python3 get_device_token_at.py\r
+```\r
+\r
+## 完整流程说明\r
+\r
+### 步骤1: 模块初始化\r
+- AT+GMR 查询版本号\r
+- 检查PPP拨号状态\r
+\r
+### 步骤2: 配置HTTPS参数(单向认证)\r
+1. **加载CA证书文件**\r
+ ```python\r
+ ca_cert_data = load_certificate_file("certs/ca.crt")\r
+ ```\r
+\r
+2. **添加CA证书到模块**\r
+ ```\r
+ AT+CERTADD=ca.crt,<length>\r
+ CONNECT\r
+ <证书内容>\r
+ +CERTADD: <length>,OK\r
+ ```\r
+\r
+3. **确认证书**\r
+ ```\r
+ AT+CERTCHECK=ca.crt\r
+ +CERTCHECK: ca.crt,OK\r
+ ```\r
+\r
+4. **配置SSL/TLS参数**\r
+ ```\r
+ AT+HTTPSCFG="sslversion",3\r
+ AT+HTTPSCFG="authmode",1\r
+ AT+HTTPSCFG="cacert",ca.crt\r
+ ```\r
+\r
+5. **确认配置**\r
+ ```\r
+ AT+HTTPSCFG?\r
+ +HTTPSCFG: 3,1,ca.crt,,\r
+ ```\r
+\r
+### 步骤3: 设置HTTPS连接参数\r
+- 设置URL\r
+- 设置端口(默认443)\r
+- 可选:设置keepalive\r
+\r
+### 步骤4: 建立HTTPS连接\r
+```\r
+AT+HTTPSSETUP\r
+```\r
+\r
+### 步骤5: 发送HTTPS请求\r
+```\r
+AT+HTTPSACTION=99,<length>\r
+> <HTTP请求>\r
+```\r
+\r
+### 步骤6: 关闭连接\r
+```\r
+AT+HTTPSCLOSE\r
+```\r
+\r
+## 代码示例\r
+\r
+### 使用单向认证的完整示例\r
+\r
+```python\r
+# 在文件末尾修改\r
+if __name__ == "__main__":\r
+ # 单向认证配置\r
+ AUTH_MODE = 1\r
+ CA_CERT_PATH = "certs/ca.crt" # 根据实际路径修改\r
+ \r
+ send_get_device_token_request(\r
+ auth_mode=AUTH_MODE,\r
+ ca_cert_path=CA_CERT_PATH\r
+ )\r
+```\r
+\r
+## 注意事项\r
+\r
+1. **证书文件格式**:支持PEM格式的证书文件\r
+2. **证书长度**:确保 `AT+CERTADD` 中的长度参数与实际证书文件大小(字节数)一致\r
+3. **证书路径**:使用相对路径或绝对路径都可以\r
+4. **证书文件名**:模块中存储的证书名称就是文件名(如 `ca.crt`)\r
+\r
+## 与无需认证的对比\r
+\r
+| 项目 | 无需认证 | 单向认证 |\r
+|------|---------|---------|\r
+| auth_mode | 0 | 1 |\r
+| 需要证书 | ❌ 否 | ✅ CA证书 |\r
+| 安全性 | ⚠️ 较低 | ✅ 较高 |\r
+| 配置步骤 | 简单 | 需要添加证书 |\r
+\r
+## 错误处理\r
+\r
+如果证书添加失败,程序会:\r
+1. 打印错误信息\r
+2. 终止流程\r
+3. 不会继续执行HTTPS请求\r
+\r
+常见错误:\r
+- 证书文件不存在\r
+- 证书长度不匹配\r
+- 证书格式错误\r
+- 模块存储空间不足\r
+\r
--- /dev/null
+#!/usr/bin/env python3\r
+# -*- coding: utf-8 -*-\r
+"""\r
+TCP测试服务器\r
+监听端口8910,支持多客户端连接\r
+可以查看连接状态、接收设备数据、发送数据给设备\r
+"""\r
+\r
+import socket\r
+import threading\r
+import time\r
+import sys\r
+from datetime import datetime\r
+from collections import defaultdict\r
+\r
+class TCPTestServer:\r
+ def __init__(self, host='0.0.0.0', port=8910):\r
+ """\r
+ 初始化TCP测试服务器\r
+ \r
+ Args:\r
+ host: 监听地址,默认0.0.0.0(所有网络接口)\r
+ port: 监听端口,默认8910\r
+ """\r
+ self.host = host\r
+ self.port = port\r
+ self.server_socket = None\r
+ self.clients = {} # {client_id: {'socket': socket, 'address': (ip, port), 'connect_time': datetime}}\r
+ self.client_counter = 0\r
+ self.running = False\r
+ self.lock = threading.Lock()\r
+ \r
+ def start(self):\r
+ """启动服务器"""\r
+ try:\r
+ self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\r
+ self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\r
+ self.server_socket.bind((self.host, self.port))\r
+ self.server_socket.listen(10) # 最多10个待连接\r
+ self.server_socket.settimeout(1.0) # 设置超时以便定期检查running状态\r
+ \r
+ self.running = True\r
+ print("=" * 60)\r
+ print(f"TCP测试服务器已启动")\r
+ print(f"监听地址: {self.host}:{self.port}")\r
+ print(f"服务器地址: snicker.com.cn:{self.port}")\r
+ print("=" * 60)\r
+ print("\n命令说明:")\r
+ print(" list - 显示所有连接的客户端")\r
+ print(" send <id> - 发送数据给指定客户端(会提示输入数据)")\r
+ print(" sendall - 发送数据给所有客户端(会提示输入数据)")\r
+ print(" close <id> - 关闭指定客户端连接")\r
+ print(" closeall - 关闭所有客户端连接")\r
+ print(" help - 显示帮助信息")\r
+ print(" quit/exit - 退出服务器")\r
+ print("=" * 60)\r
+ print()\r
+ \r
+ # 启动接受连接的线程\r
+ accept_thread = threading.Thread(target=self._accept_connections, daemon=True)\r
+ accept_thread.start()\r
+ \r
+ # 启动命令行输入处理线程\r
+ input_thread = threading.Thread(target=self._handle_commands, daemon=True)\r
+ input_thread.start()\r
+ \r
+ # 主线程等待\r
+ accept_thread.join()\r
+ \r
+ except Exception as e:\r
+ print(f"❌ 服务器启动失败: {e}")\r
+ self.running = False\r
+ \r
+ def _accept_connections(self):\r
+ """接受客户端连接"""\r
+ while self.running:\r
+ try:\r
+ client_socket, address = self.server_socket.accept()\r
+ client_socket.settimeout(1.0)\r
+ \r
+ with self.lock:\r
+ self.client_counter += 1\r
+ client_id = self.client_counter\r
+ self.clients[client_id] = {\r
+ 'socket': client_socket,\r
+ 'address': address,\r
+ 'connect_time': datetime.now()\r
+ }\r
+ \r
+ # 显示连接信息\r
+ print(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] ✅ 客户端连接")\r
+ print(f" 客户端ID: {client_id}")\r
+ print(f" 地址: {address[0]}:{address[1]}")\r
+ print(f" 当前连接数: {len(self.clients)}")\r
+ print()\r
+ \r
+ # 启动接收数据的线程\r
+ recv_thread = threading.Thread(\r
+ target=self._handle_client,\r
+ args=(client_id, client_socket, address),\r
+ daemon=True\r
+ )\r
+ recv_thread.start()\r
+ \r
+ except socket.timeout:\r
+ continue\r
+ except Exception as e:\r
+ if self.running:\r
+ print(f"❌ 接受连接时出错: {e}")\r
+ \r
+ def _handle_client(self, client_id, client_socket, address):\r
+ """处理客户端数据接收"""\r
+ buffer = b""\r
+ try:\r
+ while self.running:\r
+ try:\r
+ data = client_socket.recv(4096)\r
+ if not data:\r
+ # 客户端关闭连接\r
+ break\r
+ \r
+ buffer += data\r
+ \r
+ # 显示接收到的数据\r
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')\r
+ print(f"\n[{timestamp}] 📥 收到数据 [客户端ID: {client_id}, {address[0]}:{address[1]}]")\r
+ print(f" 数据长度: {len(data)} 字节")\r
+ \r
+ # 尝试以文本和十六进制显示\r
+ try:\r
+ text = data.decode('utf-8', errors='ignore')\r
+ if text.isprintable() or '\n' in text or '\r' in text:\r
+ print(f" 文本内容:")\r
+ for line in text.splitlines():\r
+ print(f" {line}")\r
+ else:\r
+ print(f" 十六进制: {data.hex()}")\r
+ except:\r
+ print(f" 十六进制: {data.hex()}")\r
+ \r
+ # 显示前64字节的ASCII表示\r
+ if len(data) <= 64:\r
+ ascii_repr = ''.join(chr(b) if 32 <= b < 127 else '.' for b in data)\r
+ print(f" ASCII: {ascii_repr}")\r
+ else:\r
+ ascii_repr = ''.join(chr(b) if 32 <= b < 127 else '.' for b in data[:64])\r
+ print(f" ASCII (前64字节): {ascii_repr}...")\r
+ \r
+ print()\r
+ \r
+ except socket.timeout:\r
+ continue\r
+ except Exception as e:\r
+ if self.running:\r
+ print(f"❌ 接收数据时出错 [客户端ID: {client_id}]: {e}")\r
+ break\r
+ \r
+ except Exception as e:\r
+ print(f"❌ 处理客户端时出错 [客户端ID: {client_id}]: {e}")\r
+ finally:\r
+ # 客户端断开连接\r
+ self._disconnect_client(client_id, address)\r
+ \r
+ def _disconnect_client(self, client_id, address):\r
+ """断开客户端连接"""\r
+ with self.lock:\r
+ if client_id in self.clients:\r
+ try:\r
+ self.clients[client_id]['socket'].close()\r
+ except:\r
+ pass\r
+ del self.clients[client_id]\r
+ \r
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')\r
+ print(f"\n[{timestamp}] ❌ 客户端断开连接")\r
+ print(f" 客户端ID: {client_id}")\r
+ print(f" 地址: {address[0]}:{address[1]}")\r
+ print(f" 当前连接数: {len(self.clients)}")\r
+ print()\r
+ \r
+ def _handle_commands(self):\r
+ """处理命令行输入"""\r
+ while self.running:\r
+ try:\r
+ command = input().strip()\r
+ if not command:\r
+ continue\r
+ \r
+ parts = command.split(None, 1)\r
+ cmd = parts[0].lower()\r
+ args = parts[1] if len(parts) > 1 else ""\r
+ \r
+ if cmd in ['quit', 'exit', 'q']:\r
+ print("\n正在关闭服务器...")\r
+ self.stop()\r
+ break\r
+ elif cmd == 'list':\r
+ self._list_clients()\r
+ elif cmd == 'send':\r
+ if args:\r
+ try:\r
+ client_id = int(args)\r
+ self._send_to_client(client_id)\r
+ except ValueError:\r
+ print("❌ 无效的客户端ID,请输入数字")\r
+ else:\r
+ print("❌ 用法: send <客户端ID>")\r
+ elif cmd == 'sendall':\r
+ self._send_to_all_clients()\r
+ elif cmd == 'close':\r
+ if args:\r
+ try:\r
+ client_id = int(args)\r
+ self._close_client(client_id)\r
+ except ValueError:\r
+ print("❌ 无效的客户端ID,请输入数字")\r
+ else:\r
+ print("❌ 用法: close <客户端ID>")\r
+ elif cmd == 'closeall':\r
+ self._close_all_clients()\r
+ elif cmd in ['help', 'h']:\r
+ self._show_help()\r
+ else:\r
+ print(f"❌ 未知命令: {cmd},输入 help 查看帮助")\r
+ \r
+ except EOFError:\r
+ # 输入流关闭(Ctrl+D)\r
+ break\r
+ except KeyboardInterrupt:\r
+ print("\n正在关闭服务器...")\r
+ self.stop()\r
+ break\r
+ except Exception as e:\r
+ print(f"❌ 处理命令时出错: {e}")\r
+ \r
+ def _list_clients(self):\r
+ """列出所有连接的客户端"""\r
+ with self.lock:\r
+ if not self.clients:\r
+ print("\n当前没有连接的客户端")\r
+ return\r
+ \r
+ print("\n" + "=" * 60)\r
+ print("连接的客户端列表:")\r
+ print("=" * 60)\r
+ print(f"{'ID':<6} {'地址':<25} {'连接时间':<20}")\r
+ print("-" * 60)\r
+ \r
+ for client_id, client_info in self.clients.items():\r
+ address = f"{client_info['address'][0]}:{client_info['address'][1]}"\r
+ connect_time = client_info['connect_time'].strftime('%Y-%m-%d %H:%M:%S')\r
+ print(f"{client_id:<6} {address:<25} {connect_time:<20}")\r
+ \r
+ print("=" * 60)\r
+ print(f"总计: {len(self.clients)} 个客户端")\r
+ print()\r
+ \r
+ def _send_to_client(self, client_id):\r
+ """发送数据给指定客户端"""\r
+ with self.lock:\r
+ if client_id not in self.clients:\r
+ print(f"❌ 客户端ID {client_id} 不存在")\r
+ return\r
+ \r
+ client_info = self.clients[client_id]\r
+ client_socket = client_info['socket']\r
+ address = client_info['address']\r
+ \r
+ try:\r
+ print(f"\n发送数据给客户端 {client_id} ({address[0]}:{address[1]})")\r
+ print("输入数据(支持文本或十六进制,十六进制以0x或hex:开头):")\r
+ data_input = input("> ").strip()\r
+ \r
+ if not data_input:\r
+ print("❌ 数据为空,取消发送")\r
+ return\r
+ \r
+ # 解析输入数据\r
+ data = self._parse_input_data(data_input)\r
+ if data is None:\r
+ print("❌ 数据解析失败")\r
+ return\r
+ \r
+ # 发送数据\r
+ client_socket.sendall(data)\r
+ \r
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')\r
+ print(f"\n[{timestamp}] 📤 已发送数据 [客户端ID: {client_id}]")\r
+ print(f" 数据长度: {len(data)} 字节")\r
+ print(f" 十六进制: {data.hex()}")\r
+ if len(data) <= 64:\r
+ ascii_repr = ''.join(chr(b) if 32 <= b < 127 else '.' for b in data)\r
+ print(f" ASCII: {ascii_repr}")\r
+ print()\r
+ \r
+ except Exception as e:\r
+ print(f"❌ 发送数据失败: {e}")\r
+ \r
+ def _send_to_all_clients(self):\r
+ """发送数据给所有客户端"""\r
+ with self.lock:\r
+ if not self.clients:\r
+ print("❌ 当前没有连接的客户端")\r
+ return\r
+ \r
+ client_ids = list(self.clients.keys())\r
+ \r
+ try:\r
+ print(f"\n发送数据给所有客户端(共 {len(client_ids)} 个)")\r
+ print("输入数据(支持文本或十六进制,十六进制以0x或hex:开头):")\r
+ data_input = input("> ").strip()\r
+ \r
+ if not data_input:\r
+ print("❌ 数据为空,取消发送")\r
+ return\r
+ \r
+ # 解析输入数据\r
+ data = self._parse_input_data(data_input)\r
+ if data is None:\r
+ print("❌ 数据解析失败")\r
+ return\r
+ \r
+ # 发送给所有客户端\r
+ success_count = 0\r
+ failed_clients = []\r
+ \r
+ with self.lock:\r
+ for client_id in client_ids:\r
+ if client_id in self.clients:\r
+ try:\r
+ self.clients[client_id]['socket'].sendall(data)\r
+ success_count += 1\r
+ except Exception as e:\r
+ failed_clients.append((client_id, str(e)))\r
+ \r
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')\r
+ print(f"\n[{timestamp}] 📤 已发送数据给 {success_count} 个客户端")\r
+ print(f" 数据长度: {len(data)} 字节")\r
+ if failed_clients:\r
+ print(f" 失败: {len(failed_clients)} 个客户端")\r
+ for client_id, error in failed_clients:\r
+ print(f" 客户端 {client_id}: {error}")\r
+ print()\r
+ \r
+ except Exception as e:\r
+ print(f"❌ 发送数据失败: {e}")\r
+ \r
+ def _parse_input_data(self, data_input):\r
+ """\r
+ 解析输入数据\r
+ 支持文本、十六进制(0x开头或hex:开头)\r
+ \r
+ Args:\r
+ data_input: 输入字符串\r
+ \r
+ Returns:\r
+ bytes: 解析后的数据,失败返回None\r
+ """\r
+ data_input = data_input.strip()\r
+ \r
+ # 十六进制格式: 0x... 或 hex:...\r
+ if data_input.lower().startswith('0x'):\r
+ hex_str = data_input[2:].strip()\r
+ # 移除空格\r
+ hex_str = hex_str.replace(' ', '').replace('\n', '').replace('\r', '')\r
+ try:\r
+ return bytes.fromhex(hex_str)\r
+ except ValueError:\r
+ print(f"❌ 无效的十六进制数据: {hex_str}")\r
+ return None\r
+ elif data_input.lower().startswith('hex:'):\r
+ hex_str = data_input[4:].strip()\r
+ # 移除空格\r
+ hex_str = hex_str.replace(' ', '').replace('\n', '').replace('\r', '')\r
+ try:\r
+ return bytes.fromhex(hex_str)\r
+ except ValueError:\r
+ print(f"❌ 无效的十六进制数据: {hex_str}")\r
+ return None\r
+ else:\r
+ # 文本格式\r
+ return data_input.encode('utf-8')\r
+ \r
+ def _close_client(self, client_id):\r
+ """关闭指定客户端连接"""\r
+ with self.lock:\r
+ if client_id not in self.clients:\r
+ print(f"❌ 客户端ID {client_id} 不存在")\r
+ return\r
+ \r
+ client_info = self.clients[client_id]\r
+ address = client_info['address']\r
+ \r
+ try:\r
+ client_info['socket'].close()\r
+ print(f"✅ 已关闭客户端 {client_id} 的连接")\r
+ except Exception as e:\r
+ print(f"❌ 关闭客户端连接失败: {e}")\r
+ \r
+ def _close_all_clients(self):\r
+ """关闭所有客户端连接"""\r
+ with self.lock:\r
+ if not self.clients:\r
+ print("当前没有连接的客户端")\r
+ return\r
+ \r
+ client_ids = list(self.clients.keys())\r
+ count = len(client_ids)\r
+ \r
+ closed_count = 0\r
+ for client_id in client_ids:\r
+ try:\r
+ with self.lock:\r
+ if client_id in self.clients:\r
+ self.clients[client_id]['socket'].close()\r
+ closed_count += 1\r
+ except:\r
+ pass\r
+ \r
+ print(f"✅ 已关闭 {closed_count}/{count} 个客户端连接")\r
+ \r
+ def _show_help(self):\r
+ """显示帮助信息"""\r
+ print("\n" + "=" * 60)\r
+ print("命令帮助:")\r
+ print("=" * 60)\r
+ print(" list - 显示所有连接的客户端")\r
+ print(" send <id> - 发送数据给指定客户端")\r
+ print(" 例如: send 1")\r
+ print(" sendall - 发送数据给所有客户端")\r
+ print(" close <id> - 关闭指定客户端连接")\r
+ print(" 例如: close 1")\r
+ print(" closeall - 关闭所有客户端连接")\r
+ print(" help - 显示此帮助信息")\r
+ print(" quit/exit/q - 退出服务器")\r
+ print()\r
+ print("数据输入格式:")\r
+ print(" - 文本: 直接输入文本,例如: Hello World")\r
+ print(" - 十六进制: 以0x开头,例如: 0x48656c6c6f")\r
+ print(" - 十六进制: 以hex:开头,例如: hex:48656c6c6f")\r
+ print("=" * 60)\r
+ print()\r
+ \r
+ def stop(self):\r
+ """停止服务器"""\r
+ self.running = False\r
+ \r
+ # 关闭所有客户端连接\r
+ with self.lock:\r
+ for client_id, client_info in list(self.clients.items()):\r
+ try:\r
+ client_info['socket'].close()\r
+ except:\r
+ pass\r
+ self.clients.clear()\r
+ \r
+ # 关闭服务器socket\r
+ if self.server_socket:\r
+ try:\r
+ self.server_socket.close()\r
+ except:\r
+ pass\r
+ \r
+ print("✅ 服务器已关闭")\r
+\r
+\r
+def main():\r
+ """主函数"""\r
+ import argparse\r
+ \r
+ parser = argparse.ArgumentParser(description='TCP测试服务器')\r
+ parser.add_argument('--host', default='0.0.0.0', help='监听地址(默认: 0.0.0.0)')\r
+ parser.add_argument('--port', type=int, default=8910, help='监听端口(默认: 8910)')\r
+ \r
+ args = parser.parse_args()\r
+ \r
+ server = TCPTestServer(host=args.host, port=args.port)\r
+ \r
+ try:\r
+ server.start()\r
+ except KeyboardInterrupt:\r
+ print("\n正在关闭服务器...")\r
+ server.stop()\r
+ except Exception as e:\r
+ print(f"❌ 服务器运行出错: {e}")\r
+ server.stop()\r
+\r
+\r
+if __name__ == "__main__":\r
+ main()\r
+\r
--- /dev/null
+#!/usr/bin/env python3\r
+# -*- coding: utf-8 -*-\r
+"""验证两个请求的签名是否正确"""\r
+\r
+import hmac\r
+import hashlib\r
+\r
+SIGN_SECRET = "vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus="\r
+\r
+# 请求1的签名验证\r
+sign_data_1 = "DEVICE_PC_TEST_001:86123456789012345678:1762845627028:DSAgsyFYxaOR"\r
+hmac_obj_1 = hmac.new(SIGN_SECRET.encode("utf-8"), sign_data_1.encode("utf-8"), hashlib.sha256)\r
+signature_1 = hmac_obj_1.hexdigest().lower()\r
+expected_signature_1 = "0652010b2d574b034086a0efd707c94fef8250eac3fb02948c049b43cfb1f977"\r
+\r
+print("=" * 60)\r
+print("请求1签名验证:")\r
+print(f" 签名数据: {sign_data_1}")\r
+print(f" 计算签名: {signature_1}")\r
+print(f" 期望签名: {expected_signature_1}")\r
+print(f" 匹配结果: {'✅ 正确' if signature_1 == expected_signature_1 else '❌ 错误'}")\r
+print("=" * 60)\r
+\r
+# 请求2的签名验证\r
+sign_data_2 = "DEVICE_PC_TEST_001:86123456789012345678:1762845599887:ogHL53iSPoaU"\r
+hmac_obj_2 = hmac.new(SIGN_SECRET.encode("utf-8"), sign_data_2.encode("utf-8"), hashlib.sha256)\r
+signature_2 = hmac_obj_2.hexdigest().lower()\r
+expected_signature_2 = "1457b817ddeffa108341b750fa32063febc9dee9a24eaca1918fb1748b5e7987"\r
+\r
+print("请求2签名验证:")\r
+print(f" 签名数据: {sign_data_2}")\r
+print(f" 计算签名: {signature_2}")\r
+print(f" 期望签名: {expected_signature_2}")\r
+print(f" 匹配结果: {'✅ 正确' if signature_2 == expected_signature_2 else '❌ 错误'}")\r
+print("=" * 60)\r
+\r
+# 时间戳差异分析\r
+timestamp_1 = 1762845627028\r
+timestamp_2 = 1762845599887\r
+time_diff = timestamp_1 - timestamp_2\r
+print(f"\n时间戳差异分析:")\r
+print(f" 请求1时间戳: {timestamp_1}")\r
+print(f" 请求2时间戳: {timestamp_2}")\r
+print(f" 时间差: {time_diff} 毫秒 ({time_diff/1000:.2f} 秒)")\r
+print("=" * 60)\r
+\r
--- /dev/null
+# WebSocket实现可行性分析\r
+\r
+## 问题\r
+AT命令手册中的TCP指令和HTTP指令是否足够实现WebSocket(ws和wss)接口,要求支持二进制数据和文本数据传输?\r
+\r
+## 简短回答\r
+\r
+**✅ 是的,AT指令集中的TCP和SSL TCP指令足够实现WebSocket接口。**\r
+\r
+- **ws(非加密)**:使用 `AT+TCPSETUP` 建立TCP连接\r
+- **wss(加密)**:使用 `AT+SSLTCPSETUP` 建立SSL TCP连接\r
+- **二进制和文本数据**:都支持,通过HEX模式发送二进制,ASCII模式发送文本\r
+\r
+**⚠️ 但需要手动实现WebSocket协议层**(帧封装、握手处理等)\r
+\r
+## WebSocket协议要求\r
+\r
+### 1. WebSocket连接建立流程\r
+```\r
+1. 建立TCP/SSL TCP连接\r
+2. 发送HTTP/HTTPS握手请求(包含Upgrade: websocket头)\r
+3. 接收服务器握手响应(HTTP 101 Switching Protocols)\r
+4. 连接升级为WebSocket协议\r
+5. 在TCP连接上传输WebSocket帧(文本/二进制)\r
+```\r
+\r
+### 2. WebSocket帧格式\r
+```\r
+ 0 1 2 3\r
+ 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\r
++-+-+-+-+-------+-+-------------+-------------------------------+\r
+|F|R|R|R| opcode|M| Payload len | Extended payload length |\r
+|I|S|S|S| (4) |A| (7) | (16/64) |\r
+|N|V|V|V| |S| | (if payload len==126/127) |\r
+| |1|2|3| |K| | |\r
++-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +\r
+| Extended payload length continued, if payload len == 127 |\r
++ - - - - - - - - - - - - - - - +-------------------------------+\r
+| |Masking-key, if MASK set to 1 |\r
++-------------------------------+-------------------------------+\r
+| Masking-key (continued) | Payload Data |\r
++-------------------------------- - - - - - - - - - - - - - - - +\r
+: Payload Data continued ... :\r
++ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +\r
+| Payload Data continued ... |\r
++---------------------------------------------------------------+\r
+```\r
+\r
+关键点:\r
+- 需要按照WebSocket帧格式封装数据\r
+- 支持文本帧(opcode=0x1)和二进制帧(opcode=0x2)\r
+- 客户端发送的数据需要mask(MASK=1)\r
+- 服务器发送的数据不需要mask(MASK=0)\r
+\r
+## AT命令手册能力分析\r
+\r
+### ✅ 可用的指令\r
+\r
+#### 1. TCP连接管理\r
+- **AT+TCPSETUP** - 建立TCP连接(支持ws)\r
+ - 支持域名解析\r
+ - 支持0-5路连接\r
+ - 连接建立后返回OK\r
+\r
+- **AT+SSLTCPSETUP** - 建立SSL TCP连接(支持wss)\r
+ - 支持TLS/SSL加密\r
+ - 支持证书配置\r
+ - 支持0-5路连接\r
+\r
+#### 2. 数据发送能力\r
+- **AT+TCPSEND** - 发送TCP数据\r
+ - **Buffer模式**:\r
+ - ASCII模式:最大4096字节\r
+ - HEX模式:最大2048字节(二进制数据)\r
+ - **命令模式**:\r
+ - ASCII/HEX模式:最大512字节\r
+ - 支持二进制数据传输(HEX模式)\r
+\r
+- **AT+SSLTCPSEND** - 发送SSL TCP数据\r
+ - 最大4096字节\r
+ - 支持二进制数据传输\r
+\r
+#### 3. 数据接收能力\r
+- **+TCPRECV** - 接收TCP数据主动上报\r
+- **AT+TCPREAD** - 读取TCP数据\r
+- **+SSLTCPRECV** - 接收SSL TCP数据主动上报\r
+- **AT+SSLTCPREAD** - 读取SSL TCP数据\r
+- **AT+RECVMODE** - 设置接收模式(ASCII/HEX)\r
+\r
+#### 4. HTTP指令(用于握手)\r
+- **AT+HTTPSETUP/AT+HTTPSSETUP** - 建立HTTP/HTTPS连接\r
+- **AT+HTTPACTION/AT+HTTPSACTION** - 发送HTTP请求\r
+- 或者使用TCP指令手动发送HTTP握手请求\r
+\r
+### ⚠️ 限制和挑战\r
+\r
+#### 1. 数据长度限制\r
+- TCP buffer模式HEX:最大2048字节\r
+- SSL TCP:最大4096字节\r
+- WebSocket帧可能超过这些限制(需要分片)\r
+\r
+#### 2. WebSocket帧封装\r
+- **需要手动实现**WebSocket帧的编码/解码\r
+- 需要处理:\r
+ - 帧头构造(FIN, opcode, MASK, payload length)\r
+ - Masking-key生成和应用\r
+ - 扩展payload length(126/127字节)\r
+ - 分片处理(FIN=0的连续帧)\r
+\r
+#### 3. 握手处理\r
+- 需要生成WebSocket握手请求\r
+- 需要解析服务器握手响应\r
+- 需要验证Sec-WebSocket-Accept\r
+\r
+#### 4. 实时性要求\r
+- WebSocket需要及时处理接收到的数据\r
+- 需要轮询或使用主动上报机制\r
+\r
+## 实现方案\r
+\r
+### 方案1:使用TCP/SSL TCP指令(推荐)\r
+\r
+#### 优点\r
+- ✅ 完全控制数据传输\r
+- ✅ 支持二进制数据(HEX模式)\r
+- ✅ 支持长连接\r
+- ✅ 可以处理任意大小的数据(通过分片)\r
+\r
+#### 实现步骤\r
+\r
+**1. 建立连接(ws)**\r
+```\r
+AT+TCPSETUP=0,websocket.example.com,80\r
+```\r
+\r
+**2. 发送WebSocket握手请求**\r
+```\r
+AT+TCPSEND=0,<handshake_length>\r
+> GET /path HTTP/1.1\r\n\r
+ Host: websocket.example.com\r\n\r
+ Upgrade: websocket\r\n\r
+ Connection: Upgrade\r\n\r
+ Sec-WebSocket-Key: <base64_key>\r\n\r
+ Sec-WebSocket-Version: 13\r\n\r
+ \r\n\r
+```\r
+\r
+**3. 接收握手响应**\r
+```\r
++TCPRECV: 0,<length>\r
+HTTP/1.1 101 Switching Protocols\r
+Upgrade: websocket\r
+Connection: Upgrade\r
+Sec-WebSocket-Accept: <accept_key>\r
+```\r
+\r
+**4. 发送WebSocket帧(文本)**\r
+```\r
+// 构造WebSocket帧\r
+// FIN=1, opcode=0x1(文本), MASK=1, payload_len, masking_key, masked_payload\r
+AT+TCPSEND=0,<frame_length>,1 // HEX模式\r
+> <hex_encoded_frame>\r
+```\r
+\r
+**5. 发送WebSocket帧(二进制)**\r
+```\r
+// 构造WebSocket帧\r
+// FIN=1, opcode=0x2(二进制), MASK=1, payload_len, masking_key, masked_payload\r
+AT+TCPSEND=0,<frame_length>,1 // HEX模式\r
+> <hex_encoded_frame>\r
+```\r
+\r
+**6. 接收WebSocket帧**\r
+```\r
++TCPRECV: 0,<length>\r
+// 解析帧头,提取payload\r
+```\r
+\r
+### 方案2:使用HTTP指令(不推荐)\r
+\r
+#### 缺点\r
+- ❌ HTTP指令主要用于请求-响应模式\r
+- ❌ 不支持长连接的双向通信\r
+- ❌ 难以处理WebSocket帧\r
+\r
+## 关键实现点\r
+\r
+### 1. WebSocket帧编码(发送)\r
+\r
+```python\r
+def encode_websocket_frame(payload, opcode=0x1, fin=1):\r
+ """\r
+ 编码WebSocket帧\r
+ opcode: 0x1=文本, 0x2=二进制\r
+ """\r
+ # 1. 构造帧头\r
+ frame = bytearray()\r
+ \r
+ # 第一个字节:FIN(1) + RSV(000) + opcode(4)\r
+ first_byte = (fin << 7) | opcode\r
+ frame.append(first_byte)\r
+ \r
+ # 2. 生成masking key(客户端必须mask)\r
+ masking_key = os.urandom(4)\r
+ \r
+ # 3. 第二个字节:MASK(1) + payload length\r
+ payload_len = len(payload)\r
+ if payload_len < 126:\r
+ second_byte = 0x80 | payload_len # MASK=1\r
+ frame.append(second_byte)\r
+ elif payload_len < 65536:\r
+ frame.append(0x80 | 126) # MASK=1, len=126\r
+ frame.extend(payload_len.to_bytes(2, 'big'))\r
+ else:\r
+ frame.append(0x80 | 127) # MASK=1, len=127\r
+ frame.extend(payload_len.to_bytes(8, 'big'))\r
+ \r
+ # 4. 添加masking key\r
+ frame.extend(masking_key)\r
+ \r
+ # 5. 对payload进行mask\r
+ masked_payload = bytearray()\r
+ for i, byte in enumerate(payload):\r
+ masked_payload.append(byte ^ masking_key[i % 4])\r
+ \r
+ frame.extend(masked_payload)\r
+ return bytes(frame)\r
+```\r
+\r
+### 2. WebSocket帧解码(接收)\r
+\r
+```python\r
+def decode_websocket_frame(data):\r
+ """\r
+ 解码WebSocket帧\r
+ """\r
+ if len(data) < 2:\r
+ return None, data\r
+ \r
+ # 解析第一个字节\r
+ first_byte = data[0]\r
+ fin = (first_byte >> 7) & 0x1\r
+ opcode = first_byte & 0x0F\r
+ \r
+ # 解析第二个字节\r
+ second_byte = data[1]\r
+ mask = (second_byte >> 7) & 0x1\r
+ payload_len = second_byte & 0x7F\r
+ \r
+ # 解析扩展payload length\r
+ offset = 2\r
+ if payload_len == 126:\r
+ if len(data) < 4:\r
+ return None, data\r
+ payload_len = int.from_bytes(data[2:4], 'big')\r
+ offset = 4\r
+ elif payload_len == 127:\r
+ if len(data) < 10:\r
+ return None, data\r
+ payload_len = int.from_bytes(data[2:10], 'big')\r
+ offset = 10\r
+ \r
+ # 解析masking key(服务器发送的不mask)\r
+ masking_key = None\r
+ if mask:\r
+ if len(data) < offset + 4:\r
+ return None, data\r
+ masking_key = data[offset:offset+4]\r
+ offset += 4\r
+ \r
+ # 读取payload\r
+ if len(data) < offset + payload_len:\r
+ return None, data\r
+ \r
+ payload = data[offset:offset+payload_len]\r
+ \r
+ # 如果masked,进行unmask\r
+ if mask and masking_key:\r
+ payload = bytearray(payload)\r
+ for i in range(len(payload)):\r
+ payload[i] ^= masking_key[i % 4]\r
+ payload = bytes(payload)\r
+ \r
+ # 返回帧信息和剩余数据\r
+ frame_info = {\r
+ 'fin': fin,\r
+ 'opcode': opcode,\r
+ 'payload': payload,\r
+ 'is_text': opcode == 0x1,\r
+ 'is_binary': opcode == 0x2\r
+ }\r
+ \r
+ remaining = data[offset+payload_len:]\r
+ return frame_info, remaining\r
+```\r
+\r
+### 3. 握手请求生成\r
+\r
+```python\r
+import base64\r
+import hashlib\r
+import secrets\r
+\r
+def generate_websocket_handshake(host, path, port=80):\r
+ """\r
+ 生成WebSocket握手请求\r
+ """\r
+ # 生成Sec-WebSocket-Key\r
+ key = base64.b64encode(secrets.token_bytes(16)).decode('utf-8')\r
+ \r
+ handshake = (\r
+ f"GET {path} HTTP/1.1\r\n"\r
+ f"Host: {host}:{port}\r\n"\r
+ f"Upgrade: websocket\r\n"\r
+ f"Connection: Upgrade\r\n"\r
+ f"Sec-WebSocket-Key: {key}\r\n"\r
+ f"Sec-WebSocket-Version: 13\r\n"\r
+ f"\r\n"\r
+ )\r
+ \r
+ return handshake, key\r
+```\r
+\r
+## 结论\r
+\r
+### ✅ **可以实现WebSocket(ws和wss)**\r
+\r
+**理由:**\r
+1. ✅ TCP/SSL TCP指令支持建立连接\r
+2. ✅ 支持二进制数据传输(HEX模式)\r
+3. ✅ 支持文本数据传输(ASCII模式)\r
+4. ✅ 支持长连接\r
+5. ✅ 可以手动发送HTTP握手请求\r
+\r
+### ⚠️ **需要额外实现**\r
+\r
+1. **WebSocket帧编码/解码** - 需要手动实现\r
+2. **握手处理** - 需要生成和解析HTTP握手\r
+3. **分片处理** - 处理大于2048/4096字节的数据\r
+4. **心跳/Ping-Pong** - 保持连接活跃\r
+5. **错误处理** - 处理连接断开、超时等\r
+\r
+### 📊 **数据长度限制**\r
+\r
+| 传输类型 | 最大长度 | 解决方案 |\r
+|---------|---------|---------|\r
+| TCP HEX模式 | 2048字节 | 分片发送(FIN=0) |\r
+| SSL TCP | 4096字节 | 分片发送(FIN=0) |\r
+| 文本数据 | 无限制 | 分片处理 |\r
+\r
+### 🎯 **推荐实现方式**\r
+\r
+**使用TCP/SSL TCP指令 + 手动实现WebSocket协议层**\r
+\r
+1. 使用 `AT+TCPSETUP` / `AT+SSLTCPSETUP` 建立连接\r
+2. 使用 `AT+TCPSEND` / `AT+SSLTCPSEND` 发送握手和帧数据(HEX模式)\r
+3. 使用 `+TCPRECV` / `+SSLTCPRECV` 接收数据\r
+4. 在应用层实现WebSocket帧的编码/解码\r
+\r
+### 📝 **注意事项**\r
+\r
+1. **数据分片**:超过2048/4096字节的数据需要分片发送\r
+2. **实时性**:需要及时处理接收到的数据,避免buffer溢出\r
+3. **错误恢复**:实现重连机制\r
+4. **性能优化**:合理使用buffer模式和命令模式\r
+\r
+## 总结\r
+\r
+### ✅ **结论:AT指令集中的TCP和SSL TCP指令足够实现WebSocket接口**\r
+\r
+**核心能力:**\r
+1. ✅ **连接建立**:`AT+TCPSETUP`(ws)和 `AT+SSLTCPSETUP`(wss)\r
+2. ✅ **数据发送**:`AT+TCPSEND` / `AT+SSLTCPSEND` 支持二进制(HEX模式)和文本(ASCII模式)\r
+3. ✅ **数据接收**:`+TCPRECV` / `+SSLTCPRECV` 主动上报,支持二进制和文本\r
+4. ✅ **长连接**:TCP/SSL TCP连接支持保持连接,满足WebSocket要求\r
+\r
+**需要手动实现的部分:**\r
+1. ⚠️ **WebSocket握手**:生成HTTP握手请求,解析服务器响应\r
+2. ⚠️ **帧封装/解析**:按照RFC 6455标准实现WebSocket帧的编码和解码\r
+3. ⚠️ **数据分片**:处理超过2048/4096字节的数据(使用FIN=0分片)\r
+4. ⚠️ **心跳机制**:实现Ping/Pong帧保持连接活跃\r
+\r
+**推荐实现方式:**\r
+- 使用 `AT+TCPSETUP` / `AT+SSLTCPSETUP` 建立连接\r
+- 使用 `AT+TCPSEND` / `AT+SSLTCPSEND` 发送握手和WebSocket帧(HEX模式)\r
+- 使用 `+TCPRECV` / `+SSLTCPRECV` 接收数据\r
+- 在应用层实现WebSocket协议栈(帧封装、握手、心跳等)\r
+\r
+**数据长度限制:**\r
+- TCP HEX模式:最大2048字节/帧(可通过分片解决)\r
+- SSL TCP:最大4096字节/帧(可通过分片解决)\r
+\r
+**总结:AT指令集提供了足够的底层传输能力,但需要在应用层实现WebSocket协议层。**\r
+\r
--- /dev/null
+# 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=<ip/url>”进行测试。
+
+⚫
+确保证书等文件已成功导入:
+
+指令“AT+CERTADD=<file_name>,<length>”中,“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=<clientdID>,<username>, <password>”指令对用户设备ID、用
+
+户名和密码进行设置。
+
+2.
+根据需要使用指令“AT+MQTTWILLPARAM=<retained>,<qos>, <topicname>,<message>”设置
+
+遗嘱主题(最大长度128Bytes)与遗嘱消息(最大长度1024Bytes)。
+
+Neoway SSL/TLS 应用指南
+
+MQTTS 应用流程
+
+深圳市有方科技股份有限公司版权所有
+18
+
+步骤 5:执行“AT+MQTTCONN=<"host">,<clean>,<keep_alive>”指令建立MQTTS 连接。
+
+“host”为服务器地址(url:port);“clean”为是否清除session,默认为0,表示不清除;
+
+keep_alive 时间取值范围为[20,180],单位为s,默认为60s。
+
+步骤 6:执行“AT+MQTTPUB=<retained>,<qos>,<"topicname">,<"message">”指令 发布主题。
+
+其中,“retained”为保留标志,“qos”为服务质量,“topicname”为发布主题(最大长度
+
+128Bytes),“message”为发布消息(最大长度为1024Bytes)。
+
+步骤 7:执行“AT+MQTTSUB=<"topicname">,<qos>”指令进行主题的订阅。
+
+步骤 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=<type>,<type_name>中,<type>字段为rootca;
+
+⚫
+而HTTPS 中指令AT+HTTPSCFG=<type>,<type_name> 中,<type>字段为cacert。
+
+Neoway SSL/TLS 应用指南
+
+MQTTS 应用流程
+
+深圳市有方科技股份有限公司版权所有
+19
+
+6.
+执行“AT+MQTTTLS?”指令,确认当前SSL/TLS 加密参数配置内容是否正确。
+
+步骤 4:配置MQTTS 连接参数。
+
+1.
+使用指令“AT+MQTTCONNPARAM=<clientdID>,<username>, <password>”对用户设备ID、用
+
+户名和密码进行设置。
+
+2.
+根据需要使用指令“AT+MQTTWILLPARAM=<retained>,<qos>, <topicname>,<message>”设置
+
+遗嘱主题(最大长度128Bytes)与遗嘱消息(最大长度1024Bytes)。
+
+步骤 5:使用指令“AT+MQTTCONN=<"host">,<clean>,<keep_alive>”建立MQTTS 连接。
+
+其中,“host”为服务器地址(url:port);“clean”为是否清除session,默认为0,表示不清除;
+
+keep_alive 时间取值范围为[20,180],单位为s,默认为60s。
+
+步骤 6:执行“AT+MQTTPUB=<retained>,<qos>,<"topicname">,<"message">”指令发布主题。其中,
+
+“retained”为保留标志,“qos”为服务质量,“topicname”为发布主题(最大长度
+
+128Bytes),“message”为发布消息(最大长度为1024 Bytes)。
+
+步骤 7:执行“AT+MQTTSUB=<"topicname">,<qos>”指令进行主题的订阅。
+
+步骤 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=<clientdID>,<username>, <password>”对用户设备ID、用
+
+户名和密码进行设置。
+
+2.
+根据需要使用指令“AT+MQTTWILLPARAM=<retained>,<qos>, <topicname>,<message>”设置
+
+遗嘱主题(最大长度128Bytes)与遗嘱消息(最大长度1024Bytes)。
+
+步骤 5:使用指令“AT+MQTTCONN=<"host">,<clean>,<keep_alive>”建立MQTTS 连接。
+
+其中,“host”为服务器地址(url:port);“clean”为是否清除session,默认为0,表示不清除;
+
+keep_alive 时间取值范围为[20,180],单位为s,默认为60s。
+
+步骤 6:执行“AT+MQTTPUB=<retained>,<qos>,<"topicname">,<"message">”指令发布主题。其中,
+
+“retained”为保留标志,“qos”为服务质量,“topicname”为发布主题(最大长度
+
+128Bytes),“message”为发布消息(最大长度为1024Bytes)。
+
+步骤 7:执行“AT+MQTTSUB=<"topicname">,<qos>”指令进行主题的订阅。
+
+步骤 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= <n>,<ip>,<port>,<mode>”建立TCPS 连接。
+
+步骤 5:执行“AT+SSLTCPSEND=<n>,<data_length>”向服务器发送数据。
+
+当接收到服务器数据时,会主动上报收到的数据内容。
+
+步骤 6:当需要主动关闭连接时可执行“AT+SSLTCPCLOSE=<n>”关闭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= <n>,<ip>,<port>,<mode>”建立TCPS 连接。
+
+步骤 5:执行“AT+SSLTCPSEND=<n>,<data_length>”向服务器发送数据。
+
+当接收到服务器数据时,会主动上报收到的数据内容。
+
+步骤 6:需要关闭连接时可执行“AT+SSLTCPCLOSE=<n>”关闭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=<n>,<ip>,<port>,<mode>”建立TCPS 连接。
+
+步骤 5:执行“AT+SSLTCPSEND=<n>,<data_length>”向服务器发送数据。
+
+当接收到服务器数据时,会主动上报收到的数据内容。
+
+步骤 6:关闭TCPS 连接。
+
+需要主动关闭连接时可执行“AT+SSLTCPCLOSE=<n>”关闭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=<file_name>,<length
+><CR>
+
+<CR><LF>CONNECT<CR><LF>
+<CR><LF>+CERTADD: <length>,OK<CR><LF>
+Or
+<CR><LF>+CERTADD: ERROR<CR><LF>
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<file_name>
+写入模块的证书名称
+
+<length>
+写入的长度
+
+###### A.1.2 AT+CERTCHECK–SSL 证书确认
+
+检查证书。
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有
+29
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行
+AT+CERTCHECK=<file_name><CR>
+
+<CR><LF>+CERTCHECK: <file_name>,OK
+Or
+<CR><LF>+CERTCHECK: ERROR
+
+查询
+AT+CERTCHECK?<CR>
+
+<CR><LF><file_name>
+[<CR><LF><file_name>]
+<CR><LF>OK<CR><LF>
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<file_name>
+要确认的证书名称。
+
+###### A.1.3 AT+HTTPSCFG–配置HTTPS 参数
+
+配置HTTPS 加密选项。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+设置
+AT+HTTPSCFG=<type>,<type_na
+me><CR>
+
+<CR><LF>OK<CR><LF>
+Or
+<CR><LF>ERROR<CR><LF>
+
+查询
+AT+HTTPSCFG?<CR>
+
+<CR><LF>+HTTPSCFG: <sslversion
+type_name>,<authmode type_name>,<ciphersuite
+type_name>,<cacert type_name>,<clientcert
+type_name>,<clientkey type_name>
+<CR><LF>OK<CR><LF>
+
+测试
+AT+HTTPSCFG=?<CR>
+<CR><LF>+HTTPSCFG: <type>,<type_name>
+<CR><LF>OK<CR><LF>
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有
+30
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<type>
+配置SSL 选项。
+
+sslversion: SSL 协议版本
+
+authmode: 安全认证模式
+
+ciphersuite: 加密控件
+
+cacert: 根证书
+
+clientcert: 客户端证书
+
+clientkey: 客户端密匙
+
+<type_name>
+<type>和<type_name>参数的取值,对应关系如下
+
+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=<para>,<para_value><CR>
+<CR><LF>OK<CR><LF>
+Or
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有
+31
+
+<CR><LF>ERROR<CR><LF>
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<para>
+HTTPS 参数,支持两个参数设置
+
+url:目标路径
+
+port:目标端口号(未设置缺省值)
+
+keepalive:设置长连接,http 协议的长连接,默认值para_value=0,para_value
+
+为1 时打开长连接
+
+recvmode:接收模式,para_value=0 默认接收模式,一个HTTP 响应只包含一个
+
++HTTPRECV:头表示;para_value=1,数据以+HTTPRECV: <length>,<data>形
+
+式呈现
+
+<para_value>
+对应<para>的值,其中url 参数值最大为1022 个字节,url 支持域名解析,url 需加
+
+双引号
+
+###### A.1.5 AT+HTTPSSETUP–建立HTTPS 链路
+
+建立HTTPS 链接。正确设置目标地址和端口号才能链接成功。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行
+AT+HTTPSSETUP<CR>
+
+<CR><LF>OK<CR><LF>
+Or
+<CR><LF>ERROR<CR><LF>
+
+###### 响应时间
+
+最大响应时间60s。
+
+###### 参数
+
+无
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有
+32
+
+###### A.1.6 AT+HTTPSACTION–发送HTTPS 请求
+
+执行HTTPS 请求。
+
+用户自定义报文时需遵循HTTPS 协议;用户在自定义报文的时候注意使用的工具里头是否会自带
+
+回车和换行这些字符。
+
+###### 命令格式
+
+类型
+命令
+响应格式
+
+执行
+
+AT+HTTPSACTION=<mode>[,<length>
+[,<type>]<CR>
+AT+HTTPSACTION=<mode>[,<offset>,
+<size>]<CR>
+
+<CR><LF>OK<CR><LF>
+Or
+<CR><LF>><post_content><CR><LF>
+<CR><LF>OK<CR><LF>
+
+###### 响应时间
+
+响应超时时间为60 秒。
+
+###### 参数
+
+<mode>
+HTTP 请求方式,可取值为0,1,2,99
+
+0:GET
+
+1:HEAD
+
+2:POST
+
+99:OPEN_MODE,用户自己定义报文模式
+
+<length>
+POST 内容长度或自定义报文长度,在<mode>为POST 和OPEN_MODE 时必
+
+须设置,最大长度为2048
+
+<type>
+POST 请求数据类型
+
+1:text
+
+2:json
+
+3:xml
+
+4:html
+
+<offset>
+偏移量,通过GET 方式下载文件时,可以指定下载起始位置
+
+<size>
+下载长度,通过GET 方式下载文件时,可以指定下载长度
+
+<post_content>
+HTTPSPOST 发送的数据
+
+###### A.1.7 AT+HTTPSCLOSE–主动关闭HTTPS 链路
+
+关闭HTTPS 链接。
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有
+33
+
+执行+HTTPSCLOSE 命令,在关闭HTTPS 链路的同时,会清除+HTTPSPARA 命令设置的参数。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行
+AT+HTTPCLOSE<CR>
+<CR><LF>OK<CR><LF>
+<CR><LF>+HTTPSCLOSE: HTTPS Link Closed<CR><LF>
+
+###### 响应时间
+
+最大响应时间60s。
+
+###### 参数
+
+无
+
+##### A.2 MQTTS 相关指令
+
+###### A.2.1 AT+CERTADD–添加SSL 证书
+
+向模块写入证书
+
+写入过程可以通过“+++”中断。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行
+AT+CERTADD=<file_name>,<length
+><CR>
+
+<CR><LF>CONNECT<CR><LF>
+<CR><LF>+CERTADD: <length>,OK<CR><LF>
+Or
+<CR><LF>+CERTADD: ERROR<CR><LF>
+
+###### 响应时间
+
+最大响应时间300ms。
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有
+34
+
+###### 参数
+
+<file_name>
+写入模块的证书名称
+
+<length>
+写入的长度
+
+###### A.2.2 AT+CERTCHECK–SSL 证书确认
+
+检查证书。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行
+AT+CERTCHECK=<file_name><CR>
+
+<CR><LF>+CERTCHECK: <file_name>,OK
+Or
+<CR><LF>+CERTCHECK: ERROR
+
+查询
+AT+CERTCHECK?<CR>
+
+<CR><LF><file_name>
+[<CR><LF><file_name>]
+<CR><LF>OK<CR><LF>
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<file_name>
+要确认的证书名称。
+
+###### A.2.3 AT+MQTTTLS - TLS 参数配置
+
+MQTT TLS 参数配置。
+
+通过AT+CERTADD 导入证书。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+设置
+AT+MQTTTLS=<type>,<val
+ue><CR>
+
+<CR><LF>OK<CR><LF>
+Or
+<CR><LF>ERROR<CR><LF>
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有
+35
+
+查询
+AT+MQTTTLS?<CR>
+
+<CR><LF>+MQTTTLS:
+<sslmode>,<authmode>,
+
+<rootca_name>,<clientcert_name>,<clientkey_name>
+<CR><LF>OK<CR><LF>
+
+测试
+AT+MQTTTLS=?<CR>
+<CR><LF>+MQTTTLS: <type>,<value>
+<CR><LF>OK<CR><LF>
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<type>
+配置参数类型
+
+<value>
+配置参数值
+
+<sslmode>
+安全加密模式
+
+0:不加密
+
+1:加密
+
+<authmode>
+安全认证模式
+
+0:verify optional
+
+1:verify required
+
+<rootca>
+string,CA 证书
+
+<clientcert>
+string,客户端证书
+
+<clientkey>
+string,客户端密钥
+
+###### A.2.4 AT+MQTTCONNPARAM–用户参数设置
+
+设置ID、用户名、密码参数。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+设置
+AT+MQTTCONNPARAM=<clientdID>,
+<username>,<password><CR>
+
+<CR><LF>OK<CR><LF>
+Or
+<CR><LF>ERROR<CR><LF>
+
+查询
+AT+MQTTCONNPARAM?<CR>
+
+<CR><LF>+MQTTCONNPARAM:<"clientID"
+>,<"username">,<"password">
+<CR><LF>OK<CR><LF>
+
+测试
+AT+MQTTCONNPARAM=?<CR>
+<CR><LF>OK<CR><LF>
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有
+36
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<clientID>
+设备ID,最大长度256。
+
+<username>
+用户名,最大长度512。
+
+<password>
+密码,最大长度256。
+
+<pubk>
+建立连接的适配参数
+
+###### A.2.5 AT+MQTTWILLPARAM–遗嘱设置
+
+设置遗嘱信息。
+
+在MQTT 已经连接的情况下设置参数无效,掉电后参数需重新设置。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+设置
+AT+MQTTWILLPARAM=<retained>,<q
+os>,<topicname>,<message><CR>
+
+<CR><LF>+GNSSSTATE: <status><CR><LF>
+Or
+<CR><LF>OK<CR><LF>
+
+查询
+AT+MQTTWILLPARAM?<CR>
+
+<CR><LF>+MQTTWILLPARAM:<retained>,<qo
+s>,<"topicname">,<"message">
+<CR><LF>OK<CR><LF>
+
+测试
+AT+MQTTWILLPARAM=?<CR>
+<CR><LF>OK<CR><LF>
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<retained>
+保留标志,数字类型。
+
+<qos>
+服务质量,目前仅支持Qos=0 和1。
+
+<topicname>
+遗嘱主题,最大长度128。
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有
+37
+
+<message>
+遗嘱消息,最大长度1024。
+
+###### A.2.6 AT+MQTTCONN–连接命令
+
+连接MQTT 服务器。
+
+在连接的过程中等待连接返回值,没有返回值的情况下,不能再次进行连接操作。
+
+当连接成功后,没有主动断开情况下如果模块主动上报+MQTTDISCONNED: Link Closed,需手动
+
+进行连接。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行
+AT+MQTTCONN=<"host">,<clean>,<ke
+ep_alive><CR>
+
+<CR><LF>OK<CR><LF>
+Or
+<CR><LF>ERROR<CR><LF>
+
+查询
+AT+MQTTCONN?<CR>
+
+<CR><LF>+MQTTCONN:<"ip:port">,<clean>,
+<keep_alive>
+<CR><LF>OK<CR><LF>
+
+测试
+AT+MQTTCONN=?<CR>
+<CR><LF>OK<CR><LF>
+
+###### 响应时间
+
+最大响应时间60s。
+
+###### 参数
+
+<"host">
+服务器地址(url:port)。
+
+<clean>
+是否清除session,数字类型
+
+0-不清除(默认)
+
+1-清除
+
+<keep_alive>
+keepAlive 时间设置,取值范围[20,180],单位s。
+
+###### A.2.7 AT+MQTTSUB–订阅命令
+
+订阅主题。
+
+当SUB 失败后,查询MQTT 和网络状态再进行操作。当网络不佳的情况下,返回值较慢。
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有
+38
+
+查询指令只有连接情况下才能查询到,只能查询到当前最后一次订阅的qos 和topic。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行
+AT+MQTTSUB=<"topicname">,<qos><CR>
+
+<CR><LF>OK<CR><LF>
+Or
+<CR><LF>ERROR<CR><LF>
+
+查询
+AT+MQTTSUB?<CR>
+
+<CR><LF>+MQTTSUB:<"topicname">,
+<qos>
+<CR><LF>OK<CR><LF>
+
+测试
+AT+MQTTSUB=?<CR>
+<CR><LF>OK<CR><LF>
+
+###### 响应时间
+
+最大响应时间30s。
+
+###### 参数
+
+<"topicname"> 订阅的主题,最大长度128。
+
+<qos>
+服务质量,目前支持Qos=0,1 和2
+
+###### A.2.8 AT+MQTTPUB–发布主题
+
+主题发布。
+
+当网络不佳的情况下,返回值较慢。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行
+AT+MQTTPUB=<retained>,<qos>,<"topic
+name">,<"message"><CR>
+
+<CR><LF>OK<CR><LF>
+Or
+<CR><LF>ERROR<CR><LF>
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有
+39
+
+###### 响应时间
+
+最大响应时间30s。
+
+###### 参数
+
+<retained>
+保留标志,数字类型,0 和1。
+
+<qos>
+服务质量,目前仅支持Qos=0,1 和2。
+
+<"topicname">
+发布的主题,最大长度128。
+
+<"message">
+发布的消息,最大长度1024。
+
+###### A.2.9 AT+MQTTDISCONN–关闭MQTT 连接
+
+关闭MQTT 连接。
+
+终端主动断开和服务端的连接,然后释放MQTT 资源,参数设置也会释放,如果需要pub 消息,
+
+需要重新发起参数设置再连接设备。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行
+AT+MQTTDISCONN<CR>
+<CR><LF>OK<CR><LF>
+
+###### 响应时间
+
+最大响应时间30s。
+
+###### 参数
+
+无
+
+##### A.3 TCPS 相关指令
+
+###### A.3.1 AT+CERTADD–添加SSL 证书
+
+向模块写入证书
+
+写入过程可以通过“+++”中断。
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有
+40
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行
+AT+CERTADD=<file_name>,<length
+><CR>
+
+<CR><LF>CONNECT<CR><LF>
+<CR><LF>+CERTADD: <length>,OK<CR><LF>
+Or
+<CR><LF>+CERTADD: ERROR<CR><LF>
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<file_name>
+写入模块的证书名称
+
+<length>
+写入的长度
+
+###### A.3.2 AT+CERTCHECK–SSL 证书确认
+
+检查证书。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行
+AT+CERTCHECK=<file_name><CR>
+
+<CR><LF>+CERTCHECK: <file_name>,OK
+Or
+<CR><LF>+CERTCHECK: ERROR
+
+查询
+AT+CERTCHECK?<CR>
+
+<CR><LF><file_name>
+[<CR><LF><file_name>]
+<CR><LF>OK<CR><LF>
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<file_name>
+要确认的证书名称。
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有
+41
+
+###### A.3.3 AT+SSLTCPCFG–SSL TCP 配置参数
+
+配置SSL 加密选项。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+设置
+AT+SSLTCPCFG=<type>,<typ
+e_name><CR>
+
+<CR><LF>OK<CR><LF>
+Or
+<CR><LF>ERROR<CR><LF>
+
+查询
+AT+SSLTCPCFG?<CR>
+
+<CR><LF>+SSLTCPCFG: <sslversion
+type_name>,<authmode type_name>,<ciphersuite
+type_name>,<cacert type_name>,<clientcert
+type_name>,<clientkey type_name>
+<CR><LF>OK<CR><LF>
+
+测试
+AT+SSLTCPCFG=?<CR>
+<CR><LF>+SSLTCPCFG: <type>,<type_name>
+<CR><LF>OK<CR><LF>
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<type>
+配置SSL 选项。
+
+sslversion: SSL 协议版本
+
+authmode: 安全认证模式
+
+ciphersuite: 加密控件
+
+cacert: 根证书
+
+clientcert: 客户端证书
+
+clientkey: 客户端密匙
+
+<type_name>
+<type>和<type_name>参数的取值,对应关系如下:
+
+⚫
+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=<n>,<ip>,<por
+t>, <mode><CR>
+
+<CR><LF>OK<CR><LF>
+<CR><LF>+SSLTCPSETUP: <n>,<status>
+Or
+<CR><LF>CONNECT
+Or
+<CR><LF>+SSLTCPSETUP: ERROR
+Or
+<CR><LF>+SSLTCPSETUP: GPRS
+DISCONNECTION
+
+查询
+AT+SSLTCPSETUP?
+
+<CR><LF>+SSLTCPSETUP: <socket_id>,<ip>,
+<port>,<mode>
+[<CR><LF>+SSLTCPSETUP: <socket_id>,<ip>,
+<port>,<mode>]…
+<CR><LF>OK<CR><LF>
+
+测试
+AT+SSLTCPSETUP=?
+<CR><LF>+SSLTCPSETUP: <socket_id>,<ip>,
+<port>,<mode><CR><LF>
+
+###### 响应时间
+
+最大响应时间60s。
+
+###### 参数
+
+<n>
+链路号,范围0-5,用来区分与服务器的连接
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有
+43
+
+<IP>
+服务器的IP 地址/域名
+
+<Port>
+服务器的端口
+
+<Mode>
+传输模式
+
+0:非透传
+
+1:透传
+
+<status>
+OK
+ERROR1
+AUTHFAIL
+FAIL
+
+###### A.3.5 AT+SSLTCPSEND–SSL TCP 数据发送
+
+SSL TCP 数据发送。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行
+AT+SSLTCPSEND=<n>,<data_length>
+
+<CR><LF>>
+<CR><LF>+SSLTCPSEND:
+<socket_id>,<result>
+Or
+<CR><LF>+SSLTCPSEND: Data length
+error<CR><LF>
+
+测试
+AT+SSLTCPSEND=?
+
+<CR><LF>+SSLTCPSEND: (value range
+of<n>),(value range
+of<data_length>)<CR><LF>
+
+###### 响应时间
+
+最大响应时间30s。
+
+###### 参数
+
+<n>
+链路号 0~5
+
+<data_length> 要发送的数据长度,取值范围 1-4096
+
+<result>
+OK
+FAIL
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有
+44
+
+###### A.3.6 AT+SSLTCPCLOSE–SSL TCP 关闭连接指令
+
+关闭SSL TCP 连接。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行
+AT+SSLTCPCLOSE=<socket_id>
+
+<CR><LF>+SSLTCPCLOSE:
+<socket_id>,<result>
+Or
+<CR><LF>+SSLTCPCLOSE: ERROR
+
+主动上报
++SSLTCPCLOSE: <socket_id>,Link Closed
+
+###### 响应时间
+
+最大响应时间30s。
+
+###### 参数
+
+<n>
+链路号 0~5
+
+<result>
+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: <signal>,<ber>
+ 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的参数保持一致。
--- /dev/null
+#!/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<!-- Page {page_num + 1} -->\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"<!-- Page {page_num + 2} -->\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<!-- Page {page_num + 1} -->\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()
+
--- /dev/null
+#!/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()
+