diff --git a/AppCheck.xcodeproj/project.pbxproj b/AppCheck.xcodeproj/project.pbxproj index 13350cf..8a13156 100644 --- a/AppCheck.xcodeproj/project.pbxproj +++ b/AppCheck.xcodeproj/project.pbxproj @@ -7,13 +7,17 @@ objects = { /* Begin PBXBuildFile section */ + 540C6457240D929300E948F9 /* EditableRows.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540C6456240D929300E948F9 /* EditableRows.swift */; }; 541A957623E602DF00C09C19 /* LaunchIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 541A957523E602DF00C09C19 /* LaunchIcon.png */; }; 541AC5D82399498A00A769D7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541AC5D72399498A00A769D7 /* AppDelegate.swift */; }; 541AC5DD2399498A00A769D7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 541AC5DB2399498A00A769D7 /* Main.storyboard */; }; 541AC5DF2399498B00A769D7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 541AC5DE2399498B00A769D7 /* Assets.xcassets */; }; 541AC5E22399498B00A769D7 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 541AC5E02399498B00A769D7 /* LaunchScreen.storyboard */; }; + 542E2A982404973F001462DC /* TBCMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542E2A972404973F001462DC /* TBCMain.swift */; }; + 542E2A9A24051556001462DC /* TVCSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542E2A9924051556001462DC /* TVCSettings.swift */; }; 543CDB2023EEE61900B7F323 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543CDB1F23EEE61900B7F323 /* PacketTunnelProvider.swift */; }; 543CDB2523EEE61900B7F323 /* GlassVPN.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 543CDB1D23EEE61900B7F323 /* GlassVPN.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 544C95262407B1C700AB89D0 /* SharedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544C95252407B1C700AB89D0 /* SharedState.swift */; }; 546063D023FC2565008F505A /* CocoaAsyncSocket.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 546063B723FC254C008F505A /* CocoaAsyncSocket.framework */; }; 546063D123FC2565008F505A /* CocoaAsyncSocket.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 546063B723FC254C008F505A /* CocoaAsyncSocket.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 546063D223FC2565008F505A /* CocoaLumberjack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 546063B523FC254B008F505A /* CocoaLumberjack.framework */; }; @@ -40,6 +44,14 @@ 54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54953E6023E0D69A0054345C /* TVCHosts.swift */; }; 54953E6F23E44CD00054345C /* TVCHostDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54953E6E23E44CD00054345C /* TVCHostDetails.swift */; }; 54953E7123E473F10054345C /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 54953E7023E473F10054345C /* Settings.bundle */; }; + 54B34594240E6343004C53CC /* TVCFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B34593240E6343004C53CC /* TVCFilter.swift */; }; + 54B34596240F0513004C53CC /* TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B34595240F0513004C53CC /* TableView.swift */; }; + 54B345992414F491004C53CC /* DBWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345982414F491004C53CC /* DBWrapper.swift */; }; + 54B345A6241BB982004C53CC /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345A5241BB982004C53CC /* Notifications.swift */; }; + 54B345A9241BBA0B004C53CC /* Generic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345A8241BBA0B004C53CC /* Generic.swift */; }; + 54B345AB241BBA5B004C53CC /* AlertSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345AA241BBA5B004C53CC /* AlertSheet.swift */; }; + 54B345AD241BBB00004C53CC /* GroupedDomain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345AC241BBB00004C53CC /* GroupedDomain.swift */; }; + 54B345B0242264F8004C53CC /* third-level.txt in Resources */ = {isa = PBXBuildFile; fileRef = 54B345AF242264F8004C53CC /* third-level.txt */; }; 54C056DB23E9E36E00214A3F /* AppInfoType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C056DA23E9E36E00214A3F /* AppInfoType.swift */; }; 54C056DD23E9EEF700214A3F /* BundleIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C056DC23E9EEF700214A3F /* BundleIcon.swift */; }; /* End PBXBuildFile section */ @@ -89,6 +101,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 540C6456240D929300E948F9 /* EditableRows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableRows.swift; sourceTree = ""; }; 541A957523E602DF00C09C19 /* LaunchIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = LaunchIcon.png; sourceTree = ""; }; 541AC5D42399498A00A769D7 /* AppCheck.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AppCheck.app; sourceTree = BUILT_PRODUCTS_DIR; }; 541AC5D72399498A00A769D7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -97,10 +110,13 @@ 541AC5E12399498B00A769D7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 541AC5E32399498B00A769D7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 541AC5EA2399499A00A769D7 /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; }; + 542E2A972404973F001462DC /* TBCMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TBCMain.swift; sourceTree = ""; }; + 542E2A9924051556001462DC /* TVCSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCSettings.swift; sourceTree = ""; }; 543CDB1D23EEE61900B7F323 /* GlassVPN.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = GlassVPN.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 543CDB1F23EEE61900B7F323 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = ""; }; 543CDB2123EEE61900B7F323 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 543CDB2223EEE61900B7F323 /* GlassVPN.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GlassVPN.entitlements; sourceTree = ""; }; + 544C95252407B1C700AB89D0 /* SharedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedState.swift; sourceTree = ""; }; 546063B123FC254B008F505A /* Sodium.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sodium.framework; path = Carthage/Build/iOS/Sodium.framework; sourceTree = ""; }; 546063B223FC254B008F505A /* NEKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NEKit.framework; path = Carthage/Build/iOS/NEKit.framework; sourceTree = ""; }; 546063B323FC254B008F505A /* tun2socks.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = tun2socks.framework; path = Carthage/Build/iOS/tun2socks.framework; sourceTree = ""; }; @@ -116,6 +132,14 @@ 54953E6023E0D69A0054345C /* TVCHosts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCHosts.swift; sourceTree = ""; }; 54953E6E23E44CD00054345C /* TVCHostDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCHostDetails.swift; sourceTree = ""; }; 54953E7023E473F10054345C /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; + 54B34593240E6343004C53CC /* TVCFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCFilter.swift; sourceTree = ""; }; + 54B34595240F0513004C53CC /* TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableView.swift; sourceTree = ""; }; + 54B345982414F491004C53CC /* DBWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBWrapper.swift; sourceTree = ""; }; + 54B345A5241BB982004C53CC /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = ""; }; + 54B345A8241BBA0B004C53CC /* Generic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Generic.swift; sourceTree = ""; }; + 54B345AA241BBA5B004C53CC /* AlertSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertSheet.swift; sourceTree = ""; }; + 54B345AC241BBB00004C53CC /* GroupedDomain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupedDomain.swift; sourceTree = ""; }; + 54B345AF242264F8004C53CC /* third-level.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "third-level.txt"; sourceTree = ""; }; 54B7562223D7B2DC008F0C41 /* SQDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SQDB.swift; sourceTree = ""; }; 54C056DA23E9E36E00214A3F /* AppInfoType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoType.swift; sourceTree = ""; }; 54C056DC23E9EEF700214A3F /* BundleIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIcon.swift; sourceTree = ""; }; @@ -149,11 +173,30 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 540C6454240D5BAE00E948F9 /* Requests */ = { + isa = PBXGroup; + children = ( + 54953E5E23DEBE840054345C /* TVCDomains.swift */, + 54953E6023E0D69A0054345C /* TVCHosts.swift */, + 54953E6E23E44CD00054345C /* TVCHostDetails.swift */, + ); + path = Requests; + sourceTree = ""; + }; + 540C6455240D5BD200E948F9 /* Settings */ = { + isa = PBXGroup; + children = ( + 542E2A9924051556001462DC /* TVCSettings.swift */, + 54B34593240E6343004C53CC /* TVCFilter.swift */, + ); + path = Settings; + sourceTree = ""; + }; 541AC5CB2399498A00A769D7 = { isa = PBXGroup; children = ( - 54B7562223D7B2DC008F0C41 /* SQDB.swift */, 541AC5D62399498A00A769D7 /* main */, + 542E2A9B24051F79001462DC /* media */, 543CDB1E23EEE61900B7F323 /* GlassVPN */, 541AC5D52399498A00A769D7 /* Products */, 541AC5E92399499A00A769D7 /* Frameworks */, @@ -172,15 +215,16 @@ 541AC5D62399498A00A769D7 /* main */ = { isa = PBXGroup; children = ( + 54B3459A2415651C004C53CC /* DB */, + 54B345A4241BB975004C53CC /* Extensions */, 548B1F9423D338EC005B047C /* main.entitlements */, 541AC5D72399498A00A769D7 /* AppDelegate.swift */, - 54C056DC23E9EEF700214A3F /* BundleIcon.swift */, - 54C056DA23E9E36E00214A3F /* AppInfoType.swift */, - 54953E5E23DEBE840054345C /* TVCDomains.swift */, - 54953E6023E0D69A0054345C /* TVCHosts.swift */, - 54953E6E23E44CD00054345C /* TVCHostDetails.swift */, + 542E2A972404973F001462DC /* TBCMain.swift */, + 54B34597240F18DD004C53CC /* TVC Extensions */, + 540C6454240D5BAE00E948F9 /* Requests */, + 540C6455240D5BD200E948F9 /* Settings */, + 54B345B12422E029004C53CC /* unused */, 541AC5DB2399498A00A769D7 /* Main.storyboard */, - 541A957523E602DF00C09C19 /* LaunchIcon.png */, 541AC5E02399498B00A769D7 /* LaunchScreen.storyboard */, 541AC5DE2399498B00A769D7 /* Assets.xcassets */, 541AC5E32399498B00A769D7 /* Info.plist */, @@ -207,6 +251,15 @@ name = Frameworks; sourceTree = ""; }; + 542E2A9B24051F79001462DC /* media */ = { + isa = PBXGroup; + children = ( + 541A957523E602DF00C09C19 /* LaunchIcon.png */, + 54B345AF242264F8004C53CC /* third-level.txt */, + ); + path = media; + sourceTree = ""; + }; 543CDB1E23EEE61900B7F323 /* GlassVPN */ = { isa = PBXGroup; children = ( @@ -217,6 +270,45 @@ path = GlassVPN; sourceTree = ""; }; + 54B34597240F18DD004C53CC /* TVC Extensions */ = { + isa = PBXGroup; + children = ( + 540C6456240D929300E948F9 /* EditableRows.swift */, + ); + path = "TVC Extensions"; + sourceTree = ""; + }; + 54B3459A2415651C004C53CC /* DB */ = { + isa = PBXGroup; + children = ( + 54B7562223D7B2DC008F0C41 /* SQDB.swift */, + 54B345982414F491004C53CC /* DBWrapper.swift */, + ); + path = DB; + sourceTree = ""; + }; + 54B345A4241BB975004C53CC /* Extensions */ = { + isa = PBXGroup; + children = ( + 544C95252407B1C700AB89D0 /* SharedState.swift */, + 54B345A8241BBA0B004C53CC /* Generic.swift */, + 54B345A5241BB982004C53CC /* Notifications.swift */, + 54B345AA241BBA5B004C53CC /* AlertSheet.swift */, + 54B345AC241BBB00004C53CC /* GroupedDomain.swift */, + 54B34595240F0513004C53CC /* TableView.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 54B345B12422E029004C53CC /* unused */ = { + isa = PBXGroup; + children = ( + 54C056DC23E9EEF700214A3F /* BundleIcon.swift */, + 54C056DA23E9E36E00214A3F /* AppInfoType.swift */, + ); + path = unused; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -316,6 +408,7 @@ 541AC5E22399498B00A769D7 /* LaunchScreen.storyboard in Resources */, 541AC5DF2399498B00A769D7 /* Assets.xcassets in Resources */, 541AC5DD2399498A00A769D7 /* Main.storyboard in Resources */, + 54B345B0242264F8004C53CC /* third-level.txt in Resources */, 541A957623E602DF00C09C19 /* LaunchIcon.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -354,13 +447,24 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 54B345AD241BBB00004C53CC /* GroupedDomain.swift in Sources */, + 54B345A6241BB982004C53CC /* Notifications.swift in Sources */, + 54B345AB241BBA5B004C53CC /* AlertSheet.swift in Sources */, + 544C95262407B1C700AB89D0 /* SharedState.swift in Sources */, + 54B345A9241BBA0B004C53CC /* Generic.swift in Sources */, + 54B34596240F0513004C53CC /* TableView.swift in Sources */, 54953E3323DC752E0054345C /* SQDB.swift in Sources */, 54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */, + 540C6457240D929300E948F9 /* EditableRows.swift in Sources */, + 542E2A9A24051556001462DC /* TVCSettings.swift in Sources */, 54953E5F23DEBE840054345C /* TVCDomains.swift in Sources */, 54C056DB23E9E36E00214A3F /* AppInfoType.swift in Sources */, 54953E6F23E44CD00054345C /* TVCHostDetails.swift in Sources */, 54C056DD23E9EEF700214A3F /* BundleIcon.swift in Sources */, + 542E2A982404973F001462DC /* TBCMain.swift in Sources */, 541AC5D82399498A00A769D7 /* AppDelegate.swift in Sources */, + 54B345992414F491004C53CC /* DBWrapper.swift in Sources */, + 54B34594240E6343004C53CC /* TVCFilter.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -455,10 +559,12 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_SWIFT_FLAGS = ""; + "OTHER_SWIFT_FLAGS[sdk=iphonesimulator*]" = "-D IOS_SIMULATOR"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -512,9 +618,10 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.3; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = ""; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; @@ -530,6 +637,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = main/main.entitlements; CURRENT_PROJECT_VERSION = 7; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + ); INFOPLIST_FILE = main/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -550,6 +661,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = main/main.entitlements; CURRENT_PROJECT_VERSION = 7; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + ); INFOPLIST_FILE = main/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -573,7 +688,6 @@ "$(PROJECT_DIR)/Carthage/Build/iOS", ); INFOPLIST_FILE = GlassVPN/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.2; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -597,7 +711,6 @@ "$(PROJECT_DIR)/Carthage/Build/iOS", ); INFOPLIST_FILE = GlassVPN/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.2; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/GlassVPN/PacketTunnelProvider.swift b/GlassVPN/PacketTunnelProvider.swift index 449af8e..af36f1c 100644 --- a/GlassVPN/PacketTunnelProvider.swift +++ b/GlassVPN/PacketTunnelProvider.swift @@ -2,8 +2,7 @@ import NetworkExtension import NEKit fileprivate var db: SQLiteDatabase? -fileprivate var blockedDomains: [String] = [] -fileprivate var ignoredDomains: [String] = [] +fileprivate var domainFilters: [String : FilterOptions] = [:] // MARK: ObserverFactory @@ -17,19 +16,13 @@ class LDObserverFactory: ObserverFactory { override func signal(_ event: ProxySocketEvent) { switch event { case .receivedRequest(let session, let socket): - QLog("DNS: \(session.host)") - if ignoredDomains.allSatisfy({ session.host != $0 && !session.host.hasSuffix("." + $0) }) { - try? db?.insertDNSQuery(session.host) - } else { - QLog("ignored") - } - for domain in blockedDomains { - if (session.host == domain || session.host.hasSuffix("." + domain)) { - QLog("blocked") - socket.forceDisconnect() - return - } - } + ZLog("DNS: \(session.host)") + let match = domainFilters.first { session.host == $0.key || session.host.hasSuffix("." + $0.key) } + let block = match?.value.contains(.blocked) ?? false + let ignore = match?.value.contains(.ignored) ?? false + if !ignore { try? db?.insertDNSQuery(session.host, blocked: block) } + else { ZLog("ignored") } + if block { ZLog("blocked"); socket.forceDisconnect() } default: break } @@ -47,9 +40,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { var proxyServer: GCDHTTPProxyServer! override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { - QLog("startTunnel") - ignoredDomains = ["signal.org", "whispersystems.org"] - // TODO: init blocked & ignored + ZLog("startTunnel") do { db = try SQLiteDatabase.open(path: DB_PATH) try db!.createTable(table: DNSQuery.self) @@ -62,6 +53,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider { } proxyServer = nil + // Load domain filter + domainFilters = db!.loadFilters() ?? [:] + + // Create proxy let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: proxyServerAddress) settings.mtu = NSNumber(value: 1500) @@ -81,11 +76,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider { self.setTunnelNetworkSettings(settings) { error in guard error == nil else { - QLog("setTunnelNetworkSettings error: \(String(describing: error))") + ZLog("setTunnelNetworkSettings error: \(String(describing: error))") completionHandler(error) return } - QLog("setTunnelNetworkSettings success \(self.packetFlow)") + ZLog("setTunnelNetworkSettings success \(self.packetFlow)") completionHandler(nil) self.proxyServer = GCDHTTPProxyServer(address: IPAddress(fromString: self.proxyServerAddress), port: Port(port: self.proxyServerPort)) @@ -94,7 +89,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { completionHandler(nil) } catch let proxyError { - QLog("Error starting proxy server \(proxyError)") + ZLog("Error starting proxy server \(proxyError)") completionHandler(proxyError) } } @@ -102,26 +97,26 @@ class PacketTunnelProvider: NEPacketTunnelProvider { override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { - QLog("stopTunnel") + ZLog("stopTunnel") db = nil DNSServer.currentServer = nil RawSocketFactory.TunnelProvider = nil ObserverFactory.currentFactory = nil proxyServer.stop() proxyServer = nil - QLog("error on stopping: \(reason)") + ZLog("error on stopping: \(reason)") completionHandler() exit(EXIT_SUCCESS) } override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { - QLog("handleAppMessage") + ZLog("handleAppMessage") if let handler = completionHandler { handler(messageData) } } } -public func QLog(_ message: String) { +fileprivate func ZLog(_ message: String) { NSLog("TUN: \(message)") } diff --git a/SQDB.swift b/SQDB.swift deleted file mode 100644 index 2e498bb..0000000 --- a/SQDB.swift +++ /dev/null @@ -1,219 +0,0 @@ -import Foundation -import SQLite3 - -//let basePath = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) -let basePath = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.de.uni-bamberg.psi.AppCheck") -public let DB_PATH = basePath!.appendingPathComponent("dnslog.sqlite").relativePath - -enum SQLiteError: Error { - case OpenDatabase(message: String) - case Prepare(message: String) - case Step(message: String) - case Bind(message: String) -} - -//: ## The Database Connection -class SQLiteDatabase { - private let dbPointer: OpaquePointer? - private init(dbPointer: OpaquePointer?) { - self.dbPointer = dbPointer - } - - fileprivate var errorMessage: String { - if let errorPointer = sqlite3_errmsg(dbPointer) { - let errorMessage = String(cString: errorPointer) - return errorMessage - } else { - return "No error message provided from sqlite." - } - } - - deinit { - sqlite3_close(dbPointer) -// SQLiteDatabase.destroyDatabase(path: DB_PATH) - } - - static func destroyDatabase(path: String) { - do { - if FileManager.default.fileExists(atPath: path) { - try FileManager.default.removeItem(atPath: path) - } - } catch { - print("Could not destroy database file: \(path)") - } - } - - func destroyContent() throws { - let deleteStatement = try prepareStatement(sql: "DELETE FROM req;") - defer { - sqlite3_finalize(deleteStatement) - } - guard sqlite3_step(deleteStatement) == SQLITE_DONE else { - throw SQLiteError.Step(message: errorMessage) - } - } - - static func open(path: String) throws -> SQLiteDatabase { - var db: OpaquePointer? - //sqlite3_open_v2(path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_SHAREDCACHE, nil) - if sqlite3_open(path, &db) == SQLITE_OK { - return SQLiteDatabase(dbPointer: db) - } else { - defer { - if db != nil { - sqlite3_close(db) - } - } - if let errorPointer = sqlite3_errmsg(db) { - let message = String(cString: errorPointer) - throw SQLiteError.OpenDatabase(message: message) - } else { - throw SQLiteError.OpenDatabase(message: "No error message provided from sqlite.") - } - } - } - - func prepareStatement(sql: String) throws -> OpaquePointer? { - var statement: OpaquePointer? - guard sqlite3_prepare_v2(dbPointer, sql, -1, &statement, nil) == SQLITE_OK else { - throw SQLiteError.Prepare(message: errorMessage) - } - return statement - } - - func createTable(table: SQLTable.Type) throws { - let createTableStatement = try prepareStatement(sql: table.createStatement) - defer { - sqlite3_finalize(createTableStatement) - } - guard sqlite3_step(createTableStatement) == SQLITE_DONE else { - throw SQLiteError.Step(message: errorMessage) - } - } -} - -protocol SQLTable { - static var createStatement: String { get } -} - -struct DNSQuery: SQLTable { - let ts: Int64 - let domain: String - let host: String? - static var createStatement: String { - return """ - CREATE TABLE IF NOT EXISTS req( - ts BIGINT DEFAULT (strftime('%s','now')), - domain VARCHAR(255) NOT NULL, - host VARCHAR(2047) - ); - """ - } -} - -extension SQLiteDatabase { - - func insertDNSQuery(_ dnsQuery: String) throws { - // Split dns query into subdomain part - var domain: String = dnsQuery - var host: String? = nil - let lastChr = dnsQuery.last?.asciiValue ?? 0 - if lastChr > UInt8(ascii: "9") || lastChr < UInt8(ascii: "0") { // if not IP address - guard let last1 = dnsQuery.lastIndex(of: ".") else { - return - } - let last2 = dnsQuery[...dnsQuery.index(before: last1)].lastIndex(of: ".") - if let idx = last2 { - domain = String(dnsQuery.suffix(from: dnsQuery.index(after: idx))) - host = String(dnsQuery.prefix(upTo: idx)) - } - } - // perform query - let insertSql = "INSERT INTO req (domain, host) VALUES (?, ?);" - let insertStatement = try prepareStatement(sql: insertSql) - defer { - sqlite3_finalize(insertStatement) - } - guard - sqlite3_bind_text(insertStatement, 1, (domain as NSString).utf8String, -1, nil) == SQLITE_OK && - sqlite3_bind_text(insertStatement, 2, (host as NSString?)?.utf8String, -1, nil) == SQLITE_OK - else { - throw SQLiteError.Bind(message: errorMessage) - } - guard sqlite3_step(insertStatement) == SQLITE_DONE else { - throw SQLiteError.Step(message: errorMessage) - } - } - - func domainList() -> [GroupedDomain] { -// let querySql = "SELECT DISTINCT domain FROM req;" - let querySql = "SELECT domain, COUNT(*), MAX(ts) FROM req GROUP BY domain ORDER BY 3 DESC;" - guard let queryStatement = try? prepareStatement(sql: querySql) else { - print("Error preparing statement for insert") - return [] - } - defer { - sqlite3_finalize(queryStatement) - } - var res: [GroupedDomain] = [] - while (sqlite3_step(queryStatement) == SQLITE_ROW) { - let d = sqlite3_column_text(queryStatement, 0) - let c = sqlite3_column_int64(queryStatement, 1) - let l = sqlite3_column_int64(queryStatement, 2) - res.append(GroupedDomain(label: String(cString: d!), count: c, lastModified: l)) - } - return res - } - - func hostsForDomain(_ domain: NSString) -> [GroupedDomain] { - let querySql = "SELECT host, COUNT(*), MAX(ts) FROM req WHERE domain = ? GROUP BY host ORDER BY 1 ASC;" - guard let queryStatement = try? prepareStatement(sql: querySql) else { - print("Error preparing statement for insert") - return [] - } - defer { - sqlite3_finalize(queryStatement) - } - guard sqlite3_bind_text(queryStatement, 1, domain.utf8String, -1, nil) == SQLITE_OK else { - print("Error binding insert key") - return [] - } - var res: [GroupedDomain] = [] - while (sqlite3_step(queryStatement) == SQLITE_ROW) { - let h = sqlite3_column_text(queryStatement, 0) - let c = sqlite3_column_int64(queryStatement, 1) - let l = sqlite3_column_int64(queryStatement, 2) - res.append(GroupedDomain(label: h != nil ? String(cString: h!) : "", count: c, lastModified: l)) - } - return res - } - - func timesForDomain(_ domain: String, host: String?) -> [Timestamp] { - let querySql = "SELECT ts FROM req WHERE domain = ? AND host = ?;" - guard let queryStatement = try? prepareStatement(sql: querySql) else { - print("Error preparing statement for insert") - return [] - } - defer { - sqlite3_finalize(queryStatement) - } - guard - sqlite3_bind_text(queryStatement, 1, (domain as NSString).utf8String, -1, nil) == SQLITE_OK && - sqlite3_bind_text(queryStatement, 2, (host as NSString?)?.utf8String, -1, nil) == SQLITE_OK - else { - print("Error binding insert key") - return [] - } - var res: [Timestamp] = [] - while (sqlite3_step(queryStatement) == SQLITE_ROW) { - let ts = sqlite3_column_int64(queryStatement, 0) - res.append(ts) - } - return res - } -} - -typealias Timestamp = Int64 -struct GroupedDomain { - let label: String, count: Int64, lastModified: Timestamp -} diff --git a/main/AppDelegate.swift b/main/AppDelegate.swift index 8a5507a..4281712 100644 --- a/main/AppDelegate.swift +++ b/main/AppDelegate.swift @@ -2,7 +2,6 @@ import UIKit import NetworkExtension let VPNConfigBundleIdentifier = "de.uni-bamberg.psi.AppCheck.VPN" -let dateFormatter = DateFormatter() @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { @@ -11,15 +10,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var managerVPN: NETunnelProviderManager? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" - -// if UserDefaults.standard.bool(forKey: "kill_proxy") { -// UserDefaults.standard.set(false, forKey: "kill_proxy") -// disableDNS() -// } else { -// postDNSState() -// } - if UserDefaults.standard.bool(forKey: "kill_db") { UserDefaults.standard.set(false, forKey: "kill_db") SQLiteDatabase.destroyDatabase(path: DB_PATH) @@ -27,19 +17,21 @@ class AppDelegate: UIResponder, UIApplicationDelegate { do { let db = try SQLiteDatabase.open(path: DB_PATH) try db.createTable(table: DNSQuery.self) + try db.createTable(table: DNSFilter.self) } catch {} - self.postVPNState(.invalid) + DBWrp.initContentOfDB() + loadVPN { mgr in self.managerVPN = mgr self.postVPNState() } + NSNotification.Name.NEVPNStatusDidChange.observe(call: #selector(vpnStatusChanged(_:)), on: self) return true } - - func applicationDidBecomeActive(_ application: UIApplication) { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. -// postVPNState() + + @objc private func vpnStatusChanged(_ notification: Notification) { + postRawVPNState((notification.object as? NETunnelProviderSession)?.status ?? .invalid) } func setProxyEnabled(_ newState: Bool) { @@ -69,7 +61,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { mgr.protocolConfiguration = proto mgr.isEnabled = true mgr.saveToPreferences { error in - guard error == nil else { return } + guard error == nil else { + self.postProcessedVPNState(.off) + //ErrorAlert(error!).presentIn(self.window?.rootViewController) + return + } success(mgr) } } @@ -105,16 +101,29 @@ class AppDelegate: UIResponder, UIApplicationDelegate { private func postVPNState() { guard let mgr = self.managerVPN else { - self.postVPNState(.invalid) + self.postRawVPNState(.invalid) return } mgr.loadFromPreferences { _ in - self.postVPNState(mgr.connection.status) + self.postRawVPNState(mgr.connection.status) } } - private func postVPNState(_ state: NEVPNStatus) { - NotificationCenter.default.post(name: .init("ChangedStateGlassVPN"), object: state) + // MARK: Notifications + + private func postRawVPNState(_ origState: NEVPNStatus) { + let state: VPNState + switch origState { + case .connected: state = .on + case .connecting, .disconnecting, .reasserting: state = .inbetween + case .invalid, .disconnected: fallthrough + @unknown default: state = .off + } + postProcessedVPNState(state) + } + + private func postProcessedVPNState(_ state: VPNState) { + currentVPNState = state + NotifyVPNStateChanged.post(state) } } - diff --git a/main/Assets.xcassets/block_ignore.imageset/Contents.json b/main/Assets.xcassets/block_ignore.imageset/Contents.json new file mode 100644 index 0000000..98fc308 --- /dev/null +++ b/main/Assets.xcassets/block_ignore.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "img.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "img@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "img@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/main/Assets.xcassets/block_ignore.imageset/img.png b/main/Assets.xcassets/block_ignore.imageset/img.png new file mode 100644 index 0000000..8abed8d Binary files /dev/null and b/main/Assets.xcassets/block_ignore.imageset/img.png differ diff --git a/main/Assets.xcassets/block_ignore.imageset/img@2x.png b/main/Assets.xcassets/block_ignore.imageset/img@2x.png new file mode 100644 index 0000000..603d0f2 Binary files /dev/null and b/main/Assets.xcassets/block_ignore.imageset/img@2x.png differ diff --git a/main/Assets.xcassets/block_ignore.imageset/img@3x.png b/main/Assets.xcassets/block_ignore.imageset/img@3x.png new file mode 100644 index 0000000..7096c35 Binary files /dev/null and b/main/Assets.xcassets/block_ignore.imageset/img@3x.png differ diff --git a/main/Assets.xcassets/journal.imageset/Contents.json b/main/Assets.xcassets/journal.imageset/Contents.json new file mode 100644 index 0000000..98fc308 --- /dev/null +++ b/main/Assets.xcassets/journal.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "img.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "img@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "img@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/main/Assets.xcassets/journal.imageset/img.png b/main/Assets.xcassets/journal.imageset/img.png new file mode 100644 index 0000000..088893d Binary files /dev/null and b/main/Assets.xcassets/journal.imageset/img.png differ diff --git a/main/Assets.xcassets/journal.imageset/img@2x.png b/main/Assets.xcassets/journal.imageset/img@2x.png new file mode 100644 index 0000000..91fdc53 Binary files /dev/null and b/main/Assets.xcassets/journal.imageset/img@2x.png differ diff --git a/main/Assets.xcassets/journal.imageset/img@3x.png b/main/Assets.xcassets/journal.imageset/img@3x.png new file mode 100644 index 0000000..4d17dbd Binary files /dev/null and b/main/Assets.xcassets/journal.imageset/img@3x.png differ diff --git a/main/Assets.xcassets/quicklook-not.imageset/Contents.json b/main/Assets.xcassets/quicklook-not.imageset/Contents.json new file mode 100644 index 0000000..98fc308 --- /dev/null +++ b/main/Assets.xcassets/quicklook-not.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "img.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "img@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "img@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/main/Assets.xcassets/quicklook-not.imageset/img.png b/main/Assets.xcassets/quicklook-not.imageset/img.png new file mode 100644 index 0000000..146b5e5 Binary files /dev/null and b/main/Assets.xcassets/quicklook-not.imageset/img.png differ diff --git a/main/Assets.xcassets/quicklook-not.imageset/img@2x.png b/main/Assets.xcassets/quicklook-not.imageset/img@2x.png new file mode 100644 index 0000000..38802c1 Binary files /dev/null and b/main/Assets.xcassets/quicklook-not.imageset/img@2x.png differ diff --git a/main/Assets.xcassets/quicklook-not.imageset/img@3x.png b/main/Assets.xcassets/quicklook-not.imageset/img@3x.png new file mode 100644 index 0000000..025811a Binary files /dev/null and b/main/Assets.xcassets/quicklook-not.imageset/img@3x.png differ diff --git a/main/Assets.xcassets/settings.imageset/Contents.json b/main/Assets.xcassets/settings.imageset/Contents.json new file mode 100644 index 0000000..98fc308 --- /dev/null +++ b/main/Assets.xcassets/settings.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "img.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "img@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "img@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/main/Assets.xcassets/settings.imageset/img.png b/main/Assets.xcassets/settings.imageset/img.png new file mode 100644 index 0000000..41ff40f Binary files /dev/null and b/main/Assets.xcassets/settings.imageset/img.png differ diff --git a/main/Assets.xcassets/settings.imageset/img@2x.png b/main/Assets.xcassets/settings.imageset/img@2x.png new file mode 100644 index 0000000..e1e66e6 Binary files /dev/null and b/main/Assets.xcassets/settings.imageset/img@2x.png differ diff --git a/main/Assets.xcassets/settings.imageset/img@3x.png b/main/Assets.xcassets/settings.imageset/img@3x.png new file mode 100644 index 0000000..19c5a6a Binary files /dev/null and b/main/Assets.xcassets/settings.imageset/img@3x.png differ diff --git a/main/Assets.xcassets/shield-x.imageset/Contents.json b/main/Assets.xcassets/shield-x.imageset/Contents.json new file mode 100644 index 0000000..98fc308 --- /dev/null +++ b/main/Assets.xcassets/shield-x.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "img.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "img@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "img@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/main/Assets.xcassets/shield-x.imageset/img.png b/main/Assets.xcassets/shield-x.imageset/img.png new file mode 100644 index 0000000..dd46d46 Binary files /dev/null and b/main/Assets.xcassets/shield-x.imageset/img.png differ diff --git a/main/Assets.xcassets/shield-x.imageset/img@2x.png b/main/Assets.xcassets/shield-x.imageset/img@2x.png new file mode 100644 index 0000000..2bf1683 Binary files /dev/null and b/main/Assets.xcassets/shield-x.imageset/img@2x.png differ diff --git a/main/Assets.xcassets/shield-x.imageset/img@3x.png b/main/Assets.xcassets/shield-x.imageset/img@3x.png new file mode 100644 index 0000000..7912f53 Binary files /dev/null and b/main/Assets.xcassets/shield-x.imageset/img@3x.png differ diff --git a/main/Assets.xcassets/tag.imageset/Contents.json b/main/Assets.xcassets/tag.imageset/Contents.json new file mode 100644 index 0000000..98fc308 --- /dev/null +++ b/main/Assets.xcassets/tag.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "img.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "img@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "img@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/main/Assets.xcassets/tag.imageset/img.png b/main/Assets.xcassets/tag.imageset/img.png new file mode 100644 index 0000000..cb24240 Binary files /dev/null and b/main/Assets.xcassets/tag.imageset/img.png differ diff --git a/main/Assets.xcassets/tag.imageset/img@2x.png b/main/Assets.xcassets/tag.imageset/img@2x.png new file mode 100644 index 0000000..19aa3a1 Binary files /dev/null and b/main/Assets.xcassets/tag.imageset/img@2x.png differ diff --git a/main/Assets.xcassets/tag.imageset/img@3x.png b/main/Assets.xcassets/tag.imageset/img@3x.png new file mode 100644 index 0000000..9d90b0f Binary files /dev/null and b/main/Assets.xcassets/tag.imageset/img@3x.png differ diff --git a/main/Base.lproj/Main.storyboard b/main/Base.lproj/Main.storyboard index 67fe63e..7b1ea0c 100644 --- a/main/Base.lproj/Main.storyboard +++ b/main/Base.lproj/Main.storyboard @@ -1,13 +1,14 @@ - + + - + @@ -15,29 +16,9 @@ - - - - - - - - - AppCheck helps you identify which applications communicate with third parties. It does so by logging DNS network requests. AppCheck learns only the destination addresses, not the actual data that is exchanged. - -Your data belongs to you. Therefore, monitoring and analysis take place on your device only. The app does not share any data with us or any other third-party. - -⒈ Tap the red button in the upper right corner to start the DNS proxy. The proxy is only accessible locally to apps on this device. -⒉ The proxy monitors DNS requests in the background. -⒊ Use your apps as usual. -⒋ Come back to AppCheck to see the results. - - - - - + @@ -69,27 +50,13 @@ Your data belongs to you. Therefore, monitoring and analysis take place on your - - - - - - - - - - - - - - - + - + - + @@ -131,11 +98,11 @@ Your data belongs to you. Therefore, monitoring and analysis take place on your - + - + @@ -173,12 +140,13 @@ Your data belongs to you. Therefore, monitoring and analysis take place on your - + - + + @@ -189,7 +157,284 @@ Your data belongs to you. Therefore, monitoring and analysis take place on your - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Your data belongs to you. Therefore, monitoring and analysis take place on your device only. The app does not share any data with us or any other third-party. + + + + + + + + + + + + + + + + + + + + diff --git a/main/DB/DBWrapper.swift b/main/DB/DBWrapper.swift new file mode 100644 index 0000000..6dc40af --- /dev/null +++ b/main/DB/DBWrapper.swift @@ -0,0 +1,276 @@ +import UIKit + +let DBWrp = DBWrapper() + +class DBWrapper { + private var latestModification: Timestamp = 0 + private var dataA: [GroupedDomain] = [] // Domains + private var dataB: [[GroupedDomain]] = [] // Hosts + private var dataF: [String : FilterOptions] = [:] // Filters + private let Q = DispatchQueue(label: "de.uni-bamberg.psi.AppCheck.db-wrapper-queue", attributes: .concurrent) + + // auto update rows callback + var currentlyOpenParent: String? + weak var dataA_delegate: IncrementalDataSourceUpdate? + weak var dataB_delegate: IncrementalDataSourceUpdate? + func dataB_delegate(_ parent: String) -> IncrementalDataSourceUpdate? { + (currentlyOpenParent == parent) ? dataB_delegate : nil + } + + + // MARK: - Data Source Getter + + func listOfDomains() -> [GroupedDomain] { + Q.sync() { dataA } + } + + func listOfHosts(_ parent: String) -> [GroupedDomain] { + Q.sync() { dataB[ifExist: dataA_index(of: parent)] ?? [] } + } + + func dataF_list(_ filter: FilterOptions) -> [String] { + Q.sync() { dataF.compactMap { $1.contains(filter) ? $0 : nil } } + } + + func dataF_counts() -> (blocked: Int, ignored: Int) { + Q.sync() { dataF.reduce((0, 0)) { + ($0.0 + ($1.1.contains(.blocked) ? 1 : 0), + $0.1 + ($1.1.contains(.ignored) ? 1 : 0)) }} + } + + func listOfTimes(_ domain: String?) -> [(Timestamp, Bool)] { + guard let domain = domain else { return [] } + return AppDB?.timesForDomain(domain)?.reversed() ?? [] + } + + + // MARK: - Init + + func initContentOfDB() { + DispatchQueue.global().async { +#if IOS_SIMULATOR +// self.generateTestData() +// DispatchQueue.main.async { +// // dont know why main queue is needed, wont start otherwise +// Timer.repeating(2, call: #selector(self.insertRandomEntry), on: self) +// } +#endif + self.dataF_init() + self.dataAB_init() + self.autoSyncTimer_init() + } + } + + private func dataF_init() { + let list = AppDB?.loadFilters() ?? [:] + Q.async(flags: .barrier) { + self.dataF = list + NotifyFilterChanged.postOnMainThread() + } + } + + private func dataAB_init() { + let list = AppDB?.domainList() + Q.async(flags: .barrier) { + self.dataA = [] + self.dataB = [] + self.latestModification = 0 + if let allDomains = list { + for (parent, parts) in self.groupBySubdomains(allDomains) { + self.dataA.append(parent) + self.dataB.append(parts) + self.latestModification = max(parent.lastModified, self.latestModification) + } + } + NotifyLogHistoryReset.postOnMainThread() + } + } + + /// Auto sync new logs every 7 seconds. + private func autoSyncTimer_init() { + Q.async() { // using Q to start timer only after init data A,B,F + DispatchQueue.main.async { + // dont know why main queue is needed, wont start otherwise + Timer.repeating(7, call: #selector(self.syncNewestLogs), on: self) + } + } + } + + + // MARK: - Partial Update History + + @objc private func syncNewestLogs() { + QLog.Debug("\(#function)") +#if !IOS_SIMULATOR + guard currentVPNState == .on else { return } +#endif + guard let res = AppDB?.domainList(since: latestModification), res.count > 0 else { + return + } + QLog.Info("auto sync \(res.count) new logs") + Q.async(flags: .barrier) { + var c = 0 + for (parent, parts) in self.groupBySubdomains(res) { + if let i = self.dataA_index(of: parent.domain) { + self.mergeExistingParts(parent.domain, at: i, newChildren: parts) + + let merged = parent + self.dataA.remove(at: i) + self.dataA.insert(merged, at: c) + self.dataB.insert(self.dataB.remove(at: i), at: c) + self.dataA_delegate?.moveRow(merged, from: i, to: c) + } else { + self.dataA.insert(parent, at: c) + self.dataB.insert(parts, at: c) + self.dataA_delegate?.insertRow(parent, at: c) + } + c += 1 + self.latestModification = max(parent.lastModified, self.latestModification) + } + } + } + + private func mergeExistingParts(_ dom: String, at index: Int, newChildren: [GroupedDomain]) { + let tvc = dataB_delegate(dom) + var i = 0 + for child in newChildren { + if let u = dataB[index].firstIndex(where: { $0.domain == child.domain }) { + let merged = child + dataB[index].remove(at: u) + dataB[index].insert(merged, at: i) + tvc?.moveRow(merged, from: u, to: i) + } else { + dataB[index].insert(child, at: i) + tvc?.insertRow(child, at: i) + } + i += 1 + } + } + + + // MARK: - Delete History + + func deleteHistory() { + DispatchQueue.global().async { + try? AppDB?.destroyContent() + AppDB?.vacuum() + self.dataAB_init() + } + } + + func deleteHistory(domain: String, since ts: Timestamp) { + DispatchQueue.global().async { + let modified = (try? AppDB?.deleteRows(matching: domain, since: ts)) ?? 0 + guard modified > 0 else { + return // nothing has changed + } + AppDB?.vacuum() + self.Q.async(flags: .barrier) { + guard let index = self.dataA_index(of: domain) else { + return // nothing has changed + } + let parentDom = self.dataA[index].domain + guard let list = AppDB?.domainList(matching: parentDom), list.count > 0 else { + self.dataA.remove(at: index) + self.dataB.remove(at: index) + self.dataA_delegate?.deleteRow(at: index) + self.dataB_delegate(parentDom)?.replaceData(with: []) + return // nothing left, after deleting matching rows + } + // else: incremental update, replace whole list + self.dataA[index] = list.merge(parentDom, options: self.dataF[parentDom]) + self.dataA_delegate?.replaceRow(self.dataA[index], at: index) + self.dataB[index].removeAll() + for var child in list { + child.options = self.dataF[child.domain] + self.dataB[index].append(child) + } + self.dataB_delegate(parentDom)?.replaceData(with: self.dataB[index]) + } + } + } + + + // MARK: - Partial Update Filter + + func updateFilter(_ domain: String, add: FilterOptions) { + updateFilter(domain, set: (dataF[domain] ?? FilterOptions()).union(add)) + } + + func updateFilter(_ domain: String, remove: FilterOptions) { + updateFilter(domain, set: dataF[domain]?.subtracting(remove)) + } + + /// - Parameters: + /// - set: Remove a filter with `nil` or `.none` + private func updateFilter(_ domain: String, set: FilterOptions?) { + AppDB?.setFilter(domain, set) + Q.async(flags: .barrier) { + self.dataF[domain] = set + if let i = self.dataA_index(of: domain) { + if domain == self.dataA[i].domain { + self.dataA[i].options = (set == FilterOptions.none) ? nil : set + self.dataA_delegate?.replaceRow(self.dataA[i], at: i) + } + if let u = self.dataB[i].firstIndex(where: { $0.domain == domain }) { + self.dataB[i][u].options = (set == FilterOptions.none) ? nil : set + self.dataB_delegate(self.dataA[i].domain)?.replaceRow(self.dataB[i][u], at: u) + } + } + NotifyFilterChanged.postOnMainThread() + } + } + + + // MARK: - Helper methods + + private func dataA_index(of domain: String) -> Int? { + dataA.firstIndex { domain.isSubdomain(of: $0.domain) } + } + + private func groupBySubdomains(_ allDomains: [GroupedDomain]) -> [(parent: GroupedDomain, parts: [GroupedDomain])] { + var i: Int = 0 + var indexOf: [String: Int] = [:] + var res: [(domain: String, list: [GroupedDomain])] = [] + for var x in allDomains { + let domain = x.domain.splitDomainAndHost().domain + x.options = dataF[x.domain] + if let y = indexOf[domain] { + res[y].list.append(x) + } else { + res.append((domain, [x])) + indexOf[domain] = i + i += 1 + } + } + return res.map { ($1.merge($0, options: self.dataF[$0]), $1) } + } +} + + +// MARK: - Test Data + +extension DBWrapper { + private func generateTestData() { + guard let db = AppDB else { return } + let deleted = (try? db.deleteRows(matching: "test.com")) ?? 0 + QLog.Debug("Deleting \(deleted) rows matching 'test.com'") + + QLog.Debug("Writing 33 test logs") + try? db.insertDNSQuery("keeptest.com", blocked: false) + for _ in 1...4 { try? db.insertDNSQuery("test.com", blocked: false) } + for _ in 1...7 { try? db.insertDNSQuery("i.test.com", blocked: false) } + for i in 1...8 { try? db.insertDNSQuery("b.test.com", blocked: i>5) } + for i in 1...13 { try? db.insertDNSQuery("bi.test.com", blocked: i%2==0) } + + QLog.Debug("Creating 4 filters") + db.setFilter("b.test.com", .blocked) + db.setFilter("i.test.com", .ignored) + db.setFilter("bi.test.com", [.blocked, .ignored]) + + QLog.Debug("Done") + } + + @objc private func insertRandomEntry() { + QLog.Debug("Inserting 1 periodic log entry") + try? AppDB?.insertDNSQuery("\(arc4random() % 5).count.test.com", blocked: true) + } +} diff --git a/main/DB/SQDB.swift b/main/DB/SQDB.swift new file mode 100644 index 0000000..29319dc --- /dev/null +++ b/main/DB/SQDB.swift @@ -0,0 +1,302 @@ +import Foundation +import SQLite3 + +let exportPath = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) +let basePath = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.de.uni-bamberg.psi.AppCheck") +let DB_PATH = basePath!.appendingPathComponent("dns-logs.sqlite").relativePath + +typealias Timestamp = Int64 +struct GroupedDomain { + let domain: String, total: Int32, blocked: Int32, lastModified: Timestamp + var options: FilterOptions? = nil +} + +struct FilterOptions: OptionSet { + let rawValue: Int32 + static let none = FilterOptions(rawValue: 0) + static let blocked = FilterOptions(rawValue: 1 << 0) + static let ignored = FilterOptions(rawValue: 1 << 1) + static let any = FilterOptions(rawValue: 0b11) +} + +enum SQLiteError: Error { + case OpenDatabase(message: String) + case Prepare(message: String) + case Step(message: String) + case Bind(message: String) +} + + +// MARK: - SQLiteDatabase + +var AppDB: SQLiteDatabase? { get { try? SQLiteDatabase.open(path: DB_PATH) } } + +class SQLiteDatabase { + private let dbPointer: OpaquePointer? + private init(dbPointer: OpaquePointer?) { +// print("SQLite path: \(basePath!.absoluteString)") + self.dbPointer = dbPointer + } + + fileprivate var errorMessage: String { + if let errorPointer = sqlite3_errmsg(dbPointer) { + let errorMessage = String(cString: errorPointer) + return errorMessage + } else { + return "No error message provided from sqlite." + } + } + + deinit { + sqlite3_close(dbPointer) +// SQLiteDatabase.destroyDatabase(path: DB_PATH) + } + + static func destroyDatabase(path: String) { + if FileManager.default.fileExists(atPath: path) { + do { try FileManager.default.removeItem(atPath: path) } + catch { print("Could not destroy database file: \(path)") } + } + } + +// static func export() throws -> URL { +// let fmt = DateFormatter() +// fmt.dateFormat = "yyyy-MM-dd" +// let dest = exportPath.appendingPathComponent("\(fmt.string(from: Date()))-dns-log.sqlite") +// try? FileManager.default.removeItem(at: dest) +// try FileManager.default.copyItem(atPath: DB_PATH, toPath: dest.relativePath) +// return dest +// } + + static func open(path: String) throws -> SQLiteDatabase { + var db: OpaquePointer? + //sqlite3_open_v2(path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_SHAREDCACHE, nil) + if sqlite3_open(path, &db) == SQLITE_OK { + return SQLiteDatabase(dbPointer: db) + } else { + defer { + if db != nil { + sqlite3_close(db) + } + } + if let errorPointer = sqlite3_errmsg(db) { + let message = String(cString: errorPointer) + throw SQLiteError.OpenDatabase(message: message) + } else { + throw SQLiteError.OpenDatabase(message: "No error message provided from sqlite.") + } + } + } + + func run(sql: String, bind: ((OpaquePointer) -> Bool)?, step: (OpaquePointer) throws -> T) throws -> T { + var statement: OpaquePointer? + guard sqlite3_prepare_v2(dbPointer, sql, -1, &statement, nil) == SQLITE_OK, + let stmt = statement else { + throw SQLiteError.Prepare(message: errorMessage) + } + defer { sqlite3_finalize(stmt) } + guard bind?(stmt) ?? true else { + throw SQLiteError.Bind(message: errorMessage) + } + return try step(stmt) + } + + func ifStep(_ stmt: OpaquePointer, _ expected: Int32) throws { + guard sqlite3_step(stmt) == expected else { + throw SQLiteError.Step(message: errorMessage) + } + } + + func createTable(table: SQLTable.Type) throws { + try run(sql: table.createStatement, bind: nil) { + try ifStep($0, SQLITE_DONE) + } + } + + func vacuum() { + try? run(sql: "VACUUM;", bind: nil) { try ifStep($0, SQLITE_DONE) } + } +} + +protocol SQLTable { + static var createStatement: String { get } +} + + +// MARK: - Easy Access func + +private extension SQLiteDatabase { + func bindInt(_ stmt: OpaquePointer, _ col: Int32, _ value: Int32) -> Bool { + sqlite3_bind_int(stmt, col, value) == SQLITE_OK + } + + func bindInt64(_ stmt: OpaquePointer, _ col: Int32, _ value: sqlite3_int64) -> Bool { + sqlite3_bind_int64(stmt, col, value) == SQLITE_OK + } + + func bindText(_ stmt: OpaquePointer, _ col: Int32, _ value: String) -> Bool { + sqlite3_bind_text(stmt, col, (value as NSString).utf8String, -1, nil) == SQLITE_OK + } + + func readText(_ stmt: OpaquePointer, _ col: Int32) -> String? { + let val = sqlite3_column_text(stmt, col) + return (val != nil ? String(cString: val!) : nil) + } + + func readGroupedDomain(_ stmt: OpaquePointer) -> GroupedDomain { + GroupedDomain(domain: readText(stmt, 0) ?? "", + total: sqlite3_column_int(stmt, 1), + blocked: sqlite3_column_int(stmt, 2), + lastModified: sqlite3_column_int64(stmt, 3)) + } + + func allRows(_ stmt: OpaquePointer, _ fn: (OpaquePointer) -> T) -> [T] { + var r: [T] = [] + while (sqlite3_step(stmt) == SQLITE_ROW) { r.append(fn(stmt)) } + return r + } + + func allRowsKeyed(_ stmt: OpaquePointer, _ fn: (OpaquePointer) -> (key: T, value: U)) -> [T:U] { + var r: [T:U] = [:] + while (sqlite3_step(stmt) == SQLITE_ROW) { let (k,v) = fn(stmt); r[k] = v } + return r + } +} + + +// MARK: - DNSQuery + +struct DNSQuery: SQLTable { + let ts: Timestamp + let domain: String + let wasBlocked: Bool + let options: FilterOptions + static var createStatement: String { + return """ + CREATE TABLE IF NOT EXISTS req( + ts BIGINT DEFAULT (strftime('%s','now')), + domain VARCHAR(255) NOT NULL, + logOpt INT DEFAULT 0 + ); + """ + } +} + +extension SQLiteDatabase { + + // MARK: insert + + func insertDNSQuery(_ domain: String, blocked: Bool) throws { + try? run(sql: "INSERT INTO req (domain, logOpt) VALUES (?, ?);", bind: { + self.bindText($0, 1, domain) && self.bindInt($0, 2, blocked ? 1 : 0) + }) { + try ifStep($0, SQLITE_DONE) + } + } + + // MARK: delete + + func destroyContent() throws { + try? run(sql: "DROP TABLE IF EXISTS req;", bind: nil) { + try ifStep($0, SQLITE_DONE) + } + try? createTable(table: DNSQuery.self) + } + + /// Delete rows matching `ts >= ? AND "domain" OR "*.domain"` + @discardableResult func deleteRows(matching domain: String, since ts: Timestamp = 0) throws -> Int32 { + try run(sql: "DELETE FROM req WHERE ts >= ? AND (domain = ? OR domain LIKE '%.' || ?);", bind: { + self.bindInt64($0, 1, ts) && self.bindText($0, 2, domain) && self.bindText($0, 3, domain) + }) { stmt -> Int32 in + try ifStep(stmt, SQLITE_DONE) + return sqlite3_changes(dbPointer) + } + } + + // MARK: read + + func domainList(since ts: Timestamp = 0) -> [GroupedDomain]? { + try? run(sql: "SELECT domain, COUNT(*), SUM(logOpt&1), MAX(ts) FROM req \(ts == 0 ? "" : "WHERE ts > ?") GROUP BY domain ORDER BY 4 DESC;", bind: { + ts == 0 || self.bindInt64($0, 1, ts) + }) { + allRows($0) { readGroupedDomain($0) } + } + } + + func domainList(matching domain: String) -> [GroupedDomain]? { + try? run(sql: "SELECT domain, COUNT(*), SUM(logOpt&1), MAX(ts) FROM req WHERE (domain = ? OR domain LIKE '%.' || ?) GROUP BY domain ORDER BY 4 DESC;", bind: { + self.bindText($0, 1, domain) && self.bindText($0, 2, domain) + }) { + allRows($0) { readGroupedDomain($0) } + } + } + + func timesForDomain(_ fullDomain: String) -> [(Timestamp, Bool)]? { + try? run(sql: "SELECT ts, logOpt FROM req WHERE domain = ?;", bind: { + self.bindText($0, 1, fullDomain) + }) { + allRows($0) { (sqlite3_column_int64($0, 0), sqlite3_column_int($0, 1) > 0) } + } + } +} + + +// MARK: - DNSFilter + +struct DNSFilter: SQLTable { + let domain: String + let options: FilterOptions + static var createStatement: String { + return """ + CREATE TABLE IF NOT EXISTS filter( + domain VARCHAR(255) UNIQUE NOT NULL, + opt INT DEFAULT 0 + ); + """ + } +} + +extension SQLiteDatabase { + + // MARK: read + + func loadFilters() -> [String : FilterOptions]? { + try? run(sql: "SELECT domain, opt FROM filter ORDER BY domain ASC;", bind: nil) { + allRowsKeyed($0) { + (key: readText($0, 0) ?? "", + value: FilterOptions(rawValue: sqlite3_column_int($0, 1))) + } + } + } + + // MARK: write + + func setFilter(_ domain: String, _ value: FilterOptions?) { + func removeFilter() { + try? run(sql: "DELETE FROM filter WHERE domain = ? LIMIT 1;", bind: { + self.bindText($0, 1, domain) + }) { stmt -> Void in + sqlite3_step(stmt) + } + } + guard let rv = value?.rawValue, rv > 0 else { + removeFilter() + return + } + func createFilter() throws { + try run(sql: "INSERT OR FAIL INTO filter (domain, opt) VALUES (?, ?);", bind: { + self.bindText($0, 1, domain) && self.bindInt($0, 2, rv) + }) { + try ifStep($0, SQLITE_DONE) + } + } + func updateFilter() { + try? run(sql: "UPDATE filter SET opt = ? WHERE domain = ? LIMIT 1;", bind: { + self.bindInt($0, 1, rv) && self.bindText($0, 2, domain) + }) { stmt -> Void in + sqlite3_step(stmt) + } + } + do { try createFilter() } catch { updateFilter() } + } +} diff --git a/main/Extensions/AlertSheet.swift b/main/Extensions/AlertSheet.swift new file mode 100644 index 0000000..91fab91 --- /dev/null +++ b/main/Extensions/AlertSheet.swift @@ -0,0 +1,57 @@ +import UIKit + +// MARK: Basic Alerts + +func Alert(title: String?, text: String?, buttonText: String = "Dismiss") -> UIAlertController { + let alert = UIAlertController(title: title, message: text, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: buttonText, style: .cancel, handler: nil)) + return alert +} + +func ErrorAlert(_ error: Error, buttonText: String = "Dismiss") -> UIAlertController { + return Alert(title: "Error", text: error.localizedDescription, buttonText: buttonText) +} + +func AskAlert(title: String?, text: String?, buttonText: String = "Continue", buttonStyle: UIAlertAction.Style = .default, action: @escaping () -> Void) -> UIAlertController { + let alert = Alert(title: title, text: text, buttonText: "Cancel") + alert.addAction(UIAlertAction(title: buttonText, style: buttonStyle) { _ in action() }) + return alert +} + +extension UIAlertController { + func presentIn(_ viewController: UIViewController?) { + viewController?.present(self, animated: true, completion: nil) + } +} + +// MARK: Alert with multiple options + +func AlertWithOptions(title: String?, text: String?, buttons: [String], lastIsDestructive: Bool = false, callback: @escaping (_ index: Int?) -> Void) -> UIAlertController { + let alert = UIAlertController(title: title, message: text, preferredStyle: .actionSheet) + for (i, btn) in buttons.enumerated() { + let dangerous = (lastIsDestructive && i + 1 == buttons.count) + alert.addAction(UIAlertAction(title: btn, style: dangerous ? .destructive : .default) { _ in callback(i) }) + } + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in callback(nil) }) + return alert +} + +func AlertDeleteLogs(_ domain: String, latest: Timestamp, success: @escaping (_ tsMin: Timestamp) -> Void) -> UIAlertController { + let sinceNow = TimestampNow() - latest + var buttons = ["Last 5 minutes", "Last 15 minutes", "Last hour", "Last 24 hours", "Delete everything"] + var times: [Timestamp] = [300, 900, 3600, 86400] + while times.count > 0, times[0] < sinceNow { + buttons.removeFirst() + times.removeFirst() + } + return AlertWithOptions(title: "Delete logs", text: "Delete logs for domain '\(domain)'", buttons: buttons, lastIsDestructive: true) { + guard let idx = $0 else { + return + } + if idx >= times.count { + success(0) + } else { + success(Timestamp(Date().timeIntervalSince1970) - times[idx]) + } + } +} diff --git a/main/Extensions/Generic.swift b/main/Extensions/Generic.swift new file mode 100644 index 0000000..b9888c5 --- /dev/null +++ b/main/Extensions/Generic.swift @@ -0,0 +1,91 @@ +import Foundation + +struct QLog { + private init() {} + static func m(_ message: String) { write("", message) } + static func Info(_ message: String) { write("[INFO] ", message) } +#if DEBUG + static func Debug(_ message: String) { write("[DEBUG] ", message) } +#else + static func Debug(_ _: String) {} +#endif + static func Error(_ message: String) { write("[ERROR] ", message) } + static func Warning(_ message: String) { write("[WARN] ", message) } + private static func write(_ tag: String, _ message: String) { + print(String(format: "%1.3f %@%@", Date().timeIntervalSince1970, tag, message)) + } +} + +extension Collection { + subscript(ifExist i: Index?) -> Iterator.Element? { + guard let i = i else { return nil } + return indices.contains(i) ? self[i] : nil + } +} + +var listOfSLDs: [String : [String : Bool]] = { + let path = Bundle.main.url(forResource: "third-level", withExtension: "txt") + let content = try! String(contentsOf: path!) + var res: [String : [String : Bool]] = [:] + content.enumerateLines { line, _ in + let dom = line.split(separator: ".") + let tld = String(dom.first!) + let sld = String(dom.last!) + if res[tld] == nil { res[tld] = [:] } + res[tld]![sld] = true + } + return res +}() + +extension String { + /// Check if string is equal to `domain` or ends with `.domain` + func isSubdomain(of domain: String) -> Bool { self == domain || self.hasSuffix("." + domain) } + /// Split string into top level domain part and host part + func splitDomainAndHost() -> (domain: String, host: String?) { + let lastChr = last?.asciiValue ?? 0 + guard lastChr > UInt8(ascii: "9") || lastChr < UInt8(ascii: "0") else { // IP address + return (domain: "# IP connection", host: self) + } + var parts = components(separatedBy: ".") + guard let tld = parts.popLast(), let sld = parts.popLast() else { + return (domain: self, host: nil) // no subdomains, just plain SLD + } + var ending = sld + "." + tld + if listOfSLDs[tld]?[sld] ?? false, let rld = parts.popLast() { + ending = rld + "." + ending + } + return (domain: ending, host: parts.joined(separator: ".")) + +// var allDots = enumerated().compactMap { $1 == "." ? $0 : nil } +// let d1 = allDots.popLast() // we dont care about TLD +// guard let d2 = allDots.popLast() else { +// return (domain: self, host: nil) // no subdomains, just plain SLD +// } +// // TODO: check third level domains +//// let d3 = allDots.popLast() +// return (String(suffix(count - d2 - 1)), String(prefix(d2))) + } +} + +extension Timer { + @discardableResult static func repeating(_ interval: TimeInterval, call selector: Selector, on target: Any, userInfo: Any? = nil) -> Timer { + Timer.scheduledTimer(timeInterval: interval, target: target, selector: selector, + userInfo: userInfo, repeats: true) + } +} + +extension DateFormatter { + convenience init(withFormat: String) { + self.init() + dateFormat = withFormat + } + func with(format: String) -> Self { + dateFormat = format + return self + } + func string(from ts: Timestamp) -> String { + string(from: Date.init(timeIntervalSince1970: Double(ts))) + } +} + +func TimestampNow() -> Timestamp { Timestamp(Date().timeIntervalSince1970) } diff --git a/main/Extensions/GroupedDomain.swift b/main/Extensions/GroupedDomain.swift new file mode 100644 index 0000000..9042bd1 --- /dev/null +++ b/main/Extensions/GroupedDomain.swift @@ -0,0 +1,20 @@ +import Foundation + +extension GroupedDomain { + static func +(a: GroupedDomain, b: GroupedDomain) -> Self { + GroupedDomain(domain: a.domain, total: a.total + b.total, blocked: a.blocked + b.blocked, + lastModified: max(a.lastModified, b.lastModified), options: a.options ?? b.options ) + } +} + +extension Array where Element == GroupedDomain { + func merge(_ domain: String, options opt: FilterOptions? = nil) -> GroupedDomain { + var b: Int32 = 0, t: Int32 = 0, m: Timestamp = 0 + for x in self { + b += x.blocked + t += x.total + m = Swift.max(m, x.lastModified) + } + return GroupedDomain(domain: domain, total: t, blocked: b, lastModified: m, options: opt) + } +} diff --git a/main/Extensions/Notifications.swift b/main/Extensions/Notifications.swift new file mode 100644 index 0000000..f7695cc --- /dev/null +++ b/main/Extensions/Notifications.swift @@ -0,0 +1,23 @@ +import Foundation + +let NotifyVPNStateChanged = NSNotification.Name("GlassVPNStateChanged") // VPNState! +let NotifyFilterChanged = NSNotification.Name("PSIFilterSettingsChanged") // nil! +let NotifyLogHistoryReset = NSNotification.Name("PSILogHistoryReset") // nil! + + +extension NSNotification.Name { + func post(_ obj: Any? = nil) { + NotificationCenter.default.post(name: self, object: obj) + } + func postOnMainThread(_ obj: Any? = nil) { + DispatchQueue.main.async { NotificationCenter.default.post(name: self, object: obj) } + } + /// You are responsible for removing the returned object in a `deinit` block. +// @discardableResult func observe(queue: OperationQueue? = nil, using block: @escaping (Notification) -> Void) -> NSObjectProtocol { +// NotificationCenter.default.addObserver(forName: self, object: nil, queue: queue, using: block) +// } + /// On iOS 9.0+ you don't need to unregister the observer. + func observe(call: Selector, on target: Any, obj: Any? = nil) { + NotificationCenter.default.addObserver(target, selector: call, name: self, object: obj) + } +} diff --git a/main/Extensions/SharedState.swift b/main/Extensions/SharedState.swift new file mode 100644 index 0000000..1050d50 --- /dev/null +++ b/main/Extensions/SharedState.swift @@ -0,0 +1,8 @@ +import Foundation + +let dateTimeFormat = DateFormatter(withFormat: "yyyy-MM-dd HH:mm:ss") +var currentVPNState: VPNState = .off + +public enum VPNState : Int { + case on = 1, inbetween, off +} diff --git a/main/Extensions/TableView.swift b/main/Extensions/TableView.swift new file mode 100644 index 0000000..757e206 --- /dev/null +++ b/main/Extensions/TableView.swift @@ -0,0 +1,93 @@ +import UIKit + +extension GroupedDomain { + var detailCellText: String { get { + return blocked > 0 + ? "\(dateTimeFormat.string(from: lastModified)) — \(blocked)/\(total) blocked" + : "\(dateTimeFormat.string(from: lastModified)) — \(total)" + } + } +} + +extension FilterOptions { + func tableRowImage() -> UIImage? { + let blocked = contains(.blocked) + let ignored = contains(.ignored) + if blocked { return UIImage(named: ignored ? "block_ignore" : "shield-x") } + if ignored { return UIImage(named: "quicklook-not") } + return nil + } +} + +extension NSMutableAttributedString { + func withColor(_ color: UIColor, fromBack: Int) -> Self { + let l = length - fromBack + let r = (l < 0) ? NSMakeRange(0, length) : NSMakeRange(l, fromBack) + self.addAttribute(.foregroundColor, value: color, range: r) + return self + } +} + +// MARK: Pull-to-Refresh + +extension UIRefreshControl { + convenience init(call: Selector, on: UITableViewController) { + self.init() + addTarget(on, action: call, for: .valueChanged) + addTarget(self, action: #selector(endRefreshing), for: .valueChanged) + } + +} + +// MARK: - Incremental Update Delegate + +protocol IncrementalDataSourceUpdate : UITableViewController { + var dataSource: [GroupedDomain] { get set } +} + +extension IncrementalDataSourceUpdate { + func ifDisplayed(_ block: () -> Void) { + DispatchQueue.main.sync { + if self.tableView.window?.isKeyWindow ?? false { + block() + // TODO: custom handling if cell is being edited + } else { + self.tableView.reloadData() + } + } + } + func insertRow(_ obj: GroupedDomain, at index: Int) { + dataSource.insert(obj, at: index) + ifDisplayed { + self.tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .left) + } + } + func moveRow(_ obj: GroupedDomain, from: Int, to: Int) { + dataSource.remove(at: from) + dataSource.insert(obj, at: to) + ifDisplayed { + let source = IndexPath(row: from, section: 0) + let cell = self.tableView.cellForRow(at: source) + cell?.detailTextLabel?.text = obj.detailCellText + self.tableView.moveRow(at: source, to: IndexPath(row: to, section: 0)) + } + } + func replaceRow(_ obj: GroupedDomain, at index: Int) { + dataSource[index] = obj + ifDisplayed { + self.tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic) + } + } + func deleteRow(at index: Int) { + dataSource.remove(at: index) + ifDisplayed { + self.tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) + } + } + func replaceData(with newData: [GroupedDomain]) { + dataSource = newData + ifDisplayed { + self.tableView.reloadData() + } + } +} diff --git a/main/Info.plist b/main/Info.plist index 63ce9be..223868a 100644 --- a/main/Info.plist +++ b/main/Info.plist @@ -41,5 +41,9 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIFileSharingEnabled + + LSSupportsOpeningDocumentsInPlace + diff --git a/main/Requests/TVCDomains.swift b/main/Requests/TVCDomains.swift new file mode 100644 index 0000000..165751b --- /dev/null +++ b/main/Requests/TVCDomains.swift @@ -0,0 +1,41 @@ +import UIKit + +class TVCDomains: UITableViewController, IncrementalDataSourceUpdate { + + internal var dataSource: [GroupedDomain] = [] + + override func viewDidLoad() { + super.viewDidLoad() + if #available(iOS 10.0, *) { + tableView.refreshControl = UIRefreshControl(call: #selector(reloadDataSource), on: self) + } + NotifyLogHistoryReset.observe(call: #selector(reloadDataSource), on: self) + reloadDataSource() + DBWrp.dataA_delegate = self + } + + @objc func reloadDataSource() { + dataSource = DBWrp.listOfDomains() + tableView.reloadData() + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if let index = tableView.indexPathForSelectedRow?.row { + (segue.destination as? TVCHosts)?.parentDomain = dataSource[index].domain + } + } + + + // MARK: - Table View Delegate + + override func tableView(_ _: UITableView, numberOfRowsInSection _: Int) -> Int { dataSource.count } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "DomainCell")! + let entry = dataSource[indexPath.row] + cell.textLabel?.text = entry.domain + cell.detailTextLabel?.text = entry.detailCellText + cell.imageView?.image = entry.options?.tableRowImage() + return cell + } +} diff --git a/main/Requests/TVCHostDetails.swift b/main/Requests/TVCHostDetails.swift new file mode 100644 index 0000000..20510b3 --- /dev/null +++ b/main/Requests/TVCHostDetails.swift @@ -0,0 +1,32 @@ +import UIKit + +class TVCHostDetails: UITableViewController { + + public var fullDomain: String! + private var dataSource: [(ts: Timestamp, blocked: Bool)] = [] + + override func viewDidLoad() { + super.viewDidLoad() + navigationItem.prompt = fullDomain + if #available(iOS 10.0, *) { + tableView.refreshControl = UIRefreshControl(call: #selector(reloadDataSource), on: self) + } + NotifyLogHistoryReset.observe(call: #selector(reloadDataSource), on: self) + reloadDataSource() + } + + @objc func reloadDataSource() { + dataSource = DBWrp.listOfTimes(fullDomain) + tableView.reloadData() + } + + override func tableView(_ _: UITableView, numberOfRowsInSection _: Int) -> Int { dataSource.count } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "HostDetailCell")! + let src = dataSource[indexPath.row] + cell.textLabel?.text = dateTimeFormat.string(from: src.ts) + cell.imageView?.image = (src.blocked ? UIImage(named: "shield-x") : nil) + return cell + } +} diff --git a/main/Requests/TVCHosts.swift b/main/Requests/TVCHosts.swift new file mode 100644 index 0000000..bcffa02 --- /dev/null +++ b/main/Requests/TVCHosts.swift @@ -0,0 +1,54 @@ +import UIKit + +class TVCHosts: UITableViewController, IncrementalDataSourceUpdate { + + public var parentDomain: String! + internal var dataSource: [GroupedDomain] = [] + private var isSpecial: Bool = false + + override func viewDidLoad() { + super.viewDidLoad() + navigationItem.prompt = parentDomain + isSpecial = (parentDomain.first == "#") // aka: "# IP address" + if #available(iOS 10.0, *) { + tableView.refreshControl = UIRefreshControl(call: #selector(reloadDataSource), on: self) + } + NotifyLogHistoryReset.observe(call: #selector(reloadDataSource), on: self) + reloadDataSource() + DBWrp.currentlyOpenParent = parentDomain + DBWrp.dataB_delegate = self + } + deinit { + DBWrp.currentlyOpenParent = nil + } + + @objc func reloadDataSource() { + dataSource = DBWrp.listOfHosts(parentDomain) + tableView.reloadData() + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if let index = tableView.indexPathForSelectedRow?.row { + (segue.destination as? TVCHostDetails)?.fullDomain = dataSource[index].domain + } + } + + // MARK: - Data Source + + override func tableView(_ _: UITableView, numberOfRowsInSection _: Int) -> Int { dataSource.count } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "HostCell")! + let entry = dataSource[indexPath.row] + if isSpecial { + // currently only used for IP addresses + cell.textLabel?.text = entry.domain + } else { + cell.textLabel?.attributedText = NSMutableAttributedString(string: entry.domain) + .withColor(.darkGray, fromBack: parentDomain.count + 1) + } + cell.detailTextLabel?.text = entry.detailCellText + cell.imageView?.image = entry.options?.tableRowImage() + return cell + } +} diff --git a/main/Settings.bundle/Root.plist b/main/Settings.bundle/Root.plist index daa2a4b..bbc5d82 100644 --- a/main/Settings.bundle/Root.plist +++ b/main/Settings.bundle/Root.plist @@ -6,16 +6,6 @@ Root PreferenceSpecifiers - - Type - PSToggleSwitchSpecifier - Title - Kill proxy on startup - Key - kill_proxy - DefaultValue - - Type PSToggleSwitchSpecifier diff --git a/main/Settings/TVCFilter.swift b/main/Settings/TVCFilter.swift new file mode 100644 index 0000000..47d48d9 --- /dev/null +++ b/main/Settings/TVCFilter.swift @@ -0,0 +1,40 @@ +import UIKit + +class TVCFilter: UITableViewController, EditActionsRemove { + var currentFilter: FilterOptions = .none + private var dataSource: [String] = [] + + override func viewDidLoad() { + super.viewDidLoad() + if #available(iOS 10.0, *) { + tableView.refreshControl = UIRefreshControl(call: #selector(reloadDataSource), on: self) + } + NotifyFilterChanged.observe(call: #selector(reloadDataSource), on: self) + reloadDataSource() + } + + @objc func reloadDataSource() { + dataSource = DBWrp.dataF_list(currentFilter) + tableView.reloadData() + } + + // MARK: - Table View Delegate + + override func tableView(_ _: UITableView, numberOfRowsInSection _: Int) -> Int { dataSource.count } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "DomainFilterCell")! + cell.textLabel?.text = dataSource[indexPath.row] + return cell + } + + // MARK: - Editing + + func editableRowCallback(_ index: IndexPath, _ action: RowAction, _ userInfo: Any?) -> Bool { + let domain = self.dataSource[index.row] + DBWrp.updateFilter(domain, remove: currentFilter) + self.dataSource.remove(at: index.row) + self.tableView.deleteRows(at: [index], with: .automatic) + return true + } +} diff --git a/main/Settings/TVCSettings.swift b/main/Settings/TVCSettings.swift new file mode 100644 index 0000000..f4f8634 --- /dev/null +++ b/main/Settings/TVCSettings.swift @@ -0,0 +1,100 @@ +import UIKit + +class TVCSettings: UITableViewController { + + private let appDelegate = UIApplication.shared.delegate as! AppDelegate + @IBOutlet var vpnToggle: UISwitch! + @IBOutlet var cellDomainsIgnored: UITableViewCell! + @IBOutlet var cellDomainsBlocked: UITableViewCell! + + override func viewDidLoad() { + super.viewDidLoad() + NotifyVPNStateChanged.observe(call: #selector(vpnStateChanged(_:)), on: self) + changedState(currentVPNState) + NotifyFilterChanged.observe(call: #selector(reloadDataSource), on: self) + reloadDataSource() + } + + @objc func reloadDataSource() { + let (blocked, ignored) = DBWrp.dataF_counts() + DispatchQueue.main.async { + self.cellDomainsIgnored.detailTextLabel?.text = "\(ignored) Domains" + self.cellDomainsBlocked.detailTextLabel?.text = "\(blocked) Domains" + } + } + + @IBAction func toggleVPNProxy(_ sender: UISwitch) { + appDelegate.setProxyEnabled(sender.isOn) + } + + @IBAction func exportDB(_ sender: Any) { + // TODO: export partly? + // TODO: show header-banner of success + // Share Sheet + let sheet = UIActivityViewController(activityItems: [URL(fileURLWithPath: DB_PATH)], applicationActivities: nil) + self.present(sheet, animated: true) + // Save to Files app +// self.present(UIDocumentPickerViewController(url: URL(fileURLWithPath: DB_PATH), in: .exportToService), animated: true) + // Shows Alert and exports to Documents directory +// AskAlert(title: "Export results?", text: """ +// This action will copy the internal database to the app's local Documents directory. You can use the Files app to access the database file. +// +// Note: This will make your DNS requests available to other apps! +// """, buttonText: "Export") { +// do { +// let dest = try SQLiteDatabase.export() +// let folder = dest.deletingLastPathComponent() +// let out = folder.lastPathComponent + "/" + dest.lastPathComponent +// Alert(title: "Successful", text: "File exported to '\(out)'", buttonText: "OK").presentIn(self) +// } catch { +// ErrorAlert(error).presentIn(self) +// } +// }.presentIn(self) + } + + @IBAction func clearDatabaseResults(_ sender: Any) { + AskAlert(title: "Clear results?", text: """ + You are about to delete all results that have been logged in the past. Your preference for blocked and ignored domains is preserved. + Continue? + """, buttonText: "Delete", buttonStyle: .destructive) { + DBWrp.deleteHistory() + }.presentIn(self) + } + + @objc func vpnStateChanged(_ notification: Notification) { + changedState(notification.object as! VPNState) + } + + func changedState(_ newState: VPNState) { + vpnToggle.isOn = (newState != .off) + vpnToggle.onTintColor = (newState == .inbetween ? .systemYellow : nil) + } + + override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) { + let t:String, d: String + switch tableView.cellForRow(at: indexPath)?.reuseIdentifier { + case "settingsIgnoredCell": + t = "Ignored Domains" + d = "Ignored domains won't show up in session recordings nor in the requests overview. Requests to ignored domains are not logged." + case "settingsBlockedCell": + t = "Blocked Domains" + d = "Blocked domains prohibit all requests to that domain. Unless a domain is also ignored, the request will be logged and appear in session recordings and the requests overview." + default: return + } + Alert(title: t, text: d).presentIn(self) + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + guard let dest = (segue.destination as? TVCFilter) else { return } + switch segue.identifier { + case "segueFilterIgnored": + dest.navigationItem.title = "Ignored Domains" + dest.currentFilter = .ignored + case "segueFilterBlocked": + dest.navigationItem.title = "Blocked Domains" + dest.currentFilter = .blocked + default: + break + } + } +} diff --git a/main/TBCMain.swift b/main/TBCMain.swift new file mode 100644 index 0000000..6e1de53 --- /dev/null +++ b/main/TBCMain.swift @@ -0,0 +1,36 @@ +import UIKit +import NetworkExtension + +class TBCMain: UITabBarController { + + override func viewDidLoad() { + super.viewDidLoad() +// perform(#selector(showWelcomeMessage), with: nil, afterDelay: 3) + NotifyVPNStateChanged.observe(call: #selector(vpnStateChanged(_:)), on: self) + changedState(currentVPNState) + } + + @objc func showWelcomeMessage() { + performSegue(withIdentifier: "welcome", sender: nil) + } + + @objc func vpnStateChanged(_ notification: Notification) { + changedState(notification.object as! VPNState) + } + + func changedState(_ newState: VPNState) { + let stateView = self.tabBar.items?.last + switch newState { + case .on: stateView?.badgeValue = "✓" + case .inbetween: stateView?.badgeValue = "⋯" + case .off: stateView?.badgeValue = "✗" + } + if #available(iOS 10.0, *) { + switch newState { + case .on: stateView?.badgeColor = .systemGreen + case .inbetween: stateView?.badgeColor = .systemYellow + case .off: stateView?.badgeColor = .systemRed + } + } + } +} diff --git a/main/TVC Extensions/EditableRows.swift b/main/TVC Extensions/EditableRows.swift new file mode 100644 index 0000000..156f8de --- /dev/null +++ b/main/TVC Extensions/EditableRows.swift @@ -0,0 +1,123 @@ +import UIKit + +public enum RowAction { + case ignore, block, delete +// static let all: [RowAction] = [.ignore, .block, .delete] +} + +// MARK: - Generic + +protocol EditableRows { + func editableRowUserInfo(_ index: IndexPath) -> Any? + func editableRowActions(_ index: IndexPath) -> [(RowAction, String)] + func editableRowActionColor(_ index: IndexPath, _ action: RowAction) -> UIColor? + @discardableResult func editableRowCallback(_ atIndexPath: IndexPath, _ action: RowAction, _ userInfo: Any?) -> Bool +} + +extension EditableRows where Self: UITableViewController { + fileprivate func getRowActionsIOS9(_ index: IndexPath) -> [UITableViewRowAction]? { + let userInfo = editableRowUserInfo(index) + return editableRowActions(index).compactMap { a,t in + let x = UITableViewRowAction(style: a == .delete ? .destructive : .normal, title: t) { self.editableRowCallback($1, a, userInfo) } + x.backgroundColor = editableRowActionColor(index, a) + return x + } + } + @available(iOS 11.0, *) + fileprivate func getRowActionsIOS11(_ index: IndexPath) -> UISwipeActionsConfiguration? { + let userInfo = editableRowUserInfo(index) + return UISwipeActionsConfiguration(actions: editableRowActions(index).compactMap { a,t in + let x = UIContextualAction(style: a == .delete ? .destructive : .normal, title: t) { $2(self.editableRowCallback(index, a, userInfo)) } + x.backgroundColor = editableRowActionColor(index, a) + return x + }) + } + func editableRowUserInfo(_ index: IndexPath) -> Any? { nil } +} + + + +// MARK: - Edit Ignore-Block-Delete + +protocol EditActionsIgnoreBlockDelete : EditableRows { + var dataSource: [GroupedDomain] { get set } +} +extension EditActionsIgnoreBlockDelete where Self: UITableViewController { + func editableRowActions(_ index: IndexPath) -> [(RowAction, String)] { + let x = dataSource[index.row] + QLog.m(x.domain) + let b = x.options?.contains(.blocked) ?? false + let i = x.options?.contains(.ignored) ?? false + return [(.delete, "Delete"), (.block, b ? "Unblock" : "Block"), (.ignore, i ? "Unignore" : "Ignore")] + } + + func editableRowActionColor(_: IndexPath, _ action: RowAction) -> UIColor? { + action == .block ? .systemOrange : nil + } + + func editableRowUserInfo(_ index: IndexPath) -> Any? { dataSource[index.row] } + + func editableRowCallback(_ index: IndexPath, _ action: RowAction, _ userInfo: Any?) -> Bool { + let entry = userInfo as! GroupedDomain + switch action { + case .ignore: showFilterSheet(entry, .ignored) + case .block: showFilterSheet(entry, .blocked) + case .delete: + AlertDeleteLogs(entry.domain, latest: entry.lastModified) { + DBWrp.deleteHistory(domain: entry.domain, since: $0) + }.presentIn(self) + } + return true + } + + private func showFilterSheet(_ entry: GroupedDomain, _ filter: FilterOptions) { + if entry.options?.contains(filter) ?? false { + DBWrp.updateFilter(entry.domain, remove: filter) + } else { + // TODO: alert sheet + DBWrp.updateFilter(entry.domain, add: filter) + } + } +} + +// MARK: Extensions +extension TVCDomains : EditActionsIgnoreBlockDelete { + override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { + getRowActionsIOS9(indexPath) + } + @available(iOS 11.0, *) + override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + getRowActionsIOS11(indexPath) + } +} + +extension TVCHosts : EditActionsIgnoreBlockDelete { + override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { + getRowActionsIOS9(indexPath) + } + @available(iOS 11.0, *) + override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + getRowActionsIOS11(indexPath) + } +} + + + +// MARK: - Edit Remove + +protocol EditActionsRemove : EditableRows {} +extension EditActionsRemove where Self: UITableViewController { + func editableRowActions(_: IndexPath) -> [(RowAction, String)] { [(.delete, "Remove")] } + func editableRowActionColor(_: IndexPath, _: RowAction) -> UIColor? { nil } +} + +// MARK: Extensions +extension TVCFilter : EditableRows { + override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { + getRowActionsIOS9(indexPath) + } + @available(iOS 11.0, *) + override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + getRowActionsIOS11(indexPath) + } +} diff --git a/main/TVCDomains.swift b/main/TVCDomains.swift deleted file mode 100644 index b59df2d..0000000 --- a/main/TVCDomains.swift +++ /dev/null @@ -1,138 +0,0 @@ -import UIKit -import NetworkExtension - - -class TVCDomains: UITableViewController { - - private let appDelegate = UIApplication.shared.delegate as! AppDelegate - private var dataSource: [GroupedDomain] = [] - @IBOutlet private var welcomeMessage: UITextView! - - override func viewDidLoad() { - super.viewDidLoad() - self.welcomeMessage.frame.size.height = 0 -// AppInfoType.initWorkingDir() - - NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: nil, queue: OperationQueue.main) { [weak self] notification in - self?.changeState((notification.object as? NETunnelProviderSession)?.status ?? .invalid) - } - NotificationCenter.default.addObserver(forName: .init("ChangedStateGlassVPN"), object: nil, queue: OperationQueue.main) { [weak self] notification in - self?.changeState((notification.object as? NEVPNStatus) ?? .invalid) - } - - // pull-to-refresh - tableView.refreshControl = UIRefreshControl() - tableView.refreshControl?.addTarget(self, action: #selector(reloadDataSource(_:)), for: .valueChanged) - performSelector(inBackground: #selector(reloadDataSource(_:)), with: nil) - NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: OperationQueue.main) { [weak self] _ in - self?.reloadDataSource(nil) - } -// navigationItem.leftBarButtonItem?.title = "\u{2699}\u{0000FE0E}☰" -// navigationItem.leftBarButtonItem?.setTitleTextAttributes([NSAttributedString.Key.font : UIFont.systemFont(ofSize: 32)], for: .normal) - } - - @IBAction func clickToolbarLeft(_ sender: Any) { - let alert = UIAlertController(title: "Clear results?", - message: "You are about to delete all results that have been logged in the past. Continue?", preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) - alert.addAction(UIAlertAction(title: "Delete", style: .destructive, handler: { [weak self] _ in - try? SQLiteDatabase.open(path: DB_PATH).destroyContent() - self?.reloadDataSource(nil) - })) - self.present(alert, animated: true, completion: nil) - } - - @IBAction func clickToolbarRight(_ sender: Any) { - let active = (self.navigationItem.rightBarButtonItem?.tag == NEVPNStatus.connected.rawValue) - let alert = UIAlertController(title: "\(active ? "Dis" : "En")able Proxy?", - message: "The VPN proxy is currently \(active ? "en" : "dis")abled, do you want to proceed and \(active ? "dis" : "en")able logging?", preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) - alert.addAction(UIAlertAction(title: active ? "Disable" : "Enable", style: .default, handler: { [weak self] _ in - self?.appDelegate.setProxyEnabled(!active) - })) - self.present(alert, animated: true, completion: nil) - } - - func changeState(_ newState: NEVPNStatus) { - let stateView = self.navigationItem.rightBarButtonItem - if stateView?.tag == newState.rawValue { - return // don't need to change, already correct state - } - stateView?.tag = newState.rawValue - switch newState { - case .connected: - stateView?.title = "Active" - stateView?.tintColor = .systemGreen - case .connecting, .disconnecting, .reasserting: - stateView?.title = "Updating" - stateView?.tintColor = .systemYellow - case .invalid, .disconnected: - fallthrough - @unknown default: - stateView?.title = "Inactive" - stateView?.tintColor = .systemRed - } -// let newButton = UIBarButtonItem(barButtonSystemItem: (active ? .pause : .play), target: self, action: #selector(clickToolbarRight(_:))) -// newButton.tintColor = (active ? .systemRed : .systemGreen) -// newButton.tag = (active ? 1 : 0) -// self.navigationItem.setRightBarButton(newButton, animated: true) - } - - private func updateCellAt(_ index: Int) { - DispatchQueue.main.async { - guard index >= 0 else { - self.welcomeMessage.frame.size.height = (self.dataSource.count == 0 ? self.view.frame.size.height : 0) - self.tableView.reloadData() - return - } - if let idx = self.tableView.indexPathsForVisibleRows?.first(where: { indexPath -> Bool in - indexPath.row == index - }) { - self.tableView.reloadRows(at: [idx], with: .automatic) - } - } - } - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if let index = tableView.indexPathForSelectedRow?.row { - let dom = dataSource[index].label - segue.destination.navigationItem.prompt = dom - (segue.destination as? TVCHosts)?.domain = dom - } - } - - - // MARK: - Table View Delegate - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return dataSource.count - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "DomainCell")! - let entry = dataSource[indexPath.row] - let last = Date.init(timeIntervalSince1970: Double(entry.lastModified)) - - cell.textLabel?.text = entry.label - cell.detailTextLabel?.text = "\(dateFormatter.string(from: last)) — \(entry.count)" - return cell - } - - - // MARK: - Data Source - - @objc private func reloadDataSource(_ sender : Any?) { - self.dataSource = self.sqliteAppList() - if let refreshControl = sender as? UIRefreshControl { - DispatchQueue.main.async { refreshControl.endRefreshing() } - } - self.updateCellAt(-1) - } - - private func sqliteAppList() -> [GroupedDomain] { - guard let db = try? SQLiteDatabase.open(path: DB_PATH) else { - return [] - } - return db.domainList() - } -} diff --git a/main/TVCHostDetails.swift b/main/TVCHostDetails.swift deleted file mode 100644 index 1a5360b..0000000 --- a/main/TVCHostDetails.swift +++ /dev/null @@ -1,44 +0,0 @@ -import UIKit - -class TVCHostDetails: UITableViewController { - - public var domain: String? - public var host: String? - private var dataSource: [Timestamp] = [] - - override func viewDidLoad() { - super.viewDidLoad() - // pull-to-refresh - tableView.refreshControl = UIRefreshControl() - tableView.refreshControl?.addTarget(self, action: #selector(reloadDataSource(_:)), for: .valueChanged) - performSelector(inBackground: #selector(reloadDataSource(_:)), with: nil) - NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: OperationQueue.main) { [weak self] _ in - self?.reloadDataSource(nil) - } - } - - @objc private func reloadDataSource(_ sender : Any?) { - dataSource = [] - guard let dom = domain, let db = try? SQLiteDatabase.open(path: DB_PATH) else { - return - } - dataSource = db.timesForDomain(dom, host: host) - DispatchQueue.main.async { - if let refreshControl = sender as? UIRefreshControl { - refreshControl.endRefreshing() - } - self.tableView.reloadData() - } - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return dataSource.count - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "HostDetailCell")! - let date = Date.init(timeIntervalSince1970: Double(dataSource[indexPath.row])) - cell.textLabel?.text = dateFormatter.string(from: date) - return cell - } -} diff --git a/main/TVCHosts.swift b/main/TVCHosts.swift deleted file mode 100644 index 284cf54..0000000 --- a/main/TVCHosts.swift +++ /dev/null @@ -1,80 +0,0 @@ -import UIKit - -class TVCHosts: UITableViewController { - - private var attributedDomain: NSAttributedString = NSAttributedString(string: "") - public var domain: String? { - willSet { - attributedDomain = NSAttributedString(string: ".\(newValue ?? "")", - attributes: [.foregroundColor : UIColor.darkGray]) - } - } - private var dataSource: [GroupedDomain] = [] - - override func viewDidLoad() { - super.viewDidLoad() - // pull-to-refresh - tableView.refreshControl = UIRefreshControl() - tableView.refreshControl?.addTarget(self, action: #selector(reloadDataSource(_:)), for: .valueChanged) - performSelector(inBackground: #selector(reloadDataSource(_:)), with: nil) - NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: OperationQueue.main) { [weak self] _ in - self?.reloadDataSource(nil) - } - } - - @objc private func reloadDataSource(_ sender : Any?) { -// dataSource = [("hi", [1, 2]), ("there", [2, 4, 8, 1580472632]), ("dude", [1, 2, 3])] -// return () - dataSource = [] - guard let dom = domain, let db = try? SQLiteDatabase.open(path: DB_PATH) else { - return - } - dataSource = db.hostsForDomain(dom as NSString) - -// var list: [String: [Int64]] = [:] -// db.subdomainsForDomain(appIdentifier: dom as NSString) { query in -//// let x = query.dns.split(separator: ".").reversed().joined(separator: ".") -// let x = query.host ?? "" -// if list[x] == nil { -// list[x] = [] -// } -// list[x]?.append(query.ts) -// } -// dataSource = list.sorted{ $0.0 < $1.0 } - DispatchQueue.main.async { - if let refreshControl = sender as? UIRefreshControl { - refreshControl.endRefreshing() - } - self.tableView.reloadData() - } - } - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if let index = tableView.indexPathForSelectedRow?.row { - - let entry = dataSource[index] - segue.destination.navigationItem.prompt = "\(entry.label).\(domain ?? "")" - let vc = (segue.destination as? TVCHostDetails) - vc?.domain = domain - vc?.host = entry.label - } - } - - // MARK: - Data Source - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return dataSource.count - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "HostCell")! - let entry = dataSource[indexPath.row] - let last = Date.init(timeIntervalSince1970: Double(entry.lastModified)) - let x = NSMutableAttributedString(string: entry.label) - x.append(attributedDomain) - cell.textLabel?.attributedText = x -// cell.textLabel?.text = "\(entry.label).\(domain ?? "")" - cell.detailTextLabel?.text = "\(dateFormatter.string(from: last)) — \(entry.count)" - return cell - } -} diff --git a/main/AppInfoType.swift b/main/unused/AppInfoType.swift similarity index 100% rename from main/AppInfoType.swift rename to main/unused/AppInfoType.swift diff --git a/main/BundleIcon.swift b/main/unused/BundleIcon.swift similarity index 100% rename from main/BundleIcon.swift rename to main/unused/BundleIcon.swift diff --git a/main/LaunchIcon.png b/media/LaunchIcon.png similarity index 100% rename from main/LaunchIcon.png rename to media/LaunchIcon.png diff --git a/media/third-level.txt b/media/third-level.txt new file mode 100644 index 0000000..eeca54e --- /dev/null +++ b/media/third-level.txt @@ -0,0 +1,556 @@ +at.ac +at.co +at.gv +at.or +at.priv +au.asn +au.com +au.csiro +au.edu +au.gov +au.id +au.net +au.org +bm.com +bm.edu +bm.gov +bm.net +bm.org +br.adm +br.adv +br.agr +br.am +br.arq +br.art +br.ato +br.b +br.bio +br.blog +br.bmd +br.cim +br.cng +br.cnt +br.com +br.coop +br.ecn +br.edu +br.eng +br.esp +br.etc +br.eti +br.far +br.flog +br.fm +br.fnd +br.fot +br.fst +br.g12 +br.ggf +br.gov +br.imb +br.ind +br.inf +br.jor +br.jus +br.leg +br.lel +br.mat +br.med +br.mil +br.mus +br.net +br.nom +br.not +br.ntr +br.odo +br.ong +br.org +br.ppg +br.pro +br.psc +br.psi +br.qsl +br.radio +br.rec +br.slg +br.srv +br.taxi +br.teo +br.tmp +br.trd +br.tur +br.tv +br.vet +br.vlog +br.wiki +br.zlg +es.com +es.edu +es.gob +es.nom +es.org +fk.ac +fk.co +fk.gov +fk.net +fk.nom +fk.org +fr.aeroport +fr.avocat +fr.avoues +fr.cci +fr.chambagri +fr.chirurgiens-dentistes +fr.experts-comptables +fr.geometre-expert +fr.greta +fr.huissier-justice +fr.medecin +fr.notaires +fr.pharmacien +fr.port +fr.prd +fr.veterinaire +gi.com +gi.edu +gi.gov +gi.ltd +gi.mod +gi.org +hu.2000 +hu.agrar +hu.bolt +hu.casino +hu.city +hu.co +hu.edu +hu.erotica +hu.erotika +hu.film +hu.forum +hu.games +hu.gov +hu.hotel +hu.info +hu.ingatlan +hu.jogasz +hu.konyvelo +hu.lakas +hu.media +hu.mobi +hu.net +hu.news +hu.org +hu.priv +hu.reklam +hu.sex +hu.shop +hu.sport +hu.suli +hu.szex +hu.tm +hu.tozsde +hu.utazas +hu.video +il.ac +il.co +il.gov +il.idf +il.k12 +il.muni +il.net +il.org +im.ac +im.co +im.com +im.gov +im.net +im.org +im.ro +in.ac +in.co +in.edu +in.ernet +in.firm +in.gen +in.gov +in.ind +in.mil +in.net +in.org +in.res +je.co +je.net +je.org +kr.ac +kr.busan +kr.chungbuk +kr.chungnam +kr.co +kr.daegu +kr.daejeon +kr.es +kr.gangwon +kr.go +kr.gwangju +kr.gyeongbuk +kr.gyeonggi +kr.gyeongnam +kr.hs +kr.incheon +kr.jeju +kr.jeonbuk +kr.jeonnam +kr.kg +kr.mil +kr.ms +kr.ne +kr.or +kr.pe +kr.re +kr.sc +kr.seoul +kr.ulsan +ky.com +ky.edu +ky.gov +ky.net +ky.org +lk.ac +lk.assn +lk.com +lk.edu +lk.gov +lk.grp +lk.hotel +lk.int +lk.ltd +lk.net +lk.ngo +lk.org +lk.sch +lk.soc +lk.web +ms.com +ms.edu +ms.gov +ms.net +ms.org +nz.ac +nz.co +nz.cri +nz.geek +nz.gen +nz.govt +nz.health +nz.iwi +nz.kiwi +nz.maori +nz.mil +nz.net +nz.org +nz.parliament +nz.school +pn.ac +pn.co +pn.in +pn.net +pn.org +re.asso +re.com +re.nom +ru, mari.ru.mari-el +ru.ac +ru.adygeya +ru.altai +ru.amur +ru.amursk +ru.arkhangelsk +ru.astrakhan +ru.baikal +ru.bashkiria +ru.belgorod +ru.bir +ru.bryansk +ru.buryatia +ru.cap +ru.cbg +ru.chel +ru.chelyabinsk +ru.chita +ru.chukotka +ru.cmw +ru.com +ru.dagestan +ru.e-burg +ru.edu +ru.fareast +ru.gov +ru.grozny +ru.int +ru.irkutsk +ru.ivanovo +ru.izhevsk +ru.jamal +ru.jar +ru.joshkar-ola +ru.k-uralsk +ru.kalmykia +ru.kaluga +ru.kamchatka +ru.karelia +ru.kazan +ru.kchr +ru.kemerovo +ru.khabarovsk +ru.khakassia +ru.khv +ru.kirov +ru.kms +ru.koenig +ru.komi +ru.kostroma +ru.krasnoyarsk +ru.kuban +ru.kurgan +ru.kursk +ru.kustanai +ru.kuzbass +ru.lipetsk +ru.magadan +ru.magnitka +ru.marine +ru.mil +ru.mordovia +ru.mos +ru.mosreg +ru.msk +ru.murmansk +ru.mytis +ru.nakhodka +ru.nalchik +ru.net +ru.nkz +ru.nnov +ru.norilsk +ru.nov +ru.novosibirsk +ru.nsk +ru.omsk +ru.orenburg +ru.org +ru.oryol +ru.oskol +ru.penza +ru.perm +ru.pp +ru.pskov +ru.ptz +ru.pyatigorsk +ru.rnd +ru.rubtsovsk +ru.ryazan +ru.sakhalin +ru.samara +ru.saratov +ru.simbirsk +ru.smolensk +ru.snz +ru.spb +ru.stavropol +ru.stv +ru.surgut +ru.syzran +ru.tambov +ru.tatarstan +ru.tlt +ru.tom +ru.tomsk +ru.tsaritsyn +ru.tsk +ru.tula +ru.tuva +ru.tver +ru.tyumen +ru.udm +ru.udmurtia +ru.ulan-ude +ru.vdonsk +ru.vladikavkaz +ru.vladimir +ru.vladivostok +ru.volgograd +ru.vologda +ru.voronezh +ru.vrn +ru.vyatka +ru.yakutia +ru.yamal +ru.yaroslavl +ru.yekaterinburg +ru.yuzhno-sakhalinsk +sg.com +sg.edu +sg.gov +sg.net +sg.org +sg.per +sh.co +sh.com +sh.edu +sh.gov +sh.net +sh.nom +sh.org +tc.com +tc.net +tc.org +tc.pro +th.ac +th.co +th.go +th.in +th.mi +th.net +th.or +tr.av +tr.bbs +tr.bel +tr.biz +tr.com +tr.dr +tr.edu +tr.gen +tr.gov +tr.info +tr.k12 +tr.mil +tr.name +tr.net +tr.org +tr.pol +tr.tel +tr.tv +tr.web +tt.aero +tt.biz +tt.charity +tt.co +tt.com +tt.coop +tt.edu +tt.gov +tt.info +tt.int +tt.jobs +tt.mil +tt.mobi +tt.museum +tt.name +tt.net +tt.org +tt.pro +tt.tel +tt.travel +ua.cherkassy +ua.cherkasy +ua.chernigov +ua.chernivtsi +ua.chernovtsy +ua.ck +ua.cn +ua.com +ua.cr +ua.crimea +ua.cv +ua.dn +ua.dnepropetrovsk +ua.dnipropetrovsk +ua.donetsk +ua.dp +ua.edu +ua.gov +ua.if +ua.in +ua.ivano-frankivsk +ua.kh +ua.kharkiv +ua.kharkov +ua.kherson +ua.khmelnitskiy +ua.kiev +ua.kirovograd +ua.km +ua.kr +ua.ks +ua.kv +ua.kyiv +ua.lg +ua.lugansk +ua.lutsk +ua.lv +ua.lviv +ua.mk +ua.net +ua.nikolaev +ua.od +ua.odesa +ua.odessa +ua.org +ua.pl +ua.poltava +ua.pp +ua.rivne +ua.rovno +ua.rv +ua.sevastopol +ua.sm +ua.sumy +ua.te +ua.ternopil +ua.uz +ua.uzhgorod +ua.vinnica +ua.vl +ua.vn +ua.volyn +ua.yalta +ua.zaporizhzhe +ua.zhitomir +ua.zp +ua.zt +uk.ac +uk.co +uk.gov +uk.ltd +uk.me +uk.mil +uk.mod +uk.net +uk.nhs +uk.nic +uk.org +uk.parliament +uk.plc +uk.police +uk.sch +us.fed +us.isa +us.nsn +za.ac +za.agric +za.alt +za.co +za.edu +za.gov +za.grondar +za.law +za.mil +za.net +za.ngo +za.nis +za.nom +za.org +za.school +za.tm +za.web \ No newline at end of file