From 6dcc2086e668037b856e0780cb199e992330d667 Mon Sep 17 00:00:00 2001 From: relikd Date: Sun, 28 Jun 2020 23:55:08 +0200 Subject: [PATCH] Auto-delete logs finished + custom App-to-VPN messages --- AppCheck.xcodeproj/project.pbxproj | 2 + GlassVPN/PacketTunnelProvider.swift | 69 ++++++++++++++++++++++++++--- main/AppDelegate.swift | 4 ++ main/DB/DBCommon.swift | 14 +++++- main/DB/TheGreatDestroyer.swift | 16 ++++++- main/GlassVPN.swift | 40 ++++++++++++++--- main/Settings/TVCSettings.swift | 15 ++++--- 7 files changed, 142 insertions(+), 18 deletions(-) diff --git a/AppCheck.xcodeproj/project.pbxproj b/AppCheck.xcodeproj/project.pbxproj index 4688aaf..cb95a6b 100644 --- a/AppCheck.xcodeproj/project.pbxproj +++ b/AppCheck.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 5404AEEB24A90717003B2F54 /* PrefsShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E67E4524A8B0FE0025D261 /* PrefsShared.swift */; }; 540E6780242D2CF100871BBE /* VCRecordings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540E677F242D2CF100871BBE /* VCRecordings.swift */; }; 540E67822433483D00871BBE /* VCEditRecording.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540E67812433483D00871BBE /* VCEditRecording.swift */; }; 540E67842433FAFE00871BBE /* TVCPreviousRecords.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540E67832433FAFE00871BBE /* TVCPreviousRecords.swift */; }; @@ -917,6 +918,7 @@ 54CA02722426B2FD003A5E04 /* IPInterval.swift in Sources */, 54CA029A2426B2FD003A5E04 /* Observer.swift in Sources */, 54CA025C2426B2FD003A5E04 /* ConnectSession.swift in Sources */, + 5404AEEB24A90717003B2F54 /* PrefsShared.swift in Sources */, 54CA02C42426DCCD003A5E04 /* GCDAsyncUdpSocket.m in Sources */, 54CA02BA2426B2FD003A5E04 /* ProxySocket.swift in Sources */, 54CA025E2426B2FD003A5E04 /* ResponseGeneratorFactory.swift in Sources */, diff --git a/GlassVPN/PacketTunnelProvider.swift b/GlassVPN/PacketTunnelProvider.swift index e8288c3..dfd7468 100644 --- a/GlassVPN/PacketTunnelProvider.swift +++ b/GlassVPN/PacketTunnelProvider.swift @@ -80,18 +80,26 @@ class LDObserverFactory: ObserverFactory { class PacketTunnelProvider: NEPacketTunnelProvider { - let proxyServerPort: UInt16 = 9090 - let proxyServerAddress = "127.0.0.1" - var proxyServer: GCDHTTPProxyServer! + private let proxyServerPort: UInt16 = 9090 + private let proxyServerAddress = "127.0.0.1" + private var proxyServer: GCDHTTPProxyServer! + + private var autoDeleteTimer: Timer? = nil + + private func reloadSettings() { + reloadDomainFilter() + setAutoDelete(PrefsShared.AutoDeleteLogsDays) + } override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { + DDLogVerbose("startTunnel with with options: \(String(describing: options))") do { try SQLiteDatabase.open().initCommonScheme() } catch { completionHandler(error) return } - reloadDomainFilter() + reloadSettings() if proxyServer != nil { proxyServer.stop() @@ -145,12 +153,63 @@ class PacketTunnelProvider: NEPacketTunnelProvider { proxyServer = nil filterDomains = nil filterOptions = nil + autoDeleteTimer?.fire() // one last time before we quit + autoDeleteTimer?.invalidate() completionHandler() exit(EXIT_SUCCESS) } override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { - reloadDomainFilter() + let message = String(data: messageData, encoding: .utf8) + if let msg = message, let i = msg.firstIndex(of: ":") { + let action = msg.prefix(upTo: i) + let value = msg.suffix(from: msg.index(after: i)) + switch action { + case "filter-update": + reloadDomainFilter() // TODO: reload only selected domain? + return + case "auto-delete": + setAutoDelete(Int(value) ?? PrefsShared.AutoDeleteLogsDays) + return + default: break + } + } + DDLogWarn("This should never happen! Received unknown handleAppMessage: \(message ?? messageData.base64EncodedString())") + reloadSettings() // just in case we fallback to do everything } } + +// ################################################################ +// # +// # MARK: - Auto-delete Timer +// # +// ################################################################ + +extension PacketTunnelProvider { + + private func setAutoDelete(_ days: Int) { + autoDeleteTimer?.invalidate() + guard days > 0 else { return } + // Repeat interval uses days as hours. min 1 hr, max 24 hrs. + let interval = TimeInterval(min(24, days) * 60 * 60) + autoDeleteTimer = Timer.scheduledTimer(timeInterval: interval, + target: self, selector: #selector(autoDeleteNow), + userInfo: days, repeats: true) + autoDeleteTimer!.fire() + } + + @objc private func autoDeleteNow(_ sender: Timer) { + DDLogInfo("Auto-delete old logs") + queue.async { + do { + try AppDB?.dnsLogsDeleteOlderThan(days: sender.userInfo as! Int) + } catch { + DDLogWarn("Couldn't delete logs, will retry in 5 minutes. \(error)") + if sender.isValid { + sender.fireDate = Date().addingTimeInterval(300) // retry in 5 min + } + } + } + } +} diff --git a/main/AppDelegate.swift b/main/AppDelegate.swift index c41d970..b299704 100644 --- a/main/AppDelegate.swift +++ b/main/AppDelegate.swift @@ -22,4 +22,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { sync.start() return true } + + func applicationDidBecomeActive(_ application: UIApplication) { + TheGreatDestroyer.deleteLogs(olderThan: PrefsShared.AutoDeleteLogsDays) + } } diff --git a/main/DB/DBCommon.swift b/main/DB/DBCommon.swift index 25e3b56..68f361d 100644 --- a/main/DB/DBCommon.swift +++ b/main/DB/DBCommon.swift @@ -39,9 +39,21 @@ extension SQLiteDatabase { /// `INSERT INTO cache (dns, opt) VALUES (?, ?);` func logWrite(_ domain: String, blocked: Bool = false) throws { try self.run(sql: "INSERT INTO cache (dns, opt) VALUES (?, ?);", - bind: [BindText(domain), BindInt32(blocked ? 1 : 0)]) + bind: [BindText(domain), BindInt32(blocked ? 1 : 0)]) { try ifStep($0, SQLITE_DONE) } } + + /// `DELETE FROM cache WHERE ts < (now - ? days);` + /// - Parameter days: if `0` or negative, this function does nothing. + /// - Returns: `true` if at least one row was deleted. + @discardableResult func dnsLogsDeleteOlderThan(days: Int) throws -> Bool { + guard days > 0 else { return false } + return try self.run(sql: "DELETE FROM cache WHERE ts < strftime('%s', 'now', ?);", + bind: [BindText("-\(days) days")]) { + try ifStep($0, SQLITE_DONE) + return numberOfChanges > 0 + } + } } diff --git a/main/DB/TheGreatDestroyer.swift b/main/DB/TheGreatDestroyer.swift index 60bb59a..156fcd1 100644 --- a/main/DB/TheGreatDestroyer.swift +++ b/main/DB/TheGreatDestroyer.swift @@ -15,7 +15,7 @@ struct TheGreatDestroyer { } } - /// Fired when user taps on Settings -> Delete All Logs + /// Fired when user taps on Settings -> "Delete All Logs" static func deleteAllLogs() { sync.pause() DispatchQueue.global().async { @@ -26,4 +26,18 @@ struct TheGreatDestroyer { } catch {} } } + + /// Fired when user changes Settings -> "Auto-delete logs" and every time the App enters foreground + static func deleteLogs(olderThan days: Int) { + guard days > 0 else { return } + sync.pause() + DispatchQueue.global().async { + defer { sync.continue() } + QLog.Info("Auto-delete logs") + guard let success = try? AppDB?.dnsLogsDeleteOlderThan(days: days), success else { + return // nothing changed + } + sync.needsReloadDB() + } + } } diff --git a/main/GlassVPN.swift b/main/GlassVPN.swift index 297765e..10a68bb 100644 --- a/main/GlassVPN.swift +++ b/main/GlassVPN.swift @@ -43,6 +43,19 @@ final class GlassVPNManager { } } + /// Notify VPN extension about changes + /// - Returns: `true` on success, `false` if VPN is off or message could not be converted to `.utf8` + @discardableResult func send(_ message: VPNAppMessage) -> Bool { + if let session = self.managerVPN?.connection as? NETunnelProviderSession, + session.status == .connected, let data = message.raw { + do { + try session.sendProviderMessage(data, responseHandler: nil) + return true + } catch {} + } + return false + } + // MARK: - Notify callback @@ -50,12 +63,8 @@ final class GlassVPNManager { postRawVPNState((notification.object as? NETunnelProviderSession)?.status ?? .invalid) } - @objc private func didChangeDomainFilter() { - // Notify VPN extension about changes - if let session = self.managerVPN?.connection as? NETunnelProviderSession, - session.status == .connected { - try? session.sendProviderMessage("filter-update".data(using: .ascii)!, responseHandler: nil) - } + @objc private func didChangeDomainFilter(_ notification: Notification) { + send(.filterUpdate(domain: notification.object as? String)) } @@ -109,3 +118,22 @@ final class GlassVPNManager { NotifyVPNStateChanged.post() } } + + +// --------------------------------------------------------------- +// | +// | MARK: - VPN message +// | +// --------------------------------------------------------------- + +struct VPNAppMessage { + let raw: Data? + init(_ string: String) { raw = string.data(using: .utf8) } + + static func filterUpdate(domain: String? = nil) -> Self { + .init("filter-update:\(domain ?? "")") + } + static func autoDelete(after interval: Int) -> Self { + .init("auto-delete:\(interval)") + } +} diff --git a/main/Settings/TVCSettings.swift b/main/Settings/TVCSettings.swift index 3ddf6ab..eda9364 100644 --- a/main/Settings/TVCSettings.swift +++ b/main/Settings/TVCSettings.swift @@ -77,14 +77,19 @@ class TVCSettings: UITableViewController { let picker = DurationPickerAlert( title: "Auto-delete logs", - detail: "Logs will be deleted on app launch or periodically as long as the VPN is running.", + detail: "Warning: Logs older than the selected interval are deleted immediately! " + + "Logs are also deleted on each app launch, and periodically in the background as long as the VPN is running.", options: [(0...30).map{"\($0)"}, ["Days", "Weeks", "Months"]], widths: [0.4, 0.6]) picker.pickerView.setSelection([min(30, one), two]) - picker.present(in: self) { - PrefsShared.AutoDeleteLogsDays = $1[0] * multiplier[$1[1]] - cell.detailTextLabel?.text = autoDeleteString($1[0], unit: $1[1]) - // TODO: notify VPN and local delete timer + picker.present(in: self) { _, idx in + cell.detailTextLabel?.text = autoDeleteString(idx[0], unit: idx[1]) + let asDays = idx[0] * multiplier[idx[1]] + PrefsShared.AutoDeleteLogsDays = asDays + if !GlassVPN.send(.autoDelete(after: asDays)) { + // if VPN isn't active, fallback to immediate local delete + TheGreatDestroyer.deleteLogs(olderThan: asDays) + } } } }