跳到主要内容

固件请求UOTA下载地址之HTTP协议(备份)

核心概念:文档中心 | 固件请求UOTA下载地址之HTTP协议(备份) | 概览

核心概念:文档中心 | 固件请求UOTA下载地址之HTTP协议(备份) | 一、域名解析

一、域名解析


核心概念:文档中心 | 固件请求UOTA下载地址之HTTP协议(备份) | 测试域名

测试域名

域名解析测试.png


核心概念:文档中心 | 固件请求UOTA下载地址之HTTP协议(备份) | 解析说明

解析说明

域名解析 weight.akrdapp.cn是权重模式, polling.akrdapp.cn是轮询模式

域名解析分为轮询和权重两种,客户端需先验证可以通过域名可以拿到几个IP,如果拿到一个就直接使用。 如果拿到多个IP,则需要判断是权重还是轮询后进行选择,其实轮询也是权重全部为1的一种特殊情况。
千万不要使用域名直接访问服务器接口,防止被追踪到域名和IP的关联关系。


核心概念:文档中心 | 固件请求UOTA下载地址之HTTP协议(备份) | 二 请求接口

二 请求接口


核心概念:文档中心 | 固件请求UOTA下载地址之HTTP协议(备份) | 2.1 整体情况说明

2.1 整体情况说明

1.对于设备端请求服务器的请求,除了获取时间戳的请求,其他请求都带签名,如果不带签名,服务端统一返回404错误码,包含http状态码也是404。
2.所有http post请求中content-type是字符串类型,content-type=text/plain
3.所有请求的返回格式为text,即content-type=text/plain
4.所有请求只能用IP请求,禁止使用域名请求,后期会做拦截,发现是域名的请求,会返回404错误码。

每个固件里面都包含2个值,这2个值需要混淆避免被反编译获得,如果发布固件时修改了这2个值,需要同步在服务器端配置
1、sdkVersion:版本号。内置在固件中,建议每次发固件都不一样。
【测试环境暂时使用:allwinner-202510-01/amlogic-202510-01、rockchip-202510-01】
2、secretKey:对称加解密秘钥。内置在固件中,建议每次发固件都不一样。密钥长度必须是16、24或32字节(分别对应 AES-128、AES-192 和 AES-256 三种加密标准)
【测试环境暂时使用:12345678123456781234567812345678】

测试环境可通过添加请求头【ignore-verification-time=true】忽略时间戳校验,通过添加【encrypt=false】返回明文,便于测试

服务器地址信息

测试环境:http://192.168.1.87:8419
线上环境:域名待定。


核心概念:文档中心 | 固件请求UOTA下载地址之HTTP协议(备份) | 2.2 盒子请求最新时间戳

2.2 盒子请求最新时间戳

请求地址:/app-api/get8852/yygtnow
请求方式:GET
响应格式content-type:text/plain
curl请求示例 :curl -X GET http://192.168.1.87:8419/app-api/get8852/yygtnow
响应结果示例(13位时间戳):1760951487809


核心概念:文档中心 | 固件请求UOTA下载地址之HTTP协议(备份) | 2.3 盒子封装固件请求参数并加密

2.3 盒子封装固件请求参数并加密

未加密前的请求体

{
"mac": "A8:20:06:B7:C8:B9",
"cpu": "02c0008145f0462078a3840038350b53",
"time": 1760951487809,
"platform": "allwinner"
}

参数含义

mac: 设备mac
cpu: 设备cpu,获取不到则传null
sdkVersion:2.1提到的sdkVersion
time: 2.2服务端返回的时间戳
platform:固件平台,有amlogic、rockchip和allwinner

加密方法如下

  /**
* 加密方法
* @param plainText 明文
* @param secretKey 固件中的加解密秘钥。
* @return 密文
* @throws Exception
*/
public static String encrypt(String plainText, String secretKey) throws Exception {
// 将密钥转换为字节数组并验证长度
byte[] keyBytes = validateKey(secretKey);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");

// 生成随机初始化向量(IV)
byte[] ivBytes = new byte[16];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(ivBytes);
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);


// 初始化加密器
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);

// 执行加密
byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));

// 组合IV和密文(IV前置方案)
byte[] combined = new byte[ivBytes.length + encryptedBytes.length];
System.arraycopy(ivBytes, 0, combined, 0, ivBytes.length);
System.arraycopy(encryptedBytes, 0, combined, ivBytes.length, encryptedBytes.length);

// 返回Base64编码的字符串
return Base64.getEncoder().encodeToString(combined);
}

核心概念:文档中心 | 固件请求UOTA下载地址之HTTP协议(备份) | 2.4 设备端请求服务端获取固件

2.4 设备端请求服务端获取固件

