HttpClient 是整个 OTX 代理插件云端通信栈的传输基石——它封装了 libcurl 的底层细节,向上提供统一的 HTTP GET/POST 接口,同时承担客户端证书注入、Bearer Token 鉴权和请求体脱敏三项关键职责。JiduClient 通过公有继承复用其传输能力,在 callServer 的路由分发逻辑中按需注入 PKI 客户端证书或诊断服务 Token,形成"基类负责传输,子类负责策略"的清晰分层。
Sources: HttpClient.h, jidu_client.h
类结构总览
HttpClient 是一个无虚析构函数的实体基类,其设计目标不是实现多态替换,而是提供可继承复用的 HTTP 能力。所有核心方法均非 virtual,子类 JiduClient 直接调用基类的 post、get、setClientCert、setClientKey、setToken 完成网络操作。
classDiagram
class HttpClient {
-bool debugVerbose
-string clientCert
-string clientKey
-string token
-string deviceSn
-desensitizeReport(cJSON* obj)
+post(string url, string postParams, string& response) bool
+get(string url, string& response) bool
+setClientCert(string& cert) bool
+setClientKey(string& key) bool
+setToken(string token) bool
+setDeviceSn(string deviceSn) bool
}
class JiduClient {
+string certPem
+string keyPem
+callServer(int iType, string inParams, char* outBuffer, unsigned int outBufferSize) int
-gen_pem(string p12Path, string passwd, string& pemCertBuffer, string& pemKeyBuffer)
-getAccessToken(string& token) bool
-loadParam() bool
}
class parse_cert_p12 {
<<OpenSSL function>>
+parse_cert_p12(char* p12Path, char* passwd, char* pemBuffer, char* keyBuffer) int
}
HttpClient <|-- JiduClient : public 继承
JiduClient ..> parse_cert_p12 : 调用Sources: HttpClient.h, jidu_client.h, algorithm.h
libcurl 全局配置与回调机制
HttpClient 在模块级定义了两个 curl_blob 结构体——blob_clientCert 和 blob_clientKey——作为证书数据的内存载体。这两个 blob 不属于任何类实例,而是文件作用域内的全局变量,这意味着同一进程中所有 HttpClient 实例共享同一份证书 blob 引用。这并非最佳实践,但由于实际使用场景中 JiduClient 是单例(getInstance()),因此不会产生竞态问题。
Sources: HttpClient.cpp
响应数据的接收通过静态回调函数 responseCallBack 完成:
size_t responseCallBack(char* contents, size_t size, size_t nmemb, void* userp) {
size_t realsize = size * nmemb;
std::string* buffer = reinterpret_cast<std::string*>(userp);
if (buffer)
buffer->append((char*)contents, realsize);
return realsize;
}该回调被 libcurl 在每次收到数据块时调用,将分块数据追加到 CURLOPT_WRITEDATA 指定的 std::string 缓冲区中。采用 append 而非覆盖策略,确保大数据量响应被完整拼接。
Sources: HttpClient.cpp
POST 请求:全链路分析
HttpClient::post 是系统中最核心的传输方法,承载了向集度服务器上报诊断数据、请求安全常量、获取车辆配置等几乎所有写操作。其执行流程如下:
sequenceDiagram
participant Caller as JiduClient::callServer
participant HC as HttpClient::post
participant Curl as libcurl
participant Tracer as Tracer 日志
Caller->>HC: post(url, postParams, response)
Note over HC: 请求体日志与脱敏
HC->>HC: cJSON_Parse(postParams)
HC->>HC: desensitizeReport(obj)
HC->>Tracer: json("callServer.body", obj)
Note over HC: curl 初始化与请求头设置
HC->>Curl: curl_easy_init()
HC->>Curl: 设置 User-Agent (IE11)
HC->>Curl: 设置 Content-Type: application/json
HC->>Curl: 设置 Authorization: Bearer {token}
Note over HC: TCP 与 SSL 配置
HC->>Curl: TCP Keepalive (idle=120s, intvl=60s)
HC->>Curl: SSL_VERIFYPEER=false, SSL_VERIFYHOST=false
HC->>Curl: 注入 clientCert/clientKey blob (如已设置)
Note over HC: 超时与执行
HC->>Curl: CONNECTTIMEOUT=20s, TIMEOUT=20s
HC->>Curl: curl_easy_perform()
alt 请求成功
HC->>HC: 清除 clientCert/clientKey
HC-->>Caller: return true
else 请求失败
HC->>Tracer: error("curl_easy_perform()", errStr)
HC-->>Caller: return false
endSources: HttpClient.cpp
POST 方法中的关键配置项总结如下:
| 配置项 | 设置值 | 说明 |
|---|---|---|
CURLOPT_HTTPHEADER | User-Agent + Content-Type + Authorization | 模拟 IE11 浏览器,JSON 内容类型,Bearer Token 鉴权 |
CURLOPT_HEADER | false | 不接收响应头,仅保留响应体 |
CURLOPT_FOLLOWLOCATION | 1 | 支持 HTTP 重定向跟随 |
CURLOPT_TCP_KEEPALIVE | 1L | 启用 TCP 长连接保活 |
CURLOPT_TCP_KEEPIDLE | 120L | 空闲 120 秒后开始发送 keepalive 探测 |
CURLOPT_TCP_KEEPINTVL | 60L | keepalive 探测间隔 60 秒 |
CURLOPT_SSL_VERIFYPEER | false | 跳过服务端证书验证 |
CURLOPT_SSL_VERIFYHOST | false | 跳过主机名验证 |
CURLOPT_SSLCERTTYPE | "PEM" | 客户端证书格式为 PEM |
CURLOPT_SSLCERT_BLOB | &blob_clientCert | 内存中的客户端证书 |
CURLOPT_SSLKEYTYPE | "PEM" | 客户端私钥格式为 PEM |
CURLOPT_SSLKEY_BLOB | &blob_clientKey | 内存中的客户端私钥 |
CURLOPT_CONNECTTIMEOUT | 20 | 连接超时 20 秒 |
CURLOPT_TIMEOUT | 20 | 整体请求超时 20 秒 |
CURLOPT_VERBOSE | debugVerbose | 调试模式下输出 curl 详细信息 |
Sources: HttpClient.cpp
值得特别关注的是 SSL 验证被完全关闭(SSL_VERIFYPEER=false 且 SSL_VERIFYHOST=false)。这意味着 libcurl 不会验证服务端证书链的有效性,也不会检查主机名是否与证书匹配。这一设计可能出于内网环境或自签名证书的考虑,但在公网场景下存在中间人攻击风险。客户端侧的认证通过双向 TLS 实现——子类 JiduClient 在处理 PKI 请求前注入 PEM 格式的客户端证书和私钥。
Sources: HttpClient.cpp
另一个值得注意的设计是:每次 POST 请求完成后,clientCert 和 clientKey 被无条件清空(第 103-104 行)。这意味着证书是一次性注入的——每次需要双向认证的请求前,调用方必须重新调用 setClientCert/setClientKey。这种"即用即弃"策略避免了证书在内存中长期驻留,但也要求上层调用者(JiduClient::callServer)在每次 PKI 请求前重新设置。
Sources: HttpClient.cpp, jidu_client.cpp
GET 请求:差异分析
HttpClient::get 与 POST 方法共享相同的 SSL 和证书注入逻辑,但存在以下关键差异:
| 维度 | POST | GET |
|---|---|---|
| HTTP 方法 | CURLOPT_POST=true + CURLOPT_POSTFIELDS | CURLOPT_CUSTOMREQUEST="GET" |
| 请求头 | Authorization: Bearer {token} | 多 Accept: application/json + source: diagnosis |
| 重定向跟随 | FOLLOWLOCATION=1 | FOLLOWLOCATION=false |
| TCP Keepalive | 启用(idle 120s, intvl 60s) | 未设置 |
| 超时设置 | CONNECTTIMEOUT=20 + TIMEOUT=20 | 仅 TIMEOUT=20 |
| 请求体脱敏 | 调用 desensitizeReport | 无 |
Sources: HttpClient.cpp
GET 请求不跟随重定向是为了避免在诊断场景下被意外跳转到非预期的端点,确保请求的确定性。GET 方法添加了 source: diagnosis 头,用于服务端识别请求来源。
证书管理:P12 → PEM 的完整转换链
客户端证书的生命周期始于 P12 文件,终于内存中的 PEM blob,其间经历了一条精密的转换链:
flowchart TD
A["pver 文件\n(加密的 certPwd + certName)"] --> B["loadParam()\n读取并解析 JSON"]
B --> C["base64_decode\n解码 certPwd"]
C --> D["des_cbc_pkcs5_decrypt\nDES-CBC 解密 certPwd"]
D --> E["拼接 certPath:\ncertDir + certName"]
E --> F["gen_pem()\nP12 路径 + 解密后密码"]
F --> G["parse_cert_p12()\nOpenSSL PKCS12_parse"]
G --> H["PEM_write_bio_X509\n提取客户端证书 → certPem"]
G --> I["PEM_write_bio_PrivateKey\n提取私钥 → keyPem"]
H --> J["JiduClient::certPem\nJiduClient::keyPem"]
I --> J
J --> K["callServer 中:\nsetClientCert(certPem)\nsetClientKey(keyPem)"]
K --> L["curl_blob\nCURLOPT_SSLCERT_BLOB\nCURLOPT_SSLKEY_BLOB"]Sources: jidu_client.cpp, jidu_client.cpp, algorithm.cpp
loadParam:证书加载的入口
JiduClient::loadParam 是证书加载的协调者。它从 pver 文件中读取 JSON 结构,依次提取 deviceID(设备序列号)、certPwd(加密的证书密码)和 certName(证书文件名),然后执行解密和转换。
证书密码采用了两层保护:首先通过 Base64 解码将密文从字符串转为二进制,然后通过 DES-CBC-PKCS5 解密(密钥固定为 "siuhuali")还原明文密码。这种设计确保了即使 pver 文件被非授权访问,P12 证书密码也不直接暴露。
证书目录 certDir 由 loadEnvirment() 根据当前环境(Dev/Test/Staging/Prod)和工厂类型(PMA/DMA)预先设置。最终证书路径由 certDir + certName 拼接而成,不同环境和工厂对应不同的物理目录,例如 pkiCertificate/PMA/Prod/ 或 pkiCertificate/DMA/Dev/。
Sources: jidu_client.cpp, jidu_client.cpp
parse_cert_p12:OpenSSL 核心转换
parse_cert_p12(位于 algorithm.cpp)是证书格式转换的底层实现。它使用 OpenSSL 的 PKCS#12 API 完成以下步骤:
- BIO 读取:通过
BIO_read_filename将 P12 文件读入内存 BIO - DER 解析:
d2i_PKCS12_bio将 DER 编码的 P12 数据解析为PKCS12结构 - 密码解密:
PKCS12_parse使用提供的密码解密并提取EVP_PKEY(私钥)、X509(证书)和STACK_OF(X509)(CA 证书链) - PEM 导出:通过内存 BIO(
BIO_s_mem)分别将 X509 证书通过PEM_write_bio_X509和私钥通过PEM_write_bio_PrivateKey写入 PEM 格式的字符缓冲区
这里有一个值得注意的实现细节:gen_pem 中调用 parse_cert_p12 时,传入的缓冲区大小为固定的 1024 字节(char tempCertBuff[1024]、char tempKeyBuff[1024])。如果 P12 证书或私钥的 PEM 表示超过此限制,将发生缓冲区溢出。
Sources: algorithm.cpp, jidu_client.cpp
curl_blob:内存证书注入
setClientCert 和 setClientKey 方法将 PEM 字符串包装为 curl_blob 结构体:
bool HttpClient::setClientCert(string& cert) {
this->clientCert = cert;
blob_clientCert.data = (void*)this->clientCert.c_str();
blob_clientCert.len = this->clientCert.size();
blob_clientCert.flags = CURL_BLOB_COPY;
return true;
}CURL_BLOB_COPY 标志指示 libcurl 复制 blob 内容而非引用原始指针。这意味着即使后续 this->clientCert 被清空(POST/GET 完成后),libcurl 内部仍持有证书副本,直到 curl_easy_cleanup 释放。结合 CURLOPT_SSLCERT_BLOB 和 CURLOPT_SSLKEY_BLOB 选项,证书数据完全在内存中流转,避免了写入临时文件的磁盘 I/O 和安全风险。
Sources: HttpClient.cpp
请求体脱敏:desensitizeReport
HttpClient::post 在发送请求前会对 JSON 请求体执行脱敏处理,防止敏感诊断数据以明文形式写入日志。脱敏逻辑聚焦于特定诊断服务的数据写入操作:
flowchart TD
A["POST 请求体 JSON"] --> B{"包含 testData?"}
B -->|否| Z["跳过脱敏"]
B -->|是| C["遍历 testData 数组"]
C --> D{"包含 testItemList?"}
D -->|否| C
D -->|是| E["遍历 testItemList 数组"]
E --> F{"包含 testItem?"}
F -->|否| E
F -->|是| G["遍历 testItem 数组"]
G --> H{"serverId == '2E'?"}
H -->|是| I["脱敏 did 字段\n保留 8 位明文"]
H -->|否| J{"serverId == '31'\n且 did 以 '01 80 20' 开头?"}
J -->|是| I
J -->|否| GserverId "2E" 对应 UDS 诊断中的 WriteDataByIdentifier(通过 DID 写入数据)服务,serverId "31" 对应 RoutineControl(例程控制)服务。脱敏条件仅在 DID 数据以 "01 80 20" 开头时触发,这通常对应于安全访问或密钥写入等敏感操作的 DID 前缀。
脱敏操作调用 MyStringUtils::desensitize(srcString, 8),保留字符串前 8 个字符的明文,其余部分替换为脱敏字符(如 *)。原始 JSON 对象中的 did 字段被就地替换为脱敏后的值,然后通过 Tracer::json 输出到日志。
Sources: HttpClient.cpp
继承体系中的调用模式
JiduClient 作为 HttpClient 的唯一子类,在 callServer 中根据目标主机类型采用三种不同的调用策略:
| 目标主机 | 鉴权方式 | 调用的 HttpClient 方法 | 典型场景 |
|---|---|---|---|
| eol (EOL 服务) | 无需额外鉴权 | get() / post() 直接调用 | 车辆订单信息、软件包配置查询 |
| pki (PKI 服务) | 客户端证书(双向 TLS) | 先 setClientCert/setClientKey,再 get()/post() | 证书申请、安全常量获取 |
| diag / cfg (诊断/配置服务) | Bearer Token | 先 setToken(token),再构造代理 JSON 通过 post() 发送 | 诊断数据上报、配置查询 |
PKI 路由的调用模式最能体现 HttpClient 证书管理的设计意图:
// jidu_client.cpp callServer 中 pki 分支
url = this->pkiHost + vtable[iType].url;
setClientCert(this->certPem); // 注入客户端证书
setClientKey(this->keyPem); // 注入客户端私钥
// ... 然后调用 post/get,完成后基类自动清除证书这种"注入-使用-自动清除"的模式将证书的生命周期严格限定在单次请求的范围内。对于 diag/cfg 路由,Token 通过 setToken 设置后也仅在当前 post 调用中通过 Authorization 请求头携带。
Sources: jidu_client.cpp, jidu_client.cpp
设计权衡与注意事项
SSL 验证关闭的风险:CURLOPT_SSL_VERIFYPEER 和 CURLOPT_SSL_VERIFYHOST 均被设为 false,这绕过了 TLS 最核心的安全保障——服务端身份验证。在生产环境中,这意味着任何能够劫持网络路径的攻击者都可以冒充集度服务器。客户端证书的双向认证部分缓解了此问题(服务端仍需验证客户端证书),但无法防御主动中间人攻击。建议在生产部署中启用 SSL_VERIFYPEER 并配置正确的 CA 证书包。
证书 blob 的全局状态:blob_clientCert 和 blob_clientKey 是文件作用域的全局变量,而非类成员。虽然 JiduClient 的单例模式避免了多实例竞争,但这一设计限制了 HttpClient 作为独立基类的可复用性——如果将来需要多个不同证书的并发 HTTP 客户端,当前结构无法直接支持。
缓冲区大小硬编码:gen_pem 中 PEM 缓冲区固定为 1024 字节。标准 RSA 2048 位私钥的 PEM 表示通常在 1700 字节左右,如果使用更长的密钥(如 RSA 4096),可能超出缓冲区容量。
超时策略:POST 和 GET 均使用 20 秒固定超时,无重试机制。在网络不稳定或服务端响应慢的场景下,调用方需要通过上层逻辑(如 callServer 的返回码检查)自行处理超时重试。
Sources: HttpClient.cpp, HttpClient.cpp, jidu_client.cpp, HttpClient.cpp
与其他模块的关系
- JiduClient:与集度服务器的 HTTPS 双向认证通信客户端:
JiduClient通过公有继承复用HttpClient的传输能力,在callServer中按路由策略注入证书或 Token。 - callServer 接口设计:请求类型路由、预处理/后处理与脱敏机制:详细描述了
callServer中如何根据iType和vtable分发到不同的 HTTP 调用策略。 - 加密算法集:AES、DES-CBC、SHA256、ECDSA 签名与 Base64 编解码:
parse_cert_p12和 ECDSA 签名的底层实现位于algorithm.cpp。 - 证书管理:P12 解析、OTA 证书与 ECU 级安全常量获取:更详细的证书分类与 ECU 级证书管理见该文档。
- Tracer:分级日志系统(Off/Logger/Error/Warn/Info/Debug):
HttpClient中所有日志输出均通过Tracer静态方法完成。