Files
simapi-cj/src/helpers/SimApiHttpClient.cj
T

167 lines
6.7 KiB
Plaintext
Raw Normal View History

2026-08-16 12:46:15 +08:00
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
*/
package simapi.helpers
import std.collection.*
import std.io.*
import stdx.net.tls.*
import stdx.net.tls.common.*
import soulsoft_net_http.{HttpClient, HttpRequestMessage, JsonContent}
import soulsoft_net_http.{HttpMethod as NetHttpMethod}
import simapi_serialization.*
2026-08-16 12:46:15 +08:00
import simapi.communications.*
import simapi.configurations.*
import simapi.exceptions.*
/**
* HTTP 客户端:用于调用其他带签名/AES 的 SimApi 服务。
* 对齐 C# 的 SimApi.Helpers.SimApiHttpClient
* - 内部使用 soulsoft_net_http 的 HttpClient(等价 .NET 的 System.Net.Http.HttpClient
* - 返回泛型 T(反序列化响应 body 的 data 字段),不再返回 String
* @param T 响应 data 的数据类型(任意类,simapi_serialization 反射反序列化)。
2026-08-16 12:46:15 +08:00
*/
2026-08-16 22:47:04 +08:00
public open class SimApiHttpClient {
2026-08-16 12:46:15 +08:00
public var server: String
public var appId: String
public var appKey: String
public var signName: String = "sign"
public var timestampName: String = "timestamp"
public var nonceName: String = "nonce"
public var appIdName: ?String = Some("appId")
public var signFields: Array<String> = []
public init(options!: SimApiOptions = SimApiOptions()) {
let httpOptions = options.simApiHttpClientOptions
server = httpOptions.server
appId = httpOptions.appId
appKey = httpOptions.appKey
}
/**
* 发起签名请求(GET query 签名 + POST body)。
* 对齐 C# SignQuery<T>query 串 = SignFields + AppId + timestamp + nonce,整体拼 AppKey 取 MD5 作为 sign。
* @param url 请求路径(相对路径,自动拼接 server)。
* @param body 请求体 JSON 字符串(可选)。
* @param queries 额外查询参数(可选)。
* @return 响应 data 字段反序列化后的 T。
*/
public func signQuery<T>(url: String, body!: String = "", queries!: HashMap<String, String> = HashMap<String, String>()): T {
2026-08-16 12:46:15 +08:00
var queryUrl = StringBuilder()
for (field in signFields) {
queryUrl.append("${field}=")
if (let Some(v) <- queries.get(field)) {
queryUrl.append(v)
}
queryUrl.append("&")
}
if (let Some(name) <- appIdName) {
queryUrl.append("${name}=${appId}&")
}
let timestamp = Int64(SimApiUtil.timestampNow)
let nonce = generateNonce()
queryUrl.append("${timestampName}=${timestamp}&${nonceName}=${nonce}")
let signStr = "${queryUrl.toString()}&${appKey}"
var path = "${server}${url}?${queryUrl.toString()}&${signName}=${SimApiUtil.md5(signStr)}"
for ((k, v) in queries) {
if (!signFields.contains(k)) {
path = "${path}&${k}=${v}"
}
}
return query<T>(path, body)
}
/**
* 发起 AES 加密请求:body 加密后放入 {"data": "..."} 提交。
* 对齐 C# AesQuery<T>SimApiOneFieldRequest<string> { Data = Encrypt(body, AppKey) })。
* @param url 请求路径(相对路径,自动拼接 server)。
* @param body 请求体 JSON 字符串。
* @return 响应 data 字段反序列化后的 T。
*/
public func aesQuery<T>(url: String, body: String): T {
2026-08-16 12:46:15 +08:00
var target = "${server}${url}"
if (let Some(name) <- appIdName) {
target = "${target}?${name}=${appId}"
}
let encrypted = aesEncrypt(body)
let req = "{\"data\":\"${encrypted}\"}"
return query<T>(target, req)
}
/**
* 发起 AES 加密 + 签名请求。
* 对齐 C# AesSignQuery<T>。
* @param url 请求路径(相对路径,自动拼接 server)。
* @param body 请求体 JSON 字符串。
* @param queries 额外查询参数(可选)。
* @return 响应 data 字段反序列化后的 T。
*/
public func aesSignQuery<T>(url: String, body: String, queries!: HashMap<String, String> = HashMap<String, String>()): T {
2026-08-16 12:46:15 +08:00
let encrypted = aesEncrypt(body)
let req = "{\"data\":\"${encrypted}\"}"
return signQuery<T>(url, body: req, queries: queries)
}
/**
* 发起 POST 请求并反序列化 SimApiResponse<T>,返回 data 字段。
2026-08-16 12:46:15 +08:00
* 对齐 C# Query<T>
* ErrorWhenFalse(IsSuccessStatusCode) → ReadFromJsonAsync<SimApiResponse<T>> → ErrorWhen(Code != 200) → return Data。
2026-08-16 12:46:15 +08:00
* 注意:必须 noProxy(),否则会走系统代理(192.168.0.250:8118)导致连接被拒。
* 反序列化使用 simapi_serializationDeserialize<T> 免约束)。
2026-08-16 12:46:15 +08:00
*/
private func query<T>(url: String, body: String): T {
2026-08-16 12:46:15 +08:00
let client = HttpClient.create { builder =>
builder.noProxy()
// 支持 https:配置 TLS(信任所有证书 + SNI 域名)
var tls = TlsClientConfig()
tls.verifyMode = CertificateVerifyMode.TrustAll
let host = extractHost(url)
if (!host.isEmpty()) {
tls.serverName = Some(host)
}
builder.tlsConfig(tls)
}
try {
let request = HttpRequestMessage(NetHttpMethod.Post, url)
request.content = JsonContent.create(body)
let response = client.send(request)
try {
SimApiError.errorWhenFalse(response.isSuccessStatusCode, code: response.statusCode, message: "HTTP ERROR: ${response.statusCode}")
let json = response.content.readAsString()
let result = JsonSerializer.Deserialize<SimApiResponse<T>>(json)
2026-08-16 12:46:15 +08:00
SimApiError.errorWhen(result._code != 200, code: result._code, message: result._message)
return result._data.getOrThrow()
} finally {
response.close()
}
} finally {
client.close()
}
}
private func aesEncrypt(plain: String): String {
// 对齐 C#SimApiAesUtil.Encrypt(plain, AppKey)AES-256-CBC + PKCS7Base64(IV + 密文)
SimApiAesUtil.encrypt(plain, appKey)
}
private static func generateNonce(): String {
// 对齐 C#nonce 直接用 Guid.NewGuid()
SimApiUtil.newGuid()
}
/// 从完整 URL 提取 hosthttps://host[:port]/path → host)。
private static func extractHost(fullUrl: String): String {
match (fullUrl.indexOf("://")) {
case Some(i) =>
let rest = fullUrl[i + 3..]
let slash = rest.indexOf("/") ?? rest.size
let q = rest.indexOf("?") ?? rest.size
let end = if (slash < q) { slash } else { q }
return rest[0..end]
case None => return ""
}
}
}