请求地址:/app-api/get8852/uncheck
请求方式:POST
请求格式content-type:text/plain
请求头(2.1提到的sdkVersion):sdk-version= allwinner-202510-01
请求头(是否忽略时间戳校验,仅测试环境有效):ignore-verification-time=true

请求body:2.5中的加密结果
响应格式content-type:text/plain
响应结果示例(加密后的字符串):DC15hFmkezrskGUxijhd47DMxzS8IXKtuXAwJ6YC0C6mrBBxHJi0Cw6VVJxkIDxSeHU/nqd6yftwUFfEdy505HyNMXrZfKio7c/GQzQGJlM9/i7B+n1vOgc8wMfUsttdOI6U2d4v06VKKuPQd2nLIC7Eq3MQCSf+cCJdT8kKPN2Jd0A86v1PiwCI4cCmYhV5QHdqmvAw2+Wjqn51qNytOFQA4CBnD3qprDDEbm/qNHcvW8TBSIfg5iK4PfZ9MFog32QyNjN9vVGjugYZpPkf/nncnYoM+ZAIBr3O9Qc8PV9C3XCMjkXRlBGDNUkQqp3lSpdwoQY2wHorYpY0krS04K0TmcbQM8YS7Xz5HiEdcZLAjBNZcvxhsPG1N2rSkQcShFHsYBfYwWVCSWVa3hwlIARwgul7PK1rM/IuU5eCpbPxo635mCyUZWHrk3wARt6MQUP2hyUy9ddfAnCVkU0P5Q==


核心概念:文档中心 | 固件请求UOTA下载地址之HTTP协议(备份) | 2.5 设备端解密固件信息

2.5 设备端解密固件信息

 /**
* 解密方法
* @param encryptedText 密文
* @param secretKey 密钥。
* @return 明文
* @throws Exception
*/
// 解密方法
public static String decrypt(String encryptedText, String secretKey) throws Exception {
// 将密钥转换为字节数组并验证长度
byte[] keyBytes = validateKey(secretKey);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");

// Base64解码
byte[] combined = Base64.getDecoder().decode(encryptedText);

// 分离IV和密文(前16字节是IV)
byte[] ivBytes = new byte[16];
byte[] encryptedBytes = new byte[combined.length - ivBytes.length];
System.arraycopy(combined, 0, ivBytes, 0, ivBytes.length);
System.arraycopy(combined, ivBytes.length, encryptedBytes, 0, encryptedBytes.length);
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);

// 初始化解密器
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);

// 执行解密并返回UTF-8字符串
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
return new String(decryptedBytes, StandardCharsets.UTF_8);
}

// 密钥验证(支持128/192/256位)
private static byte[] validateKey(String key) {
byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
if (keyBytes.length != 16 && keyBytes.length != 24 && keyBytes.length != 32) {
throw new IllegalArgumentException("密钥长度必须是16、24或32字节(对应128/192/256位)");
}
return keyBytes;
}

解密结果示例(已格式化,原数据时无换行符和空格符)

{
"url": "http://192.168.1.87:8742/app-api/infra/file/download/redirect/1757560816080/app-netservice-3.4.9-allwinner-20250911-debug.apk",
"size": 4747480,
"packageName": "com.android.netservice",
"versionCode": 49,
"md5": "c7d614b68b94ef0ca470fb30fc3a79ec"
}

核心概念:文档中心 | 固件请求UOTA下载地址之HTTP协议(备份) | 2.6 从解析结果中下载apk进行安装

2.6 从解析结果中下载apk进行安装

设备判断安装同版本的apk,没有则下载安装,文件支持 http中的RFC 7233规范,可以在请求头中加入Range参数来实现断点续下载和分片下载。
特别注意,如上的文件大小为4747480时。Range的结束为应该为4747479,即一次性下载完整文件时【Range: bytes=0-(size-1=4747479)】,注意如果下载的文件字节超过了size证明文件损坏,请清理缓存重新下载

Range示例:
Range: bytes=0-499 -> 请求前 500 个字节(0 到 499)。
Range: bytes=500-999 -> 请求第 500 到第 999 个字节。
Range: bytes=500- -> 请求从第 500 个字节到文件末尾的所有数据。
Range: bytes=-500 -> 请求文件的最后 500 个字节。
Range: bytes=0-0 -> 请求第一个字节(常用于探测文件大小或类型)。

服务器响应头
当服务器支持范围请求时,对于成功的范围请求,它会返回:
状态码:206 Partial Content
这表示服务器已经成功处理了部分 GET 请求。
响应头:Content-Range
作用:告诉客户端返回的数据在完整文件中的具体范围,以及文件的总大小。
语法:Content-Range: bytes <range-start>-<range-end>/<total-size>


核心概念:文档中心 | 固件请求UOTA下载地址之HTTP协议(备份) | 三 示例

