diff --git a/artifacts/wss/.gitignore b/artifacts/wss/.gitignore new file mode 100644 index 0000000..7c9d611 --- /dev/null +++ b/artifacts/wss/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!README.md diff --git a/artifacts/wss/README.md b/artifacts/wss/README.md new file mode 100644 index 0000000..dedfb33 --- /dev/null +++ b/artifacts/wss/README.md @@ -0,0 +1,9 @@ +# 旧板 WSS 联调包 + +`EG800AKCN_11LG-legacy-wss.zip` 是供无 GNSS 旧板联调的完整多分区烧录包,默认连接 `wss://banban.api.talkingq.com:443/ws`。默认音量仍为 100(已有保存值按原逻辑读取)。 + +APP 编译和主机测试已通过;具体校验与 SHA-256 见本目录生成的 `verification.json`。没有进行上板验证。 + +2026-09-16 后台实测:443 证书有效,但 `/ws` Upgrade 返回 404;8080 明文 WS 返回 101,WSS 握手失败。这个包当前不能被标记为已完成 WSS 业务联调。后台修复 TLS 入口转发后才能继续验收。 + +原旧板包保留在 `../legacy-no-gnss/`。构建日志、探测 JSON、导出的公共证书和包体在此目录本地保存并被 Git 忽略。源码和详细步骤见 `../../docs/talkingq_wss.md`。 diff --git a/docs/talkingq_wss.md b/docs/talkingq_wss.md new file mode 100644 index 0000000..67dc9df --- /dev/null +++ b/docs/talkingq_wss.md @@ -0,0 +1,79 @@ +# 旧板 WSS 适配与联调 + +本次修改位于 `legacy-board-wss`(基于 `legacy-board`),默认地址为 `wss://banban.api.talkingq.com:443/ws`。沿用无 GNSS、默认音量 100 的旧板配置。本次只修改 APP;复用此前支持旧板的内核和其中的 mbedTLS API。 + +## 后台实测(2026-09-16) + +| 地址 | 结果 | +| --- | --- | +| `wss://banban.api.talkingq.com:443/ws` | TLS 1.2、证书链、域名校验通过;WebSocket Upgrade 返回 HTTP 404 | +| `wss://banban.api.talkingq.com:8080/ws` | TLS 握手失败,端口未提供可用的 TLS 服务 | +| `ws://banban.api.talkingq.com:8080/ws` | 明文 Upgrade 返回 HTTP 101,Accept 值正确 | + +因此 8080 当前是可用的明文 WebSocket 服务,不能通过把 URL 的 `ws` 改成 `wss` 就获得加密。后台需要在 HTTPS 入口配置 `/ws` 转发,或提供另一个已经启用 TLS 的 WebSocket 入口。404 的准确原因仍需后台检查路由配置。测试没有发送设备身份或业务数据,也没有修改服务器。 + +若 nginx 与 8080 服务部署在同一主机,可在现有 `listen 443 ssl` 的域名配置中参考以下转发(由后台按真实部署调整): + +```nginx +location /ws { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 300s; +} +``` + +修复后重新运行 `powershell.exe -NoProfile -ExecutionPolicy Bypass -File tools\probe_talkingq_wss.ps1`。必须得到有效 HTTP 101 和匹配的 `Sec-WebSocket-Accept`;仅 HTTPS 打得开或证书正常不代表 WebSocket 已可用。 + +## 固件行为 + +- `inc/talkingq_wss.h` 集中定义默认地址。解析独立处理域名、端口、路径,默认端口 443、无路径时默认 `/ws`;拒绝明文 URL、非法端口、过长字段和 CR/LF 注入。现有存储容量为 64 字节;不支持 IPv6 字面量。 +- `hal_os_get_talkingq_url()` 在没有保存地址、或保存值与已知旧默认地址完全匹配时,返回新默认 WSS 地址。此迁移在读取时生效,不改写 Flash。自定义地址保留;自定义明文地址会明确失败,不自动降级。 +- 启用 librws 的 mbedTLS 实现,统一公共和私有头文件中的宏。TCP 继续使用现有蜂窝 PDP profile 1,避免 SDK 定制 `mbedtls_net_connect` 的 CID/端口 ABI 差异。 +- 设置 SNI 和预期服务器域名,使用 `MBEDTLS_SSL_VERIFY_REQUIRED` 和 TLS 1.2 下限。没有客户端证书、客户端私钥或双向 TLS。 +- 使用现有 NITZ/RTC 时间检查证书有效期;时间明显未初始化会拒绝连接并打印原因。不会通过关闭校验来绕过时间问题。上板需要确认运营商时间同步有效。 +- 修复 TLS 短写、WANT_READ/WANT_WRITE、断线处理与资源清理;TLS 握手最多等待 20 秒,HTTP Upgrade 最多 10 秒,应用连接等待 45 秒。DNS/TCP 调用和任务调度还会消耗时间,45 秒不是所有底层调用的硬性总期限。 +- WSS 每次产生随机 WebSocket nonce,校验 HTTP 101、Upgrade、Connection、对应 Accept;支持拆分到多个 TLS 记录的响应头,保留响应头后的 WebSocket 数据。 +- 连接失败唤醒等待方;超时停止待连接 socket,回调只处理对应 socket。工作任务栈从 4 KiB 增至 16 KiB,给 TLS 证书和密码运算留出空间;仍需上板观察实际栈和堆余量。 +- WebSocket 成功后,仍由原 `sendAuthentication()` 发送 `device_id`、`serial_number`。本次不改变 MQTT、HTTP 下载及其他业务连接。 + +## 根证书来源与验证 + +固件内置的是公共根 CA **ISRG Root X2**,不是短期服务器证书。 + +- 来源:对目标域名进行 Windows 系统信任和域名校验后导出的证书链根。 +- 根证书 DER SHA-256:`69729b8e15a86efc177a57afb7171dfc64add28c2fca8cf1507e34453ccb1470`。 +- 根证书有效期到 2040-09-17。 +- 实测链:`banban.api.talkingq.com → Let's Encrypt YE1 → Root YE(由 X2 交叉签名)→ ISRG Root X2`。 +- 当前服务器证书有效期:2026-08-28 至 2026-11-26。后台正常续期且仍能链到内置根时,不需要随服务器证书续期更新固件。 +- Python/OpenSSL 仅加载从固件源码提取的这一张根 CA,没有加载系统证书库或自动下载中间证书,TLS 1.2 验证成功;错误域名和空信任库均验证失败。 +- 固件进行证书链、有效期和域名验证;未增加 OCSP/CRL 吊销查询。后端更换到其他根 CA 时,需要评估并更新信任根。 + +可重跑:`ql-cross-tool\win32\owtoolchain\Python\python.exe tools\probe_wss_root.py`。该测试验证服务器链完整性和内置信任根,不能代替板端 mbedTLS 实测。 + +## 构建与验证 + +```bat +tools\test_wss.cmd +build.bat app +build.bat firmware +``` + +主机测试使用 MSVC 编译实际 URL 和握手解析源码;另从真实实现提取 TLS 写入和取消连接函数,注入桩测试短写、重试、超时、时钟回绕和资源释放。解析测试覆盖合法地址、非法端口、明文拒绝、缓冲区边界、旧地址识别、每个响应头分片边界、非 101、错误/重复 Accept、缺失 Upgrade/Connection 和响应大小上限。 + +导出后运行 `ql-cross-tool\win32\owtoolchain\Python\python.exe tools\verify_wss_package.py`,核对 ZIP CRC、APP/内核与当前构建的字节一致性、无 GNSS 入口以及 WSS 链接符号。SDK 有既有编译警告,不能仅凭外层批处理退出码认定成功。 + +测试包位于 `artifacts/wss/EG800AKCN_11LG-legacy-wss.zip`。这是完整多分区烧录包,不是 APP-only 包。原可回退的旧板包保留在 `artifacts/legacy-no-gnss/`。 + +## 上板验收 + +目前未烧录、未完成设备业务联调,且服务器 443 `/ws` 尚返回 404。确认后台转发修复后,再检查: + +1. 无 GNSS 旧板正常启动、蜂窝网络就绪、RTC 时间正确。 +2. 出现 `WSS TLS handshake and certificate/hostname verification passed`。 +3. 出现 `WebSocket upgrade verified: HTTP 101` 和 `ws connect okay`。 +4. 服务器确认原设备鉴权成功,再测试收发音频、断网重连和休眠唤醒。 + +TLS 成功、WebSocket 成功、设备鉴权成功是三个不同阶段,不能只看其中一条日志就认定业务已跑通。 diff --git a/ql-application/threadx/interface/custom_app/Makefile b/ql-application/threadx/interface/custom_app/Makefile index 9fd9032..aaac915 100644 --- a/ql-application/threadx/interface/custom_app/Makefile +++ b/ql-application/threadx/interface/custom_app/Makefile @@ -13,6 +13,7 @@ MAKEFILE_PATH :=$(subst $(strip \),/,$(abspath $(lastword $(MAKEFILE_LIST)))) SRC_FILES+= \ custom_app.c \ lib/cJSON/cJSON.c\ + lib/librws/src/rws_handshake.c\ lib/librws/src/rws_error.c\ lib/librws/src/rws_frame.c\ lib/librws/src/rws_list.c\ @@ -67,6 +68,8 @@ SRC_FILES+= \ http_aofei.c \ net_mgt.c \ ws_talkingq.c \ + talkingq_wss.c \ + talkingq_root_ca.c \ hal_key.c \ audio.c \ hal_led.c \ diff --git a/ql-application/threadx/interface/custom_app/inc/talkingq_wss.h b/ql-application/threadx/interface/custom_app/inc/talkingq_wss.h new file mode 100644 index 0000000..f14556b --- /dev/null +++ b/ql-application/threadx/interface/custom_app/inc/talkingq_wss.h @@ -0,0 +1,20 @@ +#ifndef TALKINGQ_WSS_H +#define TALKINGQ_WSS_H + +#include + +#define TALKINGQ_WSS_URL "wss://banban.api.talkingq.com:443/ws" +#define TALKINGQ_URL_CAPACITY 64 +#define TALKINGQ_HOST_CAPACITY 64 +#define TALKINGQ_PATH_CAPACITY 128 + +extern const char talkingq_root_ca[]; +extern const size_t talkingq_root_ca_size; + +/* Reject cleartext, malformed authorities, invalid ports and oversized URLs. */ +int talkingq_parse_wss_url(const char *url, char *host, size_t host_size, + int *port, char *path, size_t path_size); +/* Only the former built-in endpoints migrate; custom WSS endpoints are kept. */ +int talkingq_is_legacy_ws_url(const char *url); + +#endif diff --git a/ql-application/threadx/interface/custom_app/lib/hal/hal.c b/ql-application/threadx/interface/custom_app/lib/hal/hal.c index 656d4f7..ec98b88 100644 --- a/ql-application/threadx/interface/custom_app/lib/hal/hal.c +++ b/ql-application/threadx/interface/custom_app/lib/hal/hal.c @@ -30,6 +30,7 @@ #include "hal_api.h" #include "build_macro.h" #include "ql_rtc.h" +#include "talkingq_wss.h" #include "ql_cell_locator.h" @@ -979,17 +980,17 @@ int save_talkingq_url(uint8_t *buff, int len) #endif int hal_os_get_talkingq_url(char *pdevice_id) { - char temp_device_id[64]; - memset(temp_device_id, 0, sizeof(temp_device_id)); - hal_kv_get("talkingq_url", temp_device_id, sizeof(temp_device_id)); + char temp_device_id[TALKINGQ_URL_CAPACITY] = {0}; + int saved = hal_kv_get("talkingq_url", temp_device_id, sizeof(temp_device_id)); + temp_device_id[sizeof(temp_device_id) - 1] = 0; //read_talkingq_url(temp_device_id,sizeof(temp_device_id)); - if(strlen(temp_device_id)>0){ + if(saved > 0 && strlen(temp_device_id)>0 && !talkingq_is_legacy_ws_url(temp_device_id)){ strcpy(pdevice_id, temp_device_id); }else{ //strcpy(pdevice_id,"ws://api.talkingq.com:8080"); //strcpy(pdevice_id,"ws://175.24.73.253:80"); - strcpy(pdevice_id,"ws://101.35.224.118:8080"); + strcpy(pdevice_id, TALKINGQ_WSS_URL); return -1; } return 0; diff --git a/ql-application/threadx/interface/custom_app/lib/librws/inc/librws.h b/ql-application/threadx/interface/custom_app/lib/librws/inc/librws.h index 6005cfb..fb647f0 100644 --- a/ql-application/threadx/interface/custom_app/lib/librws/inc/librws.h +++ b/ql-application/threadx/interface/custom_app/lib/librws/inc/librws.h @@ -22,6 +22,7 @@ #ifndef __LIBRWS_H__ #define __LIBRWS_H__ +#include "rws_tls_config.h" #include diff --git a/ql-application/threadx/interface/custom_app/lib/librws/inc/rws_common.h b/ql-application/threadx/interface/custom_app/lib/librws/inc/rws_common.h index fb55d72..391308e 100644 --- a/ql-application/threadx/interface/custom_app/lib/librws/inc/rws_common.h +++ b/ql-application/threadx/interface/custom_app/lib/librws/inc/rws_common.h @@ -23,6 +23,8 @@ #ifndef __RWS_COMMON_H__ #define __RWS_COMMON_H__ 1 +#include "rws_tls_config.h" + #include #include "ql_type.h" #include "ql_rtos.h" diff --git a/ql-application/threadx/interface/custom_app/lib/librws/inc/rws_handshake.h b/ql-application/threadx/interface/custom_app/lib/librws/inc/rws_handshake.h new file mode 100644 index 0000000..8a94765 --- /dev/null +++ b/ql-application/threadx/interface/custom_app/lib/librws/inc/rws_handshake.h @@ -0,0 +1,8 @@ +#ifndef RWS_HANDSHAKE_H +#define RWS_HANDSHAKE_H +#include +#define RWS_HANDSHAKE_MAX 8192 +/* 1: valid upgrade, 0: incomplete, -1: rejected. Never needs a NUL terminator. */ +int rws_check_handshake(const char *data, size_t length, const char *expected_accept, + size_t *header_length, int *http_status); +#endif diff --git a/ql-application/threadx/interface/custom_app/lib/librws/inc/rws_socket.h b/ql-application/threadx/interface/custom_app/lib/librws/inc/rws_socket.h index b9b1c53..ca51d6e 100644 --- a/ql-application/threadx/interface/custom_app/lib/librws/inc/rws_socket.h +++ b/ql-application/threadx/interface/custom_app/lib/librws/inc/rws_socket.h @@ -87,9 +87,11 @@ typedef struct _rws_socket_struct char *host; char *path; + char expected_accept[29]; char *sec_ws_accept; // "Sec-WebSocket-Accept" field from handshake rws_thread work_thread; + rws_bool worker_started; int command; diff --git a/ql-application/threadx/interface/custom_app/lib/librws/inc/rws_tls_config.h b/ql-application/threadx/interface/custom_app/lib/librws/inc/rws_tls_config.h new file mode 100644 index 0000000..fd59757 --- /dev/null +++ b/ql-application/threadx/interface/custom_app/lib/librws/inc/rws_tls_config.h @@ -0,0 +1,5 @@ +#ifndef RWS_TLS_CONFIG_H +#define RWS_TLS_CONFIG_H +/* Shared by public and private headers so every socket layout agrees. */ +#define LIBRWS_USING_MBED_TLS 1 +#endif diff --git a/ql-application/threadx/interface/custom_app/lib/librws/src/rws_handshake.c b/ql-application/threadx/interface/custom_app/lib/librws/src/rws_handshake.c new file mode 100644 index 0000000..98fc2c2 --- /dev/null +++ b/ql-application/threadx/interface/custom_app/lib/librws/src/rws_handshake.c @@ -0,0 +1,79 @@ +#include "rws_handshake.h" +#include + +static int equal_ci(const char *s, size_t n, const char *literal) +{ + size_t i; + if (strlen(literal) != n) return 0; + for (i = 0; i < n; ++i) { + unsigned char c = (unsigned char)s[i]; + if (c >= 'A' && c <= 'Z') c += 'a' - 'A'; + if (c != (unsigned char)literal[i]) return 0; + } + return 1; +} + +static int has_token(const char *s, size_t n, const char *token) +{ + size_t begin = 0, end, next; + while (begin < n) { + for (end = begin; end < n && s[end] != ','; ++end) {} + next = end + 1; + while (begin < end && (s[begin] == ' ' || s[begin] == '\t')) ++begin; + while (end > begin && (s[end-1] == ' ' || s[end-1] == '\t')) --end; + if (equal_ci(s + begin, end - begin, token)) return 1; + begin = next; + } + return 0; +} + +int rws_check_handshake(const char *data, size_t length, const char *expected_accept, + size_t *header_length, int *http_status) +{ + size_t total = 0, i, line, end, colon, first, last; + int accept = 0, upgrade = 0, connection = 0; + *header_length = 0; + *http_status = 0; + if (!data) return length ? -1 : 0; + for (i = 0; i + 3 < length && i + 4 <= RWS_HANDSHAKE_MAX; ++i) { + if (!memcmp(data + i, "\r\n\r\n", 4)) { total = i + 4; break; } + } + if (!total) return length >= RWS_HANDSHAKE_MAX ? -1 : 0; + *header_length = total; + for (i = 0; i < total; ++i) { + unsigned char c = (unsigned char)data[i]; + if (c == 0 || (c < 32 && c != '\r' && c != '\n' && c != '\t')) return -1; + } + for (end = 0; end + 1 < total && memcmp(data + end, "\r\n", 2); ++end) {} + if (end < 12 || memcmp(data, "HTTP/1.1 ", 9) || + (end > 12 && data[12] != ' ')) return -1; + for (i = 9; i < 12; ++i) { + if (data[i] < '0' || data[i] > '9') return -1; + *http_status = *http_status * 10 + data[i] - '0'; + } + if (*http_status != 101) return -1; + line = end + 2; + while (line < total - 2) { + for (end = line; end + 1 < total && memcmp(data + end, "\r\n", 2); ++end) {} + for (colon = line; colon < end && data[colon] != ':'; ++colon) {} + if (colon == end) return -1; + first = colon + 1; + last = end; + while (first < last && (data[first] == ' ' || data[first] == '\t')) ++first; + while (last > first && (data[last-1] == ' ' || data[last-1] == '\t')) --last; + if (equal_ci(data + line, colon - line, "sec-websocket-accept")) { + if (accept || strlen(expected_accept) != last - first || + memcmp(data + first, expected_accept, last - first)) return -1; + accept = 1; + } else if (equal_ci(data + line, colon - line, "upgrade")) { + upgrade |= has_token(data + first, last - first, "websocket"); + } else if (equal_ci(data + line, colon - line, "connection")) { + connection |= has_token(data + first, last - first, "upgrade"); + } else if (equal_ci(data + line, colon - line, "sec-websocket-extensions") || + equal_ci(data + line, colon - line, "sec-websocket-protocol")) { + return -1; /* This client did not request extensions or a subprotocol. */ + } + line = end + 2; + } + return accept && upgrade && connection ? 1 : -1; +} diff --git a/ql-application/threadx/interface/custom_app/lib/librws/src/rws_socketpriv.c b/ql-application/threadx/interface/custom_app/lib/librws/src/rws_socketpriv.c index 57942e4..a0c4f4b 100644 --- a/ql-application/threadx/interface/custom_app/lib/librws/src/rws_socketpriv.c +++ b/ql-application/threadx/interface/custom_app/lib/librws/src/rws_socketpriv.c @@ -27,6 +27,12 @@ #include #include "sockets.h" #include "ql_rtos.h" +#include +#include "rws_handshake.h" +#ifdef LIBRWS_USING_MBED_TLS +#include "mbedtls/base64.h" +#include "mbedtls/sha1.h" +#endif const char *k_rws_socket_min_http_ver = "1.1"; @@ -127,86 +133,12 @@ void rws_socket_inform_recvd_frames(_rws_socket *s) } } -void rws_socket_read_handshake_responce_value(const char *str, char **value) -{ - const char *s = NULL; - size_t len = 0; - - while (*str == ':' || *str == ' ') - { - str++; - } - s = str; - while (*s != '\r' && *s != '\n') - { - s++; - len++; - } - if (len > 0) - { - *value = rws_string_copy_len(str, len); - } -} - rws_bool rws_socket_process_handshake_responce(_rws_socket *s) { - const char *str = (const char *)s->received; - const char *sub = NULL; - // float http_ver = -1; - // int http_code = -1; - - rws_error_delete_clean(&s->error); - - // LOG_D("responce herader %s",str); - - sub = strstr(str, "HTTP/"); - if (!sub) - { - LOG_E("herader not find HTTP ERROR"); - return rws_false; - } - - sub += 5; - #if 0 - if (rws_sscanf(sub, "%f %i", &http_ver, &http_code) != 2) - { - LOG_D("responce sub %s",sub); - http_ver = -1; - http_code = -1; - } - #endif - - // "Sec-WebSocket-Accept" - sub = strstr(str, k_rws_socket_sec_websocket_accept); - if (sub) - { - sub += strlen(k_rws_socket_sec_websocket_accept); - rws_socket_read_handshake_responce_value(sub, &s->sec_ws_accept); - } - - // "Sec-WebSocket-Accept" - sub = strstr(str, "sec-websocket-accept"); - if (sub) - { - sub += strlen(k_rws_socket_sec_websocket_accept); - rws_socket_read_handshake_responce_value(sub, &s->sec_ws_accept); - } - - // if (http_code != 101 || !s->sec_ws_accept) - if (!s->sec_ws_accept) - { - #if 0 - s->error = rws_error_new_code_descr(rws_error_code_parse_handshake, - (http_code != 101) ? "HTPP code not found or non 101" : "Accept key not found"); - - LOG_I("responce herader http_code %d",http_code); - if(!s->sec_ws_accept){ - LOG_E("Accept key not found"); - } - #endif - return rws_false; - } - return rws_true; + size_t header_length; + int status; + return rws_check_handshake((const char *)s->received, s->received_len, + s->expected_accept, &header_length, &status) == 1; } static int reliable_send(int sock, const void *data, size_t total_len) { @@ -255,6 +187,27 @@ static int reliable_send(int sock, const void *data, size_t total_len) { return sent_bytes; } +#ifdef LIBRWS_USING_MBED_TLS +static int rws_tls_send_all(_rws_socket *s, const unsigned char *data, size_t size) +{ + size_t sent = 0; + unsigned int started = ql_rtos_get_systicks(); + if (!s->ssl) return -1; + while (sent < size) { + int ret = mbedtls_ssl_write(&s->ssl->ssl_ctx, data + sent, size - sent); + if (ret > 0) { + sent += (size_t)ret; + } else if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { + return -1; + } else { + rws_thread_sleep(5); + } + if (sent < size && (unsigned int)(ql_rtos_get_systicks() - started) >= 1000) return -1; + } + return (int)sent; +} +#endif + // need close socket on error rws_bool rws_socket_send(_rws_socket *s, const void *data, const size_t data_size) { @@ -265,7 +218,7 @@ rws_bool rws_socket_send(_rws_socket *s, const void *data, const size_t data_siz if (s->scheme && strcmp(s->scheme, "wss") == 0) { LOG_D("[T:%.8d, L:%.5d, %*.*s] try send data with tls", ql_rtos_get_systicks(), __LINE__, DBG_FUNCTION_NUM, DBG_FUNCTION_NUM, __FUNCTION__); - sended = mbedtls_ssl_write(&(s->ssl->ssl_ctx), (const unsigned char *)data, (int)data_size); + sended = rws_tls_send_all(s, (const unsigned char *)data, data_size); } else #endif /* LIBRWS_USING_MBED_TLS */ @@ -289,7 +242,7 @@ rws_bool rws_socket_send(_rws_socket *s, const void *data, const size_t data_siz } #endif - if (sended > 0) + if (sended == (int)data_size) { rws_socket_error_cnt = 0; rws_socket_error_staus = 0; @@ -297,6 +250,15 @@ rws_bool rws_socket_send(_rws_socket *s, const void *data, const size_t data_siz } printf("errno:%d\n", errno); +#ifdef LIBRWS_USING_MBED_TLS + if (s->scheme && strcmp(s->scheme, "wss") == 0) { + rws_error_delete_clean(&s->error); + s->error = rws_error_new_code_descr(rws_error_code_read_write_socket, "TLS write failed or timed out"); + rws_socket_close(s); + s->command = COMMAND_INFORM_DISCONNECTED; + return rws_false; + } +#endif rws_socket_error_cnt++; if(rws_socket_error_cnt>3){ rws_socket_error_staus = 1; @@ -345,7 +307,21 @@ rws_bool rws_socket_recv(_rws_socket *s) if (s->scheme && strcmp(s->scheme, "wss") == 0) { // LOG_D("[T:%.8d, L:%.5d, %*.*s] try recv data with tls", ql_rtos_get_systicks(), __LINE__, DBG_FUNCTION_NUM, DBG_FUNCTION_NUM, __FUNCTION__); + if (!s->ssl) { + s->error = rws_error_new_code_descr(rws_error_code_read_write_socket, "TLS context is closed"); + s->command = COMMAND_INFORM_DISCONNECTED; + result = rws_false; + goto _exit; + } len = mbedtls_ssl_read(&(s->ssl->ssl_ctx), (unsigned char *)(s->payload_buff), RWS_RECEIVE_BUFF_SIZE); + if (len == MBEDTLS_ERR_SSL_WANT_READ || len == MBEDTLS_ERR_SSL_WANT_WRITE) break; + if (len <= 0) { + LOG_E("WSS TLS peer closed or read failed: %d\n", len); + s->error = rws_error_new_code_descr(rws_error_code_read_write_socket, "TLS peer closed or read failed"); + s->command = COMMAND_INFORM_DISCONNECTED; + result = rws_false; + goto _exit; + } } else #endif /* LIBRWS_USING_MBED_TLS */ @@ -385,6 +361,12 @@ rws_bool rws_socket_recv(_rws_socket *s) if (len > 0) { total_len += len; + if (s->command == COMMAND_WAIT_HANDSHAKE_RESPONCE && + s->received_len + (size_t)len > RWS_HANDSHAKE_MAX + RWS_RECEIVE_BUFF_SIZE) { + s->error = rws_error_new_code_descr(rws_error_code_parse_handshake, "WebSocket headers too large"); + result = rws_false; + goto _exit; + } if (s->received_size - s->received_len < len) { // LOG_D("[T:%.8d, L:%.5d, %*.*s] s->received_size = %d, s->received_len = %d", ql_rtos_get_systicks(), __LINE__, DBG_FUNCTION_NUM, DBG_FUNCTION_NUM, __FUNCTION__, s->received_size, s->received_len); @@ -398,6 +380,7 @@ rws_bool rws_socket_recv(_rws_socket *s) } memcpy(received, s->payload_buff, len); s->received_len += len; + if (s->command == COMMAND_WAIT_HANDSHAKE_RESPONCE) break; } else { @@ -1020,54 +1003,32 @@ void rws_socket_idle_send(_rws_socket *s) void rws_socket_wait_handshake_responce(_rws_socket *s) { - int cnt = 0; - // LOG_I("[T:%.8d, L:%.5d, %*.*s] try wait recv handshake responce", ql_rtos_get_systicks(), __LINE__, DBG_FUNCTION_NUM, DBG_FUNCTION_NUM, __FUNCTION__); - - /* Todo */ - while (!rws_socket_recv(s)) - { - // LOG_D("[T:%.8d, L:%.5d, %*.*s] no find handshake responce", ql_rtos_get_systicks(), __LINE__, DBG_FUNCTION_NUM, DBG_FUNCTION_NUM, __FUNCTION__); - - if (s->error) - { - LOG_E("[T:%.8d, L:%.5d, %*.*s] recv responce failed, handshake failed, error = %d", ql_rtos_get_systicks(), __LINE__, DBG_FUNCTION_NUM, DBG_FUNCTION_NUM, __FUNCTION__, s->error); - LOG_I("[T:%.8d, L:%.5d, %*.*s] try inform websocket disconnect", ql_rtos_get_systicks(), __LINE__, DBG_FUNCTION_NUM, DBG_FUNCTION_NUM, __FUNCTION__); - s->command = COMMAND_INFORM_DISCONNECTED; - return; - } - if(cnt++>2){ - s->command = COMMAND_SEND_HANDSHAKE; - return; - } - rws_thread_sleep(1000); + unsigned int started = ql_rtos_get_systicks(); + size_t header_length = 0; + int status = 0, result = 0; + while (result == 0) { + rws_socket_recv(s); + if (s->error) break; + result = rws_check_handshake((const char *)s->received, s->received_len, + s->expected_accept, &header_length, &status); + if (result != 0) break; + if ((unsigned int)(ql_rtos_get_systicks() - started) >= 2000) break; + rws_thread_sleep(10); } - - - if (s->received_len == 0) - { - LOG_D("[T:%.8d, L:%.5d, %*.*s] handshake responce len is empty, len = 0", ql_rtos_get_systicks(), __LINE__, DBG_FUNCTION_NUM, DBG_FUNCTION_NUM, __FUNCTION__); - return; - } - - // LOG_I("recv len %d",s->received_len); - - // LOG_I("[T:%.8d, L:%.5d, %*.*s] try process handshake responce", ql_rtos_get_systicks(), __LINE__, DBG_FUNCTION_NUM, DBG_FUNCTION_NUM, __FUNCTION__); - if (rws_socket_process_handshake_responce(s)) - { - s->received_len = 0; + if (result == 1 && !s->error) { + s->received_len -= header_length; + if (s->received_len) + memmove(s->received, (char *)s->received + header_length, s->received_len); s->is_connected = rws_true; s->command = COMMAND_INFORM_CONNECTED; - - // LOG_D("[T:%.8d, L:%.5d, %*.*s] process handshake responce succeed", ql_rtos_get_systicks(), __LINE__, DBG_FUNCTION_NUM, DBG_FUNCTION_NUM, __FUNCTION__); - LOG_I("[T:%.8d, L:%.5d, %*.*s] try inform websocket connect", ql_rtos_get_systicks(), __LINE__, DBG_FUNCTION_NUM, DBG_FUNCTION_NUM, __FUNCTION__); - } - else - { + LOG_I("WebSocket upgrade verified: HTTP 101\n"); + } else { + LOG_E("WebSocket upgrade failed: HTTP %d (0 means incomplete/invalid headers)\n", status); + if (!s->error) + s->error = rws_error_new_code_descr(rws_error_code_parse_handshake, + "Invalid or timed out WebSocket upgrade"); rws_socket_close(s); s->command = COMMAND_INFORM_DISCONNECTED; - - //LOG_E("[T:%.8d, L:%.5d, %*.*s] process handshake responce failed", ql_rtos_get_systicks(), __LINE__, DBG_FUNCTION_NUM, DBG_FUNCTION_NUM, __FUNCTION__); - LOG_I("[T:%.8d, L:%.5d, %*.*s] try inform websocket disconnect", ql_rtos_get_systicks(), __LINE__, DBG_FUNCTION_NUM, DBG_FUNCTION_NUM, __FUNCTION__); } } @@ -1093,6 +1054,24 @@ void rws_socket_send_handshake(_rws_socket *s) char buff[512]; char *ptr = buff; size_t writed = 0; + char key[25] = "dGhlIHNhbXBsZSBub25jZQ=="; + strcpy(s->expected_accept, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="); +#ifdef LIBRWS_USING_MBED_TLS + if (s->scheme && !strcmp(s->scheme, "wss")) { + unsigned char nonce[16], digest[20]; + char challenge[61]; + size_t encoded; + if (!s->ssl || mbedtls_ctr_drbg_random(&s->ssl->ctr_drbg, nonce, sizeof(nonce)) || + mbedtls_base64_encode((unsigned char *)key, sizeof(key), &encoded, nonce, sizeof(nonce))) + goto key_error; + strcpy(challenge, key); + strcat(challenge, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); + mbedtls_sha1((const unsigned char *)challenge, strlen(challenge), digest); + if (mbedtls_base64_encode((unsigned char *)s->expected_accept, + sizeof(s->expected_accept), &encoded, digest, sizeof(digest))) + goto key_error; + } +#endif writed = rws_sprintf(ptr, 512, "GET %s HTTP/%s\r\n", s->path, k_rws_socket_min_http_ver); if (s->port == 80) @@ -1114,7 +1093,7 @@ void rws_socket_send_handshake(_rws_socket *s) "Sec-WebSocket-Key: %s\r\n" "Sec-WebSocket-Version: 13\r\n" "\r\n", - "dGhlIHNhbXBsZSBub25jZQ=="); + key); if (rws_socket_send(s, buff, writed)) { @@ -1133,6 +1112,13 @@ void rws_socket_send_handshake(_rws_socket *s) rws_socket_close(s); s->command = COMMAND_INFORM_DISCONNECTED; } + return; +#ifdef LIBRWS_USING_MBED_TLS +key_error: + s->error = rws_error_new_code_descr(rws_error_code_send_handshake, "WebSocket nonce generation failed"); + rws_socket_close(s); + s->command = COMMAND_INFORM_DISCONNECTED; +#endif } struct addrinfo *rws_socket_connect_getaddr_info(_rws_socket *s) @@ -1313,6 +1299,7 @@ void rws_socket_connect_to_host(_rws_socket *s) ret = getsockopt(sock, SOL_SOCKET, SO_ERROR, &sock_error, &optlen); if(ret == 0 && sock_error == 0) { + ret = 1; printf("connect success\r\n"); } else @@ -1366,6 +1353,7 @@ void rws_socket_connect_to_host(_rws_socket *s) LOG_E("[T:%.8d, L:%.5d, %*.*s] socket connect failed\n", ql_rtos_get_systicks(), __LINE__, DBG_FUNCTION_NUM, DBG_FUNCTION_NUM, __FUNCTION__); RWS_SOCK_CLOSE(sock); + sock = RWS_INVALID_SOCKET; } else { @@ -1380,6 +1368,7 @@ void rws_socket_connect_to_host(_rws_socket *s) } exit: + if (s->socket == RWS_INVALID_SOCKET && sock != RWS_INVALID_SOCKET) RWS_SOCK_CLOSE(sock); freeaddrinfo(result); if (s->socket == RWS_INVALID_SOCKET) @@ -1559,7 +1548,7 @@ static void rws_socket_work_th_func(void *user_object) LOG_D("[T:%.8d, L:%.5d, %*.*s] COMMAND_INFORM_DISCONNECTED\n", ql_rtos_get_systicks(), __LINE__, DBG_FUNCTION_NUM, DBG_FUNCTION_NUM, __FUNCTION__); s->command = COMMAND_END; - rws_socket_send_disconnect(s); + if (s->is_connected) rws_socket_send_disconnect(s); if (s->on_disconnected) { @@ -1619,27 +1608,16 @@ static void rws_socket_work_th_func(void *user_object) rws_bool rws_socket_create_start_work_thread(_rws_socket *s) { rws_error_delete_clean(&s->error); - //s->work_thread = rws_thread_create(&rws_socket_work_th_func, s); - - if (rws_socket_thread_status!=0){ - // rt_thread_delete(rws_work_task); - s->command = COMMAND_END; - // rws_work_task =NULL; - while(rws_socket_thread_status){ - ql_rtos_task_sleep_ms(20); - } - } - - s->command = COMMAND_NONE; - - ql_rtos_task_create(&rws_work_task,1024*4,100,"rws_work", - rws_socket_work_th_func, s); - - if (rws_work_task) - { - s->command = COMMAND_CONNECT_TO_HOST; + while (rws_socket_thread_status) ql_rtos_task_sleep_ms(20); + s->command = COMMAND_CONNECT_TO_HOST; + s->worker_started = rws_true; + rws_work_task = NULL; + /* TLS certificate parsing and crypto need more stack than plain WS. */ + if (ql_rtos_task_create(&rws_work_task, 1024*16, 100, "rws_work", + rws_socket_work_th_func, s) == 0) return rws_true; - } + s->worker_started = rws_false; + s->command = COMMAND_NONE; return rws_false; } @@ -1867,36 +1845,17 @@ void rws_debug(void *ctx, int level, const char *file, int line, const char *str LOG_I("%s", str ? str : NULL); } -// TODO: mbedtls_ssl_set_hostname() should be called. int rws_ssl_conn(_rws_socket *s) { - int authmode = MBEDTLS_SSL_VERIFY_NONE; - const char *pers = "https"; - int value, ret = 0; + const char *pers = "talkingq-wss"; + int ret = -1; + unsigned int started; uint32_t flags; - _rws_ssl *ssl = NULL; - char portstr[16] = {0}; - - //value = value; + _rws_ssl *ssl; s->ssl = rws_malloc_zero(sizeof(_rws_ssl)); - if (!s->ssl) - { - LOG_I("Memory malloc error."); - ret = -1; - goto exit; - } + if (!s->ssl) goto exit; ssl = s->ssl; - - if (s->server_cert) - authmode = MBEDTLS_SSL_VERIFY_OPTIONAL; - - /* - * Initialize the RNG and the session data - */ -#if defined(MBEDTLS_DEBUG_C) - mbedtls_debug_set_threshold(DEBUG_LEVEL); -#endif mbedtls_net_init(&ssl->net_ctx); mbedtls_ssl_init(&ssl->ssl_ctx); mbedtls_ssl_config_init(&ssl->ssl_conf); @@ -1904,212 +1863,89 @@ int rws_ssl_conn(_rws_socket *s) mbedtls_x509_crt_init(&ssl->clicert); mbedtls_pk_init(&ssl->pkey); mbedtls_ctr_drbg_init(&ssl->ctr_drbg); - - /* - * 0. Initialize the RNG and the session data - */ mbedtls_entropy_init(&ssl->entropy); - if ((value = mbedtls_ctr_drbg_seed(&ssl->ctr_drbg, - mbedtls_entropy_func, - &ssl->entropy, - (const unsigned char *)pers, - strlen(pers))) != 0) - { - LOG_I("mbedtls_ctr_drbg_seed() failed, value:-0x%x.", -value); - ret = -1; + + if (!s->server_cert || s->server_cert_len <= 0) { + LOG_E("WSS requires a trusted root CA\n"); goto exit; } - - /* - * Load the Client certificate - */ - if (s->client_cert && s->client_pk) - { - ret = mbedtls_x509_crt_parse(&ssl->clicert, (const unsigned char *)s->client_cert, s->client_cert_len); - if (ret < 0) - { - LOG_I("Loading cli_cert failed! mbedtls_x509_crt_parse returned -0x%x.", -ret); - goto exit; - } - - ret = mbedtls_pk_parse_key(&ssl->pkey, (const unsigned char *)s->client_pk, s->client_pk_len, NULL, 0); - if (ret != 0) - { - LOG_I("failed! mbedtls_pk_parse_key returned -0x%x.", -ret); - goto exit; - } - } - - /* - * Load the trusted CA - */ - /* cert_len passed in is gotten from sizeof not strlen */ - if (s->server_cert && ((value = mbedtls_x509_crt_parse(&ssl->cacert, - (const unsigned char *)s->server_cert, - s->server_cert_len)) < 0)) - { - LOG_I("mbedtls_x509_crt_parse() failed, value:-0x%x.", -value); - ret = -1; + if (time(NULL) < 1704067200) { + LOG_E("WSS requires valid network/RTC time before certificate verification\n"); goto exit; } - - // rws_socket_set_option(ssl->net_ctx.fd, SO_ERROR, 1); // When an error occurs on a socket, set error variable so_error and notify process - // rws_socket_set_option(ssl->net_ctx.fd, SO_KEEPALIVE, 1); // Periodically test if connection is alive - - // { - // struct timeval timeout; - - // timeout.tv_sec = 1; - // timeout.tv_usec = 0; - // setsockopt(ssl->net_ctx.fd, SOL_SOCKET, SO_SNDTIMEO, (void *)&timeout, sizeof(timeout)); - // setsockopt(ssl->net_ctx.fd, SOL_SOCKET, SO_RCVTIMEO, (void *)&timeout, sizeof(timeout)); - // } - - /* - * Start the connection - */ - rws_sprintf(portstr, 16, "%i", s->port); - if ((ret = mbedtls_net_connect(&ssl->net_ctx, s->host, portstr, MBEDTLS_NET_PROTO_TCP)) != 0) - { - LOG_I("failed! mbedtls_net_connect returned %d, port:%s.", ret, portstr); - goto exit; - } - - s->received_len = 0; - s->socket = ssl->net_ctx.fd; - - /* - * Setup stuff - */ - if ((value = mbedtls_ssl_config_defaults(&ssl->ssl_conf, - MBEDTLS_SSL_IS_CLIENT, - MBEDTLS_SSL_TRANSPORT_STREAM, - MBEDTLS_SSL_PRESET_DEFAULT)) != 0) - { - LOG_I("mbedtls_ssl_config_defaults() failed, value:-0x%x.", -value); - ret = -1; - goto exit; - } - - // TODO: add customerization encryption algorithm - memcpy(&ssl->profile, ssl->ssl_conf.cert_profile, sizeof(mbedtls_x509_crt_profile)); - ssl->profile.allowed_mds = ssl->profile.allowed_mds | MBEDTLS_X509_ID_FLAG(MBEDTLS_MD_MD5); - mbedtls_ssl_conf_cert_profile(&ssl->ssl_conf, &ssl->profile); - - mbedtls_ssl_conf_authmode(&ssl->ssl_conf, authmode); + ret = mbedtls_ctr_drbg_seed(&ssl->ctr_drbg, mbedtls_entropy_func, + &ssl->entropy, (const unsigned char *)pers, strlen(pers)); + if (ret != 0) goto exit; + ret = mbedtls_x509_crt_parse(&ssl->cacert, (const unsigned char *)s->server_cert, + s->server_cert_len); + if (ret != 0) goto exit; + ret = mbedtls_ssl_config_defaults(&ssl->ssl_conf, MBEDTLS_SSL_IS_CLIENT, + MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT); + if (ret != 0) goto exit; + mbedtls_ssl_conf_authmode(&ssl->ssl_conf, MBEDTLS_SSL_VERIFY_REQUIRED); mbedtls_ssl_conf_ca_chain(&ssl->ssl_conf, &ssl->cacert, NULL); - mbedtls_ssl_conf_max_frag_len(&ssl->ssl_conf, MBEDTLS_SSL_MAX_FRAG_LEN_4096); - - if (s->client_cert && (ret = mbedtls_ssl_conf_own_cert(&ssl->ssl_conf, &ssl->clicert, &ssl->pkey)) != 0) - { - LOG_I(" failed! mbedtls_ssl_conf_own_cert returned %d.", ret); - goto exit; - } - + mbedtls_ssl_conf_min_version(&ssl->ssl_conf, MBEDTLS_SSL_MAJOR_VERSION_3, MBEDTLS_SSL_MINOR_VERSION_3); mbedtls_ssl_conf_rng(&ssl->ssl_conf, mbedtls_ctr_drbg_random, &ssl->ctr_drbg); - mbedtls_ssl_conf_dbg(&ssl->ssl_conf, rws_debug, NULL); + ret = mbedtls_ssl_setup(&ssl->ssl_ctx, &ssl->ssl_conf); + if (ret != 0) goto exit; + /* Sets both SNI and the expected certificate hostname. */ + ret = mbedtls_ssl_set_hostname(&ssl->ssl_ctx, s->host); + if (ret != 0) goto exit; - if ((value = mbedtls_ssl_setup(&ssl->ssl_ctx, &ssl->ssl_conf)) != 0) - { - LOG_I("mbedtls_ssl_setup() failed, value:-0x%x.", -value); - ret = -1; + /* Reuse the working PDP profile 1 TCP path. The ASR net_connect API has + a vendor-specific CID/port ABI, unlike upstream mbedTLS. */ + rws_socket_connect_to_host(s); + if (s->socket == RWS_INVALID_SOCKET) { + ret = MBEDTLS_ERR_NET_CONNECT_FAILED; goto exit; } - - if ((ret = mbedtls_ssl_set_hostname(&ssl->ssl_ctx, s->host)) != 0) - { - LOG_I("failed\n! mbedtls_ssl_set_hostname returned %d\n", ret); - goto exit; - } - + ssl->net_ctx.fd = s->socket; +#ifdef MBEDTLS_WITH_ASR + ssl->net_ctx.cid = 1; +#endif + ret = mbedtls_net_set_nonblock(&ssl->net_ctx); + if (ret != 0) goto exit; mbedtls_ssl_set_bio(&ssl->ssl_ctx, &ssl->net_ctx, mbedtls_net_send, mbedtls_net_recv, NULL); - - /* - * Handshake - */ - while ((ret = mbedtls_ssl_handshake(&ssl->ssl_ctx)) != 0) - { - if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) - { - LOG_I("mbedtls_ssl_handshake() failed, ret:-0x%x.", -ret); - ret = -1; + started = ql_rtos_get_systicks(); + while ((ret = mbedtls_ssl_handshake(&ssl->ssl_ctx)) != 0) { + if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) goto exit; + if ((unsigned int)(ql_rtos_get_systicks() - started) >= 4000) { + ret = MBEDTLS_ERR_SSL_TIMEOUT; /* 20 seconds, 5 ms SDK ticks */ goto exit; } - - LOG_I("mbedtls_ssl_handshake() while"); + rws_thread_sleep(10); } - - LOG_I("mbedtls_ssl_handshake() ok"); - - // Todo - // mbedtls_net_set_nonblock(&ssl->net_ctx); - - { - struct timeval timeout; - - timeout.tv_sec = 3; - timeout.tv_usec = 0; - setsockopt(ssl->net_ctx.fd, SOL_SOCKET, SO_SNDTIMEO, (void *)&timeout, sizeof(timeout)); - - timeout.tv_sec = 0; - timeout.tv_usec = 1000; - setsockopt(ssl->net_ctx.fd, SOL_SOCKET, SO_RCVTIMEO, (void *)&timeout, sizeof(timeout)); - } - - /* - * Verify the server certificate - */ - /* In real life, we would have used MBEDTLS_SSL_VERIFY_REQUIRED so that the - * handshake would not succeed if the peer's cert is bad. Even if we used - * MBEDTLS_SSL_VERIFY_OPTIONAL, we would bail out here if ret != 0 */ - if ((flags = mbedtls_ssl_get_verify_result(&ssl->ssl_ctx)) != 0) - { - char vrfy_buf[512]; - LOG_I("svr_cert varification failed. authmode:%d", authmode); - mbedtls_x509_crt_verify_info(vrfy_buf, sizeof(vrfy_buf), " ! ", flags); - LOG_I("%s", vrfy_buf); - } - else - { - LOG_I("svr_cert varification ok. authmode:%d", authmode); + flags = mbedtls_ssl_get_verify_result(&ssl->ssl_ctx); + if (flags != 0) { + LOG_E("WSS certificate verification failed: 0x%08lx\n", (unsigned long)flags); + ret = MBEDTLS_ERR_X509_CERT_VERIFY_FAILED; + goto exit; } + LOG_I("WSS TLS handshake and certificate/hostname verification passed: %s:%d\n", s->host, s->port); + s->received_len = 0; + s->command = COMMAND_SEND_HANDSHAKE; + return 0; exit: - if (ret != 0) - { -#ifdef MBEDTLS_ERROR_C - char error_buf[100]; - - extern void mbedtls_strerror(int ret, char *buf, size_t buflen); - mbedtls_strerror(ret, error_buf, 100); - LOG_I("Last error was: %d - %s", ret, error_buf); -#endif - LOG_I("ret=%d.", ret); - s->error = rws_error_new_code_descr(rws_error_code_connect_to_host, "Failed connect to host"); - LOG_I("%s: code:%d, error:%s", __FUNCTION__, s->error->code, s->error->description); - s->command = COMMAND_INFORM_DISCONNECTED; - - if (s->ssl) - { - mbedtls_net_free(&s->ssl->net_ctx); - mbedtls_x509_crt_free(&s->ssl->cacert); - mbedtls_x509_crt_free(&s->ssl->clicert); - mbedtls_pk_free(&s->ssl->pkey); - mbedtls_ssl_free(&s->ssl->ssl_ctx); - mbedtls_ssl_config_free(&s->ssl->ssl_conf); - mbedtls_ctr_drbg_free(&s->ssl->ctr_drbg); - mbedtls_entropy_free(&s->ssl->entropy); - - rws_free(s->ssl); - s->ssl = NULL; - } - - s->socket = RWS_INVALID_SOCKET; - } - else - { - s->command = COMMAND_SEND_HANDSHAKE; + LOG_E("WSS TLS connection failed: %d\n", ret); + rws_error_delete_clean(&s->error); + s->error = rws_error_new_code_descr(rws_error_code_connect_to_host, "TLS connection or certificate verification failed"); + if (s->ssl) { + /* Do not send close_notify for an incomplete handshake. */ + mbedtls_net_free(&s->ssl->net_ctx); + mbedtls_x509_crt_free(&s->ssl->cacert); + mbedtls_x509_crt_free(&s->ssl->clicert); + mbedtls_pk_free(&s->ssl->pkey); + mbedtls_ssl_free(&s->ssl->ssl_ctx); + mbedtls_ssl_config_free(&s->ssl->ssl_conf); + mbedtls_ctr_drbg_free(&s->ssl->ctr_drbg); + mbedtls_entropy_free(&s->ssl->entropy); + rws_free(s->ssl); + s->ssl = NULL; } + s->socket = RWS_INVALID_SOCKET; + s->is_connected = rws_false; + s->command = COMMAND_INFORM_DISCONNECTED; return ret; } diff --git a/ql-application/threadx/interface/custom_app/lib/librws/src/rws_socketpub.c b/ql-application/threadx/interface/custom_app/lib/librws/src/rws_socketpub.c index b2fb20a..91882b7 100644 --- a/ql-application/threadx/interface/custom_app/lib/librws/src/rws_socketpub.c +++ b/ql-application/threadx/interface/custom_app/lib/librws/src/rws_socketpub.c @@ -94,7 +94,7 @@ void rws_socket_disconnect_and_release(rws_socket socket) s->command = COMMAND_DISCONNECT; rws_mutex_unlock(s->work_mutex); } - else if (s->work_thread) + else if (s->worker_started || s->work_thread) { // disconnected in loop s->command = COMMAND_END; @@ -104,7 +104,7 @@ void rws_socket_disconnect_and_release(rws_socket socket) { // not in loop rws_mutex_unlock(s->work_mutex); - // rws_socket_delete(s); + rws_socket_delete(s); }else{ rws_mutex_unlock(s->work_mutex); } @@ -408,7 +408,7 @@ rws_bool rws_socket_is_connected(rws_socket socket) #ifdef LIBRWS_USING_MBED_TLS void rws_socket_set_server_cert(rws_socket socket, const char *server_cert, int server_cert_len) { - LOG_D("[T:%.8d, L:%.5d, %*.*s] cert:\n%s", ql_rtos_get_systicks(), __LINE__, DBG_FUNCTION_NUM, DBG_FUNCTION_NUM, __FUNCTION__, server_cert_len, server_cert ? server_cert : ""); + LOG_D("WSS trust anchor loaded: %d bytes\n", server_cert_len); _rws_socket *s = (_rws_socket *)socket; if (s) diff --git a/ql-application/threadx/interface/custom_app/talkingq_root_ca.c b/ql-application/threadx/interface/custom_app/talkingq_root_ca.c new file mode 100644 index 0000000..23555ad --- /dev/null +++ b/ql-application/threadx/interface/custom_app/talkingq_root_ca.c @@ -0,0 +1,18 @@ +#include "talkingq_wss.h" + +/* ISRG Root X2; SHA-256 fingerprint and provenance in docs/talkingq_wss.md. */ +const char talkingq_root_ca[] = + "-----BEGIN CERTIFICATE-----\n" + "MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQswCQYDVQQGEwJV\n" + "UzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElT\n" + "UkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVT\n" + "MSkwJwYDVQQKEyBJbnRlcm5ldCBTZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNS\n" + "RyBSb290IFgyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0H\n" + "ttwW+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9ItgKbppb\n" + "d9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\n" + "HQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZIzj0EAwMDaAAwZQIwe3lORlCEwkSHRhtF\n" + "cP9Ymd70/aTSVaYgLXTWNLxBo1BfASdWtL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5\n" + "U6VR5CmD1/iQMVtCnwr1/q4AaOeMSQ+2b1tbFfLn\n" + "-----END CERTIFICATE-----\n" +; +const size_t talkingq_root_ca_size = sizeof(talkingq_root_ca); diff --git a/ql-application/threadx/interface/custom_app/talkingq_wss.c b/ql-application/threadx/interface/custom_app/talkingq_wss.c new file mode 100644 index 0000000..25d7bf6 --- /dev/null +++ b/ql-application/threadx/interface/custom_app/talkingq_wss.c @@ -0,0 +1,53 @@ +#include "talkingq_wss.h" +#include + +int talkingq_is_legacy_ws_url(const char *url) +{ + return url && (!strcmp(url, "ws://101.35.224.118:8080") || + !strcmp(url, "ws://101.35.224.118:8080/ws") || + !strcmp(url, "ws://api.talkingq.com:8080") || + !strcmp(url, "ws://175.24.73.253:80")); +} + +int talkingq_parse_wss_url(const char *url, char *host, size_t host_size, + int *port, char *path, size_t path_size) +{ + const char *begin, *end, *colon, *cursor, *resource; + size_t length; + unsigned int value = 0; + if (!url || !host || !port || !path || strncmp(url, "wss://", 6)) + return -1; + begin = url + 6; + end = strchr(begin, '/'); + if (!end) end = begin + strlen(begin); + colon = memchr(begin, ':', (size_t)(end - begin)); + length = (size_t)((colon ? colon : end) - begin); + if (!length || length >= host_size) return -1; + for (cursor = begin; cursor < begin + length; ++cursor) { + char c = *cursor; + if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '.' || c == '-')) return -1; + } + if (colon) { + if (colon + 1 == end) return -1; + for (cursor = colon + 1; cursor < end; ++cursor) { + if (*cursor < '0' || *cursor > '9') return -1; + value = value * 10 + (unsigned int)(*cursor - '0'); + if (value > 65535) return -1; + } + if (!value) return -1; + } else { + value = 443; + } + resource = *end ? end : "/ws"; + if (strlen(resource) >= path_size) return -1; + for (cursor = resource; *cursor; ++cursor) { + if ((unsigned char)*cursor <= 32 || *cursor == '#' || + (unsigned char)*cursor >= 127) return -1; + } + memcpy(host, begin, length); + host[length] = '\0'; + strcpy(path, resource); + *port = (int)value; + return 0; +} diff --git a/ql-application/threadx/interface/custom_app/ws_talkingq.c b/ql-application/threadx/interface/custom_app/ws_talkingq.c index eed21b5..47abe9c 100644 --- a/ql-application/threadx/interface/custom_app/ws_talkingq.c +++ b/ql-application/threadx/interface/custom_app/ws_talkingq.c @@ -3,6 +3,7 @@ #include #include "cJSON.h" #include "app_inc.h" +#include "talkingq_wss.h" #include "ql_rtos.h" #include "ql_application.h" #include "ql_audio.h" @@ -129,6 +130,7 @@ unsigned char get_xiaoice_connected_status(void) static void onopen(rws_socket socket) { + if (socket != app->socket || !app->init) return; wss_connect_counter=0; LOG_D("websocket connected. "); @@ -153,12 +155,14 @@ static void onclose(rws_socket socket) LOG_D("websocket disconnect! "); } - xiaoice_wss_connect_status = 0; - - rws_socket_disconnect_and_release(app->socket); + if (socket != app->socket) return; + xiaoice_wss_connect_status = 0; + app->socket = NULL; + /* The worker owns and releases this socket after the callback. */ RLOGD("onclose end\r\n"); app->init = 0; + if (ws_connect_sem) ql_rtos_semaphore_release(ws_connect_sem); hal_led_b_off(); } @@ -305,8 +309,8 @@ int talkingq_ws_connect() { int result; int ret = -1; - char wss_path[56]={0}; - char ws_talkingq_url[56]={0}; + char wss_path[TALKINGQ_PATH_CAPACITY]={0}; + char ws_talkingq_url[TALKINGQ_URL_CAPACITY]={0}; int port; // char session_id[56]={0}; // char ip_name[56]={0}; @@ -329,40 +333,33 @@ int talkingq_ws_connect() ql_rtos_semaphore_create(&ws_connect_sem, 0); } - sprintf(wss_path,"%s","/ws"); + if (ws_connect_sem) while (ql_rtos_semaphore_wait(ws_connect_sem, 0) == 0) {} hal_os_get_talkingq_url(ws_talkingq_url); printf("ws_talkingq_url %s",ws_talkingq_url); - if(strncmp(ws_talkingq_url,"ws://",5)){ - printf("error ws url %s",ws_talkingq_url); - return -1; - } - - char *testp = strstr(ws_talkingq_url+5,":"); - if(!testp){ - strcpy(talkingq_host,ws_talkingq_url+5); - port = 80; - }else{ - port = atoi(testp+1); - *testp = 0; - strcpy(talkingq_host,ws_talkingq_url+5); - } + if (talkingq_parse_wss_url(ws_talkingq_url, talkingq_host, sizeof(talkingq_host), + &port, wss_path, sizeof(wss_path)) != 0) { + LOG_E("Invalid WSS endpoint; cleartext fallback is disabled\n"); + app->init = 0; + xiaoice_wss_connect_status = 0; + return -1; + } app->socket = rws_socket_create(); if(app->socket == NULL) { - app->init = 0; + app->init = 0; + xiaoice_wss_connect_status = 0; LOG_E("librws socket create failed. "); return (-1); } printf("talkingq host %s , port %d",talkingq_host,port); - //todo wss - //rws_socket_set_url(app->socket, "ws", "hz.api.talkingq.com", 8080, wss_path); - //118.25.13.180:8080 - rws_socket_set_url(app->socket, "ws", talkingq_host, port, wss_path); + /* Server-authenticated TLS; application authentication stays in onopen. */ + rws_socket_set_url(app->socket, "wss", talkingq_host, port, wss_path); + rws_socket_set_server_cert(app->socket, talkingq_root_ca, talkingq_root_ca_size); rws_socket_set_on_connected(app->socket, &onopen); rws_socket_set_on_disconnected(app->socket, &onclose); @@ -374,7 +371,9 @@ int talkingq_ws_connect() ret = rws_socket_connect(app->socket); if(ret == rws_false) { - app->init = 0; + rws_socket_disconnect_and_release(app->socket); + app->socket = NULL; + app->init = 0; LOG_E("rws_socket_connect failed. "); ret = -1; } @@ -383,14 +382,17 @@ int talkingq_ws_connect() ret = -1; LOG_D("try rws_socket_connect. "); if(ws_connect_sem){ - result = ql_rtos_semaphore_wait(ws_connect_sem, 10000); - if(result==0){ + result = ql_rtos_semaphore_wait(ws_connect_sem, 45000); + if(result == 0 && app->init && app->socket && rws_socket_is_connected(app->socket)){ LOG_I("ws connect okay"); wss_connect_counter = 0; ret = 0; }else{ - app->init = 0; - LOG_E("ws connect wait timeout"); + /* Stop the pending worker before allowing a new attempt. */ + if (app->socket) rws_socket_disconnect_and_release(app->socket); + app->socket = NULL; + app->init = 0; + LOG_E("ws connect failed or wait timeout"); wss_connect_counter++; } }else{ @@ -399,7 +401,7 @@ int talkingq_ws_connect() } } - if(app->init == 1){ + if(ret == 0 && app->init == 1){ xiaoice_wss_connect_status = 2; hal_led_b_on(); } diff --git a/tools/probe_talkingq_wss.ps1 b/tools/probe_talkingq_wss.ps1 new file mode 100644 index 0000000..59e5237 --- /dev/null +++ b/tools/probe_talkingq_wss.ps1 @@ -0,0 +1,59 @@ +param([string]$OutputDir = "$PSScriptRoot/../artifacts/wss", [ValidateSet(443, 8080)][int]$ServerPort = 443) +$ErrorActionPreference = 'Stop' +$serverHost = 'banban.api.talkingq.com' +New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null +$OutputDir = (Resolve-Path -LiteralPath $OutputDir).Path +$tcp = New-Object System.Net.Sockets.TcpClient +$tls = $null +try { + $pending = $tcp.BeginConnect($serverHost, $serverPort, $null, $null) + if (-not $pending.AsyncWaitHandle.WaitOne(15000)) { throw 'TCP connect timeout' } + $tcp.EndConnect($pending) + $tcp.ReceiveTimeout = 15000 + $tcp.SendTimeout = 15000 + # Default SslStream validation checks the trusted chain and server hostname. + $tls = New-Object System.Net.Security.SslStream($tcp.GetStream(), $false) + $tls.ReadTimeout = 15000 + $tls.WriteTimeout = 15000 + $tls.AuthenticateAsClient($serverHost, $null, [System.Security.Authentication.SslProtocols]::Tls12, $false) + $leaf = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($tls.RemoteCertificate) + $chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain + $chain.ChainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck + if (-not $chain.Build($leaf)) { throw 'Trusted certificate chain could not be built' } + $certificates = @() + for ($i = 0; $i -lt $chain.ChainElements.Count; $i++) { + $cert = $chain.ChainElements[$i].Certificate + $pem = "-----BEGIN CERTIFICATE-----`n" + [Convert]::ToBase64String($cert.RawData, [Base64FormattingOptions]::InsertLineBreaks).Replace("`r`n", "`n") + "`n-----END CERTIFICATE-----`n" + [IO.File]::WriteAllText((Join-Path $OutputDir "chain-$i.pem"), $pem, [Text.Encoding]::ASCII) + $certificates += [ordered]@{subject=$cert.Subject; issuer=$cert.Issuer; not_before=$cert.NotBefore.ToUniversalTime().ToString('o'); not_after=$cert.NotAfter.ToUniversalTime().ToString('o'); thumbprint=$cert.Thumbprint} + if ($i -eq $chain.ChainElements.Count - 1) { + if ($cert.Subject -ne $cert.Issuer) { throw 'Chain did not terminate at a root CA' } + [IO.File]::WriteAllText((Join-Path $OutputDir 'root-ca.pem'), $pem, [Text.Encoding]::ASCII) + } + } + $nonce = New-Object byte[] 16 + $rng = [Security.Cryptography.RandomNumberGenerator]::Create() + $rng.GetBytes($nonce) + $rng.Dispose() + $key = [Convert]::ToBase64String($nonce) + $request = "GET /ws HTTP/1.1`r`nHost: ${serverHost}:${serverPort}`r`nUpgrade: websocket`r`nConnection: Upgrade`r`nSec-WebSocket-Key: $key`r`nSec-WebSocket-Version: 13`r`n`r`n" + $bytes = [Text.Encoding]::ASCII.GetBytes($request) + $tls.Write($bytes, 0, $bytes.Length) + $response = '' + while (-not $response.EndsWith("`r`n`r`n") -and $response.Length -lt 16384) { + $next = $tls.ReadByte() + if ($next -lt 0) { break } + $response += [char]$next + } + $sha1 = [Security.Cryptography.SHA1]::Create() + $expected = [Convert]::ToBase64String($sha1.ComputeHash([Text.Encoding]::ASCII.GetBytes($key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))) + $sha1.Dispose() + $accepted = $response -match '^HTTP/1\.1 101 ' -and $response -match ('(?im)^Sec-WebSocket-Accept:\s*' + [regex]::Escape($expected) + '\s*$') + $report = [ordered]@{url="wss://${serverHost}:${serverPort}/ws"; tested_utc=[DateTime]::UtcNow.ToString('o'); tls_protocol=$tls.SslProtocol.ToString(); cipher=$tls.CipherAlgorithm.ToString(); cipher_bits=$tls.CipherStrength; certificate_validation='System trust and hostname validation passed'; certificates=$certificates; websocket_upgrade=$accepted; response_headers=$response; device_auth_sent=$false} + $report | ConvertTo-Json -Depth 6 | Set-Content -Encoding UTF8 -LiteralPath (Join-Path $OutputDir 'server-probe.json') + $report | ConvertTo-Json -Depth 6 + if (-not $accepted) { throw 'WebSocket upgrade did not return a valid HTTP 101 response' } +} finally { + if ($tls) { $tls.Dispose() } + $tcp.Close() +} diff --git a/tools/probe_wss_root.py b/tools/probe_wss_root.py new file mode 100644 index 0000000..c57ef97 --- /dev/null +++ b/tools/probe_wss_root.py @@ -0,0 +1,36 @@ +import hashlib +import json +import re +import socket +import ssl +from pathlib import Path + +root = Path(__file__).resolve().parents[1] +source = (root / 'ql-application/threadx/interface/custom_app/talkingq_root_ca.c').read_text() +pem = ''.join(json.loads('"' + value + '"') for value in re.findall(r'"(.*?)"', source) if 'CERTIFICATE' in value or value.endswith(r'\n')) +context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) +context.load_verify_locations(cadata=pem) +context.minimum_version = ssl.TLSVersion.TLSv1_2 +context.maximum_version = ssl.TLSVersion.TLSv1_2 +server = 'banban.api.talkingq.com' +report = {'root_sha256': hashlib.sha256(ssl.PEM_cert_to_DER_cert(pem)).hexdigest(), 'trust': 'only the compiled root CA; no OS trust or intermediate download'} +with socket.create_connection((server, 443), timeout=15) as tcp: + with context.wrap_socket(tcp, server_hostname=server) as tls: + report['tls'] = tls.version() + report['cipher'] = tls.cipher() + report['root_only_validation'] = 'PASS' +try: + with socket.create_connection((server, 443), timeout=15) as tcp: + with context.wrap_socket(tcp, server_hostname='wrong.invalid'): + raise AssertionError('wrong hostname accepted') +except ssl.SSLCertVerificationError: + report['wrong_hostname_rejected'] = True +empty = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) +try: + with socket.create_connection((server, 443), timeout=15) as tcp: + with empty.wrap_socket(tcp, server_hostname=server): + raise AssertionError('untrusted CA accepted') +except ssl.SSLCertVerificationError: + report['untrusted_ca_rejected'] = True +(root / 'artifacts/wss/root-only-probe.json').write_text(json.dumps(report, indent=2)) +print(json.dumps(report, indent=2)) diff --git a/tools/test_wss.c b/tools/test_wss.c new file mode 100644 index 0000000..2f68845 --- /dev/null +++ b/tools/test_wss.c @@ -0,0 +1,58 @@ +#include +#include +#include +#include "talkingq_wss.h" +#include "rws_handshake.h" + +static const char accept[] = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="; +static const char good[] = "HTTP/1.1 101 Switching Protocols\r\n" + "uPgRaDe: WebSocket\r\nConnection: keep-alive, Upgrade\r\n" + "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; + +int main(void) +{ + char host[64], path[128], data[RWS_HANDSHAKE_MAX + 20]; + int port, status; + size_t i, header; + const char *bad[] = {"ws://example.com/ws", "https://example.com/ws", "wss:///ws", + "wss://user@example.com/ws", "wss://example.com:0/ws", "wss://example.com:65536/ws", + "wss://example.com:-1/ws", "wss://example.com:/ws", "wss://example.com:abc/ws", + "wss://example.com/ws#frag", "wss://example.com/ws\r\nInjected:yes", + "wss://example.com:99999999999999999999999/ws", "wss://example.com/bad path"}; + assert(!talkingq_parse_wss_url(TALKINGQ_WSS_URL, host, sizeof(host), &port, path, sizeof(path))); + assert(!strcmp(host, "banban.api.talkingq.com") && port == 443 && !strcmp(path, "/ws")); + assert(!talkingq_parse_wss_url("wss://example.com", host, sizeof(host), &port, path, sizeof(path))); + assert(port == 443 && !strcmp(path, "/ws")); + assert(!talkingq_parse_wss_url("wss://example.com:8080/ws?x=y", host, sizeof(host), &port, path, sizeof(path))); + assert(port == 8080 && !strcmp(path, "/ws?x=y")); + for (i = 0; i < sizeof(bad)/sizeof(bad[0]); ++i) + assert(talkingq_parse_wss_url(bad[i], host, sizeof(host), &port, path, sizeof(path)) == -1); + assert(talkingq_parse_wss_url(TALKINGQ_WSS_URL, host, 3, &port, path, sizeof(path)) == -1); + assert(talkingq_parse_wss_url(TALKINGQ_WSS_URL, host, sizeof(host), &port, path, 3) == -1); + assert(talkingq_is_legacy_ws_url("ws://101.35.224.118:8080")); + assert(talkingq_is_legacy_ws_url("ws://101.35.224.118:8080/ws")); + assert(!talkingq_is_legacy_ws_url(TALKINGQ_WSS_URL)); + assert(!talkingq_is_legacy_ws_url("ws://custom.example:8080/ws")); + /* Every possible TLS fragment boundary, with no NUL terminator available. */ + for (i = 0; i < sizeof(good)-1; ++i) { + memset(data, 'X', sizeof(data)); + memcpy(data, good, i); + assert(rws_check_handshake(data, i, accept, &header, &status) == 0); + } + memcpy(data, good, sizeof(good)-1); + ((unsigned char *)data)[sizeof(good)-1] = 0x81; data[sizeof(good)] = 0; + assert(rws_check_handshake(data, sizeof(good)+1, accept, &header, &status) == 1); + assert(status == 101 && header == sizeof(good)-1); + assert(rws_check_handshake(data, sizeof(good)-1, "wrong", &header, &status) == -1); + memcpy(data + 9, "404", 3); + assert(rws_check_handshake(data, sizeof(good)-1, accept, &header, &status) == -1 && status == 404); + strcpy(data, "HTTP/1.1 101 OK\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"); + assert(rws_check_handshake(data, strlen(data), accept, &header, &status) == -1); + memcpy(data, good, sizeof(good)); + strcpy(data + sizeof(good)-3, "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"); + assert(rws_check_handshake(data, strlen(data), accept, &header, &status) == -1); + memset(data, 'X', sizeof(data)); + assert(rws_check_handshake(data, sizeof(data), accept, &header, &status) == -1); + puts("PASS: WSS URL policy, legacy migration, fragmented upgrade, status, accept, headers and limits"); + return 0; +} diff --git a/tools/test_wss.cmd b/tools/test_wss.cmd new file mode 100644 index 0000000..73ddaea --- /dev/null +++ b/tools/test_wss.cmd @@ -0,0 +1,13 @@ +@echo off +setlocal +cd /d "%~dp0.." +if not exist artifacts\wss mkdir artifacts\wss +where cl >nul 2>nul +if errorlevel 1 call "C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\VC\Auxiliary\Build\vcvars64.bat" >nul +if errorlevel 1 exit /b 1 +cl /nologo /W4 /WX /D_CRT_SECURE_NO_WARNINGS /TC /Iql-application\threadx\interface\custom_app\inc /Iql-application\threadx\interface\custom_app\lib\librws\inc tools\test_wss.c ql-application\threadx\interface\custom_app\talkingq_wss.c ql-application\threadx\interface\custom_app\lib\librws\src\rws_handshake.c /Foartifacts\wss\ /Feartifacts\wss\test_wss.exe +if errorlevel 1 exit /b 1 +artifacts\wss\test_wss.exe +if errorlevel 1 exit /b 1 +ql-cross-tool\win32\owtoolchain\Python\python.exe tools\test_wss_io.py +exit /b %errorlevel% diff --git a/tools/test_wss_io.py b/tools/test_wss_io.py new file mode 100644 index 0000000..1d2f3a1 --- /dev/null +++ b/tools/test_wss_io.py @@ -0,0 +1,80 @@ +"""Compile actual SDK-independent function bodies with scripted TLS/RTOS stubs.""" +from pathlib import Path +import subprocess + +root = Path(__file__).resolve().parents[1] +lib = root / 'ql-application/threadx/interface/custom_app/lib/librws/src' +private = (lib / 'rws_socketpriv.c').read_bytes().decode('latin1') +public = (lib / 'rws_socketpub.c').read_bytes().decode('latin1') +send = private[private.index('static int rws_tls_send_all('):].split('#endif', 1)[0] +cancel = public[public.index('void rws_socket_disconnect_and_release('):].split('rws_bool rws_socket_send_text(', 1)[0] +harness = r''' +#include +#include +#include +#define MBEDTLS_ERR_SSL_WANT_READ -2 +#define MBEDTLS_ERR_SSL_WANT_WRITE -3 +#define COMMAND_NONE 0 +#define COMMAND_DISCONNECT 6 +#define COMMAND_END 9999 +typedef struct {int ssl_ctx;} tls; +typedef struct {tls *ssl; int work_mutex, is_connected, worker_started, work_thread, command;} _rws_socket; +typedef _rws_socket *rws_socket; +static int replies[16], calls, reply_count, expected_offset, freed, locks; +static unsigned int ticks; +static const unsigned char bytes[] = "abcdef"; +static unsigned int ql_rtos_get_systicks(void) { return ticks; } +static void rws_thread_sleep(int ms) { ticks += (unsigned int)ms; } +static int mbedtls_ssl_write(int *ctx, const unsigned char *data, size_t size) { + int ret; + (void)ctx; + assert(data == bytes + expected_offset); + assert(size == 6 - (size_t)expected_offset); + ret = calls < reply_count ? replies[calls] : MBEDTLS_ERR_SSL_WANT_WRITE; + ++calls; + if (ret > 0) expected_offset += ret; + return ret; +} +static void rws_mutex_lock(int mutex) {(void)mutex; ++locks;} +static void rws_mutex_unlock(int mutex) {(void)mutex; --locks;} +static void rws_socket_delete(_rws_socket *s) {(void)s; assert(locks == 0); ++freed;} +''' +harness += send + cancel +harness += r''' +int main(void) { + tls ctx = {0}; + _rws_socket s = {0}; + s.ssl = &ctx; + replies[0] = -2; replies[1] = 2; replies[2] = -3; replies[3] = 4; reply_count = 4; + assert(rws_tls_send_all(&s, bytes, 6) == 6 && calls == 4 && expected_offset == 6); + calls = 0; expected_offset = 0; replies[0] = 2; replies[1] = -99; reply_count = 2; + assert(rws_tls_send_all(&s, bytes, 6) == -1 && calls == 2); + calls = 0; expected_offset = 0; replies[0] = 0; reply_count = 1; + assert(rws_tls_send_all(&s, bytes, 6) == -1 && calls == 1); + calls = 0; expected_offset = 0; reply_count = 0; ticks = 0; + assert(rws_tls_send_all(&s, bytes, 6) == -1 && ticks >= 1000 && calls < 1000); + calls = 0; ticks = (unsigned int)-500; + assert(rws_tls_send_all(&s, bytes, 6) == -1 && calls < 1000); + s.ssl = NULL; calls = 0; + assert(rws_tls_send_all(&s, bytes, 6) == -1 && calls == 0); + s.is_connected = 1; rws_socket_disconnect_and_release(&s); + assert(s.command == COMMAND_DISCONNECT && locks == 0 && !freed); + s.is_connected = 0; s.worker_started = 1; s.command = 1; + rws_socket_disconnect_and_release(&s); + assert(s.command == COMMAND_END && locks == 0 && !freed); + s.worker_started = 0; s.command = COMMAND_NONE; + rws_socket_disconnect_and_release(&s); + assert(freed == 1 && locks == 0); + rws_socket_disconnect_and_release(NULL); + puts("PASS: TLS partial writes, WANT retry, fatal/EOF, timeout, tick wrap, null context, worker cancellation"); + return 0; +} +''' +output = root / 'artifacts/wss' +output.mkdir(parents=True, exist_ok=True) +cfile = output / 'test_wss_io.c' +cfile.write_text(harness, encoding='utf-8') +exe = output / 'test_wss_io.exe' +subprocess.run(['cl', '/nologo', '/W4', '/WX', '/TC', str(cfile), + '/Fo' + str(output / 'test_wss_io.obj'), '/Fe' + str(exe)], check=True) +subprocess.run([str(exe)], check=True) diff --git a/tools/verify_wss_package.py b/tools/verify_wss_package.py new file mode 100644 index 0000000..d7d4101 --- /dev/null +++ b/tools/verify_wss_package.py @@ -0,0 +1,46 @@ +"""Verify the WSS flash package against the current legacy-board build.""" +import hashlib +import json +import re +import subprocess +import zipfile +from pathlib import Path + +root = Path(__file__).resolve().parents[1] +output = root / 'artifacts/wss' +package = output / 'EG800AKCN_11LG-legacy-wss.zip' +objdump = root / 'ql-cross-tool/win32/owtoolchain/gcc-arm-none-eabi/bin/arm-none-eabi-objdump.exe' +app = root / 'ql-application/threadx/build/app.elf' +symbols = subprocess.check_output([str(objdump.with_name('arm-none-eabi-nm.exe')), str(app)], text=True) +required = ['talkingq_parse_wss_url', 'talkingq_root_ca', 'rws_check_handshake', 'rws_ssl_conn', + 'mbedtls_ssl_set_hostname', 'mbedtls_ssl_get_verify_result', 'mbedtls_ssl_conf_authmode', + 'mbedtls_ctr_drbg_random', 'mbedtls_sha1', 'mbedtls_base64_encode'] +for symbol in required: + assert re.search(r'\b' + symbol + r'$', symbols, re.M), 'Missing linked symbol: ' + symbol +report = {'package': package.name, 'sha256': hashlib.sha256(package.read_bytes()).hexdigest(), + 'linked_symbols': required, 'hardware_test': 'Not performed', + 'backend': '443 /ws returns HTTP 404; 8080 is plaintext WS'} +with zipfile.ZipFile(package) as archive: + assert archive.testzip() is None, 'ZIP CRC failure' + for name, source in {'cp.bin': 'ql-kernel/threadx/build/release_kernel.bin', + 'customer_app.bin': 'ql-application/threadx/build/app.bin'}.items(): + data = archive.read(name) + assert data == (root / source).read_bytes(), 'Stale image: ' + name + report[name] = {'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest()} + assert b'wss://banban.api.talkingq.com:443/ws' in archive.read('customer_app.bin') + report['download_manifest'] = json.loads(archive.read('download.json')) +with zipfile.ZipFile(root / 'artifacts/legacy-no-gnss/EG800AKCN_11LG-legacy-no-gnss.zip') as old: + with zipfile.ZipFile(package) as new: + assert old.read('cp.bin') == new.read('cp.bin'), 'Legacy kernel changed unexpectedly' +printf_address = int(re.search(r'^([0-9a-fA-F]+) T printf$', symbols, re.M).group(1), 16) +for symbol, binary, expected in [ + ('quec_get_gnssid_task', root / 'ql-kernel/threadx/build/kernel.axf', ''), + ('app_gnss_init', app, '0x%08x' % (printf_address | 1)), + ('set_gnss_mode', app, 'bx\tlr')]: + body = subprocess.check_output([str(objdump), '-d', '--disassemble=' + symbol, str(binary)], text=True) + assert '<' + symbol + '>:' in body and expected in body and 'task_create' not in body + (output / (symbol + '.disasm.txt')).write_text(body) +report['verification'] = 'PASS: ZIP CRC, current APP/kernel identity, unchanged legacy kernel, GNSS bypass, WSS URL and TLS symbols' +(output / 'verification.json').write_text(json.dumps(report, indent=2)) +print(report['verification']) +print('SHA256:', report['sha256'])