This commit is contained in:
relikd
2020-03-19 00:05:43 +01:00
parent 188a130825
commit 126da073a5
53 changed files with 2476 additions and 593 deletions

View File

@@ -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 = "<group>"; };
541A957523E602DF00C09C19 /* LaunchIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = LaunchIcon.png; sourceTree = "<group>"; };
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 = "<group>"; };
@@ -97,10 +110,13 @@
541AC5E12399498B00A769D7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
541AC5E32399498B00A769D7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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 = "<group>"; };
542E2A9924051556001462DC /* TVCSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCSettings.swift; sourceTree = "<group>"; };
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 = "<group>"; };
543CDB2123EEE61900B7F323 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
543CDB2223EEE61900B7F323 /* GlassVPN.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GlassVPN.entitlements; sourceTree = "<group>"; };
544C95252407B1C700AB89D0 /* SharedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedState.swift; sourceTree = "<group>"; };
546063B123FC254B008F505A /* Sodium.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sodium.framework; path = Carthage/Build/iOS/Sodium.framework; sourceTree = "<group>"; };
546063B223FC254B008F505A /* NEKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NEKit.framework; path = Carthage/Build/iOS/NEKit.framework; sourceTree = "<group>"; };
546063B323FC254B008F505A /* tun2socks.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = tun2socks.framework; path = Carthage/Build/iOS/tun2socks.framework; sourceTree = "<group>"; };
@@ -116,6 +132,14 @@
54953E6023E0D69A0054345C /* TVCHosts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCHosts.swift; sourceTree = "<group>"; };
54953E6E23E44CD00054345C /* TVCHostDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCHostDetails.swift; sourceTree = "<group>"; };
54953E7023E473F10054345C /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
54B34593240E6343004C53CC /* TVCFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCFilter.swift; sourceTree = "<group>"; };
54B34595240F0513004C53CC /* TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableView.swift; sourceTree = "<group>"; };
54B345982414F491004C53CC /* DBWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBWrapper.swift; sourceTree = "<group>"; };
54B345A5241BB982004C53CC /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
54B345A8241BBA0B004C53CC /* Generic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Generic.swift; sourceTree = "<group>"; };
54B345AA241BBA5B004C53CC /* AlertSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertSheet.swift; sourceTree = "<group>"; };
54B345AC241BBB00004C53CC /* GroupedDomain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupedDomain.swift; sourceTree = "<group>"; };
54B345AF242264F8004C53CC /* third-level.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "third-level.txt"; sourceTree = "<group>"; };
54B7562223D7B2DC008F0C41 /* SQDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SQDB.swift; sourceTree = "<group>"; };
54C056DA23E9E36E00214A3F /* AppInfoType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoType.swift; sourceTree = "<group>"; };
54C056DC23E9EEF700214A3F /* BundleIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIcon.swift; sourceTree = "<group>"; };
@@ -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 = "<group>";
};
540C6455240D5BD200E948F9 /* Settings */ = {
isa = PBXGroup;
children = (
542E2A9924051556001462DC /* TVCSettings.swift */,
54B34593240E6343004C53CC /* TVCFilter.swift */,
);
path = Settings;
sourceTree = "<group>";
};
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 = "<group>";
};
542E2A9B24051F79001462DC /* media */ = {
isa = PBXGroup;
children = (
541A957523E602DF00C09C19 /* LaunchIcon.png */,
54B345AF242264F8004C53CC /* third-level.txt */,
);
path = media;
sourceTree = "<group>";
};
543CDB1E23EEE61900B7F323 /* GlassVPN */ = {
isa = PBXGroup;
children = (
@@ -217,6 +270,45 @@
path = GlassVPN;
sourceTree = "<group>";
};
54B34597240F18DD004C53CC /* TVC Extensions */ = {
isa = PBXGroup;
children = (
540C6456240D929300E948F9 /* EditableRows.swift */,
);
path = "TVC Extensions";
sourceTree = "<group>";
};
54B3459A2415651C004C53CC /* DB */ = {
isa = PBXGroup;
children = (
54B7562223D7B2DC008F0C41 /* SQDB.swift */,
54B345982414F491004C53CC /* DBWrapper.swift */,
);
path = DB;
sourceTree = "<group>";
};
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 = "<group>";
};
54B345B12422E029004C53CC /* unused */ = {
isa = PBXGroup;
children = (
54C056DC23E9EEF700214A3F /* BundleIcon.swift */,
54C056DA23E9E36E00214A3F /* AppInfoType.swift */,
);
path = unused;
sourceTree = "<group>";
};
/* 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",

View File

@@ -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)")
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

View File

@@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

View File

@@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 B

View File

@@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 B

View File

@@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 B

View File

@@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

View File

@@ -1,13 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RcB-4v-fd4">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="sfA-EG-18J">
<device id="retina4_0" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Requests-->
<!--Domains-->
<scene sceneID="MN1-aZ-cZt">
<objects>
<tableViewController id="pdd-aM-sKl" customClass="TVCDomains" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
@@ -15,29 +16,9 @@
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<textView key="tableHeaderView" clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" editable="NO" selectable="NO" id="QWn-iX-27k">
<rect key="frame" x="0.0" y="0.0" width="320" height="317"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<accessibility key="accessibilityConfiguration">
<accessibilityTraits key="traits" staticText="YES" notEnabled="YES"/>
<bool key="isElement" value="YES"/>
</accessibility>
<string key="text">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.</string>
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" name=".AppleSystemUIFont" family=".AppleSystemUIFont" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="default" hidesAccessoryWhenEditing="NO" indentationWidth="10" reuseIdentifier="DomainCell" textLabel="0HB-5f-eB1" detailTextLabel="MRe-Eq-gvc" style="IBUITableViewCellStyleSubtitle" id="F8D-aK-j1W">
<rect key="frame" x="0.0" y="345" width="320" height="55.5"/>
<rect key="frame" x="0.0" y="28" width="320" height="55.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="F8D-aK-j1W" id="FY2-xr-hqh">
<rect key="frame" x="0.0" y="0.0" width="320" height="55.5"/>
@@ -69,27 +50,13 @@ Your data belongs to you. Therefore, monitoring and analysis take place on your
<outlet property="delegate" destination="pdd-aM-sKl" id="3RN-az-SYU"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Requests" id="nY5-jL-QT9">
<barButtonItem key="leftBarButtonItem" systemItem="trash" id="PPO-Wv-RWf">
<connections>
<action selector="clickToolbarLeft:" destination="pdd-aM-sKl" id="iFj-4n-lvg"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" tag="-1" title="&lt;State&gt;" id="SfA-a1-N1w">
<connections>
<action selector="clickToolbarRight:" destination="pdd-aM-sKl" id="zgS-Wn-apQ"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="welcomeMessage" destination="QWn-iX-27k" id="jA3-Dz-7zu"/>
</connections>
<navigationItem key="navigationItem" title="Domains" id="nY5-jL-QT9"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="jfx-iA-E0v" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="742.5" y="167.95774647887325"/>
<point key="canvasLocation" x="686" y="-1245"/>
</scene>
<!--Subdomains-->
<!--Hosts-->
<scene sceneID="ZCV-Yx-jjW">
<objects>
<tableViewController id="WcC-nb-Vf5" customClass="TVCHosts" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
@@ -131,11 +98,11 @@ Your data belongs to you. Therefore, monitoring and analysis take place on your
<outlet property="delegate" destination="WcC-nb-Vf5" id="sBd-BW-Wg6"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Subdomains" prompt="com.app.Example" id="TvD-8U-F05"/>
<navigationItem key="navigationItem" title="Hosts" prompt="com.app.Example" id="TvD-8U-F05"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Gdi-Xi-JUL" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1448" y="168"/>
<point key="canvasLocation" x="1391" y="-1245"/>
</scene>
<!--Occurrences-->
<scene sceneID="ws3-sK-l8m">
@@ -173,12 +140,13 @@ Your data belongs to you. Therefore, monitoring and analysis take place on your
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="UxH-PH-KQy" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2147" y="168"/>
<point key="canvasLocation" x="2096" y="-1245"/>
</scene>
<!--Navigation Controller-->
<!--Requests-->
<scene sceneID="bDO-X1-bCe">
<objects>
<navigationController id="RcB-4v-fd4" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Requests" image="journal" id="Sj5-Kb-Li8"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="HWd-73-m8j">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
@@ -189,7 +157,284 @@ Your data belongs to you. Therefore, monitoring and analysis take place on your
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="8j4-AX-JBN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="40" y="168"/>
<point key="canvasLocation" x="-21" y="-1245"/>
</scene>
<!--Settings-->
<scene sceneID="gEe-ny-NaU">
<objects>
<tableViewController id="qdB-ZO-LHY" customClass="TVCSettings" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" bounces="NO" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="8kq-PY-wp7">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<sections>
<tableViewSection headerTitle="General Settings" id="w58-6X-Jea">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="ghM-ze-fvp">
<rect key="frame" x="0.0" y="55.5" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ghM-ze-fvp" id="d2v-vz-QIB">
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kmY-ot-lJW">
<rect key="frame" x="256" y="6" width="45" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<connections>
<action selector="toggleVPNProxy:" destination="qdB-ZO-LHY" eventType="valueChanged" id="y95-2Z-Uep"/>
</connections>
</switch>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="VPN Proxy enabled" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Qha-4I-go0">
<rect key="frame" x="16" y="5" width="230" height="27"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="Qyy-0U-yhd">
<rect key="frame" x="0.0" y="99" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Qyy-0U-yhd" id="Mfs-fu-W5k">
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" id="9Ko-sD-7x0">
<rect key="frame" x="95" y="7" width="124" height="30"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" title="Export DB"/>
<connections>
<action selector="exportDB:" destination="qdB-ZO-LHY" eventType="touchUpInside" id="3gu-WF-3Xa"/>
</connections>
</button>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="wzU-8s-HGb">
<rect key="frame" x="0.0" y="142.5" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="wzU-8s-HGb" id="aNM-6U-bho">
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" id="S6B-i8-CoC">
<rect key="frame" x="94" y="7" width="125" height="30"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" title="Delete all logs"/>
<connections>
<action selector="clearDatabaseResults:" destination="qdB-ZO-LHY" eventType="touchUpInside" id="w0d-8F-GmN"/>
</connections>
</button>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Logging Filter" id="EcH-KA-eLE">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="detailDisclosureButton" indentationWidth="10" reuseIdentifier="settingsIgnoredCell" textLabel="UdM-Zm-G9p" detailTextLabel="bHb-Tw-nPR" style="IBUITableViewCellStyleValue2" id="fZR-we-Y0k">
<rect key="frame" x="0.0" y="242" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fZR-we-Y0k" id="eqc-fj-p0d">
<rect key="frame" x="0.0" y="0.0" width="261" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Ignore" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="UdM-Zm-G9p">
<rect key="frame" x="16" y="14" width="91" height="16"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="0 Domains" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="bHb-Tw-nPR">
<rect key="frame" x="113" y="14" width="64.5" height="16"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<connections>
<segue destination="q3B-Yi-1bx" kind="show" identifier="segueFilterIgnored" id="EzT-Xq-wka"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="detailDisclosureButton" indentationWidth="10" reuseIdentifier="settingsBlockedCell" textLabel="fI0-Nt-Ucf" detailTextLabel="CGG-47-cdc" style="IBUITableViewCellStyleValue2" id="3pw-7c-M6R">
<rect key="frame" x="0.0" y="285.5" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="3pw-7c-M6R" id="Smv-n1-917">
<rect key="frame" x="0.0" y="0.0" width="261" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Block" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="fI0-Nt-Ucf">
<rect key="frame" x="16" y="14" width="91" height="16"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="0 Domains" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="CGG-47-cdc">
<rect key="frame" x="113" y="14" width="64.5" height="16"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<connections>
<segue destination="q3B-Yi-1bx" kind="show" identifier="segueFilterBlocked" id="cOY-j0-75m"/>
</connections>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="qdB-ZO-LHY" id="RH3-xR-dpC"/>
<outlet property="delegate" destination="qdB-ZO-LHY" id="eYf-Xd-2Jq"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Settings" id="9Ce-p2-kGX"/>
<connections>
<outlet property="cellDomainsBlocked" destination="3pw-7c-M6R" id="AHT-FE-z0s"/>
<outlet property="cellDomainsIgnored" destination="fZR-we-Y0k" id="Huy-N3-gz7"/>
<outlet property="vpnToggle" destination="kmY-ot-lJW" id="yeS-DE-FfR"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="VNK-Z0-T0a" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="684" y="127"/>
</scene>
<!--Domains-->
<scene sceneID="218-uP-X7b">
<objects>
<tableViewController id="q3B-Yi-1bx" customClass="TVCFilter" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="GSg-ZZ-F8J">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="DomainFilterCell" textLabel="MrS-rb-RLB" style="IBUITableViewCellStyleDefault" id="EO2-ww-xuz">
<rect key="frame" x="0.0" y="28" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="EO2-ww-xuz" id="AtR-ce-uYs">
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="MrS-rb-RLB">
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="q3B-Yi-1bx" id="eWw-VO-n1c"/>
<outlet property="delegate" destination="q3B-Yi-1bx" id="02X-f0-d1a"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Domains" id="FWA-IG-VIb"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Xzo-dO-WpK" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1389" y="127"/>
</scene>
<!--Settings-->
<scene sceneID="OEQ-fb-haL">
<objects>
<navigationController id="dIk-JY-9vE" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Settings" image="settings" id="dQu-wE-a8u"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="yYW-rX-VnB">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="qdB-ZO-LHY" kind="relationship" relationship="rootViewController" id="qJW-Jc-O4D"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="bg9-bR-vlx" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-23" y="127"/>
</scene>
<!--Recordings-->
<scene sceneID="ODR-PD-nTU">
<objects>
<viewController id="hm5-7q-Zfi" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="JYr-yE-eGS">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="Jq8-ke-k0B"/>
</view>
<tabBarItem key="tabBarItem" title="Recordings" image="tag" id="mGk-aq-MRP"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Wfy-Tp-A9o" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-21" y="-560"/>
</scene>
<!--Main-->
<scene sceneID="7Rl-BK-ry5">
<objects>
<tabBarController id="sfA-EG-18J" customClass="TBCMain" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tabBar key="tabBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="qza-ey-Iaz">
<rect key="frame" x="0.0" y="0.0" width="414" height="49"/>
<autoresizingMask key="autoresizingMask"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tabBar>
<connections>
<segue destination="cGm-zQ-NnO" kind="presentation" identifier="welcome" id="aF0-OB-Mwx"/>
<segue destination="RcB-4v-fd4" kind="relationship" relationship="viewControllers" id="cmC-pu-5n2"/>
<segue destination="hm5-7q-Zfi" kind="relationship" relationship="viewControllers" id="pfK-BR-9lf"/>
<segue destination="dIk-JY-9vE" kind="relationship" relationship="viewControllers" id="AwW-3j-iAg"/>
</connections>
</tabBarController>
<placeholder placeholderIdentifier="IBFirstResponder" id="RDz-8t-yhN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-831" y="127"/>
</scene>
<!--View Controller-->
<scene sceneID="8iq-nV-o0O">
<objects>
<viewController id="cGm-zQ-NnO" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="FlS-lu-XEg">
<rect key="frame" x="0.0" y="0.0" width="320" height="548"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" editable="NO" selectable="NO" id="QWn-iX-27k">
<rect key="frame" x="16" y="20" width="288" height="508"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<accessibility key="accessibilityConfiguration">
<accessibilityTraits key="traits" staticText="YES" notEnabled="YES"/>
<bool key="isElement" value="YES"/>
</accessibility>
<string key="text">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.</string>
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" name=".AppleSystemUIFont" family=".AppleSystemUIFont" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="SJX-Gb-WTN"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="nve-Iu-WIa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-831" y="841"/>
</scene>
</scenes>
<resources>
<image name="journal" width="25" height="25"/>
<image name="settings" width="25" height="25"/>
<image name="tag" width="25" height="25"/>
</resources>
<inferredMetricsTieBreakers>
<segue reference="cOY-j0-75m"/>
</inferredMetricsTieBreakers>
</document>

276
main/DB/DBWrapper.swift Normal file
View File

@@ -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)
}
}

302
main/DB/SQDB.swift Normal file
View File

@@ -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<T>(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<T>(_ stmt: OpaquePointer, _ fn: (OpaquePointer) -> T) -> [T] {
var r: [T] = []
while (sqlite3_step(stmt) == SQLITE_ROW) { r.append(fn(stmt)) }
return r
}
func allRowsKeyed<T,U>(_ 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() }
}
}

View File

@@ -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])
}
}
}

View File

@@ -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) }

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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()
}
}
}

View File

@@ -41,5 +41,9 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIFileSharingEnabled</key>
<false/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<false/>
</dict>
</plist>

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -6,16 +6,6 @@
<string>Root</string>
<key>PreferenceSpecifiers</key>
<array>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Kill proxy on startup</string>
<key>Key</key>
<string>kill_proxy</string>
<key>DefaultValue</key>
<false/>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>

View File

@@ -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
}
}

View File

@@ -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
}
}
}

36
main/TBCMain.swift Normal file
View File

@@ -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
}
}
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

556
media/third-level.txt Normal file
View File

@@ -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