三 示例

下面的实例中加解密涉及到的secretKey都是用123456781234567812345678,便于方法转为非java语言时进行参考


核心概念:文档中心 | 固件请求UOTA下载地址之HTTP协议(备份) | 3.1 数据加密示例

3.1 数据加密示例

明文(未格式化无换行无空格的json)

{"mac":"A8:20:06:B7:C8:B9","cpu":"02c0008145f0462078a3840038350b53","time":1760951487809,"platform":"allwinner"}

密文

z7YgclIhr4Zxe+l5ev3g8/H5Yb4QTNPy3IE8N624tWeeB5x8IUMtVV+tuOeuUrkzdxOFovXZdwH5yyaF/yu9uf0E1rpgVYwLH9x0fXqER0EmLl2TEopTP1rKUlHkkCC50pU/LN7C4h5/C1HBA1xRrphIF3++rhOnu7SxsM6Sns91Eht/seT+RvW5hBRsPZDQ

核心概念:文档中心 | 固件请求UOTA下载地址之HTTP协议(备份) | 3.2 数据解密示例

3.2 数据解密示例

密文

ccWGSPEPz4Cduc6Ey/Ixq8BbWCWPFqUfiydkf7SnIetKVi0lgZC/iQzGq7oMfJmWaD73akk3rsXJlUOEcQzA+s11IrdEOdbkqFnmaLH/8uSr6e9EuXZAzFwffa2WTgB4YTr/h8ZuX/zdiubMAkBiJazkjS0gTSMaw7EOaORj3ysBan500UFGxRe9XSxcP455/aNwBcdAVZikp7XqEB/L/cGZFUFsEZLj5KtiQRIskl/R3kkPYIpUae2QxvxmSgIe8ZQ/ADiD3VGNPSLQr7oPVRgyEUKqj7Bcq2p8TgYSpnY+BCTtNED9XVqclE5G3uO/v1KVWPFH6GcdtYqKLGP24BFlVYmYyjkcD4r3k/+9CaM=

明文(未格式化无换行无空格的json)

{"url":"http://192.168.1.87:8742/app-api/infra/file/download/redirect/1757560816080/app-netservice-3.4.9-allwinner-20250911-debug.apk","size":4747480,"packageName":"com.android.netservice","versionCode":49,"md5":"c7d614b68b94ef0ca470fb30fc3a79ec"}

核心概念:文档中心 | 固件请求UOTA下载地址之HTTP协议(备份) | 3.3 apifox请求时间戳

3.3 apifox请求时间戳

请求curl脚本

curl --location --request GET 'http://192.168.1.87:8419/app-api/get8852/yygtnow' \
--header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \
--header 'Accept: */*' \
--header 'Host: 192.168.1.87:8419' \
--header 'Connection: keep-alive'

返回值

1761018914222

核心概念:文档中心 | 固件请求UOTA下载地址之HTTP协议(备份) | 3.4 apifox请求UOTA下载地址

3.4 apifox请求UOTA下载地址

请求curl脚本

curl --location --request POST 'http://192.168.1.87:8419/app-api/get8852/uncheck' \
--header 'sdk-version: allwinner-202510-01' \
--header 'ignore-verification-time: true' \
--header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \
--header 'Content-Type: text/plain' \
--header 'Accept: */*' \
--header 'Host: 192.168.1.87:8419' \
--header 'Connection: keep-alive' \
--data-raw 'z7YgclIhr4Zxe+l5ev3g8/H5Yb4QTNPy3IE8N624tWeeB5x8IUMtVV+tuOeuUrkzdxOFovXZdwH5yyaF/yu9uf0E1rpgVYwLH9x0fXqER0EmLl2TEopTP1rKUlHkkCC50pU/LN7C4h5/C1HBA1xRrphIF3++rhOnu7SxsM6Sns91Eht/seT+RvW5hBRsPZDQ'

返回值

GgUA9M6XpQRuMdpLh5xe6OCEwoe1BPf09OFOcgW+nWZKNMf7xri8sEDZy2Ek3zeexnZQbaXNj64NG7rBIeXPxCiAkbYaDZlsAZhzW1stQmS2k7CAjNsHnj79CzZM9xDGFDS9zULTOjJ+cXKy2EtJtt5t99wgt3QReKxWlc6ku4Revn7+syUmbQBPn7s5RF2dZAh8wUdiZ/Cg4gpoV2xW4te7z9tmTMbscAhdMLW7c7ZKdJM4deekZlYwp/mspLq3IF/yqOXnLY42KFAXeZVH0vVLbxKx64GeF1dsBq88HUJFccWLHzj9k6hFQbiG5/yO47VY69WEHW7l/u2n4Gk9a46rPpLh98c+v03OQsXyx0s=
AI 问答