ClashX.Meta/ClashX/General/ApiRequest.swift

731 lines
24 KiB
Swift

//
// ApiRequest.swift
// ClashX
//
// Created by CYC on 2018/7/30.
// Copyright © 2018 yichengchen. All rights reserved.
//
import Alamofire
import Cocoa
import Starscream
import SwiftyJSON
protocol ApiRequestStreamDelegate: AnyObject {
func didUpdateTraffic(up: Int, down: Int)
func didGetLog(log: String, level: String)
func didUpdateMemory(memory: Int64)
func streamStatusChanged()
}
typealias ErrorString = String
struct ClashVersion: Decodable {
let version: String
let meta: Bool?
}
class ApiRequest {
static let shared = ApiRequest()
private var proxyRespCache: ClashProxyResp?
private lazy var logQueue = DispatchQueue(label: "com.ClashX.core.log")
static let clashRequestQueue = DispatchQueue(label: "com.clashx.clashRequestQueue")
@objc enum ProviderType: Int {
case rule, proxy
func apiString() -> String {
self == .proxy ? "proxies" : "rules"
}
func logString() -> String {
self == .proxy ? "Proxy" : "Rule"
}
}
private init() {
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 604800
configuration.timeoutIntervalForResource = 604800
configuration.httpMaximumConnectionsPerHost = 100
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
alamoFireManager = Session(configuration: configuration)
}
static func authHeader() -> HTTPHeaders {
let secret = ConfigManager.shared.overrideSecret ?? ConfigManager.shared.apiSecret
return (!secret.isEmpty) ? ["Authorization": "Bearer \(secret)"] : [:]
}
@discardableResult
private static func req(
_ url: String,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
encoding: ParameterEncoding = URLEncoding.default
)
-> DataRequest {
guard ConfigManager.shared.isRunning else {
return AF.request("")
}
return shared.alamoFireManager
.request(ConfigManager.apiUrl + url,
method: method,
parameters: parameters,
encoding: encoding,
headers: authHeader())
}
weak var delegate: ApiRequestStreamDelegate?
weak var dashboardDelegate: ApiRequestStreamDelegate?
private var trafficWebSocket: WebSocket?
private var loggingWebSocket: WebSocket?
private var memoryWebSocket: WebSocket?
private var trafficWebSocketRetryDelay: TimeInterval = 1
private var loggingWebSocketRetryDelay: TimeInterval = 1
private var memoryWebSocketRetryDelay: TimeInterval = 1
private var trafficWebSocketRetryTimer: Timer?
private var loggingWebSocketRetryTimer: Timer?
private var memoryWebSocketRetryTimer: Timer?
private var alamoFireManager: Session
static func requestVersion(completeHandler: @escaping ((ClashVersion?) -> Void)) {
shared.alamoFireManager
.request(ConfigManager.apiUrl + "/version",
method: .get,
headers: authHeader())
.responseDecodable(of: ClashVersion.self) {
resp in
switch resp.result {
case let .success(ver):
completeHandler(ver)
case let .failure(err):
Logger.log("Request Version failed, \(err)", level: .error)
completeHandler(nil)
}
}
}
static func requestConfig(completeHandler: @escaping ((ClashConfig) -> Void)) {
req("/configs").responseDecodable(of: ClashConfig.self) {
resp in
switch resp.result {
case let .success(config):
completeHandler(config)
case let .failure(err):
Logger.log(err.localizedDescription)
UserNotificationCenter.shared.post(title: "Error", info: err.localizedDescription)
}
}
}
static func findConfigPath(configName: String, callback: @escaping ((String?) -> Void)) {
if ICloudManager.shared.useiCloud.value {
ICloudManager.shared.getUrl { url in
guard let url = url else {
callback(nil)
return
}
let configPath = url.appendingPathComponent(Paths.configFileName(for: configName)).path
callback(configPath)
}
} else {
let filePath = Paths.localConfigPath(for: configName)
callback(filePath)
}
}
static func requestConfigUpdate(configName: String, callback: @escaping ((ErrorString?) -> Void)) {
findConfigPath(configName: configName) { path in
guard let path = path else {
callback("icloud error")
return
}
requestConfigUpdate(configPath: path, callback: callback)
}
}
static func requestConfigUpdate(configPath: String, callback: @escaping ((ErrorString?) -> Void)) {
let placeHolderErrorDesp = "Error occoured, Please try to fix it by restarting ClashX. "
req("/configs", method: .put, parameters: ["Path": configPath], encoding: JSONEncoding.default).responseData { res in
if res.response?.statusCode == 204 {
ConfigManager.shared.isRunning = true
callback(nil)
} else {
let errorData = try? res.result.get()
let err = JSON(errorData ?? Data())["message"].string ?? placeHolderErrorDesp
Logger.log(err)
callback(err)
}
}
}
static func updateOutBoundMode(mode: ClashProxyMode, callback: ((Bool) -> Void)? = nil) {
req("/configs", method: .patch, parameters: ["mode": mode.rawValue], encoding: JSONEncoding.default)
.responseData { response in
switch response.result {
case .success:
callback?(true)
case .failure:
callback?(false)
}
}
}
static func updateLogLevel(level: ClashLogLevel, callback: ((Bool) -> Void)? = nil) {
req("/configs", method: .patch, parameters: ["log-level": level.rawValue], encoding: JSONEncoding.default).responseData(completionHandler: { response in
switch response.result {
case .success:
callback?(true)
case .failure:
callback?(false)
}
})
}
static func requestProxyGroupList(completeHandler: ((ClashProxyResp) -> Void)? = nil) {
req("/proxies").responseData {
res in
let proxies = ClashProxyResp(try? res.result.get())
ApiRequest.shared.proxyRespCache = proxies
completeHandler?(proxies)
}
}
static func requestProxyProviderList(completeHandler: ((ClashProviderResp) -> Void)? = nil) {
req("/providers/proxies")
.responseDecodable(of: ClashProviderResp.self, decoder: ClashProviderResp.decoder) { resp in
switch resp.result {
case let .success(providerResp):
completeHandler?(providerResp)
case let .failure(err):
Logger.log("requestProxyProviderList error \(err.localizedDescription)")
completeHandler?(ClashProviderResp())
}
}
}
static func updateAllowLan(allow: Bool, completeHandler: (() -> Void)? = nil) {
Logger.log("update allow lan:\(allow)", level: .debug)
req("/configs",
method: .patch,
parameters: ["allow-lan": allow],
encoding: JSONEncoding.default).response {
_ in
completeHandler?()
}
}
static func updateProxyGroup(group: String, selectProxy: String, callback: @escaping ((Bool) -> Void)) {
req("/proxies/\(group.encoded)",
method: .put,
parameters: ["name": selectProxy],
encoding: JSONEncoding.default)
.responseData { response in
callback(response.response?.statusCode == 204)
}
}
static func getAllProxyList(callback: @escaping (([ClashProxyName]) -> Void)) {
requestProxyGroupList {
proxyInfo in
let lists: [ClashProxyName] = proxyInfo.proxiesMap["GLOBAL"]?.all ?? []
callback(lists)
}
}
static func getMergedProxyData(complete: ((ClashProxyResp?) -> Void)? = nil) {
let group = DispatchGroup()
group.enter()
group.enter()
var provider: ClashProviderResp?
var proxyInfo: ClashProxyResp?
group.notify(queue: .main) {
guard let proxyInfo = proxyInfo, let proxyprovider = provider else {
assertionFailure()
complete?(nil)
return
}
proxyInfo.updateProvider(proxyprovider)
complete?(proxyInfo)
}
ApiRequest.requestProxyProviderList {
proxyprovider in
provider = proxyprovider
group.leave()
}
ApiRequest.requestProxyGroupList {
proxy in
proxyInfo = proxy
group.leave()
}
}
static func getProxyDelay(proxyName: String, callback: @escaping ((Int) -> Void)) {
req("/proxies/\(proxyName.encoded)/delay",
method: .get,
parameters: ["timeout": 2500, "url": ConfigManager.shared.benchMarkUrl])
.responseData { res in
switch res.result {
case let .success(value):
let json = JSON(value)
callback(json["delay"].intValue)
case .failure:
callback(0)
}
}
}
static func getGroupDelay(groupName: String, callback: @escaping (([String: Int]) -> Void)) {
req("/group/\(groupName.encoded)/delay",
method: .get,
parameters: ["timeout": 2500, "url": ConfigManager.shared.benchMarkUrl])
.responseData { res in
switch res.result {
case let .success(value):
let dic = try? JSONDecoder().decode([String: Int].self, from: value)
callback(dic ?? [:])
case .failure:
callback([:])
}
}
}
static func getRules(completeHandler: @escaping ([ClashRule]) -> Void) {
req("/rules").responseData { res in
guard let data = try? res.result.get() else { return }
let rule = ClashRuleResponse.fromData(data)
completeHandler(rule.rules ?? [])
}
}
static func healthCheck(proxy: ClashProviderName, completeHandler: (() -> Void)? = nil) {
Logger.log("HeathCheck for \(proxy) started")
req("/providers/proxies/\(proxy.encoded)/healthcheck").response { res in
if res.response?.statusCode == 204 {
Logger.log("HeathCheck for \(proxy) finished")
} else {
Logger.log("HeathCheck for \(proxy) failed:\(res.response?.statusCode ?? -1)")
}
completeHandler?()
}
}
}
// MARK: - Connections
extension ApiRequest {
static func getConnections(completeHandler: @escaping ([ClashConnectionBaseSnapShot.Connection]) -> Void) {
req("/connections").responseDecodable(of: ClashConnectionBaseSnapShot.self) { resp in
switch resp.result {
case let .success(snapshot):
completeHandler(snapshot.connections)
case .failure:
assertionFailure()
completeHandler([])
}
}
}
static func closeConnection(_ id: String) {
req("/connections/\(id)", method: .delete).response { _ in }
}
static func getConnections(completeHandler: @escaping (DBConnectionSnapShot) -> Void) {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(DateFormatter.js)
req("/connections").responseDecodable(of: DBConnectionSnapShot.self, decoder: decoder) { resp in
switch resp.result {
case let .success(snapshot):
completeHandler(snapshot)
case .failure:
return
// assertionFailure()
// completeHandler(DBConnectionSnapShot())
}
}
}
static func closeConnection(_ conn: ClashConnectionSnapShot.Connection) {
req("/connections/".appending(conn.id), method: .delete).response { _ in }
}
static func closeAllConnection() {
req("/connections", method: .delete).response { _ in }
}
}
// MARK: - Meta
extension ApiRequest {
static func updateAllProviders(for type: ProviderType, completeHandler: ((Int) -> Void)? = nil) {
var failuresCount = 0
let group = DispatchGroup()
group.enter()
if type == .proxy {
requestProxyProviderList { resp in
resp.allProviders.filter {
$0.value.vehicleType == .HTTP
}.forEach {
group.enter()
updateProvider(for: .proxy, name: $0.key) {
if !$0 {
failuresCount += 1
}
group.leave()
}
}
group.leave()
}
} else {
requestRuleProviderList { resp in
resp.allProviders.forEach {
group.enter()
updateProvider(for: .rule, name: $0.key) {
if !$0 {
failuresCount += 1
}
group.leave()
}
}
group.leave()
}
}
group.notify(queue: .main) {
completeHandler?(failuresCount)
}
}
static func updateProvider(for type: ProviderType, name: String, completeHandler: ((Bool) -> Void)? = nil) {
let s = "Update \(type.logString()) Provider"
Logger.log("\(s) \(name)")
req("/providers/\(type.apiString())/\(name)", method: .put).response {
let re = $0.response?.statusCode == 204
Logger.log("\(s) \(name) \(re ? "success" : "failed")")
completeHandler?(re)
}
}
static func requestRuleProviderList(completeHandler: @escaping (ClashRuleProviderResp) -> Void) {
req("/providers/rules")
.responseDecodable(of: ClashRuleProviderResp.self, decoder: ClashProviderResp.decoder) { resp in
switch resp.result {
case let .success(providerResp):
completeHandler(providerResp)
case let .failure(err):
Logger.log("Get Rule providers error \(err.errorDescription ?? "unknown")" )
completeHandler(ClashRuleProviderResp())
}
}
}
static func updateGEO(completeHandler: ((Bool) -> Void)? = nil) {
Logger.log("UpdateGEO")
req("/configs/geo", method: .post).response {
let re = $0.response?.statusCode == 204
completeHandler?(re)
// Logger.log("UpdateGEO \(re ? "success" : "failed")")
Logger.log("Updating GEO Databases...")
}
}
static func updateTun(enable: Bool, completeHandler: (() -> Void)? = nil) {
Logger.log("update tun:\(enable)", level: .debug)
req("/configs",
method: .patch,
parameters: ["tun": ["enable": enable]],
encoding: JSONEncoding.default).response {
_ in
completeHandler?()
}
}
static func updateSniffing(enable: Bool, completeHandler: (() -> Void)? = nil) {
Logger.log("update sniffing:\(enable)", level: .debug)
req("/configs",
method: .patch,
parameters: ["sniffing": enable],
encoding: JSONEncoding.default).response {
_ in
completeHandler?()
}
}
static func flushFakeipCache(completeHandler: ((Bool) -> Void)? = nil) {
Logger.log("FlushFakeipCache")
req("/cache/fakeip/flush",
method: .post).response {
let re = $0.response?.statusCode == 204
completeHandler?(re)
Logger.log("FlushFakeipCache \(re ? "success" : "failed")")
}
}
// MARK: - Providers
struct AllProviders {
var proxies = [String]()
var rules = [String]()
}
static func requestExternalProviderNames(completeHandler: @escaping (AllProviders) -> Void) {
var providers = AllProviders()
let group = DispatchGroup()
group.enter()
ApiRequest.req("/providers/proxies").responseData { resp in
switch resp.result {
case let .success(res):
let json = JSON(res)
let provoders = json["providers"].dictionaryValue
.filter { $0.value["vehicleType"] == "HTTP" }.map(\.key)
providers.proxies = provoders
case let .failure(err):
Logger.log(err.localizedDescription, level: .warning)
}
group.leave()
}
#if PRO_VERSION
group.enter()
ApiRequest.req("/providers/rules").responseData { resp in
switch resp.result {
case let .success(res):
let json = JSON(res)
let provoders = json["providers"].dictionaryValue
.filter { $0.value["vehicleType"] == "HTTP" }.map(\.key)
providers.rules = provoders
case let .failure(err):
Logger.log(err.localizedDescription, level: .warning)
}
group.leave()
}
#endif
group.notify(queue: .main) {
completeHandler(providers)
}
}
/*
enum ProviderType {
case proxy
case rule
}
*/
static func updateProvider(name: String, type: ProviderType, completeHandler: @escaping (Bool) -> Void) {
let url: String
switch type {
case .proxy:
url = "/providers/proxies/\(name.encoded)"
case .rule:
url = "/providers/rules/\(name.encoded)"
}
ApiRequest.req(url, method: .put).response { resp in
if resp.response?.statusCode == 204 {
completeHandler(true)
} else {
completeHandler(false)
}
}
}
static func resetFakeIpCache() {
ApiRequest.req("/cache/fakeip/flush", method: .post).response { resp in
Logger.log("flush fake ip: \(resp.response?.statusCode ?? -1)")
}
}
}
// MARK: - Stream Apis
extension ApiRequest {
func resetStreamApis() {
resetLogStreamApi()
resetTrafficStreamApi()
resetMemoryStreamApi()
}
func resetLogStreamApi() {
loggingWebSocketRetryTimer?.invalidate()
loggingWebSocketRetryTimer = nil
loggingWebSocketRetryDelay = 1
requestLog()
}
func resetTrafficStreamApi() {
trafficWebSocketRetryTimer?.invalidate()
trafficWebSocketRetryTimer = nil
trafficWebSocketRetryDelay = 1
requestTrafficInfo()
}
func resetMemoryStreamApi() {
memoryWebSocketRetryTimer?.invalidate()
memoryWebSocketRetryTimer = nil
memoryWebSocketRetryDelay = 1
requestMemoryInfo()
}
private func requestTrafficInfo() {
trafficWebSocketRetryTimer?.invalidate()
trafficWebSocketRetryTimer = nil
trafficWebSocket?.disconnect(forceTimeout: 0.5)
let socket = WebSocket(url: URL(string: ConfigManager.apiUrl.appending("/traffic"))!)
for header in ApiRequest.authHeader() {
socket.request.setValue(header.value, forHTTPHeaderField: header.name)
}
socket.delegate = self
socket.connect()
trafficWebSocket = socket
}
private func requestLog() {
loggingWebSocketRetryTimer?.invalidate()
loggingWebSocketRetryTimer = nil
loggingWebSocket?.disconnect(forceTimeout: 1)
let uriString = "/logs?level=".appending(ConfigManager.selectLoggingApiLevel.rawValue)
let socket = WebSocket(url: URL(string: ConfigManager.apiUrl.appending(uriString))!)
for header in ApiRequest.authHeader() {
socket.request.setValue(header.value, forHTTPHeaderField: header.name)
}
socket.delegate = self
socket.callbackQueue = logQueue
socket.connect()
loggingWebSocket = socket
}
private func requestMemoryInfo() {
memoryWebSocketRetryTimer?.invalidate()
memoryWebSocketRetryTimer = nil
memoryWebSocket?.disconnect(forceTimeout: 1)
let socket = WebSocket(url: URL(string: ConfigManager.apiUrl.appending("/memory"))!)
for header in ApiRequest.authHeader() {
socket.request.setValue(header.value, forHTTPHeaderField: header.name)
}
socket.delegate = self
socket.connect()
memoryWebSocket = socket
}
}
extension ApiRequest: WebSocketDelegate {
func websocketDidConnect(socket: WebSocketClient) {
guard let webSocket = socket as? WebSocket else { return }
switch webSocket {
case trafficWebSocket:
trafficWebSocketRetryDelay = 1
Logger.log("trafficWebSocket did Connect", level: .debug)
ConfigManager.shared.isRunning = true
delegate?.streamStatusChanged()
dashboardDelegate?.streamStatusChanged()
case loggingWebSocket:
loggingWebSocketRetryDelay = 1
Logger.log("loggingWebSocket did Connect", level: .debug)
case memoryWebSocket:
memoryWebSocketRetryDelay = 1
Logger.log("memoryWebSocket did Connect", level: .debug)
default:
return
}
}
func websocketDidDisconnect(socket: WebSocketClient, error: Error?) {
if (socket as? WebSocket) == trafficWebSocket {
ConfigManager.shared.isRunning = false
delegate?.streamStatusChanged()
dashboardDelegate?.streamStatusChanged()
}
guard let err = error else {
return
}
Logger.log(err.localizedDescription, level: .error)
guard let webSocket = socket as? WebSocket else { return }
switch webSocket {
case trafficWebSocket:
Logger.log("trafficWebSocket did disconnect", level: .debug)
trafficWebSocketRetryTimer?.invalidate()
trafficWebSocketRetryTimer =
Timer.scheduledTimer(withTimeInterval: trafficWebSocketRetryDelay, repeats: false, block: {
[weak self] _ in
if self?.trafficWebSocket?.isConnected == true { return }
self?.requestTrafficInfo()
})
trafficWebSocketRetryDelay *= 2
case loggingWebSocket:
Logger.log("loggingWebSocket did disconnect", level: .debug)
loggingWebSocketRetryTimer =
Timer.scheduledTimer(withTimeInterval: loggingWebSocketRetryDelay, repeats: false, block: {
[weak self] _ in
if self?.loggingWebSocket?.isConnected == true { return }
self?.requestLog()
})
loggingWebSocketRetryDelay *= 2
case memoryWebSocket:
Logger.log("memoryWebSocket did disconnect", level: .debug)
memoryWebSocketRetryTimer =
Timer.scheduledTimer(withTimeInterval: memoryWebSocketRetryDelay, repeats: false, block: {
[weak self] _ in
if self?.memoryWebSocket?.isConnected == true { return }
self?.requestMemoryInfo()
})
memoryWebSocketRetryDelay *= 2
default:
return
}
}
func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
guard let webSocket = socket as? WebSocket else { return }
let json = JSON(parseJSON: text)
switch webSocket {
case trafficWebSocket:
delegate?.didUpdateTraffic(up: json["up"].intValue, down: json["down"].intValue)
dashboardDelegate?.didUpdateTraffic(up: json["up"].intValue, down: json["down"].intValue)
case loggingWebSocket:
delegate?.didGetLog(log: json["payload"].stringValue, level: json["type"].string ?? "info")
dashboardDelegate?.didGetLog(log: json["payload"].stringValue, level: json["type"].string ?? "info")
case memoryWebSocket:
delegate?.didUpdateMemory(memory: json["inuse"].int64Value)
dashboardDelegate?.didUpdateMemory(memory: json["inuse"].int64Value)
default:
return
}
}
func websocketDidReceiveData(socket: WebSocketClient, data: Data) {}
}