From bbaf15f5b89ad59b92d4494beb0af9b38f796c56 Mon Sep 17 00:00:00 2001 From: mrFq1 <1xxbx0il0@mozmail.com> Date: Thu, 25 May 2023 23:20:44 +0800 Subject: [PATCH] misc: copy all clashx files --- ClashX Dashboard.xcodeproj/project.pbxproj | 159 +++-- .../ClashX Links/ApiRequest.swift | 543 ++++++++++++++++++ .../Extensions/DateFormatter+.swift | 24 + .../Extensions/String+Encode.swift | 15 + .../General/Managers/ConfigManager.swift | 59 ++ ClashX Dashboard/ClashX Links/Logger.swift | 52 ++ .../ClashX Links/Models/ClashConfig.swift | 110 ++++ .../ClashX Links/Models/ClashConnection.swift | 110 ++++ .../ClashX Links/Models/ClashProvider.swift | 67 +++ .../ClashX Links/Models/ClashProxy.swift | 248 ++++++++ .../ClashX Links/Models/ClashRule.swift | 36 ++ .../Models/ClashRuleProvider.swift | 31 + 12 files changed, 1371 insertions(+), 83 deletions(-) create mode 100644 ClashX Dashboard/ClashX Links/ApiRequest.swift create mode 100644 ClashX Dashboard/ClashX Links/Extensions/DateFormatter+.swift create mode 100644 ClashX Dashboard/ClashX Links/Extensions/String+Encode.swift create mode 100644 ClashX Dashboard/ClashX Links/General/Managers/ConfigManager.swift create mode 100644 ClashX Dashboard/ClashX Links/Logger.swift create mode 100644 ClashX Dashboard/ClashX Links/Models/ClashConfig.swift create mode 100644 ClashX Dashboard/ClashX Links/Models/ClashConnection.swift create mode 100644 ClashX Dashboard/ClashX Links/Models/ClashProvider.swift create mode 100644 ClashX Dashboard/ClashX Links/Models/ClashProxy.swift create mode 100644 ClashX Dashboard/ClashX Links/Models/ClashRule.swift create mode 100644 ClashX Dashboard/ClashX Links/Models/ClashRuleProvider.swift diff --git a/ClashX Dashboard.xcodeproj/project.pbxproj b/ClashX Dashboard.xcodeproj/project.pbxproj index 5435c95..0add89a 100644 --- a/ClashX Dashboard.xcodeproj/project.pbxproj +++ b/ClashX Dashboard.xcodeproj/project.pbxproj @@ -15,6 +15,17 @@ 0155D39629F2342F00869830 /* TrafficGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0155D39529F2342F00869830 /* TrafficGraphView.swift */; }; 0155D39829F23BDE00869830 /* OverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0155D39729F23BDE00869830 /* OverviewView.swift */; }; 0172CB5129E5AE670072DDEF /* SwiftUIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172CB5029E5AE670072DDEF /* SwiftUIViewExtensions.swift */; }; + 0172F12F2A1FB06100EE2B6D /* DateFormatter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172F12C2A1FB06100EE2B6D /* DateFormatter+.swift */; }; + 0172F1312A1FB06100EE2B6D /* String+Encode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172F12E2A1FB06100EE2B6D /* String+Encode.swift */; }; + 0172F1352A1FB0B900EE2B6D /* ConfigManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172F1342A1FB0B900EE2B6D /* ConfigManager.swift */; }; + 0172F1372A1FB0CD00EE2B6D /* ApiRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172F1362A1FB0CD00EE2B6D /* ApiRequest.swift */; }; + 0172F1392A1FB0E900EE2B6D /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172F1382A1FB0E900EE2B6D /* Logger.swift */; }; + 0172F1412A1FB10D00EE2B6D /* ClashConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172F13B2A1FB10C00EE2B6D /* ClashConnection.swift */; }; + 0172F1422A1FB10D00EE2B6D /* ClashRuleProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172F13C2A1FB10D00EE2B6D /* ClashRuleProvider.swift */; }; + 0172F1432A1FB10D00EE2B6D /* ClashRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172F13D2A1FB10D00EE2B6D /* ClashRule.swift */; }; + 0172F1442A1FB10D00EE2B6D /* ClashConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172F13E2A1FB10D00EE2B6D /* ClashConfig.swift */; }; + 0172F1452A1FB10D00EE2B6D /* ClashProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172F13F2A1FB10D00EE2B6D /* ClashProvider.swift */; }; + 0172F1462A1FB10D00EE2B6D /* ClashProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172F1402A1FB10D00EE2B6D /* ClashProxy.swift */; }; 017753C029EF7FB2006999DB /* APIServerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017753BF29EF7FB1006999DB /* APIServerItem.swift */; }; 017DCADD29E83BFD00B9622A /* RuleProviderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017DCADC29E83BFD00B9622A /* RuleProviderView.swift */; }; 017F9AAA2A0DFEBD00B81497 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 017F9AA92A0DFEBD00B81497 /* Introspect */; }; @@ -36,22 +47,10 @@ 0192318329DD70B400539EDD /* SidebarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0192318229DD70B400539EDD /* SidebarItem.swift */; }; 0192318529DD7DCD00539EDD /* SidebarItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0192318429DD7DCD00539EDD /* SidebarItemView.swift */; }; 0192318729DD83FF00539EDD /* OverviewTopItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0192318629DD83FF00539EDD /* OverviewTopItemView.swift */; }; - 0192B5B729DE5098002CDBF3 /* ApiRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0192B5B629DE5098002CDBF3 /* ApiRequest.swift */; }; 0192B5BA29DE50F8002CDBF3 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 0192B5B929DE50F8002CDBF3 /* Alamofire */; }; 0192B5BD29DE5113002CDBF3 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 0192B5BC29DE5113002CDBF3 /* SwiftyJSON */; }; 0192B5C029DE5134002CDBF3 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 0192B5BF29DE5134002CDBF3 /* Starscream */; }; - 0192B5CA29DE5151002CDBF3 /* ClashConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0192B5C229DE5150002CDBF3 /* ClashConfig.swift */; }; - 0192B5CB29DE5151002CDBF3 /* ClashProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0192B5C329DE5150002CDBF3 /* ClashProvider.swift */; }; - 0192B5CD29DE5151002CDBF3 /* ClashProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0192B5C529DE5150002CDBF3 /* ClashProxy.swift */; }; - 0192B5CF29DE5151002CDBF3 /* ClashConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0192B5C729DE5150002CDBF3 /* ClashConnection.swift */; }; - 0192B5D029DE5151002CDBF3 /* ClashRuleProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0192B5C829DE5150002CDBF3 /* ClashRuleProvider.swift */; }; - 0192B5D129DE5151002CDBF3 /* ClashRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0192B5C929DE5150002CDBF3 /* ClashRule.swift */; }; 0192B5D429DE5190002CDBF3 /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0192B5D329DE5190002CDBF3 /* CocoaLumberjackSwift */; }; - 0192B5D629DE5207002CDBF3 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0192B5D529DE5206002CDBF3 /* Logger.swift */; }; - 0192B5F629DE5262002CDBF3 /* ConfigManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0192B5E029DE5261002CDBF3 /* ConfigManager.swift */; }; - 0192B61129DE5292002CDBF3 /* Array+Safe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0192B60629DE5292002CDBF3 /* Array+Safe.swift */; }; - 0192B61429DE5292002CDBF3 /* String+Encode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0192B60929DE5292002CDBF3 /* String+Encode.swift */; }; - 0192B61A29DE5292002CDBF3 /* DateFormatter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0192B60F29DE5292002CDBF3 /* DateFormatter+.swift */; }; 019D6A8729F015DF00A6AC02 /* ArrayExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 019D6A8629F015DF00A6AC02 /* ArrayExtensions.swift */; }; 019D6A9629F194C600A6AC02 /* DSFSparkline in Frameworks */ = {isa = PBXBuildFile; productRef = 019D6A9529F194C600A6AC02 /* DSFSparkline */; }; 01A351A229DD8F440054894E /* RuleItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01A351A129DD8F440054894E /* RuleItemView.swift */; }; @@ -76,6 +75,17 @@ 0155D39529F2342F00869830 /* TrafficGraphView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrafficGraphView.swift; sourceTree = ""; }; 0155D39729F23BDE00869830 /* OverviewView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverviewView.swift; sourceTree = ""; }; 0172CB5029E5AE670072DDEF /* SwiftUIViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIViewExtensions.swift; sourceTree = ""; }; + 0172F12C2A1FB06100EE2B6D /* DateFormatter+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DateFormatter+.swift"; sourceTree = ""; }; + 0172F12E2A1FB06100EE2B6D /* String+Encode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Encode.swift"; sourceTree = ""; }; + 0172F1342A1FB0B900EE2B6D /* ConfigManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigManager.swift; sourceTree = ""; }; + 0172F1362A1FB0CD00EE2B6D /* ApiRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiRequest.swift; sourceTree = ""; }; + 0172F1382A1FB0E900EE2B6D /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + 0172F13B2A1FB10C00EE2B6D /* ClashConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClashConnection.swift; sourceTree = ""; }; + 0172F13C2A1FB10D00EE2B6D /* ClashRuleProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClashRuleProvider.swift; sourceTree = ""; }; + 0172F13D2A1FB10D00EE2B6D /* ClashRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClashRule.swift; sourceTree = ""; }; + 0172F13E2A1FB10D00EE2B6D /* ClashConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClashConfig.swift; sourceTree = ""; }; + 0172F13F2A1FB10D00EE2B6D /* ClashProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClashProvider.swift; sourceTree = ""; }; + 0172F1402A1FB10D00EE2B6D /* ClashProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClashProxy.swift; sourceTree = ""; }; 017753BF29EF7FB1006999DB /* APIServerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIServerItem.swift; sourceTree = ""; }; 017DCADC29E83BFD00B9622A /* RuleProviderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleProviderView.swift; sourceTree = ""; }; 017F9AAB2A0E0B2300B81497 /* ProxyGroupRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyGroupRowView.swift; sourceTree = ""; }; @@ -98,18 +108,6 @@ 0192318229DD70B400539EDD /* SidebarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarItem.swift; sourceTree = ""; }; 0192318429DD7DCD00539EDD /* SidebarItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarItemView.swift; sourceTree = ""; }; 0192318629DD83FF00539EDD /* OverviewTopItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverviewTopItemView.swift; sourceTree = ""; }; - 0192B5B629DE5098002CDBF3 /* ApiRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ApiRequest.swift; path = ../../../ClashX.Meta/ClashX/General/ApiRequest.swift; sourceTree = ""; }; - 0192B5C229DE5150002CDBF3 /* ClashConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClashConfig.swift; sourceTree = ""; }; - 0192B5C329DE5150002CDBF3 /* ClashProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClashProvider.swift; sourceTree = ""; }; - 0192B5C529DE5150002CDBF3 /* ClashProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClashProxy.swift; sourceTree = ""; }; - 0192B5C729DE5150002CDBF3 /* ClashConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClashConnection.swift; sourceTree = ""; }; - 0192B5C829DE5150002CDBF3 /* ClashRuleProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClashRuleProvider.swift; sourceTree = ""; }; - 0192B5C929DE5150002CDBF3 /* ClashRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClashRule.swift; sourceTree = ""; }; - 0192B5D529DE5206002CDBF3 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Logger.swift; path = ../../../ClashX.Meta/ClashX/Basic/Logger.swift; sourceTree = ""; }; - 0192B5E029DE5261002CDBF3 /* ConfigManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigManager.swift; sourceTree = ""; }; - 0192B60629DE5292002CDBF3 /* Array+Safe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Safe.swift"; sourceTree = ""; }; - 0192B60929DE5292002CDBF3 /* String+Encode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Encode.swift"; sourceTree = ""; }; - 0192B60F29DE5292002CDBF3 /* DateFormatter+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DateFormatter+.swift"; sourceTree = ""; }; 019D6A8629F015DF00A6AC02 /* ArrayExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayExtensions.swift; sourceTree = ""; }; 01A351A129DD8F440054894E /* RuleItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleItemView.swift; sourceTree = ""; }; 01A351A829DD9CB00054894E /* Connections.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connections.swift; sourceTree = ""; }; @@ -152,6 +150,44 @@ path = APISetting; sourceTree = ""; }; + 0172F12B2A1FB05900EE2B6D /* Extensions */ = { + isa = PBXGroup; + children = ( + 0172F12C2A1FB06100EE2B6D /* DateFormatter+.swift */, + 0172F12E2A1FB06100EE2B6D /* String+Encode.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 0172F1322A1FB0AA00EE2B6D /* General */ = { + isa = PBXGroup; + children = ( + 0172F1332A1FB0B000EE2B6D /* Managers */, + ); + path = General; + sourceTree = ""; + }; + 0172F1332A1FB0B000EE2B6D /* Managers */ = { + isa = PBXGroup; + children = ( + 0172F1342A1FB0B900EE2B6D /* ConfigManager.swift */, + ); + path = Managers; + sourceTree = ""; + }; + 0172F13A2A1FB10300EE2B6D /* Models */ = { + isa = PBXGroup; + children = ( + 0172F13E2A1FB10D00EE2B6D /* ClashConfig.swift */, + 0172F13B2A1FB10C00EE2B6D /* ClashConnection.swift */, + 0172F13F2A1FB10D00EE2B6D /* ClashProvider.swift */, + 0172F1402A1FB10D00EE2B6D /* ClashProxy.swift */, + 0172F13D2A1FB10D00EE2B6D /* ClashRule.swift */, + 0172F13C2A1FB10D00EE2B6D /* ClashRuleProvider.swift */, + ); + path = Models; + sourceTree = ""; + }; 018003AF2A136D3E0070226E /* Providers */ = { isa = PBXGroup; children = ( @@ -253,57 +289,15 @@ 0192B5B529DE506D002CDBF3 /* ClashX Links */ = { isa = PBXGroup; children = ( - 0192B60429DE5292002CDBF3 /* Extensions */, - 0192B5D729DE5261002CDBF3 /* General */, - 0192B5B629DE5098002CDBF3 /* ApiRequest.swift */, - 0192B5D529DE5206002CDBF3 /* Logger.swift */, - 0192B5C129DE5150002CDBF3 /* Models */, + 0172F1362A1FB0CD00EE2B6D /* ApiRequest.swift */, + 0172F1382A1FB0E900EE2B6D /* Logger.swift */, + 0172F1322A1FB0AA00EE2B6D /* General */, + 0172F12B2A1FB05900EE2B6D /* Extensions */, + 0172F13A2A1FB10300EE2B6D /* Models */, ); path = "ClashX Links"; sourceTree = ""; }; - 0192B5C129DE5150002CDBF3 /* Models */ = { - isa = PBXGroup; - children = ( - 0192B5C229DE5150002CDBF3 /* ClashConfig.swift */, - 0192B5C329DE5150002CDBF3 /* ClashProvider.swift */, - 0192B5C529DE5150002CDBF3 /* ClashProxy.swift */, - 0192B5C729DE5150002CDBF3 /* ClashConnection.swift */, - 0192B5C829DE5150002CDBF3 /* ClashRuleProvider.swift */, - 0192B5C929DE5150002CDBF3 /* ClashRule.swift */, - ); - name = Models; - path = ../../../ClashX.Meta/ClashX/Models; - sourceTree = ""; - }; - 0192B5D729DE5261002CDBF3 /* General */ = { - isa = PBXGroup; - children = ( - 0192B5D829DE5261002CDBF3 /* Managers */, - ); - name = General; - path = ../../../ClashX.Meta/ClashX/General; - sourceTree = ""; - }; - 0192B5D829DE5261002CDBF3 /* Managers */ = { - isa = PBXGroup; - children = ( - 0192B5E029DE5261002CDBF3 /* ConfigManager.swift */, - ); - path = Managers; - sourceTree = ""; - }; - 0192B60429DE5292002CDBF3 /* Extensions */ = { - isa = PBXGroup; - children = ( - 0192B60629DE5292002CDBF3 /* Array+Safe.swift */, - 0192B60929DE5292002CDBF3 /* String+Encode.swift */, - 0192B60F29DE5292002CDBF3 /* DateFormatter+.swift */, - ); - name = Extensions; - path = ../../../ClashX.Meta/ClashX/Extensions; - sourceTree = ""; - }; 01A351A029DD8F210054894E /* Rules */ = { isa = PBXGroup; children = ( @@ -459,47 +453,46 @@ 01DCEFB32A150FB300DBBDB3 /* ProxyProvidersRowView.swift in Sources */, 0192318729DD83FF00539EDD /* OverviewTopItemView.swift in Sources */, 0192318029DD5E0B00539EDD /* LogsView.swift in Sources */, + 0172F1392A1FB0E900EE2B6D /* Logger.swift in Sources */, 01505C4E2A14AAEB001ACC4F /* ProviderProxiesView.swift in Sources */, + 0172F1412A1FB10D00EE2B6D /* ClashConnection.swift in Sources */, 0192318529DD7DCD00539EDD /* SidebarItemView.swift in Sources */, + 0172F1442A1FB10D00EE2B6D /* ClashConfig.swift in Sources */, + 0172F1452A1FB10D00EE2B6D /* ClashProvider.swift in Sources */, 0192317E29DD5E0100539EDD /* ConfigView.swift in Sources */, 0155D39629F2342F00869830 /* TrafficGraphView.swift in Sources */, 01A3EF042A120103003038B5 /* DBProxyStorage.swift in Sources */, + 0172F1432A1FB10D00EE2B6D /* ClashRule.swift in Sources */, 017F9AAC2A0E0B2300B81497 /* ProxyGroupRowView.swift in Sources */, + 0172F1372A1FB0CD00EE2B6D /* ApiRequest.swift in Sources */, 017DCADD29E83BFD00B9622A /* RuleProviderView.swift in Sources */, 0192318329DD70B400539EDD /* SidebarItem.swift in Sources */, 0155D39829F23BDE00869830 /* OverviewView.swift in Sources */, - 0192B5D629DE5207002CDBF3 /* Logger.swift in Sources */, - 0192B5CB29DE5151002CDBF3 /* ClashProvider.swift in Sources */, - 0192B5CF29DE5151002CDBF3 /* ClashConnection.swift in Sources */, 018C836C29E17505006366D3 /* ClashApiDatasStorage.swift in Sources */, 018A61BD29E9A2ED008608C0 /* APISettingView.swift in Sources */, + 0172F1462A1FB10D00EE2B6D /* ClashProxy.swift in Sources */, 010F693B29ED639A00BAAFB5 /* ClashServerAppStorage.swift in Sources */, 01F885D329E04E21008241EB /* ProxyGroupView.swift in Sources */, 0192315F29DD4DCF00539EDD /* ClashX_DashboardApp.swift in Sources */, 018003B12A136DDB0070226E /* ProvidersView.swift in Sources */, - 0192B5CD29DE5151002CDBF3 /* ClashProxy.swift in Sources */, - 0192B61129DE5292002CDBF3 /* Array+Safe.swift in Sources */, 01A351A229DD8F440054894E /* RuleItemView.swift in Sources */, 01F885D529E053DE008241EB /* ProxyItemView.swift in Sources */, - 0192B5D029DE5151002CDBF3 /* ClashRuleProvider.swift in Sources */, + 0172F1312A1FB06100EE2B6D /* String+Encode.swift in Sources */, 01DCEFB12A150E8B00DBBDB3 /* RuleProvidersRowView.swift in Sources */, 01F5E3F42A1E53F4008F3DEB /* ConfigItemView.swift in Sources */, + 0172F1352A1FB0B900EE2B6D /* ConfigManager.swift in Sources */, 0192317129DD566000539EDD /* SidebarView.swift in Sources */, - 0192B5D129DE5151002CDBF3 /* ClashRule.swift in Sources */, 0192317C29DD5DF200539EDD /* ConnectionsView.swift in Sources */, + 0172F12F2A1FB06100EE2B6D /* DateFormatter+.swift in Sources */, 017753C029EF7FB2006999DB /* APIServerItem.swift in Sources */, + 0172F1422A1FB10D00EE2B6D /* ClashRuleProvider.swift in Sources */, 015278082A15F9FD00516236 /* ProxyProviderInfoView.swift in Sources */, - 0192B61429DE5292002CDBF3 /* String+Encode.swift in Sources */, 0192317829DD5DA500539EDD /* ProxiesView.swift in Sources */, - 0192B5F629DE5262002CDBF3 /* ConfigManager.swift in Sources */, - 0192B61A29DE5292002CDBF3 /* DateFormatter+.swift in Sources */, 01F885CF29DFD8DF008241EB /* CollectionsTableView.swift in Sources */, 018AFEA52A1B5F7A0076E66B /* ProgressButton.swift in Sources */, 01505C4A2A147B84001ACC4F /* DBProviderStorage.swift in Sources */, - 0192B5CA29DE5151002CDBF3 /* ClashConfig.swift in Sources */, 0172CB5129E5AE670072DDEF /* SwiftUIViewExtensions.swift in Sources */, 01A351A929DD9CB00054894E /* Connections.swift in Sources */, - 0192B5B729DE5098002CDBF3 /* ApiRequest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ClashX Dashboard/ClashX Links/ApiRequest.swift b/ClashX Dashboard/ClashX Links/ApiRequest.swift new file mode 100644 index 0000000..abf86a2 --- /dev/null +++ b/ClashX Dashboard/ClashX Links/ApiRequest.swift @@ -0,0 +1,543 @@ +// +// 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 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 trafficWebSocketRetryDelay: TimeInterval = 1 + private var loggingWebSocketRetryDelay: TimeInterval = 1 + private var trafficWebSocketRetryTimer: Timer? + private var loggingWebSocketRetryTimer: 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 (ClashConnectionSnapShot) -> Void) { + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .formatted(DateFormatter.js) + + req("/connections").responseDecodable(of: ClashConnectionSnapShot.self, decoder: decoder) { resp in + switch resp.result { + case let .success(snapshot): + completeHandler(snapshot) + case .failure: + return +// assertionFailure() +// completeHandler(ClashConnectionSnapShot()) + } + } + } + + static func closeConnection(_ conn: ClashConnection) { + 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() + } + + func resetLogStreamApi() { + loggingWebSocketRetryTimer?.invalidate() + loggingWebSocketRetryTimer = nil + loggingWebSocketRetryDelay = 1 + requestLog() + } + + func resetTrafficStreamApi() { + trafficWebSocketRetryTimer?.invalidate() + trafficWebSocketRetryTimer = nil + trafficWebSocketRetryDelay = 1 + requestTrafficInfo() + } + + 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 + } +} + +extension ApiRequest: WebSocketDelegate { + func websocketDidConnect(socket: WebSocketClient) { + guard let webSocket = socket as? WebSocket else { return } + if webSocket == trafficWebSocket { + trafficWebSocketRetryDelay = 1 + Logger.log("trafficWebSocket did Connect", level: .debug) + + ConfigManager.shared.isRunning = true + delegate?.streamStatusChanged() + } else { + loggingWebSocketRetryDelay = 1 + Logger.log("loggingWebSocket did Connect", level: .debug) + } + } + + 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 } + + if webSocket == 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 + } else { + 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 + } + } + + func websocketDidReceiveMessage(socket: WebSocketClient, text: String) { + guard let webSocket = socket as? WebSocket else { return } + let json = JSON(parseJSON: text) + if webSocket == trafficWebSocket { + delegate?.didUpdateTraffic(up: json["up"].intValue, down: json["down"].intValue) + } else { + delegate?.didGetLog(log: json["payload"].stringValue, level: json["type"].string ?? "info") + } + } + + func websocketDidReceiveData(socket: WebSocketClient, data: Data) {} +} diff --git a/ClashX Dashboard/ClashX Links/Extensions/DateFormatter+.swift b/ClashX Dashboard/ClashX Links/Extensions/DateFormatter+.swift new file mode 100644 index 0000000..22aa80a --- /dev/null +++ b/ClashX Dashboard/ClashX Links/Extensions/DateFormatter+.swift @@ -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 + } +} diff --git a/ClashX Dashboard/ClashX Links/Extensions/String+Encode.swift b/ClashX Dashboard/ClashX Links/Extensions/String+Encode.swift new file mode 100644 index 0000000..7bcb275 --- /dev/null +++ b/ClashX Dashboard/ClashX Links/Extensions/String+Encode.swift @@ -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) ?? "" + } +} diff --git a/ClashX Dashboard/ClashX Links/General/Managers/ConfigManager.swift b/ClashX Dashboard/ClashX Links/General/Managers/ConfigManager.swift new file mode 100644 index 0000000..d7b78e1 --- /dev/null +++ b/ClashX Dashboard/ClashX Links/General/Managers/ConfigManager.swift @@ -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") + } + } +} diff --git a/ClashX Dashboard/ClashX Links/Logger.swift b/ClashX Dashboard/ClashX Links/Logger.swift new file mode 100644 index 0000000..3ed5254 --- /dev/null +++ b/ClashX Dashboard/ClashX Links/Logger.swift @@ -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 ?? "" + } +} diff --git a/ClashX Dashboard/ClashX Links/Models/ClashConfig.swift b/ClashX Dashboard/ClashX Links/Models/ClashConfig.swift new file mode 100644 index 0000000..0f033a8 --- /dev/null +++ b/ClashX Dashboard/ClashX Links/Models/ClashConfig.swift @@ -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 + } +} diff --git a/ClashX Dashboard/ClashX Links/Models/ClashConnection.swift b/ClashX Dashboard/ClashX Links/Models/ClashConnection.swift new file mode 100644 index 0000000..196ecf4 --- /dev/null +++ b/ClashX Dashboard/ClashX Links/Models/ClashConnection.swift @@ -0,0 +1,110 @@ +// +// ClashConnection.swift +// ClashX +// +// Created by yicheng on 2019/10/28. +// Copyright © 2019 west2online. All rights reserved. +// + +import Cocoa +import DifferenceKit + +struct ClashConnectionSnapShot: Codable { + let downloadTotal: Int + let uploadTotal: Int + let connections: [ClashConnection] +} + +struct ClashConnection: Codable, Hashable { + let id: String + let chains: [String] + let upload: Int64 + let download: Int64 + let start: Date + let rule: String + let rulePayload: String + + let metadata: MetaConnectionData +} + +struct MetaConnectionData: Codable, Hashable { + let uid: Int + + let network: String + let type: String + let sourceIP: String + let destinationIP: String + let sourcePort: String + let destinationPort: String + let inboundIP: String + let inboundPort: String + let inboundName: String + let host: String + let dnsMode: String + let process: String + let processPath: String + let specialProxy: String + let specialRules: String + let remoteDestination: String + let sniffHost: String + +} + + +class ClashConnectionObject: NSObject, Differentiable { + @objc let id: String + @objc let host: String + @objc let sniffHost: String + @objc let process: String + @objc let download: Int64 + @objc let upload: Int64 + let downloadString: String + let uploadString: String + let chains: [String] + @objc let chainString: String + @objc let ruleString: String + @objc let startDate: Date + let startString: String + @objc let source: String + @objc let destinationIP: String? + @objc let type: String + + var differenceIdentifier: String { + return id + } + + func isContentEqual(to source: ClashConnectionObject) -> Bool { + download == source.download && + upload == source.upload && + startString == source.startString + } + + init(_ conn: ClashConnection) { + let byteCountFormatter = ByteCountFormatter() + let startFormatter = RelativeDateTimeFormatter() + startFormatter.unitsStyle = .short + + let metadata = conn.metadata + + id = conn.id + host = "\(metadata.host == "" ? metadata.destinationIP : metadata.host):\(metadata.destinationPort)" + sniffHost = metadata.sniffHost == "" ? "-" : metadata.sniffHost + process = metadata.process + download = conn.download + downloadString = byteCountFormatter.string(fromByteCount: conn.download) + upload = conn.upload + uploadString = byteCountFormatter.string(fromByteCount: conn.upload) + chains = conn.chains + chainString = conn.chains.reversed().joined(separator: "/") + ruleString = conn.rulePayload == "" ? conn.rule : "\(conn.rule) :: \(conn.rulePayload)" + startDate = conn.start + startString = startFormatter.localizedString(for: conn.start, relativeTo: Date()) + source = "\(metadata.sourceIP):\(metadata.sourcePort)" + destinationIP = [metadata.remoteDestination, + metadata.destinationIP, + metadata.host].first(where: { $0 != "" }) + + type = "\(metadata.type)(\(metadata.network))" + } + +} diff --git a/ClashX Dashboard/ClashX Links/Models/ClashProvider.swift b/ClashX Dashboard/ClashX Links/Models/ClashProvider.swift new file mode 100644 index 0000000..8486758 --- /dev/null +++ b/ClashX Dashboard/ClashX Links/Models/ClashProvider.swift @@ -0,0 +1,67 @@ +// +// 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 { + let id = UUID().uuidString + 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" + } +} diff --git a/ClashX Dashboard/ClashX Links/Models/ClashProxy.swift b/ClashX Dashboard/ClashX Links/Models/ClashProxy.swift new file mode 100644 index 0000000..f70660f --- /dev/null +++ b/ClashX Dashboard/ClashX Links/Models/ClashProxy.swift @@ -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 = UUID().uuidString + 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 + } + + 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 + }() +} diff --git a/ClashX Dashboard/ClashX Links/Models/ClashRule.swift b/ClashX Dashboard/ClashX Links/Models/ClashRule.swift new file mode 100644 index 0000000..0f60de9 --- /dev/null +++ b/ClashX Dashboard/ClashX Links/Models/ClashRule.swift @@ -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() + } +} diff --git a/ClashX Dashboard/ClashX Links/Models/ClashRuleProvider.swift b/ClashX Dashboard/ClashX Links/Models/ClashRuleProvider.swift new file mode 100644 index 0000000..3daeef8 --- /dev/null +++ b/ClashX Dashboard/ClashX Links/Models/ClashRuleProvider.swift @@ -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? +}