|
|
@ -1,4 +1,5 @@ |
|
|
import Foundation |
|
|
import Foundation |
|
|
|
|
|
import UIKit |
|
|
import AVFoundation |
|
|
import AVFoundation |
|
|
import CryptoKit |
|
|
import CryptoKit |
|
|
import os.log |
|
|
import os.log |
|
|
@ -13,6 +14,7 @@ struct StsConfig { |
|
|
var deviceId: String |
|
|
var deviceId: String |
|
|
var deviceSecret: String |
|
|
var deviceSecret: String |
|
|
let agentId: String |
|
|
let agentId: String |
|
|
|
|
|
var cmei: String = "" |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// MARK: - StsAgent |
|
|
// MARK: - StsAgent |
|
|
@ -71,6 +73,11 @@ class StsAgent: NSObject { |
|
|
// MARK: 凭证请求 |
|
|
// MARK: 凭证请求 |
|
|
private var credentialsFetched = false |
|
|
private var credentialsFetched = false |
|
|
|
|
|
|
|
|
|
|
|
// MARK: 心跳冷却 |
|
|
|
|
|
/// 1 小时冷却(毫秒),避免 StsAgent 反复重建 / fetchCredentials 反复触发时频繁上报 |
|
|
|
|
|
private let HEARTBEAT_COOLDOWN_MS: Int64 = 3_600_000 |
|
|
|
|
|
private let HEARTBEAT_PREFS_KEY = "sts_agent_last_heartbeat_ts" |
|
|
|
|
|
|
|
|
init(config: StsConfig, |
|
|
init(config: StsConfig, |
|
|
onEvent: @escaping (String, [String: Any]) -> Void, |
|
|
onEvent: @escaping (String, [String: Any]) -> Void, |
|
|
onCredentialsFetched: ((String, String, String) -> Void)? = nil) { |
|
|
onCredentialsFetched: ((String, String, String) -> Void)? = nil) { |
|
|
@ -80,6 +87,22 @@ class StsAgent: NSObject { |
|
|
super.init() |
|
|
super.init() |
|
|
let sessionConfig = URLSessionConfiguration.default |
|
|
let sessionConfig = URLSessionConfiguration.default |
|
|
urlSession = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: OperationQueue()) |
|
|
urlSession = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: OperationQueue()) |
|
|
|
|
|
os_log("StsAgent 初始化: productId=%{public}@ deviceNo=%{public}@ deviceId=%{public}@ cmei=%{public}@ hasSecret=%{public}@", |
|
|
|
|
|
log: logger, type: .info, |
|
|
|
|
|
config.productId, config.deviceNo, config.deviceId, config.cmei, |
|
|
|
|
|
config.deviceSecret.isEmpty ? "false" : "true") |
|
|
|
|
|
// 与 Android 行为对齐:凭证已有时立即尝试上报一次心跳(受 1 小时冷却保护) |
|
|
|
|
|
if !config.deviceSecret.isEmpty && !config.deviceId.isEmpty { |
|
|
|
|
|
credentialsFetched = true |
|
|
|
|
|
reportHeartbeat() |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 动态更新 CMEI(首次初始化时可能尚未获取到) |
|
|
|
|
|
func updateCmei(_ cmei: String) { |
|
|
|
|
|
guard !cmei.isEmpty, cmei != config.cmei else { return } |
|
|
|
|
|
config.cmei = cmei |
|
|
|
|
|
os_log("updateCmei: cmei 已更新为 %{public}@", log: logger, type: .info, cmei) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// MARK: - 公共接口 |
|
|
// MARK: - 公共接口 |
|
|
@ -139,8 +162,18 @@ class StsAgent: NSObject { |
|
|
|
|
|
|
|
|
// MARK: - 凭证获取 |
|
|
// MARK: - 凭证获取 |
|
|
|
|
|
|
|
|
|
|
|
/// 公开入口:主动触发认证检查(Dart 层 AgentService.fetchCredentials 会调用) |
|
|
|
|
|
func fetchCredentials() { |
|
|
|
|
|
fetchCredentialsIfNeeded { [weak self] in |
|
|
|
|
|
// 成功与否都不做额外处理;fetchCredentialsIfNeeded 内部会在命中凭证时触发心跳 |
|
|
|
|
|
_ = self |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
private func fetchCredentialsIfNeeded(completion: @escaping () -> Void) { |
|
|
private func fetchCredentialsIfNeeded(completion: @escaping () -> Void) { |
|
|
if credentialsFetched && !config.deviceId.isEmpty && !config.deviceSecret.isEmpty { |
|
|
if credentialsFetched && !config.deviceId.isEmpty && !config.deviceSecret.isEmpty { |
|
|
|
|
|
// 凭证已有,直接上报心跳(受冷却保护) |
|
|
|
|
|
reportHeartbeat() |
|
|
completion() |
|
|
completion() |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
@ -175,10 +208,95 @@ class StsAgent: NSObject { |
|
|
if let deviceNo = body["deviceNo"] as? String { self.config.deviceNo = deviceNo } |
|
|
if let deviceNo = body["deviceNo"] as? String { self.config.deviceNo = deviceNo } |
|
|
self.credentialsFetched = true |
|
|
self.credentialsFetched = true |
|
|
self.onCredentialsFetched?(self.config.deviceId, self.config.deviceSecret, self.config.deviceNo) |
|
|
self.onCredentialsFetched?(self.config.deviceId, self.config.deviceSecret, self.config.deviceNo) |
|
|
|
|
|
// 凭证获取成功后立即上报心跳(受冷却保护) |
|
|
|
|
|
self.reportHeartbeat() |
|
|
completion() |
|
|
completion() |
|
|
}.resume() |
|
|
}.resume() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// MARK: - 心跳上报 |
|
|
|
|
|
|
|
|
|
|
|
/// 向中国移动平台上报心跳;1 小时冷却通过 UserDefaults 持久化,所有调用路径共享同一把闸门 |
|
|
|
|
|
func reportHeartbeat() { |
|
|
|
|
|
guard credentialsFetched, |
|
|
|
|
|
!config.deviceId.isEmpty, |
|
|
|
|
|
!config.deviceSecret.isEmpty else { |
|
|
|
|
|
os_log("reportHeartbeat: 凭证未就绪,跳过上报", log: logger, type: .info) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
let defaults = UserDefaults.standard |
|
|
|
|
|
// 用 Double 存毫秒时间戳,避开 Int64 的 UserDefaults 桥接问题 |
|
|
|
|
|
let last = defaults.double(forKey: HEARTBEAT_PREFS_KEY) |
|
|
|
|
|
let now = Date().timeIntervalSince1970 * 1000 |
|
|
|
|
|
if last > 0, now - last < Double(HEARTBEAT_COOLDOWN_MS) { |
|
|
|
|
|
os_log("reportHeartbeat: 距上次 %{public}ds,冷却中跳过", log: logger, type: .info, Int((now - last) / 1000)) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
defaults.set(now, forKey: HEARTBEAT_PREFS_KEY) |
|
|
|
|
|
os_log("reportHeartbeat: 开始上报 deviceId=%{public}@ deviceNo=%{public}@ cmei=%{public}@", |
|
|
|
|
|
log: logger, type: .info, config.deviceId, config.deviceNo, config.cmei) |
|
|
|
|
|
|
|
|
|
|
|
let urlStr = "https://ivs.chinamobiledevice.com:11443/v2/customer/device/report" |
|
|
|
|
|
guard let url = URL(string: urlStr) else { return } |
|
|
|
|
|
var req = URLRequest(url: url) |
|
|
|
|
|
req.httpMethod = "POST" |
|
|
|
|
|
req.setValue("application/json", forHTTPHeaderField: "Content-Type") |
|
|
|
|
|
req.timeoutInterval = 10 |
|
|
|
|
|
|
|
|
|
|
|
let systemVersion = UIDevice.current.systemVersion |
|
|
|
|
|
let params: [String: Any] = [ |
|
|
|
|
|
"innerIp": ["127.0.0.1"], |
|
|
|
|
|
"netSpeed": "0Mbps", |
|
|
|
|
|
"netType": "WiFi", |
|
|
|
|
|
"platform": "iOS\(systemVersion)", |
|
|
|
|
|
"sdkVersion": "ai_http_1.0", |
|
|
|
|
|
"firmwareVersion": "1.0.0", |
|
|
|
|
|
"imei": "", |
|
|
|
|
|
"cmei": config.cmei, |
|
|
|
|
|
"mac": "", |
|
|
|
|
|
] |
|
|
|
|
|
let bodyDict: [String: Any] = [ |
|
|
|
|
|
"deviceId": config.deviceId, |
|
|
|
|
|
"deviceSecret": config.deviceSecret, |
|
|
|
|
|
"productId": config.productId, |
|
|
|
|
|
"productKey": config.productKey, |
|
|
|
|
|
"params": params, |
|
|
|
|
|
] |
|
|
|
|
|
guard let bodyData = try? JSONSerialization.data(withJSONObject: bodyDict, options: []) else { return } |
|
|
|
|
|
req.httpBody = bodyData |
|
|
|
|
|
|
|
|
|
|
|
URLSession.shared.dataTask(with: req) { [weak self] data, _, error in |
|
|
|
|
|
guard let self = self else { return } |
|
|
|
|
|
if let error = error { |
|
|
|
|
|
os_log("reportHeartbeat: 请求异常 %{public}@", log: self.logger, type: .error, error.localizedDescription) |
|
|
|
|
|
// 异常回滚时间戳,避免卡 1 小时无法重试 |
|
|
|
|
|
UserDefaults.standard.set(last, forKey: self.HEARTBEAT_PREFS_KEY) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
guard let data = data, |
|
|
|
|
|
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { |
|
|
|
|
|
os_log("reportHeartbeat: 响应解析失败", log: self.logger, type: .error) |
|
|
|
|
|
UserDefaults.standard.set(last, forKey: self.HEARTBEAT_PREFS_KEY) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
// code 可能是 String 或 NSNumber,统一转成字符串 |
|
|
|
|
|
let code: String = { |
|
|
|
|
|
if let s = json["code"] as? String { return s } |
|
|
|
|
|
if let n = json["code"] as? NSNumber { return n.stringValue } |
|
|
|
|
|
return "" |
|
|
|
|
|
}() |
|
|
|
|
|
let msg = (json["message"] as? String) ?? "" |
|
|
|
|
|
if code == "200" { |
|
|
|
|
|
os_log("reportHeartbeat: 上报成功 code=%{public}@ msg=%{public}@", log: self.logger, type: .info, code, msg) |
|
|
|
|
|
self.emit("heartbeat_reported", data: ["time": Int64(now)]) |
|
|
|
|
|
} else { |
|
|
|
|
|
os_log("reportHeartbeat: 上报失败 code=%{public}@ msg=%{public}@", log: self.logger, type: .error, code, msg) |
|
|
|
|
|
// 上报失败回滚时间戳 |
|
|
|
|
|
UserDefaults.standard.set(last, forKey: self.HEARTBEAT_PREFS_KEY) |
|
|
|
|
|
} |
|
|
|
|
|
}.resume() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// MARK: - WebSocket 连接 |
|
|
// MARK: - WebSocket 连接 |
|
|
|
|
|
|
|
|
private func connect() { |
|
|
private func connect() { |
|
|
|