管理端全部功能跑通

This commit is contained in:
robin
2026-02-27 10:35:22 +08:00
parent 4d275c921d
commit 150799f41d
263 changed files with 22664 additions and 4053 deletions

View File

@@ -1,203 +1,324 @@
import Flutter
import Foundation
import UIKit
import AlicloudHTTPDNS
import CommonCrypto
public class AliyunHttpDnsPlugin: NSObject, FlutterPlugin {
private var channel: FlutterMethodChannel!
private var appId: String?
private var secretKey: String?
private var primaryServiceHost: String?
private var backupServiceHost: String?
private var servicePort: Int = 443
// Desired states saved until build()
private var desiredAccountId: Int?
private var desiredSecretKey: String?
private var desiredAesSecretKey: String?
private var desiredPersistentCacheEnabled: Bool?
private var desiredDiscardExpiredAfterSeconds: Int?
private var desiredReuseExpiredIPEnabled: Bool?
private var desiredLogEnabled: Bool?
private var desiredHttpsEnabled: Bool?
private var desiredPreResolveAfterNetworkChanged: Bool?
private var desiredIPRankingMap: [String: NSNumber]?
private var desiredConnectTimeoutSeconds: TimeInterval = 3
private var desiredReadTimeoutSeconds: TimeInterval = 5
private lazy var sessionId: String = {
UUID().uuidString.replacingOccurrences(of: "-", with: "")
}()
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "aliyun_httpdns", binaryMessenger: registrar.messenger())
let instance = AliyunHttpDnsPlugin()
instance.channel = channel
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
// Dart: init(accountId, secretKey?, aesSecretKey?) only save desired state
case "initialize":
let options = call.arguments as? [String: Any] ?? [:]
let accountIdAny = options["accountId"]
let secretKey = options["secretKey"] as? String
let aesSecretKey = options["aesSecretKey"] as? String
guard let accountId = (accountIdAny as? Int) ?? Int((accountIdAny as? String) ?? "") else {
NSLog("AliyunHttpDns: initialize missing accountId")
guard let rawAppId = options["appId"] as? String,
let rawPrimaryHost = options["primaryServiceHost"] as? String else {
result(false)
return
}
desiredAccountId = accountId
desiredSecretKey = secretKey
desiredAesSecretKey = aesSecretKey
NSLog("AliyunHttpDns: initialize saved accountId=\(accountId)")
let normalizedAppId = rawAppId.trimmingCharacters(in: .whitespacesAndNewlines)
let normalizedPrimaryHost = rawPrimaryHost.trimmingCharacters(in: .whitespacesAndNewlines)
if normalizedAppId.isEmpty || normalizedPrimaryHost.isEmpty {
result(false)
return
}
appId = normalizedAppId
primaryServiceHost = normalizedPrimaryHost
backupServiceHost = (options["backupServiceHost"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
secretKey = (options["secretKey"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
if let p = options["servicePort"] as? Int, p > 0 {
servicePort = p
}
result(true)
// Dart: setLogEnabled(enabled) save desired
case "setLogEnabled":
let args = call.arguments as? [String: Any]
let enabled = (args?["enabled"] as? Bool) ?? false
desiredLogEnabled = enabled
NSLog("AliyunHttpDns: log desired=\(enabled)")
desiredLogEnabled = (args?["enabled"] as? Bool) ?? false
result(nil)
case "setHttpsRequestEnabled":
let args = call.arguments as? [String: Any]
let enabled = (args?["enabled"] as? Bool) ?? false
desiredHttpsEnabled = enabled
NSLog("AliyunHttpDns: https request desired=\(enabled)")
desiredHttpsEnabled = (args?["enabled"] as? Bool) ?? false
result(nil)
// Dart: setPersistentCacheIPEnabled(enabled, discardExpiredAfterSeconds?) save desired
case "setPersistentCacheIPEnabled":
let args = call.arguments as? [String: Any]
let enabled = (args?["enabled"] as? Bool) ?? false
let discard = args?["discardExpiredAfterSeconds"] as? Int
desiredPersistentCacheEnabled = enabled
desiredDiscardExpiredAfterSeconds = discard
NSLog("AliyunHttpDns: persistent cache desired=\(enabled) discard=\(discard ?? -1)")
result(nil)
// Dart: setReuseExpiredIPEnabled(enabled) save desired
case "setReuseExpiredIPEnabled":
let args = call.arguments as? [String: Any]
let enabled = (args?["enabled"] as? Bool) ?? false
desiredReuseExpiredIPEnabled = enabled
NSLog("AliyunHttpDns: reuse expired ip desired=\(enabled)")
result(nil)
case "setPreResolveAfterNetworkChanged":
let args = call.arguments as? [String: Any]
let enabled = (args?["enabled"] as? Bool) ?? false
desiredPreResolveAfterNetworkChanged = enabled
NSLog("AliyunHttpDns: preResolveAfterNetworkChanged desired=\(enabled)")
result(nil)
case "setIPRankingList":
let args = call.arguments as? [String: Any]
let hostPortMap = args?["hostPortMap"] as? [String: NSNumber]
desiredIPRankingMap = hostPortMap
NSLog("AliyunHttpDns: IP ranking list desired, hosts=\(hostPortMap?.keys.joined(separator: ", ") ?? "")")
result(nil)
case "setPreResolveHosts":
let args = call.arguments as? [String: Any]
let hosts = (args?["hosts"] as? [String]) ?? []
let ipTypeStr = (args?["ipType"] as? String) ?? "auto"
switch ipTypeStr.lowercased() {
case "ipv4", "v4":
HttpDnsService.sharedInstance().setPreResolveHosts(hosts, queryIPType: AlicloudHttpDNS_IPType.init(0))
case "ipv6", "v6":
HttpDnsService.sharedInstance().setPreResolveHosts(hosts, queryIPType: AlicloudHttpDNS_IPType.init(1))
case "both", "64":
HttpDnsService.sharedInstance().setPreResolveHosts(hosts, queryIPType: AlicloudHttpDNS_IPType.init(2))
default:
HttpDnsService.sharedInstance().setPreResolveHosts(hosts)
}
result(nil)
case "getSessionId":
let sid = HttpDnsService.sharedInstance().getSessionId()
result(sid)
result(sessionId)
case "cleanAllHostCache":
HttpDnsService.sharedInstance().cleanAllHostCache()
result(nil)
// Dart: build() construct service and apply desired states
case "build":
guard let accountId = desiredAccountId else {
result(false)
return
if desiredHttpsEnabled == false {
NSLog("AliyunHttpDns(iOS): HTTPS is required by current protocol, force enabled")
}
// Initialize singleton
if let secret = desiredSecretKey, !secret.isEmpty {
if let aes = desiredAesSecretKey, !aes.isEmpty {
_ = HttpDnsService(accountID: accountId, secretKey: secret, aesSecretKey: aes)
} else {
_ = HttpDnsService(accountID: accountId, secretKey: secret)
}
} else {
_ = HttpDnsService(accountID: accountId) // deprecated but acceptable fallback
}
let svc = HttpDnsService.sharedInstance()
// Apply desired runtime flags
if let enable = desiredPersistentCacheEnabled {
if let discard = desiredDiscardExpiredAfterSeconds, discard >= 0 {
svc.setPersistentCacheIPEnabled(enable, discardRecordsHasExpiredFor: TimeInterval(discard))
} else {
svc.setPersistentCacheIPEnabled(enable)
}
}
if let enable = desiredReuseExpiredIPEnabled {
svc.setReuseExpiredIPEnabled(enable)
}
if let enable = desiredLogEnabled {
svc.setLogEnabled(enable)
}
if let enable = desiredHttpsEnabled {
svc.setHTTPSRequestEnabled(enable)
if desiredLogEnabled == true {
NSLog("AliyunHttpDns(iOS): build success, appId=\(appId ?? "")")
}
result(appId != nil && primaryServiceHost != nil)
if let en = desiredPreResolveAfterNetworkChanged {
svc.setPreResolveAfterNetworkChanged(en)
}
if let ipRankingMap = desiredIPRankingMap, !ipRankingMap.isEmpty {
svc.setIPRankingDatasource(ipRankingMap)
}
NSLog("AliyunHttpDns: build completed accountId=\(accountId)")
result(true)
// Dart: resolveHostSyncNonBlocking(hostname, ipType, sdnsParams?, cacheKey?)
case "resolveHostSyncNonBlocking":
guard let args = call.arguments as? [String: Any], let host = args["hostname"] as? String else {
guard let args = call.arguments as? [String: Any],
let hostname = args["hostname"] as? String else {
result(["ipv4": [], "ipv6": []])
return
}
let ipTypeStr = (args["ipType"] as? String) ?? "auto"
let sdnsParams = args["sdnsParams"] as? [String: String]
let cacheKey = args["cacheKey"] as? String
let type: HttpdnsQueryIPType
switch ipTypeStr.lowercased() {
case "ipv4", "v4": type = .ipv4
case "ipv6", "v6": type = .ipv6
case "both", "64": type = .both
default: type = .auto
let ipType = ((args["ipType"] as? String) ?? "auto").lowercased()
resolveHost(hostname: hostname, ipType: ipType) { payload in
result(payload)
}
let svc = HttpDnsService.sharedInstance()
var v4: [String] = []
var v6: [String] = []
if let params = sdnsParams, let key = cacheKey, let r = svc.resolveHostSyncNonBlocking(host, by: type, withSdnsParams: params, sdnsCacheKey: key) {
if r.hasIpv4Address() { v4 = r.ips }
if r.hasIpv6Address() { v6 = r.ipv6s }
} else if let r = svc.resolveHostSyncNonBlocking(host, by: type) {
if r.hasIpv4Address() { v4 = r.ips }
if r.hasIpv6Address() { v6 = r.ipv6s }
case "resolveHostV1":
guard let args = call.arguments as? [String: Any],
let hostname = args["hostname"] as? String else {
result(["ipv4": [], "ipv6": [], "ttl": 0])
return
}
let qtype = ((args["qtype"] as? String) ?? "A").uppercased()
let cip = (args["cip"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
resolveSingle(hostname: hostname, qtype: qtype, cip: cip) { records, ttl in
if qtype == "AAAA" {
result(["ipv4": [], "ipv6": records, "ttl": ttl])
} else {
result(["ipv4": records, "ipv6": [], "ttl": ttl])
}
}
result(["ipv4": v4, "ipv6": v6])
// Legacy methods removed: preResolve / clearCache
default:
result(FlutterMethodNotImplemented)
}
}
private func resolveHost(hostname: String, ipType: String, completion: @escaping ([String: [String]]) -> Void) {
let qtypes: [String]
switch ipType {
case "ipv6", "v6":
qtypes = ["AAAA"]
case "both", "64":
qtypes = ["A", "AAAA"]
default:
qtypes = ["A"]
}
let group = DispatchGroup()
let lock = NSLock()
var ipv4: [String] = []
var ipv6: [String] = []
for qtype in qtypes {
group.enter()
resolveSingle(hostname: hostname, qtype: qtype, cip: nil) { records, _ in
lock.lock()
if qtype == "AAAA" {
ipv6.append(contentsOf: records)
} else {
ipv4.append(contentsOf: records)
}
lock.unlock()
group.leave()
}
}
group.notify(queue: .main) {
completion([
"ipv4": Array(Set(ipv4)),
"ipv6": Array(Set(ipv6))
])
}
}
private func resolveSingle(hostname: String, qtype: String, cip: String?, completion: @escaping ([String], Int) -> Void) {
guard let appId = appId,
let primary = primaryServiceHost else {
completion([], 0)
return
}
var hosts: [String] = [primary]
if let backup = backupServiceHost, !backup.isEmpty, backup != primary {
hosts.append(backup)
}
func attempt(_ index: Int) {
if index >= hosts.count {
completion([], 0)
return
}
let serviceHost = hosts[index]
var components = URLComponents()
components.scheme = "https"
components.host = serviceHost
components.port = servicePort
components.path = "/resolve"
var queryItems: [URLQueryItem] = [
URLQueryItem(name: "appId", value: appId),
URLQueryItem(name: "dn", value: hostname),
URLQueryItem(name: "qtype", value: qtype),
URLQueryItem(name: "sdk_version", value: "flutter-ios-1.0.0"),
URLQueryItem(name: "os", value: "ios"),
URLQueryItem(name: "sid", value: sessionId)
]
if let secret = secretKey, !secret.isEmpty {
let exp = String(Int(Date().timeIntervalSince1970) + 600)
let nonce = UUID().uuidString.replacingOccurrences(of: "-", with: "")
let signRaw = "\(appId)|\(hostname.lowercased())|\(qtype.uppercased())|\(exp)|\(nonce)"
let sign = hmacSHA256Hex(message: signRaw, secret: secret)
queryItems.append(URLQueryItem(name: "exp", value: exp))
queryItems.append(URLQueryItem(name: "nonce", value: nonce))
queryItems.append(URLQueryItem(name: "sign", value: sign))
}
if let cip = cip, !cip.isEmpty {
queryItems.append(URLQueryItem(name: "cip", value: cip))
}
components.queryItems = queryItems
guard let url = components.url else {
completion([], 0)
return
}
var req = URLRequest(url: url)
req.httpMethod = "GET"
req.timeoutInterval = desiredConnectTimeoutSeconds + desiredReadTimeoutSeconds
req.setValue(serviceHost, forHTTPHeaderField: "Host")
let config = URLSessionConfiguration.ephemeral
config.timeoutIntervalForRequest = desiredConnectTimeoutSeconds + desiredReadTimeoutSeconds
config.timeoutIntervalForResource = desiredConnectTimeoutSeconds + desiredReadTimeoutSeconds
let session = URLSession(configuration: config)
session.dataTask(with: req) { [weak self] data, _, error in
defer { session.finishTasksAndInvalidate() }
if let error = error {
if self?.desiredLogEnabled == true {
NSLog("AliyunHttpDns(iOS): resolve request failed host=\(serviceHost), err=\(error.localizedDescription)")
}
attempt(index + 1)
return
}
guard let data = data else {
attempt(index + 1)
return
}
let parsedIPs = Self.extractIPsFromResolveResponse(data: data, qtype: qtype)
if parsedIPs.isEmpty {
attempt(index + 1)
return
}
let ttl = Self.extractTTLFromResolveResponse(data: data)
completion(parsedIPs, ttl)
}.resume()
}
attempt(0)
}
private static func extractIPsFromResolveResponse(data: Data, qtype: String) -> [String] {
guard let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
isResolveSuccessCode(obj["code"]),
let dataObj = obj["data"] as? [String: Any],
let records = dataObj["records"] as? [[String: Any]] else {
return []
}
var ips: [String] = []
for row in records {
let type = ((row["type"] as? String) ?? "").uppercased()
if type != qtype.uppercased() {
continue
}
if let ip = row["ip"] as? String, !ip.isEmpty {
ips.append(ip)
}
}
return ips
}
private static func extractTTLFromResolveResponse(data: Data) -> Int {
guard let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let dataObj = obj["data"] as? [String: Any],
let ttlValue = dataObj["ttl"] else {
return 0
}
if let ttl = ttlValue as? Int {
return ttl
}
if let ttlString = ttlValue as? String, let ttl = Int(ttlString) {
return ttl
}
return 0
}
private static func isResolveSuccessCode(_ codeValue: Any?) -> Bool {
if let code = codeValue as? String {
let normalized = code.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
return normalized == "SUCCESS" || normalized == "0"
}
if let code = codeValue as? Int {
return code == 0
}
if let code = codeValue as? NSNumber {
return code.intValue == 0
}
return false
}
private func hmacSHA256Hex(message: String, secret: String) -> String {
guard let keyData = secret.data(using: .utf8),
let messageData = message.data(using: .utf8) else {
return ""
}
var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
keyData.withUnsafeBytes { keyBytes in
messageData.withUnsafeBytes { msgBytes in
CCHmac(
CCHmacAlgorithm(kCCHmacAlgSHA256),
keyBytes.baseAddress,
keyData.count,
msgBytes.baseAddress,
messageData.count,
&digest
)
}
}
return digest.map { String(format: "%02x", $0) }.joined()
}
}