固件请求UOTA下载地址之HTTP协议(备份)
一、域名解析
测试域名

解析说明
域名解析 weight.akrdapp.cn是权重模式, polling.akrdapp.cn是轮询模式
域名解析分为轮询和权重两种,客户端需先验证可以通过域名可以拿到几个IP,如果拿到一个就直接使用。 如果拿到多个IP,则需要判断是权重还是轮询后进行选择,其实轮询也是权重全部为1的一种特殊情况。
千万不要使用域名直接访问服务器接口,防止被追踪到域名和IP的关联关系。
二 请求接口
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
线上环境:域名待定。
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
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);
}
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==
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"
}
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>
三 示例
下面的实例中加解密涉及 到的secretKey都是用123456781234567812345678,便于方法转为非java语言时进行参考
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
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"}
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
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=