管理端全部功能跑通
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'aliyun_httpdns'
|
||||
s.version = '1.0.2'
|
||||
s.version = '1.0.0'
|
||||
s.summary = 'aliyun httpdns flutter plugin'
|
||||
s.description = <<-DESC
|
||||
aliyun httpdns flutter plugin.
|
||||
@@ -16,7 +16,6 @@ DESC
|
||||
s.public_header_files = 'Classes/**/*.h'
|
||||
s.static_framework = true
|
||||
s.dependency 'Flutter'
|
||||
s.dependency 'AlicloudHTTPDNS', '3.4.0'
|
||||
s.platform = :ios, '10.0'
|
||||
|
||||
# Flutter.framework does not contain a i386 slice.
|
||||
|
||||
Reference in New Issue
Block a user