feat: allow talkingq OTA domains

This commit is contained in:
chenjintao
2026-09-28 11:54:51 +08:00
parent f607d8b9f7
commit 20e5872a75
7 changed files with 265 additions and 20 deletions
+6
View File
@@ -236,6 +236,12 @@ ASR1605/CRANELRH,但量产包必须由 `EG800AKCN_11LG` 配置生成。
设备校验顺序:产品、layout、版本序号、URL host、大小、SHA-256,最后才允许设置升级标志。
当前最小版本保留原服务器
`http://101.35.224.118:8080/assets/firmware/`,并允许 `talkingq.com` 根域及任意合法
子域名通过 HTTP/HTTPS 提供 `.bin` 文件,例如 `ota.talkingq.com` 和
`cdn.cn.talkingq.com`。校验基于解析出的完整 host 边界,`eviltalkingq.com`、
`talkingq.com.evil.example`、userinfo 绕过、非法端口和路径穿越均会被拒绝。
设备状态建议至少包含:
- `ACCEPTED`:命令和布局校验通过;
@@ -87,6 +87,7 @@ SRC_FILES+= \
battery.c \
gnss.c \
ota_fullfota2.c \
ota_url_policy.c \
#-------------------------------------------------------------------------------
@@ -0,0 +1,6 @@
#ifndef BANBAN_OTA_URL_POLICY_H
#define BANBAN_OTA_URL_POLICY_H
int banban_ota_url_is_allowed(const char *url);
#endif
@@ -22,6 +22,7 @@
#include "build_macro.h"
#include "ql_log.h"
#include "ota_fullfota2.h"
#include "ota_url_policy.h"
@@ -46,7 +47,6 @@
#define OTA_STATE_FORMAT_VERSION 1U
#define OTA_PROMPT_PLAY_TIMEOUT_MS 10000
#define OTA_PROMPT_POLL_MS 50
#define OTA_ALLOWED_URL_PREFIX "http://101.35.224.118:8080/assets/firmware/"
static ql_timer_t mqtt_event_task_timer = NULL;
@@ -297,24 +297,6 @@ static int ota_compare_version(const char *left, const char *right, int *result)
return 0;
}
static int ota_url_is_allowed(const char *url)
{
size_t prefix_len = strlen(OTA_ALLOWED_URL_PREFIX);
if (url == NULL || url[0] == '\0') {
return 0;
}
if (strncmp(url, OTA_ALLOWED_URL_PREFIX, prefix_len) != 0) {
return 0;
}
if (url[prefix_len] == '\0' || strstr(url + prefix_len, "..") != NULL) {
return 0;
}
return strstr(url + prefix_len, ".bin") != NULL;
}
/* ==================== 解析函数 ==================== */
/**
@@ -1318,7 +1300,7 @@ static void handle_ota_upgrade(command_t *cmd)
return;
}
if (!ota_url_is_allowed(cmd->params.ota.url)) {
if (!banban_ota_url_is_allowed(cmd->params.ota.url)) {
RLOGE("OTA URL is not allowed: %s", cmd->params.ota.url);
return;
}
@@ -0,0 +1,189 @@
#include <stddef.h>
#include <string.h>
#include "ota_url_policy.h"
#define BANBAN_OTA_LEGACY_URL_PREFIX \
"http://101.35.224.118:8080/assets/firmware/"
#define BANBAN_OTA_BASE_DOMAIN "talkingq.com"
static int banban_ascii_equal_nocase(const char *left,
const char *right,
size_t length)
{
size_t i;
for (i = 0; i < length; ++i) {
unsigned char a = (unsigned char)left[i];
unsigned char b = (unsigned char)right[i];
if (a >= 'A' && a <= 'Z') {
a = (unsigned char)(a + ('a' - 'A'));
}
if (b >= 'A' && b <= 'Z') {
b = (unsigned char)(b + ('a' - 'A'));
}
if (a != b) {
return 0;
}
}
return 1;
}
static int banban_hostname_is_valid(const char *host, size_t host_length)
{
size_t i;
int label_has_character = 0;
int previous_was_hyphen = 0;
if (host_length == 0 || host_length > 253) {
return 0;
}
for (i = 0; i < host_length; ++i) {
unsigned char character = (unsigned char)host[i];
if ((character >= 'A' && character <= 'Z') ||
(character >= 'a' && character <= 'z') ||
(character >= '0' && character <= '9')) {
label_has_character = 1;
previous_was_hyphen = 0;
continue;
}
if (character == '-') {
if (!label_has_character) {
return 0;
}
previous_was_hyphen = 1;
continue;
}
if (character == '.') {
if (!label_has_character || previous_was_hyphen) {
return 0;
}
label_has_character = 0;
previous_was_hyphen = 0;
continue;
}
return 0;
}
return label_has_character && !previous_was_hyphen;
}
static int banban_host_is_talkingq(const char *host, size_t host_length)
{
const size_t base_length = sizeof(BANBAN_OTA_BASE_DOMAIN) - 1;
if (!banban_hostname_is_valid(host, host_length)) {
return 0;
}
if (host_length == base_length) {
return banban_ascii_equal_nocase(host,
BANBAN_OTA_BASE_DOMAIN,
base_length);
}
if (host_length <= base_length + 1 ||
host[host_length - base_length - 1] != '.') {
return 0;
}
return banban_ascii_equal_nocase(host + host_length - base_length,
BANBAN_OTA_BASE_DOMAIN,
base_length);
}
static int banban_port_is_valid(const char *port, size_t port_length)
{
size_t i;
unsigned long value = 0;
if (port_length == 0 || port_length > 5) {
return 0;
}
for (i = 0; i < port_length; ++i) {
if (port[i] < '0' || port[i] > '9') {
return 0;
}
value = value * 10 + (unsigned long)(port[i] - '0');
}
return value > 0 && value <= 65535;
}
static int banban_path_is_fbf(const char *path)
{
const char *path_end;
const char *query;
if (path == NULL || path[0] != '/' || path[1] == '\0' ||
strstr(path, "..") != NULL || strchr(path, '\\') != NULL ||
strchr(path, '#') != NULL) {
return 0;
}
query = strchr(path, '?');
path_end = query != NULL ? query : path + strlen(path);
if ((size_t)(path_end - path) < 4) {
return 0;
}
return banban_ascii_equal_nocase(path_end - 4, ".bin", 4);
}
int banban_ota_url_is_allowed(const char *url)
{
const char *authority;
const char *path;
const char *host_end;
const char *colon;
const size_t legacy_prefix_length =
sizeof(BANBAN_OTA_LEGACY_URL_PREFIX) - 1;
if (url == NULL || url[0] == '\0') {
return 0;
}
if (strncmp(url, BANBAN_OTA_LEGACY_URL_PREFIX,
legacy_prefix_length) == 0) {
return banban_path_is_fbf(url + legacy_prefix_length - 1);
}
if (strncmp(url, "http://", 7) == 0) {
authority = url + 7;
} else if (strncmp(url, "https://", 8) == 0) {
authority = url + 8;
} else {
return 0;
}
path = strchr(authority, '/');
if (path == NULL || path == authority) {
return 0;
}
if (memchr(authority, '@', (size_t)(path - authority)) != NULL) {
return 0;
}
colon = memchr(authority, ':', (size_t)(path - authority));
host_end = colon != NULL ? colon : path;
if (!banban_host_is_talkingq(authority,
(size_t)(host_end - authority))) {
return 0;
}
if (colon != NULL &&
!banban_port_is_valid(colon + 1, (size_t)(path - colon - 1))) {
return 0;
}
return banban_path_is_fbf(path);
}
+53
View File
@@ -0,0 +1,53 @@
#include <stdio.h>
#include "ota_url_policy.h"
typedef struct {
const char *url;
int expected;
} url_case_t;
int main(void)
{
static const url_case_t cases[] = {
{"http://101.35.224.118:8080/assets/firmware/app.bin", 1},
{"http://101.35.224.118:8080/assets/firmware/release/app.bin?token=x", 1},
{"http://talkingq.com/app.bin", 1},
{"https://talkingq.com/releases/app.bin", 1},
{"https://ota.talkingq.com/releases/app.bin", 1},
{"https://cdn.cn.talkingq.com:8443/a/B.BIN?token=x", 1},
{"https://OTA.TALKINGQ.COM/app.bin", 1},
{"https://eviltalkingq.com/app.bin", 0},
{"https://talkingq.com.evil.example/app.bin", 0},
{"https://talkingq.com@evil.example/app.bin", 0},
{"https://evil.example@talkingq.com/app.bin", 0},
{"https://a..talkingq.com/app.bin", 0},
{"https://-ota.talkingq.com/app.bin", 0},
{"https://ota-.talkingq.com/app.bin", 0},
{"https://ota.talkingq.com:0/app.bin", 0},
{"https://ota.talkingq.com:65536/app.bin", 0},
{"https://ota.talkingq.com:abc/app.bin", 0},
{"ftp://ota.talkingq.com/app.bin", 0},
{"https://ota.talkingq.com/", 0},
{"https://ota.talkingq.com/app.zip", 0},
{"https://ota.talkingq.com/app.bin/extra", 0},
{"https://ota.talkingq.com/a/../app.bin", 0},
{"https://ota.talkingq.com/app.bin#fragment", 0},
{"http://101.35.224.118:8080/other/app.bin", 0},
{"http://192.168.1.1/app.bin", 0},
};
size_t i;
for (i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) {
int actual = banban_ota_url_is_allowed(cases[i].url);
if (actual != cases[i].expected) {
fprintf(stderr, "case %lu failed: expected=%d actual=%d url=%s\n",
(unsigned long)i, cases[i].expected, actual, cases[i].url);
return 1;
}
}
printf("OTA URL policy tests passed: %lu cases\n",
(unsigned long)(sizeof(cases) / sizeof(cases[0])));
return 0;
}
+8
View File
@@ -23,6 +23,7 @@ BUILD_MACRO = ROOT / "ql-application/threadx/interface/custom_app/lib/hal/build_
APP_MAKEFILE = ROOT / "ql-application/threadx/interface/custom_app/Makefile"
MQTT_SOURCE = ROOT / "ql-application/threadx/interface/custom_app/mqtt.c"
FULLOTA2_SOURCE = ROOT / "ql-application/threadx/interface/custom_app/ota_fullfota2.c"
URL_POLICY_SOURCE = ROOT / "ql-application/threadx/interface/custom_app/ota_url_policy.c"
LEGACY_HTTP_SOURCE = ROOT / "ql-application/threadx/interface/custom_app/http_aofei.c"
FLASH_SIZE = 0x400000
@@ -146,11 +147,18 @@ def verify_fota_contract() -> None:
makefile = APP_MAKEFILE.read_text(encoding="utf-8", errors="replace")
if "ota_fullfota2.c" not in makefile:
fail("minimal FULLOTA2 implementation is not part of the application build")
if "ota_url_policy.c" not in makefile:
fail("OTA URL policy is not part of the application build")
mqtt = MQTT_SOURCE.read_text(encoding="utf-8", errors="replace")
if "banban_fullfota2_start" not in mqtt:
fail("MQTT OTA command does not start the local-staging FULLOTA2 path")
if "ql_fullfota_app_start(cmd->params.ota.url)" in mqtt:
fail("MQTT OTA command still invokes the legacy FULLOTA1 path")
if "banban_ota_url_is_allowed" not in mqtt:
fail("MQTT OTA command does not enforce the shared URL policy")
url_policy = URL_POLICY_SOURCE.read_text(encoding="utf-8", errors="replace")
if 'BANBAN_OTA_BASE_DOMAIN "talkingq.com"' not in url_policy:
fail("OTA URL policy no longer allows the talkingq.com domain family")
legacy_http = LEGACY_HTTP_SOURCE.read_text(encoding="utf-8", errors="replace")
if "return ql_fullfota_app_start(url)" in legacy_http:
fail("legacy HTTP polling path can still invoke FULLOTA1")