mirror of
https://github.com/yJason/ClashX-Dashboard.git
synced 2026-03-01 00:35:19 +08:00
fix: spm
This commit is contained in:
@@ -0,0 +1,596 @@
|
||||
//
|
||||
// ApiRequest.swift
|
||||
// ClashX
|
||||
//
|
||||
// Created by CYC on 2018/7/30.
|
||||
// Copyright © 2018年 yichengchen. All rights reserved.
|
||||
//
|
||||
|
||||
import Alamofire
|
||||
import Cocoa
|
||||
import Starscream
|
||||
import SwiftyJSON
|
||||
import SwiftUI
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
private static func authHeader() -> HTTPHeaders {
|
||||
let secret = ConfigManager.shared.overrideSecret ?? ConfigManager.shared.apiSecret
|
||||
return (secret.count > 0) ? ["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?
|
||||
|
||||
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):
|
||||
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)
|
||||
// NSUserNotificationCenter.default.post(title: "Error", info: err.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
ClashRuleProviderResp.init()
|
||||
|
||||
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 (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: - 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()
|
||||
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()
|
||||
}
|
||||
|
||||
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)
|
||||
case loggingWebSocket:
|
||||
delegate?.didGetLog(log: json["payload"].stringValue, level: json["type"].string ?? "info")
|
||||
case memoryWebSocket:
|
||||
delegate?.didUpdateMemory(memory: json["inuse"].int64Value)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func websocketDidReceiveData(socket: WebSocketClient, data: Data) {}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// DateFormatter+.swift
|
||||
// ClashX
|
||||
//
|
||||
// Created by yicheng on 2019/12/14.
|
||||
// Copyright © 2019 west2online. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
extension DateFormatter {
|
||||
static var js: DateFormatter {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.locale = Locale(identifier: NSCalendar.Identifier.ISO8601.rawValue)
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
|
||||
return dateFormatter
|
||||
}
|
||||
|
||||
static var provider: DateFormatter {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSSZZ"
|
||||
return f
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// String+Encode.swift
|
||||
// ClashX
|
||||
//
|
||||
// Created by yicheng on 2019/12/11.
|
||||
// Copyright © 2019 west2online. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
extension String {
|
||||
var encoded: String {
|
||||
return addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// ConfigManager.swift
|
||||
// ClashX
|
||||
//
|
||||
// Created by CYC on 2018/6/12.
|
||||
// Copyright © 2018年 yichengchen. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
class ConfigManager {
|
||||
static let shared = ConfigManager()
|
||||
var apiPort = "9090"
|
||||
var apiSecret: String = ""
|
||||
var overrideApiURL: URL?
|
||||
var overrideSecret: String?
|
||||
|
||||
|
||||
var isRunning: Bool = false {
|
||||
didSet {
|
||||
NotificationCenter.default.post(.init(name: .init("ClashRunningStateChanged")))
|
||||
}
|
||||
}
|
||||
|
||||
var benchMarkUrl: String = UserDefaults.standard.string(forKey: "benchMarkUrl") ?? "http://cp.cloudflare.com/generate_204" {
|
||||
didSet {
|
||||
UserDefaults.standard.set(benchMarkUrl, forKey: "benchMarkUrl")
|
||||
}
|
||||
}
|
||||
|
||||
static var apiUrl: String {
|
||||
if let override = shared.overrideApiURL {
|
||||
return override.absoluteString
|
||||
}
|
||||
return "http://127.0.0.1:\(shared.apiPort)"
|
||||
}
|
||||
|
||||
static var webSocketUrl: String {
|
||||
if let override = shared.overrideApiURL, var comp = URLComponents(url: override, resolvingAgainstBaseURL: true) {
|
||||
if comp.scheme == "https" {
|
||||
comp.scheme = "wss"
|
||||
} else {
|
||||
comp.scheme = "ws"
|
||||
}
|
||||
return comp.url?.absoluteString ?? ""
|
||||
}
|
||||
return "ws://127.0.0.1:\(shared.apiPort)"
|
||||
}
|
||||
|
||||
static var selectLoggingApiLevel: ClashLogLevel {
|
||||
get {
|
||||
return ClashLogLevel(rawValue: UserDefaults.standard.string(forKey: "selectLoggingApiLevel") ?? "") ?? .info
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue.rawValue, forKey: "selectLoggingApiLevel")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// Logger.swift
|
||||
// ClashX
|
||||
//
|
||||
// Created by CYC on 2018/8/7.
|
||||
// Copyright © 2018年 yichengchen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CocoaLumberjackSwift
|
||||
|
||||
class Logger {
|
||||
static let shared = Logger()
|
||||
var fileLogger: DDFileLogger = DDFileLogger()
|
||||
|
||||
private init() {
|
||||
#if DEBUG
|
||||
DDLog.add(DDOSLogger.sharedInstance)
|
||||
#endif
|
||||
// default time zone is "UTC"
|
||||
let dataFormatter = DateFormatter()
|
||||
dataFormatter.setLocalizedDateFormatFromTemplate("YYYY/MM/dd HH:mm:ss:SSS")
|
||||
fileLogger.logFormatter = DDLogFileFormatterDefault.init(dateFormatter: dataFormatter)
|
||||
fileLogger.rollingFrequency = TimeInterval(60 * 60 * 24) // 24 hours
|
||||
fileLogger.logFileManager.maximumNumberOfLogFiles = 3
|
||||
DDLog.add(fileLogger)
|
||||
dynamicLogLevel = ConfigManager.selectLoggingApiLevel.toDDLogLevel()
|
||||
}
|
||||
|
||||
private func logToFile(msg: String, level: ClashLogLevel) {
|
||||
switch level {
|
||||
case .debug, .silent:
|
||||
DDLogDebug(msg)
|
||||
case .error:
|
||||
DDLogError(msg)
|
||||
case .info:
|
||||
DDLogInfo(msg)
|
||||
case .warning:
|
||||
DDLogWarn(msg)
|
||||
case .unknow:
|
||||
DDLogWarn(msg)
|
||||
}
|
||||
}
|
||||
|
||||
static func log(_ msg: String, level: ClashLogLevel = .info) {
|
||||
shared.logToFile(msg: "[\(level.rawValue)] \(msg)", level: level)
|
||||
}
|
||||
|
||||
func logFilePath() -> String {
|
||||
return fileLogger.logFileManager.sortedLogFilePaths.first ?? ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// ClashConfig.swift
|
||||
// ClashX
|
||||
//
|
||||
// Created by CYC on 2018/7/30.
|
||||
// Copyright © 2018年 yichengchen. All rights reserved.
|
||||
//
|
||||
import Foundation
|
||||
import CocoaLumberjackSwift
|
||||
|
||||
enum ClashProxyMode: String, Codable {
|
||||
case rule
|
||||
case global
|
||||
case direct
|
||||
}
|
||||
|
||||
extension ClashProxyMode {
|
||||
var name: String {
|
||||
switch self {
|
||||
case .rule: return NSLocalizedString("Rule", comment: "")
|
||||
case .global: return NSLocalizedString("Global", comment: "")
|
||||
case .direct: return NSLocalizedString("Direct", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ClashLogLevel: String, Codable {
|
||||
case info
|
||||
case warning
|
||||
case error
|
||||
case debug
|
||||
case silent
|
||||
case unknow = "unknown"
|
||||
|
||||
func toDDLogLevel() -> DDLogLevel {
|
||||
switch self {
|
||||
case .info:
|
||||
return .info
|
||||
case .warning:
|
||||
return .warning
|
||||
case .error:
|
||||
return .error
|
||||
case .debug:
|
||||
return .debug
|
||||
case .silent:
|
||||
return .off
|
||||
case .unknow:
|
||||
return .error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ClashConfig: Codable {
|
||||
var port: Int
|
||||
var socksPort: Int
|
||||
var redirPort: Int
|
||||
var allowLan: Bool
|
||||
var mixedPort: Int
|
||||
var mode: ClashProxyMode
|
||||
var logLevel: ClashLogLevel
|
||||
|
||||
var sniffing: Bool
|
||||
var ipv6: Bool
|
||||
|
||||
var tun: Tun
|
||||
var interfaceName: String
|
||||
|
||||
struct Tun: Codable {
|
||||
let enable: Bool
|
||||
let device: String
|
||||
let stack: String
|
||||
// let dns-hijack: [String]
|
||||
// let auto-route: Bool
|
||||
// let auto-detect-interface: Bool
|
||||
}
|
||||
|
||||
var usedHttpPort: Int {
|
||||
if mixedPort > 0 {
|
||||
return mixedPort
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
var usedSocksPort: Int {
|
||||
if mixedPort > 0 {
|
||||
return mixedPort
|
||||
}
|
||||
return socksPort
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case port, socksPort = "socks-port", redirPort = "redir-port", mixedPort = "mixed-port", allowLan = "allow-lan", mode, logLevel = "log-level", sniffing, tun, interfaceName = "interface-name", ipv6
|
||||
}
|
||||
|
||||
static func fromData(_ data: Data) -> ClashConfig? {
|
||||
let decoder = JSONDecoder()
|
||||
do {
|
||||
return try decoder.decode(ClashConfig.self, from: data)
|
||||
} catch let err {
|
||||
Logger.log((err as NSError).description, level: .error)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func copy() -> ClashConfig? {
|
||||
guard let data = try? JSONEncoder().encode(self) else { return nil }
|
||||
let copy = try? JSONDecoder().decode(ClashConfig.self, from: data)
|
||||
return copy
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// ClashConnection.swift
|
||||
// ClashX
|
||||
//
|
||||
// Created by yicheng on 2019/10/28.
|
||||
// Copyright © 2019 west2online. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
struct ClashConnectionSnapShot: Codable {
|
||||
let connections: [Connection]
|
||||
}
|
||||
|
||||
extension ClashConnectionSnapShot {
|
||||
struct Connection: Codable {
|
||||
let id: String
|
||||
let chains: [String]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// ClashProvider.swift
|
||||
// ClashX
|
||||
//
|
||||
// Created by yichengchen on 2019/12/14.
|
||||
// Copyright © 2019 west2online. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
class ClashProviderResp: Codable {
|
||||
let allProviders: [ClashProxyName: ClashProvider]
|
||||
lazy var providers: [ClashProxyName: ClashProvider] = {
|
||||
return allProviders.filter({ $0.value.vehicleType != .Compatible })
|
||||
}()
|
||||
|
||||
init() {
|
||||
allProviders = [:]
|
||||
}
|
||||
|
||||
static var decoder: JSONDecoder {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .formatted(DateFormatter.js)
|
||||
return decoder
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case allProviders = "providers"
|
||||
}
|
||||
}
|
||||
|
||||
class ClashProvider: Codable {
|
||||
enum ProviderType: String, Codable {
|
||||
case Proxy
|
||||
case String
|
||||
}
|
||||
|
||||
enum ProviderVehicleType: String, Codable {
|
||||
case HTTP
|
||||
case File
|
||||
case Compatible
|
||||
case Unknown
|
||||
}
|
||||
|
||||
let name: ClashProviderName
|
||||
let proxies: [ClashProxy]
|
||||
let type: ProviderType
|
||||
let vehicleType: ProviderVehicleType
|
||||
let updatedAt: Date?
|
||||
|
||||
let subscriptionInfo: ClashProviderSubInfo?
|
||||
}
|
||||
|
||||
class ClashProviderSubInfo: Codable {
|
||||
let upload: Int64
|
||||
let download: Int64
|
||||
let total: Int64
|
||||
let expire: Int
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case upload = "Upload",
|
||||
download = "Download",
|
||||
total = "Total",
|
||||
expire = "Expire"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
//
|
||||
// ClashProxy.swift
|
||||
// ClashX
|
||||
//
|
||||
// Created by CYC on 2019/3/17.
|
||||
// Copyright © 2019 west2online. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import SwiftyJSON
|
||||
|
||||
enum ClashProxyType: String, Codable {
|
||||
case urltest = "URLTest"
|
||||
case fallback = "Fallback"
|
||||
case loadBalance = "LoadBalance"
|
||||
case select = "Selector"
|
||||
case direct = "Direct"
|
||||
case reject = "Reject"
|
||||
case shadowsocks = "Shadowsocks"
|
||||
case shadowsocksR = "ShadowsocksR"
|
||||
case socks5 = "Socks5"
|
||||
case http = "Http"
|
||||
case vmess = "Vmess"
|
||||
case snell = "Snell"
|
||||
case trojan = "Trojan"
|
||||
case relay = "Relay"
|
||||
|
||||
case vless = "Vless"
|
||||
case hysteria = "Hysteria"
|
||||
case wireguardMeta = "WireGuard"
|
||||
case wireguard = "Wireguard"
|
||||
case tuic = "Tuic"
|
||||
|
||||
case pass = "Pass"
|
||||
|
||||
case unknown = "Unknown"
|
||||
|
||||
static let proxyGroups: [ClashProxyType] = [.select, .urltest, .fallback, .loadBalance]
|
||||
|
||||
var isAutoGroup: Bool {
|
||||
switch self {
|
||||
case .urltest, .fallback, .loadBalance:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static func isProxyGroup(_ proxy: ClashProxy) -> Bool {
|
||||
switch proxy.type {
|
||||
case .select, .urltest, .fallback, .loadBalance, .relay: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
static func isBuiltInProxy(_ proxy: ClashProxy) -> Bool {
|
||||
switch proxy.name {
|
||||
case "DIRECT", "REJECT", "PASS": return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typealias ClashProxyName = String
|
||||
typealias ClashProviderName = String
|
||||
|
||||
class ClashProxySpeedHistory: Codable {
|
||||
let time: Date
|
||||
let delay: Int
|
||||
let meanDelay: Int?
|
||||
|
||||
class HisDateFormaterInstance {
|
||||
static let shared = HisDateFormaterInstance()
|
||||
lazy var formater: DateFormatter = {
|
||||
var f = DateFormatter()
|
||||
f.dateFormat = "HH:mm"
|
||||
return f
|
||||
}()
|
||||
}
|
||||
|
||||
lazy var delayInt: Int = {
|
||||
meanDelay ?? delay
|
||||
}()
|
||||
|
||||
lazy var delayDisplay: String = {
|
||||
switch delayInt {
|
||||
case 0: return NSLocalizedString("fail", comment: "")
|
||||
default: return "\(delayInt) ms"
|
||||
}
|
||||
}()
|
||||
|
||||
lazy var dateDisplay: String = {
|
||||
return HisDateFormaterInstance.shared.formater.string(from: time)
|
||||
}()
|
||||
|
||||
lazy var displayString: String = "\(dateDisplay) \(delayDisplay)"
|
||||
}
|
||||
|
||||
class ClashProxy: Codable {
|
||||
let id: String?
|
||||
let name: ClashProxyName
|
||||
let type: ClashProxyType
|
||||
let all: [ClashProxyName]?
|
||||
let history: [ClashProxySpeedHistory]
|
||||
let now: ClashProxyName?
|
||||
weak var enclosingResp: ClashProxyResp?
|
||||
weak var enclosingProvider: ClashProvider?
|
||||
|
||||
let udp: Bool
|
||||
let xudp: Bool
|
||||
let tfo: Bool
|
||||
|
||||
enum SpeedtestAbleItem {
|
||||
case proxy(name: ClashProxyName)
|
||||
case provider(name: ClashProxyName, provider: ClashProviderName)
|
||||
case group(name: ClashProxyName)
|
||||
}
|
||||
|
||||
private static var nameLengthCachedMap = [ClashProxyName: CGFloat]()
|
||||
static func cleanCache() {
|
||||
nameLengthCachedMap.removeAll()
|
||||
}
|
||||
|
||||
lazy var speedtestAble: [SpeedtestAbleItem] = {
|
||||
guard let resp = enclosingResp, let allProxys = all else { return [] }
|
||||
var proxys = [SpeedtestAbleItem]()
|
||||
for proxy in allProxys {
|
||||
if let p = resp.proxiesMap[proxy] {
|
||||
if !ClashProxyType.isProxyGroup(p) {
|
||||
if let provider = p.enclosingProvider {
|
||||
proxys.append(.provider(name: p.name, provider: provider.name))
|
||||
} else {
|
||||
proxys.append(.proxy(name: p.name))
|
||||
}
|
||||
} else {
|
||||
proxys.append(.group(name: p.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
return proxys
|
||||
}()
|
||||
|
||||
lazy var isSpeedTestable: Bool = {
|
||||
return speedtestAble.count > 0
|
||||
}()
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type, all, history, now, name, udp, xudp, tfo, id
|
||||
}
|
||||
|
||||
lazy var maxProxyNameLength: CGFloat = {
|
||||
let rect = CGSize(width: CGFloat.greatestFiniteMagnitude, height: 20)
|
||||
|
||||
let lengths = all?.compactMap({ name -> CGFloat in
|
||||
if let length = ClashProxy.nameLengthCachedMap[name] {
|
||||
return length
|
||||
}
|
||||
|
||||
let rects = CGSize(width: CGFloat.greatestFiniteMagnitude, height: 20)
|
||||
let attr = [NSAttributedString.Key.font: NSFont.menuBarFont(ofSize: 14)]
|
||||
let length = (name as NSString)
|
||||
.boundingRect(with: rect,
|
||||
options: .usesLineFragmentOrigin,
|
||||
attributes: attr).width
|
||||
ClashProxy.nameLengthCachedMap[name] = length
|
||||
return length
|
||||
})
|
||||
return lengths?.max() ?? 0
|
||||
}()
|
||||
}
|
||||
|
||||
class ClashProxyResp {
|
||||
var proxies: [ClashProxy]
|
||||
|
||||
var proxiesMap: [ClashProxyName: ClashProxy]
|
||||
|
||||
var enclosingProviderResp: ClashProviderResp?
|
||||
|
||||
init(_ data: Data?) {
|
||||
guard let data
|
||||
else {
|
||||
self.proxiesMap = [:]
|
||||
self.proxies = []
|
||||
return
|
||||
}
|
||||
let proxies = JSON(data)["proxies"]
|
||||
var proxiesModel = [ClashProxy]()
|
||||
|
||||
var proxiesMap = [ClashProxyName: ClashProxy]()
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .formatted(DateFormatter.js)
|
||||
for value in proxies.dictionaryValue.values {
|
||||
guard let data = try? value.rawData() else {
|
||||
continue
|
||||
}
|
||||
guard let proxy = try? decoder.decode(ClashProxy.self, from: data) else {
|
||||
continue
|
||||
}
|
||||
proxiesModel.append(proxy)
|
||||
proxiesMap[proxy.name] = proxy
|
||||
}
|
||||
self.proxiesMap = proxiesMap
|
||||
self.proxies = proxiesModel
|
||||
|
||||
for proxy in self.proxies {
|
||||
proxy.enclosingResp = self
|
||||
}
|
||||
}
|
||||
|
||||
func updateProvider(_ providerResp: ClashProviderResp) {
|
||||
enclosingProviderResp = providerResp
|
||||
for provider in providerResp.providers.values {
|
||||
for proxy in provider.proxies {
|
||||
proxy.enclosingProvider = provider
|
||||
proxiesMap[proxy.name] = proxy
|
||||
proxies.append(proxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lazy var proxiesSortMap: [ClashProxyName: Int] = {
|
||||
var map = [ClashProxyName: Int]()
|
||||
for (idx, proxy) in (self.proxiesMap["GLOBAL"]?.all ?? []).enumerated() {
|
||||
map[proxy] = idx
|
||||
}
|
||||
return map
|
||||
}()
|
||||
|
||||
lazy var proxyGroups: [ClashProxy] = {
|
||||
return proxies.filter {
|
||||
ClashProxyType.isProxyGroup($0)
|
||||
}.sorted(by: { proxiesSortMap[$0.name] ?? -1 < proxiesSortMap[$1.name] ?? -1 })
|
||||
}()
|
||||
|
||||
lazy var longestProxyGroupName = {
|
||||
return proxyGroups.max { $1.name.count > $0.name.count }?.name ?? ""
|
||||
}()
|
||||
|
||||
lazy var maxProxyNameLength: CGFloat = {
|
||||
let rect = CGSize(width: CGFloat.greatestFiniteMagnitude, height: 20)
|
||||
let attr = [NSAttributedString.Key.font: NSFont.menuBarFont(ofSize: 0)]
|
||||
return (self.longestProxyGroupName as NSString)
|
||||
.boundingRect(with: rect,
|
||||
options: .usesLineFragmentOrigin,
|
||||
attributes: attr).width
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// ClashRule.swift
|
||||
// ClashX
|
||||
//
|
||||
// Created by CYC on 2018/10/27.
|
||||
// Copyright © 2018 west2online. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
class ClashRule: NSObject, Codable, Identifiable {
|
||||
@objc let type: String
|
||||
@objc let payload: String?
|
||||
@objc let proxy: String?
|
||||
|
||||
init(type: String, payload: String?, proxy: String?) {
|
||||
self.type = type
|
||||
self.payload = payload
|
||||
self.proxy = proxy
|
||||
}
|
||||
}
|
||||
|
||||
class ClashRuleResponse: Codable {
|
||||
var rules: [ClashRule]?
|
||||
|
||||
static func empty() -> ClashRuleResponse {
|
||||
return ClashRuleResponse()
|
||||
}
|
||||
|
||||
static func fromData(_ data: Data) -> ClashRuleResponse {
|
||||
let decoder = JSONDecoder()
|
||||
let model = try? decoder.decode(ClashRuleResponse.self, from: data)
|
||||
return model ?? ClashRuleResponse.empty()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// ClashRuleProvider.swift
|
||||
// ClashX Meta
|
||||
|
||||
import Foundation
|
||||
|
||||
class ClashRuleProviderResp: Codable {
|
||||
let allProviders: [ClashProxyName: ClashRuleProvider]
|
||||
|
||||
init() {
|
||||
allProviders = [:]
|
||||
}
|
||||
|
||||
static var decoder: JSONDecoder {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .formatted(DateFormatter.js)
|
||||
return decoder
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case allProviders = "providers"
|
||||
}
|
||||
}
|
||||
|
||||
class ClashRuleProvider: NSObject, Codable {
|
||||
@objc let name: ClashProviderName
|
||||
let ruleCount: Int
|
||||
@objc let behavior: String
|
||||
@objc let type: String
|
||||
let updatedAt: Date?
|
||||
}
|
||||
Reference in New Issue
Block a user