Skip to content

HttpClient 是整个 OTX 代理插件云端通信栈的传输基石——它封装了 libcurl 的底层细节,向上提供统一的 HTTP GET/POST 接口,同时承担客户端证书注入、Bearer Token 鉴权和请求体脱敏三项关键职责。JiduClient 通过公有继承复用其传输能力,在 callServer 的路由分发逻辑中按需注入 PKI 客户端证书或诊断服务 Token,形成"基类负责传输,子类负责策略"的清晰分层。

Sources: HttpClient.h, jidu_client.h

类结构总览

HttpClient 是一个无虚析构函数的实体基类,其设计目标不是实现多态替换,而是提供可继承复用的 HTTP 能力。所有核心方法均非 virtual,子类 JiduClient 直接调用基类的 postgetsetClientCertsetClientKeysetToken 完成网络操作。

mermaid
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_clientCertblob_clientKey——作为证书数据的内存载体。这两个 blob 不属于任何类实例,而是文件作用域内的全局变量,这意味着同一进程中所有 HttpClient 实例共享同一份证书 blob 引用。这并非最佳实践,但由于实际使用场景中 JiduClient 是单例(getInstance()),因此不会产生竞态问题。

Sources: HttpClient.cpp

响应数据的接收通过静态回调函数 responseCallBack 完成:

cpp
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 是系统中最核心的传输方法,承载了向集度服务器上报诊断数据、请求安全常量、获取车辆配置等几乎所有写操作。其执行流程如下:

mermaid
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
    end

Sources: HttpClient.cpp

POST 方法中的关键配置项总结如下:

配置项设置值说明
CURLOPT_HTTPHEADERUser-Agent + Content-Type + Authorization模拟 IE11 浏览器,JSON 内容类型,Bearer Token 鉴权
CURLOPT_HEADERfalse不接收响应头,仅保留响应体
CURLOPT_FOLLOWLOCATION1支持 HTTP 重定向跟随
CURLOPT_TCP_KEEPALIVE1L启用 TCP 长连接保活
CURLOPT_TCP_KEEPIDLE120L空闲 120 秒后开始发送 keepalive 探测
CURLOPT_TCP_KEEPINTVL60Lkeepalive 探测间隔 60 秒
CURLOPT_SSL_VERIFYPEERfalse跳过服务端证书验证
CURLOPT_SSL_VERIFYHOSTfalse跳过主机名验证
CURLOPT_SSLCERTTYPE"PEM"客户端证书格式为 PEM
CURLOPT_SSLCERT_BLOB&blob_clientCert内存中的客户端证书
CURLOPT_SSLKEYTYPE"PEM"客户端私钥格式为 PEM
CURLOPT_SSLKEY_BLOB&blob_clientKey内存中的客户端私钥
CURLOPT_CONNECTTIMEOUT20连接超时 20 秒
CURLOPT_TIMEOUT20整体请求超时 20 秒
CURLOPT_VERBOSEdebugVerbose调试模式下输出 curl 详细信息

Sources: HttpClient.cpp

值得特别关注的是 SSL 验证被完全关闭SSL_VERIFYPEER=falseSSL_VERIFYHOST=false)。这意味着 libcurl 不会验证服务端证书链的有效性,也不会检查主机名是否与证书匹配。这一设计可能出于内网环境或自签名证书的考虑,但在公网场景下存在中间人攻击风险。客户端侧的认证通过双向 TLS 实现——子类 JiduClient 在处理 PKI 请求前注入 PEM 格式的客户端证书和私钥。

Sources: HttpClient.cpp

另一个值得注意的设计是:每次 POST 请求完成后,clientCertclientKey 被无条件清空(第 103-104 行)。这意味着证书是一次性注入的——每次需要双向认证的请求前,调用方必须重新调用 setClientCert/setClientKey。这种"即用即弃"策略避免了证书在内存中长期驻留,但也要求上层调用者(JiduClient::callServer)在每次 PKI 请求前重新设置。

Sources: HttpClient.cpp, jidu_client.cpp

GET 请求:差异分析

HttpClient::get 与 POST 方法共享相同的 SSL 和证书注入逻辑,但存在以下关键差异:

维度POSTGET
HTTP 方法CURLOPT_POST=true + CURLOPT_POSTFIELDSCURLOPT_CUSTOMREQUEST="GET"
请求头Authorization: Bearer {token}Accept: application/json + source: diagnosis
重定向跟随FOLLOWLOCATION=1FOLLOWLOCATION=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,其间经历了一条精密的转换链:

mermaid
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 证书密码也不直接暴露。

证书目录 certDirloadEnvirment() 根据当前环境(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 完成以下步骤:

  1. BIO 读取:通过 BIO_read_filename 将 P12 文件读入内存 BIO
  2. DER 解析d2i_PKCS12_bio 将 DER 编码的 P12 数据解析为 PKCS12 结构
  3. 密码解密PKCS12_parse 使用提供的密码解密并提取 EVP_PKEY(私钥)、X509(证书)和 STACK_OF(X509)(CA 证书链)
  4. 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:内存证书注入

setClientCertsetClientKey 方法将 PEM 字符串包装为 curl_blob 结构体:

cpp
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_BLOBCURLOPT_SSLKEY_BLOB 选项,证书数据完全在内存中流转,避免了写入临时文件的磁盘 I/O 和安全风险。

Sources: HttpClient.cpp

请求体脱敏:desensitizeReport

HttpClient::post 在发送请求前会对 JSON 请求体执行脱敏处理,防止敏感诊断数据以明文形式写入日志。脱敏逻辑聚焦于特定诊断服务的数据写入操作:

mermaid
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 -->|否| G

serverId "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 TokensetToken(token),再构造代理 JSON 通过 post() 发送诊断数据上报、配置查询

PKI 路由的调用模式最能体现 HttpClient 证书管理的设计意图:

cpp
// 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_VERIFYPEERCURLOPT_SSL_VERIFYHOST 均被设为 false,这绕过了 TLS 最核心的安全保障——服务端身份验证。在生产环境中,这意味着任何能够劫持网络路径的攻击者都可以冒充集度服务器。客户端证书的双向认证部分缓解了此问题(服务端仍需验证客户端证书),但无法防御主动中间人攻击。建议在生产部署中启用 SSL_VERIFYPEER 并配置正确的 CA 证书包。

证书 blob 的全局状态blob_clientCertblob_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

与其他模块的关系