Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e54d69ef4b | ||
|
|
be8269ad56 | ||
|
|
7118ec3b02 | ||
|
|
71045bf0dd | ||
|
|
27abdd66f5 | ||
|
|
162e18c912 | ||
|
|
d68e4ec869 | ||
|
|
762263bfbd | ||
|
|
b1cddc796e | ||
|
|
77e20f31f5 | ||
|
|
0175f5390e | ||
|
|
effc305b86 | ||
|
|
c1fe258b0d | ||
|
|
36a8f0b97b | ||
|
|
33b9cab8a8 | ||
|
|
b88874b38b | ||
|
|
f55f3ea32d | ||
|
|
c843bd76a2 | ||
|
|
4dd2339ed8 | ||
|
|
280526bef4 | ||
|
|
34caffd4a7 | ||
|
|
9e19b457e2 | ||
|
|
e6846953b7 | ||
|
|
6d78aeac7b | ||
|
|
5d94fe3a0d | ||
|
|
fb680d669b | ||
|
|
6409e5eaf3 | ||
|
|
39ca9dbdb1 | ||
|
|
27ab2a621a | ||
|
|
3f572eeb15 | ||
|
|
e83540d5de | ||
|
|
847556bec1 | ||
|
|
42b045fb85 | ||
|
|
35a211f87f | ||
|
|
d2fa67e0e3 | ||
|
|
b8660c9a35 | ||
|
|
8cd3f7fb3a | ||
|
|
2ee0272a05 | ||
|
|
4ae82fc763 | ||
|
|
aac42d7eff | ||
|
|
8bb77ef741 | ||
|
|
ff4218981f | ||
|
|
7b7c5f3d9a | ||
|
|
1c203e39c3 | ||
|
|
7dbf21d564 | ||
|
|
8fcb5ad874 | ||
|
|
b4bf705b7f | ||
|
|
69d8321180 | ||
|
|
b03daeca66 | ||
|
|
c502484bcf | ||
|
|
448d69c6d8 | ||
|
|
42aa7cf926 | ||
|
|
52fa2e460e | ||
|
|
8855ae754a | ||
|
|
908a909c87 | ||
|
|
41aee797a9 | ||
|
|
685f636d5b | ||
|
|
4af56b0cb1 | ||
|
|
a3973c7e9a | ||
|
|
b270f30f3c | ||
|
|
03177cee0b | ||
|
|
9ee094dc20 | ||
|
|
b1d49c6765 | ||
|
|
b774e2152c | ||
|
|
e398ac8bcd | ||
|
|
01523b250f | ||
|
|
a2b0f311d5 | ||
|
|
88a52fb92c | ||
|
|
723f1665a7 | ||
|
|
4f92d3d58d | ||
|
|
05d06a4f31 | ||
|
|
f9ab545e0f | ||
|
|
b10d4c8b36 | ||
|
|
5a3ca024f8 | ||
|
|
92216c0c03 | ||
|
|
9ece3474c6 | ||
|
|
6dcc2086e6 | ||
|
|
08483711e2 | ||
|
|
0e100006d3 | ||
|
|
710c617862 | ||
|
|
3ed25c92cd | ||
|
|
f7644e6048 | ||
|
|
80afa6aff1 | ||
|
|
43de81929f | ||
|
|
e315e71d07 | ||
|
|
416eb34799 | ||
|
|
b7b13f51b2 | ||
|
|
2312187670 | ||
|
|
c7d0dc7c5f | ||
|
|
895cabee80 | ||
|
|
d96ced48c9 | ||
|
|
0b6dbfd888 | ||
|
|
96656438c6 | ||
|
|
4b32df5683 | ||
|
|
0758bd7dec | ||
|
|
171dabd83a | ||
|
|
6182a99ebd | ||
|
|
8bfedda3ab | ||
|
|
26f6ea1a9a | ||
|
|
778f377e42 | ||
|
|
f284365469 | ||
|
|
5dfb7d4ba4 | ||
|
|
bb9c3a3034 | ||
|
|
8cf872a4b0 | ||
|
|
e813230824 | ||
|
|
e8bfde9243 | ||
|
|
e947ad6d4d | ||
|
|
0a53898797 | ||
|
|
946acc2460 | ||
|
|
e13b3df2c4 | ||
|
|
7df2fe421e | ||
|
|
b4b89f8bb4 | ||
|
|
db41e68f35 | ||
|
|
5acd9bbcc6 | ||
|
|
23eab2310f | ||
|
|
80829ad015 | ||
|
|
661bf5d30a | ||
|
|
38f4166503 | ||
|
|
d96038c7e3 | ||
|
|
7d6b071d8a | ||
|
|
b17fb3c354 | ||
|
|
10b43a0f67 | ||
|
|
4092a9ba55 | ||
|
|
2d35c863e4 | ||
|
|
8424c161b9 | ||
|
|
9485d7e9b5 | ||
|
|
9f26bdfba1 | ||
|
|
412d533275 | ||
|
|
245bb46e4f | ||
|
|
70508c1325 | ||
|
|
b44fd788b5 | ||
|
|
80f3503e16 | ||
|
|
d0056c0275 | ||
|
|
e7560479ee | ||
|
|
ed5298f7a2 | ||
|
|
647eca310f | ||
|
|
515c296b26 | ||
|
|
61ae50cdfa | ||
|
|
fcb6e9c5dd | ||
|
|
79f836016a | ||
|
|
144773ddaa |
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1130"
|
||||
LastUpgradeVersion = "1200"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1130"
|
||||
LastUpgradeVersion = "1200"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import NetworkExtension
|
||||
|
||||
fileprivate var db: SQLiteDatabase?
|
||||
fileprivate var domainFilters: [String : FilterOptions] = [:]
|
||||
let connectMessage: Data = "CONNECT".data(using: .ascii)!
|
||||
let swcdUserAgent: Data = "User-Agent: swcd".data(using: .ascii)!
|
||||
fileprivate var hook : GlassVPNHook!
|
||||
|
||||
// MARK: ObserverFactory
|
||||
|
||||
class LDObserverFactory: ObserverFactory {
|
||||
|
||||
|
||||
override func getObserverForProxySocket(_ socket: ProxySocket) -> Observer<ProxySocketEvent>? {
|
||||
// TODO: replace NEKit with custom proxy with minimal footprint
|
||||
return LDProxySocketObserver()
|
||||
}
|
||||
|
||||
@@ -15,103 +17,126 @@ class LDObserverFactory: ObserverFactory {
|
||||
override func signal(_ event: ProxySocketEvent) {
|
||||
switch event {
|
||||
case .receivedRequest(let session, let socket):
|
||||
DDLogDebug("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 { DDLogDebug("ignored") }
|
||||
if block { DDLogDebug("blocked"); socket.forceDisconnect() }
|
||||
var kill = !hook.isBackgroundRecording && hook.forceDisconnectUnresolvable && session.ipAddress.isEmpty
|
||||
if kill || socket.isCancelled { // isCancelled is set by branch below
|
||||
hook.silentlyPrevented(session.host)
|
||||
} else {
|
||||
kill = hook.processDNSRequest(session.host)
|
||||
}
|
||||
if kill { socket.forceDisconnect() }
|
||||
case .readData(let data, on: let socket):
|
||||
if !hook.isBackgroundRecording, hook.forceDisconnectSWCD,
|
||||
data.starts(with: connectMessage), data.range(of: swcdUserAgent) != nil {
|
||||
socket.disconnect() // sets isCancelled above
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: NEPacketTunnelProvider
|
||||
|
||||
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!
|
||||
|
||||
func reloadDomainFilter() {
|
||||
domainFilters = db?.loadFilters() ?? [:]
|
||||
}
|
||||
|
||||
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
|
||||
DDLogVerbose("startTunnel")
|
||||
// MARK: Delegate
|
||||
|
||||
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
|
||||
DDLogVerbose("startTunnel with with options: \(String(describing: options))")
|
||||
PrefsShared.registerDefaults()
|
||||
do {
|
||||
db = try SQLiteDatabase.open()
|
||||
db!.initScheme()
|
||||
try SQLiteDatabase.open().initCommonScheme()
|
||||
} catch {
|
||||
completionHandler(error)
|
||||
completionHandler(error) // if we cant open db, fail immediately
|
||||
return
|
||||
}
|
||||
if proxyServer != nil {
|
||||
proxyServer.stop()
|
||||
}
|
||||
proxyServer = nil
|
||||
|
||||
reloadDomainFilter()
|
||||
// stop previous if any
|
||||
if proxyServer != nil { proxyServer.stop() }
|
||||
proxyServer = nil
|
||||
|
||||
// Create proxy
|
||||
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: proxyServerAddress)
|
||||
settings.mtu = NSNumber(value: 1500)
|
||||
|
||||
let proxySettings = NEProxySettings()
|
||||
proxySettings.httpEnabled = true;
|
||||
proxySettings.httpServer = NEProxyServer(address: proxyServerAddress, port: Int(proxyServerPort))
|
||||
proxySettings.httpsEnabled = true;
|
||||
proxySettings.httpsServer = NEProxyServer(address: proxyServerAddress, port: Int(proxyServerPort))
|
||||
proxySettings.excludeSimpleHostnames = false;
|
||||
proxySettings.exceptionList = []
|
||||
proxySettings.matchDomains = [""]
|
||||
|
||||
settings.dnsSettings = NEDNSSettings(servers: ["127.0.0.1"])
|
||||
settings.proxySettings = proxySettings;
|
||||
RawSocketFactory.TunnelProvider = self
|
||||
ObserverFactory.currentFactory = LDObserverFactory()
|
||||
willInitProxy()
|
||||
|
||||
self.setTunnelNetworkSettings(settings) { error in
|
||||
self.setTunnelNetworkSettings(createProxy()) { error in
|
||||
guard error == nil else {
|
||||
DDLogError("setTunnelNetworkSettings error: \(String(describing: error))")
|
||||
DDLogError("setTunnelNetworkSettings error: \(error!)")
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
DDLogVerbose("setTunnelNetworkSettings success \(self.packetFlow)")
|
||||
completionHandler(nil)
|
||||
|
||||
self.proxyServer = GCDHTTPProxyServer(address: IPAddress(fromString: self.proxyServerAddress), port: Port(port: self.proxyServerPort))
|
||||
do {
|
||||
try self.proxyServer.start()
|
||||
completionHandler(nil)
|
||||
}
|
||||
catch let proxyError {
|
||||
DDLogError("Error starting proxy server \(proxyError)")
|
||||
completionHandler(proxyError)
|
||||
}
|
||||
do {
|
||||
try self.proxyServer.start()
|
||||
self.didInitProxy()
|
||||
completionHandler(nil)
|
||||
} catch let proxyError {
|
||||
DDLogError("Error starting proxy server \(proxyError)")
|
||||
completionHandler(proxyError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
||||
DDLogVerbose("stopTunnel with reason: \(reason)")
|
||||
db = nil
|
||||
shutdown()
|
||||
completionHandler()
|
||||
exit(EXIT_SUCCESS)
|
||||
}
|
||||
|
||||
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
|
||||
hook.handleAppMessage(messageData)
|
||||
}
|
||||
|
||||
// MARK: Helper
|
||||
|
||||
private func willInitProxy() {
|
||||
hook = GlassVPNHook()
|
||||
}
|
||||
|
||||
private func createProxy() -> NEPacketTunnelNetworkSettings {
|
||||
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: proxyServerAddress)
|
||||
settings.mtu = NSNumber(value: 1500)
|
||||
|
||||
let proxySettings = NEProxySettings()
|
||||
proxySettings.httpEnabled = true;
|
||||
proxySettings.httpServer = NEProxyServer(address: proxyServerAddress, port: Int(proxyServerPort))
|
||||
proxySettings.httpsEnabled = true;
|
||||
proxySettings.httpsServer = NEProxyServer(address: proxyServerAddress, port: Int(proxyServerPort))
|
||||
proxySettings.excludeSimpleHostnames = false;
|
||||
proxySettings.exceptionList = []
|
||||
proxySettings.matchDomains = [""]
|
||||
|
||||
settings.dnsSettings = NEDNSSettings(servers: ["127.0.0.1"])
|
||||
settings.proxySettings = proxySettings;
|
||||
RawSocketFactory.TunnelProvider = self
|
||||
ObserverFactory.currentFactory = LDObserverFactory()
|
||||
return settings
|
||||
}
|
||||
|
||||
private func didInitProxy() {
|
||||
if PrefsShared.RestartReminder.Enabled {
|
||||
PushNotification.scheduleRestartReminderBadge(on: false)
|
||||
PushNotification.cancel(.CantStopMeNowReminder)
|
||||
}
|
||||
}
|
||||
|
||||
private func shutdown() {
|
||||
// proxy
|
||||
DNSServer.currentServer = nil
|
||||
RawSocketFactory.TunnelProvider = nil
|
||||
ObserverFactory.currentFactory = nil
|
||||
proxyServer.stop()
|
||||
proxyServer = nil
|
||||
completionHandler()
|
||||
exit(EXIT_SUCCESS)
|
||||
}
|
||||
|
||||
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
|
||||
DDLogVerbose("handleAppMessage")
|
||||
reloadDomainFilter()
|
||||
}
|
||||
RawSocketFactory.TunnelProvider = nil
|
||||
ObserverFactory.currentFactory = nil
|
||||
proxyServer.stop()
|
||||
proxyServer = nil
|
||||
// custom
|
||||
hook.cleanUp()
|
||||
hook = nil
|
||||
if PrefsShared.RestartReminder.Enabled {
|
||||
PushNotification.scheduleRestartReminderBadge(on: true)
|
||||
PushNotification.scheduleRestartReminderBanner()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
GlassVPN/SwiftSocket/.DS_Store
vendored
Normal file
35
GlassVPN/robbiehanson-CocoaAsyncSocket/LICENSE.txt
Normal file
@@ -0,0 +1,35 @@
|
||||
This library is in the public domain.
|
||||
However, not all organizations are allowed to use such a license.
|
||||
For example, Germany doesn't recognize the Public Domain and one is not allowed to use libraries under such license (or similar).
|
||||
|
||||
Thus, the library is now dual licensed,
|
||||
and one is allowed to choose which license they would like to use.
|
||||
|
||||
##################################################
|
||||
License Option #1 :
|
||||
##################################################
|
||||
|
||||
Public Domain
|
||||
|
||||
##################################################
|
||||
License Option #2 :
|
||||
##################################################
|
||||
|
||||
Software License Agreement (BSD License)
|
||||
|
||||
Copyright (c) 2017, Deusty, LLC
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use of this software in source and binary forms,
|
||||
with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above
|
||||
copyright notice, this list of conditions and the
|
||||
following disclaimer.
|
||||
|
||||
* Neither the name of Deusty LLC nor the names of its
|
||||
contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior
|
||||
written permission of Deusty LLC.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -1,16 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public enum RuleMatchEvent: EventType {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case let .ruleMatched(session, rule: rule):
|
||||
return "Rule \(rule) matched session \(session)."
|
||||
case let .ruleDidNotMatch(session, rule: rule):
|
||||
return "Rule \(rule) did not match session \(session)."
|
||||
case let .dnsRuleMatched(session, rule: rule, type: type, result: result):
|
||||
return "Rule \(rule) matched DNS session \(session) of type \(type), the result is \(result)."
|
||||
}
|
||||
}
|
||||
|
||||
case ruleMatched(ConnectSession, rule: Rule), ruleDidNotMatch(ConnectSession, rule: Rule), dnsRuleMatched(DNSSession, rule: Rule, type: DNSSessionMatchType, result: DNSSessionMatchResult)
|
||||
}
|
||||
@@ -20,8 +20,4 @@ open class ObserverFactory {
|
||||
open func getObserverForProxyServer(_ server: ProxyServer) -> Observer<ProxyServerEvent>? {
|
||||
return nil
|
||||
}
|
||||
|
||||
open func getObserverForRuleManager(_ manager: RuleManager) -> Observer<RuleMatchEvent>? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,8 +74,6 @@ open class DNSServer: DNSResolverDelegate, IPStackProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
RuleManager.currentManager.matchDNS(session, type: .domain)
|
||||
|
||||
switch session.matchResult! {
|
||||
case .fake:
|
||||
guard setUpFakeIP(session) else {
|
||||
@@ -248,10 +246,6 @@ open class DNSServer: DNSResolverDelegate, IPStackProtocol {
|
||||
|
||||
session.realIP = message.resolvedIPv4Address
|
||||
|
||||
if session.matchResult != .fake && session.matchResult != .real {
|
||||
RuleManager.currentManager.matchDNS(session, type: .ip)
|
||||
}
|
||||
|
||||
switch session.matchResult! {
|
||||
case .fake:
|
||||
if !self.setUpFakeIP(session) {
|
||||
|
||||
@@ -7,7 +7,6 @@ open class DNSSession {
|
||||
open var fakeIP: IPAddress?
|
||||
open var realResponseMessage: DNSMessage?
|
||||
var realResponseIPPacket: IPPacket?
|
||||
open var matchedRule: Rule?
|
||||
open var matchResult: DNSSessionMatchResult?
|
||||
var indexToMatch = 0
|
||||
var expireAt: Date?
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
//import Foundation
|
||||
//
|
||||
//enum ChangeType {
|
||||
// case Address, Port
|
||||
//}
|
||||
//
|
||||
//public class IPMutablePacket {
|
||||
// // Support only IPv4 for now
|
||||
//
|
||||
// let version: IPVersion
|
||||
// let proto: TransportType
|
||||
// let IPHeaderLength: Int
|
||||
// var sourceAddress: IPv4Address {
|
||||
// get {
|
||||
// return IPv4Address(fromBytesInNetworkOrder: payload.bytes.advancedBy(12))
|
||||
// }
|
||||
// set {
|
||||
// setIPv4Address(sourceAddress, newAddress: newValue, at: 12)
|
||||
// }
|
||||
// }
|
||||
// var destinationAddress: IPv4Address {
|
||||
// get {
|
||||
// return IPv4Address(fromBytesInNetworkOrder: payload.bytes.advancedBy(16))
|
||||
// }
|
||||
// set {
|
||||
// setIPv4Address(destinationAddress, newAddress: newValue, at: 16)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// let payload: NSMutableData
|
||||
//
|
||||
// public init(payload: NSData) {
|
||||
// let vl = UnsafePointer<UInt8>(payload.bytes).memory
|
||||
// version = IPVersion(rawValue: vl >> 4)!
|
||||
// IPHeaderLength = Int(vl & 0x0F) * 4
|
||||
// let p = UnsafePointer<UInt8>(payload.bytes.advancedBy(9)).memory
|
||||
// proto = TransportType(rawValue: p)!
|
||||
// self.payload = NSMutableData(data: payload)
|
||||
// }
|
||||
//
|
||||
// func updateChecksum(oldValue: UInt16, newValue: UInt16, type: ChangeType) {
|
||||
// if type == .Address {
|
||||
// updateChecksum(oldValue, newValue: newValue, at: 10)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // swiftlint:disable:next variable_name
|
||||
// internal func updateChecksum(oldValue: UInt16, newValue: UInt16, at: Int) {
|
||||
// let oldChecksum = UnsafePointer<UInt16>(payload.bytes.advancedBy(at)).memory
|
||||
// let oc32 = UInt32(~oldChecksum)
|
||||
// let ov32 = UInt32(~oldValue)
|
||||
// let nv32 = UInt32(newValue)
|
||||
// var newChecksum32 = oc32 &+ ov32 &+ nv32
|
||||
// newChecksum32 = (newChecksum32 & 0xFFFF) + (newChecksum32 >> 16)
|
||||
// newChecksum32 = (newChecksum32 & 0xFFFF) &+ (newChecksum32 >> 16)
|
||||
// var newChecksum = ~UInt16(newChecksum32)
|
||||
// payload.replaceBytesInRange(NSRange(location: at, length: 2), withBytes: &newChecksum, length: 2)
|
||||
// }
|
||||
//
|
||||
// // swiftlint:disable:next variable_name
|
||||
// private func foldChecksum(checksum: UInt32) -> UInt32 {
|
||||
// var checksum = checksum
|
||||
// while checksum > 0xFFFF {
|
||||
// checksum = (checksum & 0xFFFF) + (checksum >> 16)
|
||||
// }
|
||||
// return checksum
|
||||
// }
|
||||
//
|
||||
// // swiftlint:disable:next variable_name
|
||||
// private func setIPv4Address(oldAddress: IPv4Address, newAddress: IPv4Address, at: Int) {
|
||||
// payload.replaceBytesInRange(NSRange(location: at, length: 4), withBytes: newAddress.bytesInNetworkOrder, length: 4)
|
||||
// updateChecksum(UnsafePointer<UInt16>(oldAddress.bytesInNetworkOrder).memory, newValue: UnsafePointer<UInt16>(newAddress.bytesInNetworkOrder).memory, type: .Address)
|
||||
// updateChecksum(UnsafePointer<UInt16>(oldAddress.bytesInNetworkOrder).advancedBy(1).memory, newValue: UnsafePointer<UInt16>(newAddress.bytesInNetworkOrder).advancedBy(1).memory, type: .Address)
|
||||
// }
|
||||
//
|
||||
//}
|
||||
@@ -1,32 +0,0 @@
|
||||
//import Foundation
|
||||
//
|
||||
//class TCPMutablePacket: IPMutablePacket {
|
||||
// var sourcePort: Port {
|
||||
// get {
|
||||
// return Port(bytesInNetworkOrder: payload.bytes.advancedBy(IPHeaderLength))
|
||||
// }
|
||||
// set {
|
||||
// setPort(sourcePort, newPort: newValue, at: 0)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// var destinationPort: Port {
|
||||
// get {
|
||||
// return Port(bytesInNetworkOrder: payload.bytes.advancedBy(IPHeaderLength + 2))
|
||||
// }
|
||||
// set {
|
||||
// setPort(destinationPort, newPort: newValue, at: 2)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override func updateChecksum(oldValue: UInt16, newValue: UInt16, type: ChangeType) {
|
||||
// super.updateChecksum(oldValue, newValue: newValue, type: type)
|
||||
// updateChecksum(oldValue, newValue: newValue, at: IPHeaderLength + 16)
|
||||
// }
|
||||
//
|
||||
// // swiftlint:disable:next variable_name
|
||||
// private func setPort(oldPort: Port, newPort: Port, at: Int) {
|
||||
// payload.replaceBytesInRange(NSRange(location: at + IPHeaderLength, length: 2), withBytes: newPort.bytesInNetworkOrder, length: 2)
|
||||
// updateChecksum(oldPort.valueInNetworkOrder, newValue: newPort.valueInNetworkOrder, type: .Port)
|
||||
// }
|
||||
//}
|
||||
27
GlassVPN/zhuhaow-NEKit/LICENSE.md
Normal file
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2016, Zhuhao Wang
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of NEKit nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -21,9 +21,6 @@ public final class ConnectSession {
|
||||
/// The requested port.
|
||||
public let port: Int
|
||||
|
||||
/// The rule to use to connect to remote.
|
||||
public var matchedRule: Rule?
|
||||
|
||||
/// Whether If the `requestedHost` is an IP address.
|
||||
public let fakeIPEnabled: Bool
|
||||
|
||||
@@ -126,11 +123,6 @@ public final class ConnectSession {
|
||||
|
||||
host = session.requestMessage.queries[0].name
|
||||
ipAddress = session.realIP?.presentation ?? ""
|
||||
matchedRule = session.matchedRule
|
||||
|
||||
// if session.countryCode != nil {
|
||||
// country = session.countryCode!
|
||||
// }
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ open class HTTPHeader {
|
||||
// Chunk is not supported yet.
|
||||
open var contentLength: Int = 0
|
||||
open var headers: [(String, String)] = []
|
||||
open var rawHeader: Data?
|
||||
|
||||
public init(headerString: String) throws {
|
||||
let lines = headerString.components(separatedBy: "\r\n")
|
||||
@@ -127,7 +126,6 @@ open class HTTPHeader {
|
||||
}
|
||||
|
||||
try self.init(headerString: headerString)
|
||||
rawHeader = headerData
|
||||
}
|
||||
|
||||
open subscript(index: String) -> String? {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// The SOCKS5 proxy server.
|
||||
public final class GCDSOCKS5ProxyServer: GCDProxyServer {
|
||||
/**
|
||||
Create an instance of SOCKS5 proxy server.
|
||||
|
||||
- parameter address: The address of proxy server.
|
||||
- parameter port: The port of proxy server.
|
||||
*/
|
||||
override public init(address: IPAddress?, port: Port) {
|
||||
super.init(address: address, port: port)
|
||||
}
|
||||
|
||||
/**
|
||||
Handle the new accepted socket as a SOCKS5 proxy connection.
|
||||
|
||||
- parameter socket: The accepted socket.
|
||||
*/
|
||||
override public func handleNewGCDSocket(_ socket: GCDTCPSocket) {
|
||||
let proxySocket = SOCKS5ProxySocket(socket: socket)
|
||||
didAcceptNewSocket(proxySocket)
|
||||
}
|
||||
}
|
||||
@@ -141,7 +141,14 @@ public class NWTCPSocket: NSObject, RawTCPSocketProtocol {
|
||||
|
||||
connection!.readMinimumLength(1, maximumLength: Opt.MAXNWTCPSocketReadDataSize) { data, error in
|
||||
guard error == nil else {
|
||||
DDLogError("NWTCPSocket got an error when reading data: \(String(describing: error))")
|
||||
let e = error! as NSError
|
||||
let ignore = (
|
||||
e.domain == "kNWErrorDomainPOSIX" && e.code == POSIXError.ECANCELED.rawValue // Operation canceled
|
||||
|| e.domain == NSPOSIXErrorDomain && e.code == POSIXError.ENOTCONN.rawValue // Socket is not connected
|
||||
)
|
||||
if !ignore {
|
||||
DDLogError("NWTCPSocket got an error when reading data: \(String(describing: error))")
|
||||
}
|
||||
self.queueCall {
|
||||
self.disconnect()
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
open class ResponseGeneratorFactory {
|
||||
static var HTTPProxyResponseGenerator: ResponseGenerator.Type?
|
||||
static var SOCKS5ProxyResponseGenerator: ResponseGenerator.Type?
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// The rule matches all DNS and connect sessions.
|
||||
open class AllRule: Rule {
|
||||
fileprivate let adapterFactory: AdapterFactory
|
||||
|
||||
open override var description: String {
|
||||
return "<AllRule>"
|
||||
}
|
||||
|
||||
/**
|
||||
Create a new `AllRule` instance.
|
||||
|
||||
- parameter adapterFactory: The factory which builds a corresponding adapter when needed.
|
||||
*/
|
||||
public init(adapterFactory: AdapterFactory) {
|
||||
self.adapterFactory = adapterFactory
|
||||
super.init()
|
||||
}
|
||||
|
||||
/**
|
||||
Match DNS session to this rule.
|
||||
|
||||
- parameter session: The DNS session to match.
|
||||
- parameter type: What kind of information is available.
|
||||
|
||||
- returns: The result of match.
|
||||
*/
|
||||
override open func matchDNS(_ session: DNSSession, type: DNSSessionMatchType) -> DNSSessionMatchResult {
|
||||
// only return real IP when we connect to remote directly
|
||||
if let _ = adapterFactory as? DirectAdapterFactory {
|
||||
return .real
|
||||
} else {
|
||||
return .fake
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Match connect session to this rule.
|
||||
|
||||
- parameter session: connect session to match.
|
||||
|
||||
- returns: The configured adapter.
|
||||
*/
|
||||
override open func match(_ session: ConnectSession) -> AdapterFactory? {
|
||||
return adapterFactory
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// The rule matches the request which failed to look up.
|
||||
open class DNSFailRule: Rule {
|
||||
fileprivate let adapterFactory: AdapterFactory
|
||||
|
||||
open override var description: String {
|
||||
return "<DNSFailRule>"
|
||||
}
|
||||
|
||||
/**
|
||||
Create a new `DNSFailRule` instance.
|
||||
|
||||
- parameter adapterFactory: The factory which builds a corresponding adapter when needed.
|
||||
*/
|
||||
public init(adapterFactory: AdapterFactory) {
|
||||
self.adapterFactory = adapterFactory
|
||||
super.init()
|
||||
}
|
||||
|
||||
/**
|
||||
Match DNS request to this rule.
|
||||
|
||||
- parameter session: The DNS session to match.
|
||||
- parameter type: What kind of information is available.
|
||||
|
||||
- returns: The result of match.
|
||||
*/
|
||||
override open func matchDNS(_ session: DNSSession, type: DNSSessionMatchType) -> DNSSessionMatchResult {
|
||||
guard type == .ip else {
|
||||
return .unknown
|
||||
}
|
||||
|
||||
// only return real IP when we connect to remote directly
|
||||
if session.realIP == nil {
|
||||
if let _ = adapterFactory as? DirectAdapterFactory {
|
||||
return .real
|
||||
} else {
|
||||
return .fake
|
||||
}
|
||||
} else {
|
||||
return .pass
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Match connect session to this rule.
|
||||
|
||||
- parameter session: connect session to match.
|
||||
|
||||
- returns: The configured adapter.
|
||||
*/
|
||||
override open func match(_ session: ConnectSession) -> AdapterFactory? {
|
||||
if session.ipAddress == "" {
|
||||
return adapterFactory
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// The rule matches every request and returns direct adapter.
|
||||
///
|
||||
/// This is equivalent to create an `AllRule` with a `DirectAdapterFactory`.
|
||||
open class DirectRule: AllRule {
|
||||
open override var description: String {
|
||||
return "<DirectRule>"
|
||||
}
|
||||
/**
|
||||
Create a new `DirectRule` instance.
|
||||
*/
|
||||
public init() {
|
||||
super.init(adapterFactory: DirectAdapterFactory())
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// The rule matches the host domain to a list of predefined criteria.
|
||||
open class DomainListRule: Rule {
|
||||
public enum MatchCriterion {
|
||||
case regex(NSRegularExpression), prefix(String), suffix(String), keyword(String), complete(String)
|
||||
|
||||
func match(_ domain: String) -> Bool {
|
||||
switch self {
|
||||
case .regex(let regex):
|
||||
return regex.firstMatch(in: domain, options: [], range: NSRange(location: 0, length: domain.utf8.count)) != nil
|
||||
case .prefix(let prefix):
|
||||
return domain.hasPrefix(prefix)
|
||||
case .suffix(let suffix):
|
||||
return domain.hasSuffix(suffix)
|
||||
case .keyword(let keyword):
|
||||
return domain.contains(keyword)
|
||||
case .complete(let match):
|
||||
return domain == match
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate let adapterFactory: AdapterFactory
|
||||
|
||||
open override var description: String {
|
||||
return "<DomainListRule>"
|
||||
}
|
||||
|
||||
/// The list of criteria to match to.
|
||||
open var matchCriteria: [MatchCriterion] = []
|
||||
|
||||
/**
|
||||
Create a new `DomainListRule` instance.
|
||||
|
||||
- parameter adapterFactory: The factory which builds a corresponding adapter when needed.
|
||||
- parameter criteria: The list of criteria to match.
|
||||
*/
|
||||
public init(adapterFactory: AdapterFactory, criteria: [MatchCriterion]) {
|
||||
self.adapterFactory = adapterFactory
|
||||
self.matchCriteria = criteria
|
||||
}
|
||||
|
||||
/**
|
||||
Match DNS request to this rule.
|
||||
|
||||
- parameter session: The DNS session to match.
|
||||
- parameter type: What kind of information is available.
|
||||
|
||||
- returns: The result of match.
|
||||
*/
|
||||
override open func matchDNS(_ session: DNSSession, type: DNSSessionMatchType) -> DNSSessionMatchResult {
|
||||
if matchDomain(session.requestMessage.queries.first!.name) {
|
||||
if let _ = adapterFactory as? DirectAdapterFactory {
|
||||
return .real
|
||||
}
|
||||
return .fake
|
||||
}
|
||||
return .pass
|
||||
}
|
||||
|
||||
/**
|
||||
Match connect session to this rule.
|
||||
|
||||
- parameter session: connect session to match.
|
||||
|
||||
- returns: The configured adapter if matched, return `nil` if not matched.
|
||||
*/
|
||||
override open func match(_ session: ConnectSession) -> AdapterFactory? {
|
||||
if matchDomain(session.host) {
|
||||
return adapterFactory
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
fileprivate func matchDomain(_ domain: String) -> Bool {
|
||||
for criterion in matchCriteria {
|
||||
if criterion.match(domain) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// The rule matches the ip of the target hsot to a list of IP ranges.
|
||||
open class IPRangeListRule: Rule {
|
||||
fileprivate let adapterFactory: AdapterFactory
|
||||
|
||||
open override var description: String {
|
||||
return "<IPRangeList>"
|
||||
}
|
||||
|
||||
/// The list of regular expressions to match to.
|
||||
open var ranges: [IPRange] = []
|
||||
|
||||
/**
|
||||
Create a new `IPRangeListRule` instance.
|
||||
|
||||
- parameter adapterFactory: The factory which builds a corresponding adapter when needed.
|
||||
- parameter ranges: The list of IP ranges to match. The IP ranges are expressed in CIDR form ("127.0.0.1/8") or range form ("127.0.0.1+16777216").
|
||||
|
||||
- throws: The error when parsing the IP range.
|
||||
*/
|
||||
public init(adapterFactory: AdapterFactory, ranges: [String]) throws {
|
||||
self.adapterFactory = adapterFactory
|
||||
self.ranges = try ranges.map {
|
||||
let range = try IPRange(withString: $0)
|
||||
return range
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Match DNS request to this rule.
|
||||
|
||||
- parameter session: The DNS session to match.
|
||||
- parameter type: What kind of information is available.
|
||||
|
||||
- returns: The result of match.
|
||||
*/
|
||||
override open func matchDNS(_ session: DNSSession, type: DNSSessionMatchType) -> DNSSessionMatchResult {
|
||||
guard type == .ip else {
|
||||
return .unknown
|
||||
}
|
||||
|
||||
// Probably we should match all answers?
|
||||
guard let ip = session.realIP else {
|
||||
return .pass
|
||||
}
|
||||
|
||||
for range in ranges {
|
||||
if range.contains(ip: ip) {
|
||||
return .fake
|
||||
}
|
||||
}
|
||||
return .pass
|
||||
}
|
||||
|
||||
/**
|
||||
Match connect session to this rule.
|
||||
|
||||
- parameter session: connect session to match.
|
||||
|
||||
- returns: The configured adapter if matched, return `nil` if not matched.
|
||||
*/
|
||||
override open func match(_ session: ConnectSession) -> AdapterFactory? {
|
||||
guard let ip = IPAddress(fromString: session.ipAddress) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
for range in ranges {
|
||||
if range.contains(ip: ip) {
|
||||
return adapterFactory
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// The rule defines what to do for DNS requests and connect sessions.
|
||||
open class Rule: CustomStringConvertible {
|
||||
open var description: String {
|
||||
return "<Rule>"
|
||||
}
|
||||
|
||||
/**
|
||||
Create a new rule.
|
||||
*/
|
||||
public init() {
|
||||
}
|
||||
|
||||
/**
|
||||
Match DNS request to this rule.
|
||||
|
||||
- parameter session: The DNS session to match.
|
||||
- parameter type: What kind of information is available.
|
||||
|
||||
- returns: The result of match.
|
||||
*/
|
||||
open func matchDNS(_ session: DNSSession, type: DNSSessionMatchType) -> DNSSessionMatchResult {
|
||||
return .real
|
||||
}
|
||||
|
||||
/**
|
||||
Match connect session to this rule.
|
||||
|
||||
- parameter session: connect session to match.
|
||||
|
||||
- returns: The configured adapter if matched, return `nil` if not matched.
|
||||
*/
|
||||
open func match(_ session: ConnectSession) -> AdapterFactory? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// The class managing rules.
|
||||
open class RuleManager {
|
||||
/// The current used `RuleManager`, there is only one manager should be used at a time.
|
||||
///
|
||||
/// - note: This should be set before any DNS or connect sessions.
|
||||
public static var currentManager: RuleManager = RuleManager(fromRules: [], appendDirect: true)
|
||||
|
||||
/// The rule list.
|
||||
var rules: [Rule] = []
|
||||
|
||||
open var observer: Observer<RuleMatchEvent>?
|
||||
|
||||
/**
|
||||
Create a new `RuleManager` from the given rules.
|
||||
|
||||
- parameter rules: The rules.
|
||||
- parameter appendDirect: Whether to append a `DirectRule` at the end of the list so any request does not match with any rule go directly.
|
||||
*/
|
||||
public init(fromRules rules: [Rule], appendDirect: Bool = false) {
|
||||
self.rules = []
|
||||
|
||||
if appendDirect || self.rules.count == 0 {
|
||||
self.rules.append(DirectRule())
|
||||
}
|
||||
|
||||
observer = ObserverFactory.currentFactory?.getObserverForRuleManager(self)
|
||||
}
|
||||
|
||||
/**
|
||||
Match DNS request to all rules.
|
||||
|
||||
- parameter session: The DNS session to match.
|
||||
- parameter type: What kind of information is available.
|
||||
*/
|
||||
func matchDNS(_ session: DNSSession, type: DNSSessionMatchType) {
|
||||
for (i, rule) in rules[session.indexToMatch..<rules.count].enumerated() {
|
||||
let result = rule.matchDNS(session, type: type)
|
||||
|
||||
observer?.signal(.dnsRuleMatched(session, rule: rule, type: type, result: result))
|
||||
|
||||
switch result {
|
||||
case .fake, .real, .unknown:
|
||||
session.matchedRule = rule
|
||||
session.matchResult = result
|
||||
session.indexToMatch = i + session.indexToMatch // add the offset
|
||||
return
|
||||
case .pass:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Match connect session to all rules.
|
||||
|
||||
- parameter session: connect session to match.
|
||||
|
||||
- returns: The matched configured adapter.
|
||||
*/
|
||||
func match(_ session: ConnectSession) -> AdapterFactory! {
|
||||
if session.matchedRule != nil {
|
||||
observer?.signal(.ruleMatched(session, rule: session.matchedRule!))
|
||||
return session.matchedRule!.match(session)
|
||||
}
|
||||
|
||||
for rule in rules {
|
||||
if let adapterFactory = rule.match(session) {
|
||||
observer?.signal(.ruleMatched(session, rule: rule))
|
||||
|
||||
session.matchedRule = rule
|
||||
return adapterFactory
|
||||
} else {
|
||||
observer?.signal(.ruleDidNotMatch(session, rule: rule))
|
||||
}
|
||||
}
|
||||
return nil // this should never happens
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// This is a very simple wrapper of a dict of type `[String: AdapterFactory]`.
|
||||
///
|
||||
/// Use it as a normal dict.
|
||||
public class AdapterFactoryManager {
|
||||
private var factoryDict: [String: AdapterFactory]
|
||||
|
||||
public subscript(index: String) -> AdapterFactory? {
|
||||
get {
|
||||
if index == "direct" {
|
||||
return DirectAdapterFactory()
|
||||
}
|
||||
return factoryDict[index]
|
||||
}
|
||||
set { factoryDict[index] = newValue }
|
||||
}
|
||||
|
||||
/**
|
||||
Initialize a new factory manager.
|
||||
|
||||
- parameter factoryDict: The factory dict.
|
||||
*/
|
||||
public init(factoryDict: [String: AdapterFactory]) {
|
||||
self.factoryDict = factoryDict
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// Factory building server adapter which requires authentication.
|
||||
open class HTTPAuthenticationAdapterFactory: ServerAdapterFactory {
|
||||
let auth: HTTPAuthentication?
|
||||
|
||||
required public init(serverHost: String, serverPort: Int, auth: HTTPAuthentication?) {
|
||||
self.auth = auth
|
||||
super.init(serverHost: serverHost, serverPort: serverPort)
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// Factory building HTTP adapter.
|
||||
open class HTTPAdapterFactory: HTTPAuthenticationAdapterFactory {
|
||||
required public init(serverHost: String, serverPort: Int, auth: HTTPAuthentication?) {
|
||||
super.init(serverHost: serverHost, serverPort: serverPort, auth: auth)
|
||||
}
|
||||
|
||||
/**
|
||||
Get a HTTP adapter.
|
||||
|
||||
- parameter session: The connect session.
|
||||
|
||||
- returns: The built adapter.
|
||||
*/
|
||||
override open func getAdapterFor(session: ConnectSession) -> AdapterSocket {
|
||||
let adapter = HTTPAdapter(serverHost: serverHost, serverPort: serverPort, auth: auth)
|
||||
adapter.socket = RawSocketFactory.getRawSocket()
|
||||
return adapter
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
open class RejectAdapterFactory: AdapterFactory {
|
||||
public let delay: Int
|
||||
|
||||
public init(delay: Int = Opt.RejectAdapterDefaultDelay) {
|
||||
self.delay = delay
|
||||
}
|
||||
|
||||
override open func getAdapterFor(session: ConnectSession) -> AdapterSocket {
|
||||
return RejectAdapter(delay: delay)
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// Factory building SOCKS5 adapter.
|
||||
open class SOCKS5AdapterFactory: ServerAdapterFactory {
|
||||
override public init(serverHost: String, serverPort: Int) {
|
||||
super.init(serverHost: serverHost, serverPort: serverPort)
|
||||
}
|
||||
|
||||
/**
|
||||
Get a SOCKS5 adapter.
|
||||
|
||||
- parameter session: The connect session.
|
||||
|
||||
- returns: The built adapter.
|
||||
*/
|
||||
override open func getAdapterFor(session: ConnectSession) -> AdapterSocket {
|
||||
let adapter = SOCKS5Adapter(serverHost: serverHost, serverPort: serverPort)
|
||||
adapter.socket = RawSocketFactory.getRawSocket()
|
||||
return adapter
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// Factory building secured HTTP (HTTP with SSL) adapter.
|
||||
open class SecureHTTPAdapterFactory: HTTPAdapterFactory {
|
||||
required public init(serverHost: String, serverPort: Int, auth: HTTPAuthentication?) {
|
||||
super.init(serverHost: serverHost, serverPort: serverPort, auth: auth)
|
||||
}
|
||||
|
||||
/**
|
||||
Get a secured HTTP adapter.
|
||||
|
||||
- parameter session: The connect session.
|
||||
|
||||
- returns: The built adapter.
|
||||
*/
|
||||
override open func getAdapterFor(session: ConnectSession) -> AdapterSocket {
|
||||
let adapter = SecureHTTPAdapter(serverHost: serverHost, serverPort: serverPort, auth: auth)
|
||||
adapter.socket = RawSocketFactory.getRawSocket()
|
||||
return adapter
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// Factory building adapter with proxy server host and port.
|
||||
open class ServerAdapterFactory: AdapterFactory {
|
||||
let serverHost: String
|
||||
let serverPort: Int
|
||||
|
||||
public init(serverHost: String, serverPort: Int) {
|
||||
self.serverHost = serverHost
|
||||
self.serverPort = serverPort
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
//import Foundation
|
||||
//
|
||||
///// Factory building Shadowsocks adapter.
|
||||
//open class ShadowsocksAdapterFactory: ServerAdapterFactory {
|
||||
// let protocolObfuscaterFactory: ShadowsocksAdapter.ProtocolObfuscater.Factory
|
||||
// let cryptorFactory: ShadowsocksAdapter.CryptoStreamProcessor.Factory
|
||||
// let streamObfuscaterFactory: ShadowsocksAdapter.StreamObfuscater.Factory
|
||||
//
|
||||
// public init(serverHost: String, serverPort: Int, protocolObfuscaterFactory: ShadowsocksAdapter.ProtocolObfuscater.Factory, cryptorFactory: ShadowsocksAdapter.CryptoStreamProcessor.Factory, streamObfuscaterFactory: ShadowsocksAdapter.StreamObfuscater.Factory) {
|
||||
// self.protocolObfuscaterFactory = protocolObfuscaterFactory
|
||||
// self.cryptorFactory = cryptorFactory
|
||||
// self.streamObfuscaterFactory = streamObfuscaterFactory
|
||||
// super.init(serverHost: serverHost, serverPort: serverPort)
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// Get a Shadowsocks adapter.
|
||||
//
|
||||
// - parameter session: The connect session.
|
||||
//
|
||||
// - returns: The built adapter.
|
||||
// */
|
||||
// override open func getAdapterFor(session: ConnectSession) -> AdapterSocket {
|
||||
// let adapter = ShadowsocksAdapter(host: serverHost, port: serverPort, protocolObfuscater: protocolObfuscaterFactory.build(), cryptor: cryptorFactory.build(), streamObfuscator: streamObfuscaterFactory.build(for: session))
|
||||
// adapter.socket = RawSocketFactory.getRawSocket()
|
||||
// return adapter
|
||||
// }
|
||||
//}
|
||||
@@ -1,26 +0,0 @@
|
||||
//import Foundation
|
||||
//
|
||||
///// Factory building speed adapter.
|
||||
//open class SpeedAdapterFactory: AdapterFactory {
|
||||
// open var adapterFactories: [(AdapterFactory, Int)]!
|
||||
//
|
||||
// public override init() {}
|
||||
//
|
||||
// /**
|
||||
// Get a speed adapter.
|
||||
//
|
||||
// - parameter session: The connect session.
|
||||
//
|
||||
// - returns: The built adapter.
|
||||
// */
|
||||
// override open func getAdapterFor(session: ConnectSession) -> AdapterSocket {
|
||||
// let adapters = adapterFactories.map { adapterFactory, delay -> (AdapterSocket, Int) in
|
||||
// let adapter = adapterFactory.getAdapterFor(session: session)
|
||||
// adapter.socket = RawSocketFactory.getRawSocket()
|
||||
// return (adapter, delay)
|
||||
// }
|
||||
// let speedAdapter = SpeedAdapter()
|
||||
// speedAdapter.adapters = adapters
|
||||
// return speedAdapter
|
||||
// }
|
||||
//}
|
||||
@@ -1,110 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public enum HTTPAdapterError: Error, CustomStringConvertible {
|
||||
case invalidURL, serailizationFailure
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .invalidURL:
|
||||
return "Invalid url when connecting through proxy"
|
||||
case .serailizationFailure:
|
||||
return "Failed to serialize HTTP CONNECT header"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This adapter connects to remote host through a HTTP proxy.
|
||||
public class HTTPAdapter: AdapterSocket {
|
||||
enum HTTPAdapterStatus {
|
||||
case invalid,
|
||||
connecting,
|
||||
readingResponse,
|
||||
forwarding,
|
||||
stopped
|
||||
}
|
||||
|
||||
/// The host domain of the HTTP proxy.
|
||||
let serverHost: String
|
||||
|
||||
/// The port of the HTTP proxy.
|
||||
let serverPort: Int
|
||||
|
||||
/// The authentication information for the HTTP proxy.
|
||||
let auth: HTTPAuthentication?
|
||||
|
||||
/// Whether the connection to the proxy should be secured or not.
|
||||
var secured: Bool
|
||||
|
||||
var internalStatus: HTTPAdapterStatus = .invalid
|
||||
|
||||
public init(serverHost: String, serverPort: Int, auth: HTTPAuthentication?) {
|
||||
self.serverHost = serverHost
|
||||
self.serverPort = serverPort
|
||||
self.auth = auth
|
||||
secured = false
|
||||
super.init()
|
||||
}
|
||||
|
||||
override public func openSocketWith(session: ConnectSession) {
|
||||
super.openSocketWith(session: session)
|
||||
|
||||
guard !isCancelled else {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
internalStatus = .connecting
|
||||
try socket.connectTo(host: serverHost, port: serverPort, enableTLS: secured, tlsSettings: nil)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
override public func didConnectWith(socket: RawTCPSocketProtocol) {
|
||||
super.didConnectWith(socket: socket)
|
||||
|
||||
guard let url = URL(string: "\(session.host):\(session.port)") else {
|
||||
observer?.signal(.errorOccured(HTTPAdapterError.invalidURL, on: self))
|
||||
disconnect()
|
||||
return
|
||||
}
|
||||
let message = CFHTTPMessageCreateRequest(kCFAllocatorDefault, "CONNECT" as CFString, url as CFURL, kCFHTTPVersion1_1).takeRetainedValue()
|
||||
if let authData = auth {
|
||||
CFHTTPMessageSetHeaderFieldValue(message, "Proxy-Authorization" as CFString, authData.authString() as CFString?)
|
||||
}
|
||||
CFHTTPMessageSetHeaderFieldValue(message, "Host" as CFString, "\(session.host):\(session.port)" as CFString?)
|
||||
CFHTTPMessageSetHeaderFieldValue(message, "Content-Length" as CFString, "0" as CFString?)
|
||||
|
||||
guard let requestData = CFHTTPMessageCopySerializedMessage(message)?.takeRetainedValue() else {
|
||||
observer?.signal(.errorOccured(HTTPAdapterError.serailizationFailure, on: self))
|
||||
disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
internalStatus = .readingResponse
|
||||
write(data: requestData as Data)
|
||||
socket.readDataTo(data: Utils.HTTPData.DoubleCRLF)
|
||||
}
|
||||
|
||||
override public func didRead(data: Data, from socket: RawTCPSocketProtocol) {
|
||||
super.didRead(data: data, from: socket)
|
||||
|
||||
switch internalStatus {
|
||||
case .readingResponse:
|
||||
internalStatus = .forwarding
|
||||
observer?.signal(.readyForForward(self))
|
||||
delegate?.didBecomeReadyToForwardWith(socket: self)
|
||||
case .forwarding:
|
||||
observer?.signal(.readData(data, on: self))
|
||||
delegate?.didRead(data: data, from: self)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
override public func didWrite(data: Data?, by socket: RawTCPSocketProtocol) {
|
||||
super.didWrite(data: data, by: socket)
|
||||
if internalStatus == .forwarding {
|
||||
observer?.signal(.wroteData(data, on: self))
|
||||
delegate?.didWrite(data: data, by: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public class RejectAdapter: AdapterSocket {
|
||||
public let delay: Int
|
||||
|
||||
public init(delay: Int) {
|
||||
self.delay = delay
|
||||
}
|
||||
|
||||
override public func openSocketWith(session: ConnectSession) {
|
||||
super.openSocketWith(session: session)
|
||||
|
||||
QueueFactory.getQueue().asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(delay)) {
|
||||
[weak self] in
|
||||
self?.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Disconnect the socket elegantly.
|
||||
*/
|
||||
public override func disconnect(becauseOf error: Error? = nil) {
|
||||
guard !isCancelled else {
|
||||
return
|
||||
}
|
||||
|
||||
_cancelled = true
|
||||
session.disconnected(becauseOf: error, by: .adapter)
|
||||
observer?.signal(.disconnectCalled(self))
|
||||
_status = .closed
|
||||
delegate?.didDisconnectWith(socket: self)
|
||||
}
|
||||
|
||||
/**
|
||||
Disconnect the socket immediately.
|
||||
*/
|
||||
public override func forceDisconnect(becauseOf error: Error? = nil) {
|
||||
guard !isCancelled else {
|
||||
return
|
||||
}
|
||||
|
||||
_cancelled = true
|
||||
session.disconnected(becauseOf: error, by: .adapter)
|
||||
observer?.signal(.forceDisconnectCalled(self))
|
||||
_status = .closed
|
||||
delegate?.didDisconnectWith(socket: self)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public class SOCKS5Adapter: AdapterSocket {
|
||||
enum SOCKS5AdapterStatus {
|
||||
case invalid,
|
||||
connecting,
|
||||
readingMethodResponse,
|
||||
readingResponseFirstPart,
|
||||
readingResponseSecondPart,
|
||||
forwarding
|
||||
}
|
||||
public let serverHost: String
|
||||
public let serverPort: Int
|
||||
|
||||
var internalStatus: SOCKS5AdapterStatus = .invalid
|
||||
|
||||
let helloData = Data(bytes: UnsafePointer<UInt8>(([0x05, 0x01, 0x00] as [UInt8])), count: 3)
|
||||
|
||||
public enum ReadTag: Int {
|
||||
case methodResponse = -20000, connectResponseFirstPart, connectResponseSecondPart
|
||||
}
|
||||
|
||||
public enum WriteTag: Int {
|
||||
case open = -21000, connectIPv4, connectIPv6, connectDomainLength, connectPort
|
||||
}
|
||||
|
||||
public init(serverHost: String, serverPort: Int) {
|
||||
self.serverHost = serverHost
|
||||
self.serverPort = serverPort
|
||||
super.init()
|
||||
}
|
||||
|
||||
public override func openSocketWith(session: ConnectSession) {
|
||||
super.openSocketWith(session: session)
|
||||
|
||||
guard !isCancelled else {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
internalStatus = .connecting
|
||||
try socket.connectTo(host: serverHost, port: serverPort, enableTLS: false, tlsSettings: nil)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
public override func didConnectWith(socket: RawTCPSocketProtocol) {
|
||||
super.didConnectWith(socket: socket)
|
||||
|
||||
write(data: helloData)
|
||||
internalStatus = .readingMethodResponse
|
||||
socket.readDataTo(length: 2)
|
||||
}
|
||||
|
||||
public override func didRead(data: Data, from socket: RawTCPSocketProtocol) {
|
||||
super.didRead(data: data, from: socket)
|
||||
|
||||
switch internalStatus {
|
||||
case .readingMethodResponse:
|
||||
var response: [UInt8]
|
||||
if session.isIPv4() {
|
||||
response = [0x05, 0x01, 0x00, 0x01]
|
||||
let address = IPAddress(fromString: session.host)!
|
||||
response += [UInt8](address.dataInNetworkOrder)
|
||||
} else if session.isIPv6() {
|
||||
response = [0x05, 0x01, 0x00, 0x04]
|
||||
let address = IPAddress(fromString: session.host)!
|
||||
response += [UInt8](address.dataInNetworkOrder)
|
||||
} else {
|
||||
response = [0x05, 0x01, 0x00, 0x03]
|
||||
response.append(UInt8(session.host.utf8.count))
|
||||
response += [UInt8](session.host.utf8)
|
||||
}
|
||||
|
||||
let portBytes: [UInt8] = Utils.toByteArray(UInt16(session.port)).reversed()
|
||||
response.append(contentsOf: portBytes)
|
||||
write(data: Data(response))
|
||||
|
||||
internalStatus = .readingResponseFirstPart
|
||||
socket.readDataTo(length: 5)
|
||||
case .readingResponseFirstPart:
|
||||
var readLength = 0
|
||||
switch data[3] {
|
||||
case 1:
|
||||
readLength = 3 + 2
|
||||
case 3:
|
||||
readLength = Int(data[4]) + 2
|
||||
case 4:
|
||||
readLength = 15 + 2
|
||||
default:
|
||||
break
|
||||
}
|
||||
internalStatus = .readingResponseSecondPart
|
||||
socket.readDataTo(length: readLength)
|
||||
case .readingResponseSecondPart:
|
||||
internalStatus = .forwarding
|
||||
observer?.signal(.readyForForward(self))
|
||||
delegate?.didBecomeReadyToForwardWith(socket: self)
|
||||
case .forwarding:
|
||||
delegate?.didRead(data: data, from: self)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
override open func didWrite(data: Data?, by socket: RawTCPSocketProtocol) {
|
||||
super.didWrite(data: data, by: socket)
|
||||
|
||||
if internalStatus == .forwarding {
|
||||
delegate?.didWrite(data: data, by: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// This adapter connects to remote host through a HTTP proxy with SSL.
|
||||
public class SecureHTTPAdapter: HTTPAdapter {
|
||||
override public init(serverHost: String, serverPort: Int, auth: HTTPAuthentication?) {
|
||||
super.init(serverHost: serverHost, serverPort: serverPort, auth: auth)
|
||||
secured = true
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
//import Foundation
|
||||
//
|
||||
//extension ShadowsocksAdapter {
|
||||
// public class CryptoStreamProcessor {
|
||||
// public class Factory {
|
||||
// let password: String
|
||||
// let algorithm: CryptoAlgorithm
|
||||
// let key: Data
|
||||
//
|
||||
// public init(password: String, algorithm: CryptoAlgorithm) {
|
||||
// self.password = password
|
||||
// self.algorithm = algorithm
|
||||
// key = CryptoHelper.getKey(password, methodType: algorithm)
|
||||
// }
|
||||
//
|
||||
// public func build() -> CryptoStreamProcessor {
|
||||
// return CryptoStreamProcessor(key: key, algorithm: algorithm)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public weak var inputStreamProcessor: StreamObfuscater.StreamObfuscaterBase!
|
||||
// public weak var outputStreamProcessor: ProtocolObfuscater.ProtocolObfuscaterBase!
|
||||
//
|
||||
// var readIV: Data!
|
||||
// let key: Data
|
||||
// let algorithm: CryptoAlgorithm
|
||||
//
|
||||
// var sendKey = false
|
||||
//
|
||||
// var buffer = Buffer(capacity: 0)
|
||||
//
|
||||
// lazy var writeIV: Data = {
|
||||
// [unowned self] in
|
||||
// CryptoHelper.getIV(self.algorithm)
|
||||
// }()
|
||||
// lazy var ivLength: Int = {
|
||||
// [unowned self] in
|
||||
// CryptoHelper.getIVLength(self.algorithm)
|
||||
// }()
|
||||
// lazy var encryptor: StreamCryptoProtocol = {
|
||||
// [unowned self] in
|
||||
// self.getCrypto(.encrypt)
|
||||
// }()
|
||||
// lazy var decryptor: StreamCryptoProtocol = {
|
||||
// [unowned self] in
|
||||
// self.getCrypto(.decrypt)
|
||||
// }()
|
||||
//
|
||||
// init(key: Data, algorithm: CryptoAlgorithm) {
|
||||
// self.key = key
|
||||
// self.algorithm = algorithm
|
||||
// }
|
||||
//
|
||||
// func encrypt(data: inout Data) {
|
||||
// return encryptor.update(&data)
|
||||
// }
|
||||
//
|
||||
// func decrypt(data: inout Data) {
|
||||
// return decryptor.update(&data)
|
||||
// }
|
||||
//
|
||||
// public func input(data: Data) throws {
|
||||
// var data = data
|
||||
//
|
||||
// if readIV == nil {
|
||||
// buffer.append(data: data)
|
||||
// readIV = buffer.get(length: ivLength)
|
||||
// guard readIV != nil else {
|
||||
// try inputStreamProcessor!.input(data: Data())
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// data = buffer.get() ?? Data()
|
||||
// buffer.release()
|
||||
// }
|
||||
//
|
||||
// decrypt(data: &data)
|
||||
// try inputStreamProcessor!.input(data: data)
|
||||
// }
|
||||
//
|
||||
// public func output(data: Data) {
|
||||
// var data = data
|
||||
// encrypt(data: &data)
|
||||
// if sendKey {
|
||||
// return outputStreamProcessor!.output(data: data)
|
||||
// } else {
|
||||
// sendKey = true
|
||||
// var out = Data(capacity: data.count + writeIV.count)
|
||||
// out.append(writeIV)
|
||||
// out.append(data)
|
||||
//
|
||||
// return outputStreamProcessor!.output(data: out)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func getCrypto(_ operation: CryptoOperation) -> StreamCryptoProtocol {
|
||||
// switch algorithm {
|
||||
// case .AES128CFB, .AES192CFB, .AES256CFB:
|
||||
// switch operation {
|
||||
// case .decrypt:
|
||||
// return CCCrypto(operation: .decrypt, mode: .cfb, algorithm: .aes, initialVector: readIV, key: key)
|
||||
// case .encrypt:
|
||||
// return CCCrypto(operation: .encrypt, mode: .cfb, algorithm: .aes, initialVector: writeIV, key: key)
|
||||
// }
|
||||
// case .CHACHA20:
|
||||
// switch operation {
|
||||
// case .decrypt:
|
||||
// return SodiumStreamCrypto(key: key, iv: readIV, algorithm: .chacha20)
|
||||
// case .encrypt:
|
||||
// return SodiumStreamCrypto(key: key, iv: writeIV, algorithm: .chacha20)
|
||||
// }
|
||||
// case .SALSA20:
|
||||
// switch operation {
|
||||
// case .decrypt:
|
||||
// return SodiumStreamCrypto(key: key, iv: readIV, algorithm: .salsa20)
|
||||
// case .encrypt:
|
||||
// return SodiumStreamCrypto(key: key, iv: writeIV, algorithm: .salsa20)
|
||||
// }
|
||||
// case .RC4MD5:
|
||||
// var combinedKey = Data(capacity: key.count + ivLength)
|
||||
// combinedKey.append(key)
|
||||
// switch operation {
|
||||
// case .decrypt:
|
||||
// combinedKey.append(readIV)
|
||||
// return CCCrypto(operation: .decrypt, mode: .rc4, algorithm: .rc4, initialVector: nil, key: MD5Hash.final(combinedKey))
|
||||
// case .encrypt:
|
||||
// combinedKey.append(writeIV)
|
||||
// return CCCrypto(operation: .encrypt, mode: .rc4, algorithm: .rc4, initialVector: nil, key: MD5Hash.final(combinedKey))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -1,371 +0,0 @@
|
||||
//import Foundation
|
||||
//
|
||||
//extension ShadowsocksAdapter {
|
||||
// public struct ProtocolObfuscater {
|
||||
// public class Factory {
|
||||
// public init() {}
|
||||
//
|
||||
// public func build() -> ProtocolObfuscaterBase {
|
||||
// return ProtocolObfuscaterBase()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public class ProtocolObfuscaterBase {
|
||||
// public weak var inputStreamProcessor: CryptoStreamProcessor!
|
||||
// public weak var outputStreamProcessor: ShadowsocksAdapter!
|
||||
//
|
||||
// public func start() {}
|
||||
// public func input(data: Data) throws {}
|
||||
// public func output(data: Data) {}
|
||||
//
|
||||
// public func didWrite() {}
|
||||
// }
|
||||
//
|
||||
// public class OriginProtocolObfuscater: ProtocolObfuscaterBase {
|
||||
//
|
||||
// public class Factory: ProtocolObfuscater.Factory {
|
||||
// public override init() {}
|
||||
//
|
||||
// public override func build() -> ShadowsocksAdapter.ProtocolObfuscater.ProtocolObfuscaterBase {
|
||||
// return OriginProtocolObfuscater()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public override func start() {
|
||||
// outputStreamProcessor.becomeReadyToForward()
|
||||
// }
|
||||
//
|
||||
// public override func input(data: Data) throws {
|
||||
// try inputStreamProcessor.input(data: data)
|
||||
// }
|
||||
//
|
||||
// public override func output(data: Data) {
|
||||
// outputStreamProcessor.output(data: data)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public class HTTPProtocolObfuscater: ProtocolObfuscaterBase {
|
||||
//
|
||||
// public class Factory: ProtocolObfuscater.Factory {
|
||||
// let method: String
|
||||
// let hosts: [String]
|
||||
// let customHeader: String?
|
||||
//
|
||||
// public init(method: String = "GET", hosts: [String], customHeader: String?) {
|
||||
// self.method = method
|
||||
// self.hosts = hosts
|
||||
// self.customHeader = customHeader
|
||||
// }
|
||||
//
|
||||
// public override func build() -> ShadowsocksAdapter.ProtocolObfuscater.ProtocolObfuscaterBase {
|
||||
// return HTTPProtocolObfuscater(method: method, hosts: hosts, customHeader: customHeader)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// static let headerLength = 30
|
||||
// static let userAgent = ["Mozilla/5.0 (Windows NT 6.3; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0",
|
||||
// "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:40.0) Gecko/20100101 Firefox/44.0",
|
||||
// "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36",
|
||||
// "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/11.10 Chromium/27.0.1453.93 Chrome/27.0.1453.93 Safari/537.36",
|
||||
// "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:35.0) Gecko/20100101 Firefox/35.0",
|
||||
// "Mozilla/5.0 (compatible; WOW64; MSIE 10.0; Windows NT 6.2)",
|
||||
// "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27",
|
||||
// "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.3; Trident/7.0; .NET4.0E; .NET4.0C)",
|
||||
// "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko",
|
||||
// "Mozilla/5.0 (Linux; Android 4.4; Nexus 5 Build/BuildID) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Mobile Safari/537.36",
|
||||
// "Mozilla/5.0 (iPad; CPU OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3",
|
||||
// "Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3"]
|
||||
//
|
||||
// let method: String
|
||||
// let hosts: [String]
|
||||
// let customHeader: String?
|
||||
//
|
||||
// var readingFakeHeader = false
|
||||
// var sendHeader = false
|
||||
// var remaining = false
|
||||
//
|
||||
// var buffer = Buffer(capacity: 8192)
|
||||
//
|
||||
// public init(method: String = "GET", hosts: [String], customHeader: String?) {
|
||||
// self.method = method
|
||||
// self.hosts = hosts
|
||||
// self.customHeader = customHeader
|
||||
// }
|
||||
//
|
||||
// private func generateHeader(encapsulating data: Data) -> String {
|
||||
// let ind = Int(arc4random_uniform(UInt32(hosts.count)))
|
||||
// let host = outputStreamProcessor.port == 80 ? hosts[ind] : "\(hosts[ind]):\(outputStreamProcessor.port)"
|
||||
// var header = "\(method) /\(hexlify(data: data)) HTTP/1.1\r\nHost: \(host)\r\n"
|
||||
// if let customHeader = customHeader {
|
||||
// header += customHeader
|
||||
// } else {
|
||||
// let ind = Int(arc4random_uniform(UInt32(HTTPProtocolObfuscater.userAgent.count)))
|
||||
// header += "User-Agent: \(HTTPProtocolObfuscater.userAgent[ind])\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Encoding: gzip, deflate\r\nDNT: 1\r\nConnection: keep-alive"
|
||||
// }
|
||||
// header += "\r\n\r\n"
|
||||
// return header
|
||||
// }
|
||||
//
|
||||
// private func hexlify(data: Data) -> String {
|
||||
// var result = ""
|
||||
// for i in data {
|
||||
// result = result.appendingFormat("%%%02x", i)
|
||||
// }
|
||||
// return result
|
||||
// }
|
||||
//
|
||||
// public override func start() {
|
||||
// readingFakeHeader = true
|
||||
// outputStreamProcessor.becomeReadyToForward()
|
||||
// }
|
||||
//
|
||||
// public override func input(data: Data) throws {
|
||||
// if readingFakeHeader {
|
||||
// buffer.append(data: data)
|
||||
// if buffer.get(to: Utils.HTTPData.DoubleCRLF) != nil {
|
||||
// readingFakeHeader = false
|
||||
// if let remainData = buffer.get() {
|
||||
// try inputStreamProcessor.input(data: remainData)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// try inputStreamProcessor.input(data: Data())
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// try inputStreamProcessor.input(data: data)
|
||||
// }
|
||||
//
|
||||
// public override func output(data: Data) {
|
||||
// if sendHeader {
|
||||
// outputStreamProcessor.output(data: data)
|
||||
// } else {
|
||||
// var fakeRequestDataLength = inputStreamProcessor.key.count + HTTPProtocolObfuscater.headerLength
|
||||
// if data.count - fakeRequestDataLength > 64 {
|
||||
// fakeRequestDataLength += Int(arc4random_uniform(64))
|
||||
// } else {
|
||||
// fakeRequestDataLength = data.count
|
||||
// }
|
||||
//
|
||||
// var outputData = generateHeader(encapsulating: data.subdata(in: 0 ..< fakeRequestDataLength)).data(using: .utf8)!
|
||||
// outputData.append(data.subdata(in: fakeRequestDataLength ..< data.count))
|
||||
// sendHeader = true
|
||||
// outputStreamProcessor.output(data: outputData)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public class TLSProtocolObfuscater: ProtocolObfuscaterBase {
|
||||
//
|
||||
// public class Factory: ProtocolObfuscater.Factory {
|
||||
// let hosts: [String]
|
||||
//
|
||||
// public init(hosts: [String]) {
|
||||
// self.hosts = hosts
|
||||
// }
|
||||
//
|
||||
// public override func build() -> ShadowsocksAdapter.ProtocolObfuscater.ProtocolObfuscaterBase {
|
||||
// return TLSProtocolObfuscater(hosts: hosts)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// let hosts: [String]
|
||||
// let clientID: Data = {
|
||||
// var id = Data(count: 32)
|
||||
// Utils.Random.fill(data: &id)
|
||||
// return id
|
||||
// }()
|
||||
//
|
||||
// private var status = 0
|
||||
//
|
||||
// private var buffer = Buffer(capacity: 1024)
|
||||
//
|
||||
// init(hosts: [String]) {
|
||||
// self.hosts = hosts
|
||||
// }
|
||||
//
|
||||
// public override func start() {
|
||||
// handleStatus0()
|
||||
// outputStreamProcessor.socket.readDataTo(length: 129)
|
||||
// }
|
||||
//
|
||||
// public override func input(data: Data) throws {
|
||||
// switch status {
|
||||
// case 8:
|
||||
// try handleInput(data: data)
|
||||
// case 1:
|
||||
// outputStreamProcessor.becomeReadyToForward()
|
||||
// default:
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public override func output(data: Data) {
|
||||
// switch status {
|
||||
// case 8:
|
||||
// handleStatus8(data: data)
|
||||
// return
|
||||
// case 1:
|
||||
// handleStatus1(data: data)
|
||||
// return
|
||||
// default:
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func authData() -> Data {
|
||||
// var time = UInt32(Date.init().timeIntervalSince1970).bigEndian
|
||||
// var output = Data(count: 32)
|
||||
// var key = inputStreamProcessor.key
|
||||
// key.append(clientID)
|
||||
//
|
||||
// withUnsafeBytes(of: &time) {
|
||||
// output.replaceSubrange(0 ..< 4, with: $0)
|
||||
// }
|
||||
//
|
||||
// Utils.Random.fill(data: &output, from: 4, length: 18)
|
||||
// output.withUnsafeBytes {
|
||||
// output.replaceSubrange(22 ..< 32, with: HMAC.final(value: $0.baseAddress!, length: 22, algorithm: .SHA1, key: key).subdata(in: 0..<10))
|
||||
// }
|
||||
// return output
|
||||
// }
|
||||
//
|
||||
// private func pack(data: Data) -> Data {
|
||||
// var output = Data()
|
||||
// var left = data.count
|
||||
// while left > 0 {
|
||||
// let blockSize = UInt16(min(Int(arc4random_uniform(UInt32(UInt16.max))) % 4096 + 100, left))
|
||||
// var blockSizeBE = blockSize.bigEndian
|
||||
// output.append(contentsOf: [0x17, 0x03, 0x03])
|
||||
// withUnsafeBytes(of: &blockSizeBE) {
|
||||
// output.append($0.baseAddress!.assumingMemoryBound(to: UInt8.self), count: $0.count)
|
||||
// }
|
||||
// output.append(data.subdata(in: data.count - left ..< data.count - left + Int(blockSize)))
|
||||
// left -= Int(blockSize)
|
||||
// }
|
||||
// return output
|
||||
// }
|
||||
//
|
||||
// private func handleStatus8(data: Data) {
|
||||
// outputStreamProcessor.output(data: pack(data: data))
|
||||
// }
|
||||
//
|
||||
// private func handleStatus0() {
|
||||
// status = 1
|
||||
//
|
||||
// var outData = Data()
|
||||
// outData.append(contentsOf: [0x03, 0x03])
|
||||
// outData.append(authData())
|
||||
// outData.append(0x20)
|
||||
// outData.append(clientID)
|
||||
// outData.append(contentsOf: [0x00, 0x1c, 0xc0, 0x2b, 0xc0, 0x2f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13, 0xc0, 0x0a, 0xc0, 0x14, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x9c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0x0a])
|
||||
// outData.append("0100".data(using: .utf8)!)
|
||||
//
|
||||
// var extData = Data()
|
||||
// extData.append(contentsOf: [0xff, 0x01, 0x00, 0x01, 0x00])
|
||||
// let hostData = hosts[Int(arc4random_uniform(UInt32(hosts.count)))].data(using: .utf8)!
|
||||
//
|
||||
// var sniData = Data(capacity: hosts.count + 2 + 1 + 2 + 2 + 2)
|
||||
//
|
||||
// sniData.append(contentsOf: [0x00, 0x00])
|
||||
//
|
||||
// var _lenBE = UInt16(hostData.count + 5).bigEndian
|
||||
// withUnsafeBytes(of: &_lenBE) {
|
||||
// sniData.append($0.baseAddress!.assumingMemoryBound(to: UInt8.self), count: $0.count)
|
||||
// }
|
||||
//
|
||||
// _lenBE = UInt16(hostData.count + 3).bigEndian
|
||||
// withUnsafeBytes(of: &_lenBE) {
|
||||
// sniData.append($0.baseAddress!.assumingMemoryBound(to: UInt8.self), count: $0.count)
|
||||
// }
|
||||
//
|
||||
// sniData.append(0x00)
|
||||
//
|
||||
// _lenBE = UInt16(hostData.count).bigEndian
|
||||
// withUnsafeBytes(of: &_lenBE) {
|
||||
// sniData.append($0.baseAddress!.assumingMemoryBound(to: UInt8.self), count: $0.count)
|
||||
// }
|
||||
//
|
||||
// sniData.append(hostData)
|
||||
//
|
||||
// extData.append(sniData)
|
||||
//
|
||||
// extData.append(contentsOf: [0x00, 0x17, 0x00, 0x00, 0x00, 0x23, 0x00, 0xd0])
|
||||
//
|
||||
// var randomData = Data(count: 208)
|
||||
// Utils.Random.fill(data: &randomData)
|
||||
// extData.append(randomData)
|
||||
//
|
||||
// extData.append(contentsOf: [0x00, 0x0d, 0x00, 0x16, 0x00, 0x14, 0x06, 0x01, 0x06, 0x03, 0x05, 0x01, 0x05, 0x03, 0x04, 0x01, 0x04, 0x03, 0x03, 0x01, 0x03, 0x03, 0x02, 0x01, 0x02, 0x03])
|
||||
// extData.append(contentsOf: [0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00])
|
||||
// extData.append(contentsOf: [0x00, 0x12, 0x00, 0x00])
|
||||
// extData.append(contentsOf: [0x75, 0x50, 0x00, 0x00])
|
||||
// extData.append(contentsOf: [0x00, 0x0b, 0x00, 0x02, 0x01, 0x00])
|
||||
// extData.append(contentsOf: [0x00, 0x0a, 0x00, 0x06, 0x00, 0x04, 0x00, 0x17, 0x00, 0x18])
|
||||
//
|
||||
// _lenBE = UInt16(extData.count).bigEndian
|
||||
// withUnsafeBytes(of: &_lenBE) {
|
||||
// outData.append($0.baseAddress!.assumingMemoryBound(to: UInt8.self), count: $0.count)
|
||||
// }
|
||||
// outData.append(extData)
|
||||
//
|
||||
// var outputData = Data(capacity: outData.count + 9)
|
||||
// outputData.append(contentsOf: [0x16, 0x03, 0x01])
|
||||
// _lenBE = UInt16(outData.count + 4).bigEndian
|
||||
// withUnsafeBytes(of: &_lenBE) {
|
||||
// outputData.append($0.baseAddress!.assumingMemoryBound(to: UInt8.self), count: $0.count)
|
||||
// }
|
||||
// outputData.append(contentsOf: [0x01, 0x00])
|
||||
// _lenBE = UInt16(outData.count).bigEndian
|
||||
// withUnsafeBytes(of: &_lenBE) {
|
||||
// outputData.append($0.baseAddress!.assumingMemoryBound(to: UInt8.self), count: $0.count)
|
||||
// }
|
||||
// outputData.append(outData)
|
||||
// outputStreamProcessor.output(data: outputData)
|
||||
// }
|
||||
//
|
||||
// private func handleStatus1(data: Data) {
|
||||
// status = 8
|
||||
//
|
||||
// var outputData = Data()
|
||||
// outputData.append(contentsOf: [0x14, 0x03, 0x03, 0x00, 0x01, 0x01, 0x16, 0x03, 0x03, 0x00, 0x20])
|
||||
// var random = Data(count: 22)
|
||||
// Utils.Random.fill(data: &random)
|
||||
// outputData.append(random)
|
||||
//
|
||||
// var key = inputStreamProcessor.key
|
||||
// key.append(clientID)
|
||||
// outputData.withUnsafeBytes {
|
||||
// outputData.append(HMAC.final(value: $0.baseAddress!, length: outputData.count, algorithm: .SHA1, key: key).subdata(in: 0..<10))
|
||||
// }
|
||||
//
|
||||
// outputData.append(pack(data: data))
|
||||
//
|
||||
// outputStreamProcessor.output(data: outputData)
|
||||
// }
|
||||
//
|
||||
// private func handleInput(data: Data) throws {
|
||||
// buffer.append(data: data)
|
||||
// var unpackedData = Data()
|
||||
// while buffer.left > 5 {
|
||||
// buffer.skip(3)
|
||||
// var length: Int = 0
|
||||
// buffer.withUnsafeBytes { (ptr: UnsafePointer<UInt16>) in
|
||||
// length = Int(ptr.pointee.byteSwapped)
|
||||
// }
|
||||
// buffer.skip(2)
|
||||
// if buffer.left >= length {
|
||||
// unpackedData.append(buffer.get(length: length)!)
|
||||
// continue
|
||||
// } else {
|
||||
// buffer.setBack(length: 5)
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// buffer.squeeze()
|
||||
// try inputStreamProcessor.input(data: unpackedData)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//}
|
||||
@@ -1,112 +0,0 @@
|
||||
//import Foundation
|
||||
//import CommonCrypto
|
||||
//
|
||||
///// This adapter connects to remote through Shadowsocks proxy.
|
||||
//public class ShadowsocksAdapter: AdapterSocket {
|
||||
// enum ShadowsocksAdapterStatus {
|
||||
// case invalid,
|
||||
// connecting,
|
||||
// connected,
|
||||
// forwarding,
|
||||
// stopped
|
||||
// }
|
||||
//
|
||||
// enum EncryptMethod: String {
|
||||
// case AES128CFB = "AES-128-CFB", AES192CFB = "AES-192-CFB", AES256CFB = "AES-256-CFB"
|
||||
//
|
||||
// static let allValues: [EncryptMethod] = [.AES128CFB, .AES192CFB, .AES256CFB]
|
||||
// }
|
||||
//
|
||||
// public let host: String
|
||||
// public let port: Int
|
||||
//
|
||||
// var internalStatus: ShadowsocksAdapterStatus = .invalid
|
||||
//
|
||||
// private let protocolObfuscater: ProtocolObfuscater.ProtocolObfuscaterBase
|
||||
// private let cryptor: CryptoStreamProcessor
|
||||
// private let streamObfuscator: StreamObfuscater.StreamObfuscaterBase
|
||||
//
|
||||
// public init(host: String, port: Int, protocolObfuscater: ProtocolObfuscater.ProtocolObfuscaterBase, cryptor: CryptoStreamProcessor, streamObfuscator: StreamObfuscater.StreamObfuscaterBase) {
|
||||
// self.host = host
|
||||
// self.port = port
|
||||
// self.protocolObfuscater = protocolObfuscater
|
||||
// self.cryptor = cryptor
|
||||
// self.streamObfuscator = streamObfuscator
|
||||
//
|
||||
// super.init()
|
||||
//
|
||||
// protocolObfuscater.inputStreamProcessor = cryptor
|
||||
// protocolObfuscater.outputStreamProcessor = self
|
||||
//
|
||||
// cryptor.inputStreamProcessor = streamObfuscator
|
||||
// cryptor.outputStreamProcessor = protocolObfuscater
|
||||
//
|
||||
// streamObfuscator.inputStreamProcessor = self
|
||||
// streamObfuscator.outputStreamProcessor = cryptor
|
||||
// }
|
||||
//
|
||||
// override public func openSocketWith(session: ConnectSession) {
|
||||
// super.openSocketWith(session: session)
|
||||
//
|
||||
// do {
|
||||
// internalStatus = .connecting
|
||||
// try socket.connectTo(host: host, port: port, enableTLS: false, tlsSettings: nil)
|
||||
// } catch let error {
|
||||
// observer?.signal(.errorOccured(error, on: self))
|
||||
// disconnect()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override public func didConnectWith(socket: RawTCPSocketProtocol) {
|
||||
// super.didConnectWith(socket: socket)
|
||||
//
|
||||
// internalStatus = .connected
|
||||
//
|
||||
// protocolObfuscater.start()
|
||||
// }
|
||||
//
|
||||
// override public func didRead(data: Data, from socket: RawTCPSocketProtocol) {
|
||||
// super.didRead(data: data, from: socket)
|
||||
//
|
||||
// do {
|
||||
// try protocolObfuscater.input(data: data)
|
||||
// } catch {
|
||||
// disconnect()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public override func write(data: Data) {
|
||||
// streamObfuscator.output(data: data)
|
||||
// }
|
||||
//
|
||||
// public func write(rawData: Data) {
|
||||
// super.write(data: rawData)
|
||||
// }
|
||||
//
|
||||
// public func input(data: Data) {
|
||||
// delegate?.didRead(data: data, from: self)
|
||||
// }
|
||||
//
|
||||
// public func output(data: Data) {
|
||||
// write(rawData: data)
|
||||
// }
|
||||
//
|
||||
// override public func didWrite(data: Data?, by socket: RawTCPSocketProtocol) {
|
||||
// super.didWrite(data: data, by: socket)
|
||||
//
|
||||
// protocolObfuscater.didWrite()
|
||||
//
|
||||
// switch internalStatus {
|
||||
// case .forwarding:
|
||||
// delegate?.didWrite(data: data, by: self)
|
||||
// default:
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func becomeReadyToForward() {
|
||||
// internalStatus = .forwarding
|
||||
// observer?.signal(.readyForForward(self))
|
||||
// delegate?.didBecomeReadyToForwardWith(socket: self)
|
||||
// }
|
||||
//}
|
||||
@@ -1,167 +0,0 @@
|
||||
//import Foundation
|
||||
//
|
||||
//extension ShadowsocksAdapter {
|
||||
// public struct StreamObfuscater {
|
||||
// public class Factory {
|
||||
// public init() {}
|
||||
//
|
||||
// public func build(for session: ConnectSession) -> StreamObfuscaterBase {
|
||||
// return StreamObfuscaterBase(for: session)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public class StreamObfuscaterBase {
|
||||
// public weak var inputStreamProcessor: ShadowsocksAdapter!
|
||||
// private weak var _outputStreamProcessor: CryptoStreamProcessor!
|
||||
// public var outputStreamProcessor: CryptoStreamProcessor! {
|
||||
// get {
|
||||
// return _outputStreamProcessor
|
||||
// }
|
||||
// set {
|
||||
// _outputStreamProcessor = newValue
|
||||
// key = _outputStreamProcessor?.key
|
||||
// writeIV = _outputStreamProcessor?.writeIV
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public var key: Data?
|
||||
// public var writeIV: Data?
|
||||
//
|
||||
// let session: ConnectSession
|
||||
//
|
||||
// init(for session: ConnectSession) {
|
||||
// self.session = session
|
||||
// }
|
||||
//
|
||||
// func output(data: Data) {}
|
||||
// func input(data: Data) throws {}
|
||||
// }
|
||||
//
|
||||
// public class OriginStreamObfuscater: StreamObfuscaterBase {
|
||||
// public class Factory: StreamObfuscater.Factory {
|
||||
// public override init() {}
|
||||
//
|
||||
// public override func build(for session: ConnectSession) -> ShadowsocksAdapter.StreamObfuscater.StreamObfuscaterBase {
|
||||
// return OriginStreamObfuscater(for: session)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private var requestSend = false
|
||||
//
|
||||
// private func requestData(withData data: Data) -> Data {
|
||||
// let hostLength = session.host.utf8.count
|
||||
// let length = 1 + 1 + hostLength + 2 + data.count
|
||||
// var response = Data(count: length)
|
||||
// response[0] = 3
|
||||
// response[1] = UInt8(hostLength)
|
||||
// response.replaceSubrange(2..<2+hostLength, with: session.host.utf8)
|
||||
// var beport = UInt16(session.port).bigEndian
|
||||
// withUnsafeBytes(of: &beport) {
|
||||
// response.replaceSubrange(2+hostLength..<4+hostLength, with: $0)
|
||||
// }
|
||||
// response.replaceSubrange(4+hostLength..<length, with: data)
|
||||
// return response
|
||||
// }
|
||||
//
|
||||
// public override func input(data: Data) throws {
|
||||
// inputStreamProcessor!.input(data: data)
|
||||
// }
|
||||
//
|
||||
// public override func output(data: Data) {
|
||||
// if requestSend {
|
||||
// return outputStreamProcessor!.output(data: data)
|
||||
// } else {
|
||||
// requestSend = true
|
||||
// return outputStreamProcessor!.output(data: requestData(withData: data))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public class OTAStreamObfuscater: StreamObfuscaterBase {
|
||||
// public class Factory: StreamObfuscater.Factory {
|
||||
// public override init() {}
|
||||
//
|
||||
// public override func build(for session: ConnectSession) -> ShadowsocksAdapter.StreamObfuscater.StreamObfuscaterBase {
|
||||
// return OTAStreamObfuscater(for: session)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private var count: UInt32 = 0
|
||||
//
|
||||
// private let DATA_BLOCK_SIZE = 0xFFFF - 12
|
||||
//
|
||||
// private var requestSend = false
|
||||
//
|
||||
// private func requestData() -> Data {
|
||||
// var response: [UInt8] = [0x13]
|
||||
// response.append(UInt8(session.host.utf8.count))
|
||||
// response += [UInt8](session.host.utf8)
|
||||
// response += [UInt8](Utils.toByteArray(UInt16(session.port)).reversed())
|
||||
// var responseData = Data(bytes: UnsafePointer<UInt8>(response), count: response.count)
|
||||
// var keyiv = Data(count: key!.count + writeIV!.count)
|
||||
//
|
||||
// keyiv.replaceSubrange(0..<writeIV!.count, with: writeIV!)
|
||||
// keyiv.replaceSubrange(writeIV!.count..<writeIV!.count + key!.count, with: key!)
|
||||
// responseData.append(HMAC.final(value: responseData, algorithm: .SHA1, key: keyiv).subdata(in: 0..<10))
|
||||
// return responseData
|
||||
// }
|
||||
//
|
||||
// public override func input(data: Data) throws {
|
||||
// inputStreamProcessor!.input(data: data)
|
||||
// }
|
||||
//
|
||||
// public override func output(data: Data) {
|
||||
// let fullBlockCount = data.count / DATA_BLOCK_SIZE
|
||||
// var outputSize = fullBlockCount * (DATA_BLOCK_SIZE + 10 + 2)
|
||||
// if data.count > fullBlockCount * DATA_BLOCK_SIZE {
|
||||
// outputSize += data.count - fullBlockCount * DATA_BLOCK_SIZE + 10 + 2
|
||||
// }
|
||||
//
|
||||
// let _requestData: Data = requestData()
|
||||
// if !requestSend {
|
||||
// outputSize += _requestData.count
|
||||
// }
|
||||
//
|
||||
// var outputData = Data(count: outputSize)
|
||||
// var outputOffset = 0
|
||||
// var dataOffset = 0
|
||||
//
|
||||
// if !requestSend {
|
||||
// requestSend = true
|
||||
// outputData.replaceSubrange(0..<_requestData.count, with: _requestData)
|
||||
// outputOffset += _requestData.count
|
||||
// }
|
||||
//
|
||||
// while outputOffset != outputSize {
|
||||
// let blockLength = min(data.count - dataOffset, DATA_BLOCK_SIZE)
|
||||
// var len = UInt16(blockLength).bigEndian
|
||||
// withUnsafeBytes(of: &len) {
|
||||
// outputData.replaceSubrange(outputOffset..<outputOffset+2, with: $0)
|
||||
// }
|
||||
//
|
||||
// var kc = Data(count: writeIV!.count + MemoryLayout.size(ofValue: count))
|
||||
// kc.replaceSubrange(0..<writeIV!.count, with: writeIV!)
|
||||
// var c = count.bigEndian
|
||||
// let ms = MemoryLayout.size(ofValue: c)
|
||||
// withUnsafeBytes(of: &c) {
|
||||
// kc.replaceSubrange(writeIV!.count..<writeIV!.count+ms, with: $0)
|
||||
// }
|
||||
//
|
||||
// data.withUnsafeBytes {
|
||||
// outputData.replaceSubrange(outputOffset+2..<outputOffset+12, with: HMAC.final(value: $0.baseAddress!.advanced(by: dataOffset), length: blockLength, algorithm: .SHA1, key: kc).subdata(in: 0..<10))
|
||||
// }
|
||||
//
|
||||
// data.withUnsafeBytes {
|
||||
// outputData.replaceSubrange(outputOffset+12..<outputOffset+12+blockLength, with: $0.baseAddress!.advanced(by: dataOffset), count: blockLength)
|
||||
// }
|
||||
//
|
||||
// count += 1
|
||||
// outputOffset += 12 + blockLength
|
||||
// dataOffset += blockLength
|
||||
// }
|
||||
//
|
||||
// return outputStreamProcessor!.output(data: outputData)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -1,113 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// This class just forwards data directly.
|
||||
/// - note: It is designed to work with tun2socks only.
|
||||
public class DirectProxySocket: ProxySocket {
|
||||
enum DirectProxyReadStatus: CustomStringConvertible {
|
||||
case invalid,
|
||||
forwarding,
|
||||
stopped
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .invalid:
|
||||
return "invalid"
|
||||
case .forwarding:
|
||||
return "forwarding"
|
||||
case .stopped:
|
||||
return "stopped"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum DirectProxyWriteStatus {
|
||||
case invalid,
|
||||
forwarding,
|
||||
stopped
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .invalid:
|
||||
return "invalid"
|
||||
case .forwarding:
|
||||
return "forwarding"
|
||||
case .stopped:
|
||||
return "stopped"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var readStatus: DirectProxyReadStatus = .invalid
|
||||
private var writeStatus: DirectProxyWriteStatus = .invalid
|
||||
|
||||
public var readStatusDescription: String {
|
||||
return readStatus.description
|
||||
}
|
||||
|
||||
public var writeStatusDescription: String {
|
||||
return writeStatus.description
|
||||
}
|
||||
|
||||
/**
|
||||
Begin reading and processing data from the socket.
|
||||
|
||||
- note: Since there is nothing to read and process before forwarding data, this just calls `delegate?.didReceiveRequest`.
|
||||
*/
|
||||
override public func openSocket() {
|
||||
super.openSocket()
|
||||
|
||||
guard !isCancelled else {
|
||||
return
|
||||
}
|
||||
|
||||
if let address = socket.destinationIPAddress, let port = socket.destinationPort {
|
||||
session = ConnectSession(host: address.presentation, port: Int(port.value))
|
||||
|
||||
observer?.signal(.receivedRequest(session!, on: self))
|
||||
delegate?.didReceive(session: session!, from: self)
|
||||
} else {
|
||||
forceDisconnect()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Response to the `AdapterSocket` on the other side of the `Tunnel` which has succefully connected to the remote server.
|
||||
|
||||
- parameter adapter: The `AdapterSocket`.
|
||||
*/
|
||||
override public func respondTo(adapter: AdapterSocket) {
|
||||
super.respondTo(adapter: adapter)
|
||||
|
||||
guard !isCancelled else {
|
||||
return
|
||||
}
|
||||
|
||||
readStatus = .forwarding
|
||||
writeStatus = .forwarding
|
||||
|
||||
observer?.signal(.readyForForward(self))
|
||||
delegate?.didBecomeReadyToForwardWith(socket: self)
|
||||
}
|
||||
|
||||
/**
|
||||
The socket did read some data.
|
||||
|
||||
- parameter data: The data read from the socket.
|
||||
- parameter from: The socket where the data is read from.
|
||||
*/
|
||||
override open func didRead(data: Data, from: RawTCPSocketProtocol) {
|
||||
super.didRead(data: data, from: from)
|
||||
delegate?.didRead(data: data, from: self)
|
||||
}
|
||||
|
||||
/**
|
||||
The socket did send some data.
|
||||
|
||||
- parameter data: The data which have been sent to remote (acknowledged). Note this may not be available since the data may be released to save memory.
|
||||
- parameter by: The socket where the data is sent out.
|
||||
*/
|
||||
override open func didWrite(data: Data?, by: RawTCPSocketProtocol) {
|
||||
super.didWrite(data: data, by: by)
|
||||
delegate?.didWrite(data: data, by: self)
|
||||
}
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public class SOCKS5ProxySocket: ProxySocket {
|
||||
enum SOCKS5ProxyReadStatus: CustomStringConvertible {
|
||||
case invalid,
|
||||
readingVersionIdentifierAndNumberOfMethods,
|
||||
readingMethods,
|
||||
readingConnectHeader,
|
||||
readingIPv4Address,
|
||||
readingDomainLength,
|
||||
readingDomain,
|
||||
readingIPv6Address,
|
||||
readingPort,
|
||||
forwarding,
|
||||
stopped
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .invalid:
|
||||
return "invalid"
|
||||
case .readingVersionIdentifierAndNumberOfMethods:
|
||||
return "reading version and methods"
|
||||
case .readingMethods:
|
||||
return "reading methods"
|
||||
case .readingConnectHeader:
|
||||
return "reading connect header"
|
||||
case .readingIPv4Address:
|
||||
return "IPv4 address"
|
||||
case .readingDomainLength:
|
||||
return "domain length"
|
||||
case .readingDomain:
|
||||
return "domain"
|
||||
case .readingIPv6Address:
|
||||
return "IPv6 address"
|
||||
case .readingPort:
|
||||
return "reading port"
|
||||
case .forwarding:
|
||||
return "forwarding"
|
||||
case .stopped:
|
||||
return "stopped"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SOCKS5ProxyWriteStatus: CustomStringConvertible {
|
||||
case invalid,
|
||||
sendingResponse,
|
||||
forwarding,
|
||||
stopped
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .invalid:
|
||||
return "invalid"
|
||||
case .sendingResponse:
|
||||
return "sending response"
|
||||
case .forwarding:
|
||||
return "forwarding"
|
||||
case .stopped:
|
||||
return "stopped"
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The remote host to connect to.
|
||||
public var destinationHost: String!
|
||||
|
||||
/// The remote port to connect to.
|
||||
public var destinationPort: Int!
|
||||
|
||||
private var readStatus: SOCKS5ProxyReadStatus = .invalid
|
||||
private var writeStatus: SOCKS5ProxyWriteStatus = .invalid
|
||||
|
||||
public var readStatusDescription: String {
|
||||
return readStatus.description
|
||||
}
|
||||
|
||||
public var writeStatusDescription: String {
|
||||
return writeStatus.description
|
||||
}
|
||||
|
||||
/**
|
||||
Begin reading and processing data from the socket.
|
||||
*/
|
||||
override public func openSocket() {
|
||||
super.openSocket()
|
||||
|
||||
guard !isCancelled else {
|
||||
return
|
||||
}
|
||||
|
||||
readStatus = .readingVersionIdentifierAndNumberOfMethods
|
||||
socket.readDataTo(length: 2)
|
||||
}
|
||||
|
||||
// swiftlint:disable function_body_length
|
||||
// swiftlint:disable cyclomatic_complexity
|
||||
/**
|
||||
The socket did read some data.
|
||||
|
||||
- parameter data: The data read from the socket.
|
||||
- parameter from: The socket where the data is read from.
|
||||
*/
|
||||
override public func didRead(data: Data, from: RawTCPSocketProtocol) {
|
||||
super.didRead(data: data, from: from)
|
||||
|
||||
switch readStatus {
|
||||
case .forwarding:
|
||||
delegate?.didRead(data: data, from: self)
|
||||
case .readingVersionIdentifierAndNumberOfMethods:
|
||||
data.withUnsafeBytes { pointer in
|
||||
let p = pointer.bindMemory(to: Int8.self)
|
||||
|
||||
guard p.baseAddress!.pointee == 5 else {
|
||||
// TODO: notify observer
|
||||
self.disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
guard p.baseAddress!.successor().pointee > 0 else {
|
||||
// TODO: notify observer
|
||||
self.disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
self.readStatus = .readingMethods
|
||||
self.socket.readDataTo(length: Int(p.baseAddress!.successor().pointee))
|
||||
}
|
||||
case .readingMethods:
|
||||
// TODO: check for 0x00 in read data
|
||||
|
||||
let response = Data([0x05, 0x00])
|
||||
// we would not be able to read anything before the data is written out, so no need to handle the dataWrote event.
|
||||
write(data: response)
|
||||
readStatus = .readingConnectHeader
|
||||
socket.readDataTo(length: 4)
|
||||
case .readingConnectHeader:
|
||||
data.withUnsafeBytes { pointer in
|
||||
let p = pointer.bindMemory(to: Int8.self)
|
||||
|
||||
guard p.baseAddress!.pointee == 5 && p.baseAddress!.successor().pointee == 1 else {
|
||||
// TODO: notify observer
|
||||
self.disconnect()
|
||||
return
|
||||
}
|
||||
switch p.baseAddress!.advanced(by: 3).pointee {
|
||||
case 1:
|
||||
readStatus = .readingIPv4Address
|
||||
socket.readDataTo(length: 4)
|
||||
case 3:
|
||||
readStatus = .readingDomainLength
|
||||
socket.readDataTo(length: 1)
|
||||
case 4:
|
||||
readStatus = .readingIPv6Address
|
||||
socket.readDataTo(length: 16)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
case .readingIPv4Address:
|
||||
var address = Data(count: Int(INET_ADDRSTRLEN))
|
||||
_ = data.withUnsafeBytes { data_ptr in
|
||||
address.withUnsafeMutableBytes { addr_ptr in
|
||||
inet_ntop(AF_INET, data_ptr.baseAddress!, addr_ptr.bindMemory(to: Int8.self).baseAddress!, socklen_t(INET_ADDRSTRLEN))
|
||||
}
|
||||
}
|
||||
|
||||
destinationHost = String(data: address, encoding: .utf8)
|
||||
|
||||
readStatus = .readingPort
|
||||
socket.readDataTo(length: 2)
|
||||
case .readingIPv6Address:
|
||||
var address = Data(count: Int(INET6_ADDRSTRLEN))
|
||||
_ = data.withUnsafeBytes { data_ptr in
|
||||
address.withUnsafeMutableBytes { addr_ptr in
|
||||
inet_ntop(AF_INET6, data_ptr.baseAddress!, addr_ptr.bindMemory(to: Int8.self).baseAddress!, socklen_t(INET6_ADDRSTRLEN))
|
||||
}
|
||||
}
|
||||
|
||||
destinationHost = String(data: address, encoding: .utf8)
|
||||
|
||||
readStatus = .readingPort
|
||||
socket.readDataTo(length: 2)
|
||||
case .readingDomainLength:
|
||||
readStatus = .readingDomain
|
||||
socket.readDataTo(length: Int(data.first!))
|
||||
case .readingDomain:
|
||||
destinationHost = String(data: data, encoding: .utf8)
|
||||
readStatus = .readingPort
|
||||
socket.readDataTo(length: 2)
|
||||
case .readingPort:
|
||||
data.withUnsafeBytes {
|
||||
destinationPort = Int($0.load(as: UInt16.self).bigEndian)
|
||||
}
|
||||
|
||||
readStatus = .forwarding
|
||||
session = ConnectSession(host: destinationHost, port: destinationPort)
|
||||
observer?.signal(.receivedRequest(session!, on: self))
|
||||
delegate?.didReceive(session: session!, from: self)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
The socket did send some data.
|
||||
|
||||
- parameter data: The data which have been sent to remote (acknowledged). Note this may not be available since the data may be released to save memory.
|
||||
- parameter from: The socket where the data is sent out.
|
||||
*/
|
||||
override public func didWrite(data: Data?, by: RawTCPSocketProtocol) {
|
||||
super.didWrite(data: data, by: by)
|
||||
|
||||
switch writeStatus {
|
||||
case .forwarding:
|
||||
delegate?.didWrite(data: data, by: self)
|
||||
case .sendingResponse:
|
||||
writeStatus = .forwarding
|
||||
observer?.signal(.readyForForward(self))
|
||||
delegate?.didBecomeReadyToForwardWith(socket: self)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Response to the `AdapterSocket` on the other side of the `Tunnel` which has succefully connected to the remote server.
|
||||
|
||||
- parameter adapter: The `AdapterSocket`.
|
||||
*/
|
||||
override public func respondTo(adapter: AdapterSocket) {
|
||||
super.respondTo(adapter: adapter)
|
||||
|
||||
guard !isCancelled else {
|
||||
return
|
||||
}
|
||||
|
||||
var responseBytes = [UInt8](repeating: 0, count: 10)
|
||||
responseBytes[0...3] = [0x05, 0x00, 0x00, 0x01]
|
||||
let responseData = Data(responseBytes)
|
||||
|
||||
writeStatus = .sendingResponse
|
||||
write(data: responseData)
|
||||
}
|
||||
}
|
||||
@@ -170,9 +170,7 @@ public class Tunnel: NSObject, SocketDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
let manager = RuleManager.currentManager
|
||||
let factory = manager.match(session)!
|
||||
adapterSocket = factory.getAdapterFor(session: session)
|
||||
adapterSocket = DirectAdapterFactory().getAdapterFor(session: session)
|
||||
adapterSocket!.delegate = self
|
||||
adapterSocket!.openSocketWith(session: session)
|
||||
}
|
||||
|
||||
9
GlassVPN/zhuhaow-Resolver/License
Normal file
@@ -0,0 +1,9 @@
|
||||
The MIT License
|
||||
|
||||
Copyright 2018 Zhuhao Wang
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
57
README.md
Normal file
@@ -0,0 +1,57 @@
|
||||
AppCheck – Privacy Monitor
|
||||
==========================
|
||||
|
||||
A pocket DNS monitor and network filter.
|
||||
|
||||

|
||||
|
||||
|
||||
## What is it?
|
||||
|
||||
AppCheck helps you identify which applications communicate with third parties.
|
||||
It does so by logging 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 – unless you choose to.
|
||||
|
||||
Join [Testflight beta](https://testflight.apple.com/join/9jjaFeHO)
|
||||
|
||||
|
||||
### How does it work?
|
||||
|
||||
AppCheck creates a local VPN tunnel to intercept all network connections.
|
||||
For each connection AppCheck looks into the DNS headers only, namely the domain names.
|
||||
These domain names are logged in the background while the VPN is active.
|
||||
That means, AppCheck does not have to be active in the foreground all the time.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- See outgoing (DNS) network requests in real-time
|
||||
- See history of previous connections
|
||||
- Block unwanted traffic based on domain names
|
||||
- Record app specific activity<sup>1</sup>
|
||||
- Apply logging filters (block or ignore) and display filters (specific range or last x minutes)
|
||||
- Sort results by time, name, or occurrence count
|
||||
- Context Analysis
|
||||
- What other domains occur often at the same time?
|
||||
- What happened immediately before or after the action?
|
||||
- Export results for custom analysis
|
||||
- Alert Monitor & reminder
|
||||
- Participate in privacy research
|
||||
- Contribute your results
|
||||
- See what others have unveiled
|
||||
- How much traffic does this app produce?
|
||||
|
||||
|
||||
<sup>1</sup> Due to technical limitations, recordings can not be restricted to a single application. Remember to force-quit all other applications before starting a recording.
|
||||
|
||||
|
||||
## Research Project
|
||||
|
||||
*information will be added soon™*
|
||||
|
||||
For now, go to the results page at [https://appchk.de/](https://appchk.de/).
|
||||
Btw. we are searching for [help](https://appchk.de/help/) on our ongoing research project.
|
||||
BIN
doc/screenshot.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
@@ -1,134 +1,39 @@
|
||||
import UIKit
|
||||
import NetworkExtension
|
||||
|
||||
let VPNConfigBundleIdentifier = "de.uni-bamberg.psi.AppCheck.VPN"
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
var managerVPN: NETunnelProviderManager?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
if UserDefaults.standard.bool(forKey: "kill_db") {
|
||||
UserDefaults.standard.set(false, forKey: "kill_db")
|
||||
SQLiteDatabase.destroyDatabase()
|
||||
}
|
||||
try? SQLiteDatabase.open().initScheme()
|
||||
|
||||
DBWrp.initContentOfDB()
|
||||
|
||||
loadVPN { mgr in
|
||||
self.managerVPN = mgr
|
||||
self.postVPNState()
|
||||
if let db = AppDB {
|
||||
db.initCommonScheme()
|
||||
db.initAppOnlyScheme()
|
||||
}
|
||||
NSNotification.Name.NEVPNStatusDidChange.observe(call: #selector(vpnStatusChanged(_:)), on: self)
|
||||
NotifyFilterChanged.observe(call: #selector(filterDidChange), on: self)
|
||||
|
||||
Prefs.registerDefaults()
|
||||
PrefsShared.registerDefaults()
|
||||
|
||||
#if IOS_SIMULATOR
|
||||
SimulatorVPN.load()
|
||||
#endif
|
||||
|
||||
sync.start()
|
||||
return true
|
||||
}
|
||||
|
||||
@objc private func vpnStatusChanged(_ notification: Notification) {
|
||||
postRawVPNState((notification.object as? NETunnelProviderSession)?.status ?? .invalid)
|
||||
}
|
||||
|
||||
@objc private func filterDidChange() {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
func setProxyEnabled(_ newState: Bool) {
|
||||
guard let mgr = self.managerVPN else {
|
||||
self.createNewVPN { manager in
|
||||
self.managerVPN = manager
|
||||
self.setProxyEnabled(newState)
|
||||
}
|
||||
return
|
||||
}
|
||||
let state = mgr.isEnabled && (mgr.connection.status == .connected)
|
||||
if state != newState {
|
||||
self.updateVPN({ mgr.isEnabled = true }) {
|
||||
newState ? try? mgr.connection.startVPNTunnel() : mgr.connection.stopVPNTunnel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: VPN
|
||||
|
||||
private func createNewVPN(_ success: @escaping (_ manager: NETunnelProviderManager) -> Void) {
|
||||
let mgr = NETunnelProviderManager()
|
||||
mgr.localizedDescription = "AppCheck Monitor"
|
||||
let proto = NETunnelProviderProtocol()
|
||||
proto.providerBundleIdentifier = VPNConfigBundleIdentifier
|
||||
proto.serverAddress = "127.0.0.1"
|
||||
mgr.protocolConfiguration = proto
|
||||
mgr.isEnabled = true
|
||||
mgr.saveToPreferences { error in
|
||||
guard error == nil else {
|
||||
self.postProcessedVPNState(.off)
|
||||
//ErrorAlert(error!).presentIn(self.window?.rootViewController)
|
||||
return
|
||||
}
|
||||
success(mgr)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadVPN(_ finally: @escaping (_ manager: NETunnelProviderManager?) -> Void) {
|
||||
NETunnelProviderManager.loadAllFromPreferences { managers, error in
|
||||
guard let mgrs = managers, mgrs.count > 0 else {
|
||||
finally(nil)
|
||||
return
|
||||
}
|
||||
for mgr in mgrs {
|
||||
if let proto = (mgr.protocolConfiguration as? NETunnelProviderProtocol) {
|
||||
if proto.providerBundleIdentifier == VPNConfigBundleIdentifier {
|
||||
finally(mgr)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
finally(nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateVPN(_ body: @escaping () -> Void, _ onSuccess: @escaping () -> Void) {
|
||||
self.managerVPN?.loadFromPreferences { error in
|
||||
guard error == nil else { return }
|
||||
body()
|
||||
self.managerVPN?.saveToPreferences { error in
|
||||
guard error == nil else { return }
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func postVPNState() {
|
||||
guard let mgr = self.managerVPN else {
|
||||
self.postRawVPNState(.invalid)
|
||||
return
|
||||
}
|
||||
mgr.loadFromPreferences { _ in
|
||||
self.postRawVPNState(mgr.connection.status)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
TheGreatDestroyer.deleteLogs(olderThan: PrefsShared.AutoDeleteLogsDays)
|
||||
// FIXME: Does not reflect changes performed by GlassVPN auto-delete while app is open.
|
||||
// It will update whenever app restarts or becomes active again (only if deleteLogs has something to delete!)
|
||||
// This is a known issue and tolerated.
|
||||
}
|
||||
}
|
||||
|
||||
extension URL {
|
||||
@discardableResult func open() -> Bool { UIApplication.shared.openURL(self) }
|
||||
}
|
||||
|
||||
BIN
main/Assets.xcassets/.DS_Store
vendored
Normal file
26
main/Assets.xcassets/circle-check.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "img.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "img@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "img@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
main/Assets.xcassets/circle-check.imageset/img.png
vendored
Normal file
|
After Width: | Height: | Size: 247 B |
BIN
main/Assets.xcassets/circle-check.imageset/img@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 423 B |
BIN
main/Assets.xcassets/circle-check.imageset/img@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 572 B |
26
main/Assets.xcassets/circle-x.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "img.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "img@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "img@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
main/Assets.xcassets/circle-x.imageset/img.png
vendored
Normal file
|
After Width: | Height: | Size: 235 B |
BIN
main/Assets.xcassets/circle-x.imageset/img@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 400 B |
BIN
main/Assets.xcassets/circle-x.imageset/img@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 530 B |
26
main/Assets.xcassets/detail-help.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "img.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "img@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "img@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
main/Assets.xcassets/detail-help.imageset/img.png
vendored
Normal file
|
After Width: | Height: | Size: 322 B |
BIN
main/Assets.xcassets/detail-help.imageset/img@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 510 B |
BIN
main/Assets.xcassets/detail-help.imageset/img@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 701 B |
26
main/Assets.xcassets/filter-clear.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "img.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "img@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "img@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
main/Assets.xcassets/filter-clear.imageset/img.png
vendored
Normal file
|
After Width: | Height: | Size: 187 B |
BIN
main/Assets.xcassets/filter-clear.imageset/img@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 289 B |
BIN
main/Assets.xcassets/filter-clear.imageset/img@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 395 B |
26
main/Assets.xcassets/filter-filled.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "img.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "img@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "img@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
main/Assets.xcassets/filter-filled.imageset/img.png
vendored
Normal file
|
After Width: | Height: | Size: 155 B |
BIN
main/Assets.xcassets/filter-filled.imageset/img@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 226 B |
BIN
main/Assets.xcassets/filter-filled.imageset/img@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 302 B |
26
main/Assets.xcassets/intersection.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "img.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "img@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "img@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
main/Assets.xcassets/intersection.imageset/img.png
vendored
Normal file
|
After Width: | Height: | Size: 385 B |
BIN
main/Assets.xcassets/intersection.imageset/img@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 695 B |
BIN
main/Assets.xcassets/intersection.imageset/img@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
26
main/Assets.xcassets/jump-to-target.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "img.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "img@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "img@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
main/Assets.xcassets/jump-to-target.imageset/img.png
vendored
Normal file
|
After Width: | Height: | Size: 230 B |
BIN
main/Assets.xcassets/jump-to-target.imageset/img@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 409 B |
BIN
main/Assets.xcassets/jump-to-target.imageset/img@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 544 B |
26
main/Assets.xcassets/line-collapse.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "img.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "img@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "img@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
main/Assets.xcassets/line-collapse.imageset/img.png
vendored
Normal file
|
After Width: | Height: | Size: 150 B |
BIN
main/Assets.xcassets/line-collapse.imageset/img@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 216 B |
BIN
main/Assets.xcassets/line-collapse.imageset/img@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 283 B |
26
main/Assets.xcassets/line-expand.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "img.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "img@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "img@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
main/Assets.xcassets/line-expand.imageset/img.png
vendored
Normal file
|
After Width: | Height: | Size: 156 B |
BIN
main/Assets.xcassets/line-expand.imageset/img@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 224 B |
BIN
main/Assets.xcassets/line-expand.imageset/img@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 282 B |
@@ -1,440 +0,0 @@
|
||||
<?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="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>
|
||||
<!--Domains-->
|
||||
<scene sceneID="MN1-aZ-cZt">
|
||||
<objects>
|
||||
<tableViewController id="pdd-aM-sKl" customClass="TVCDomains" 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="kj3-8X-TyT">
|
||||
<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" 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="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"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="0HB-5f-eB1">
|
||||
<rect key="frame" x="16" y="10" width="33.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="MRe-Eq-gvc">
|
||||
<rect key="frame" x="16" y="31.5" width="44" height="14.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<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="WcC-nb-Vf5" kind="show" id="EVQ-hO-JE9"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="pdd-aM-sKl" id="4fX-iP-7Oa"/>
|
||||
<outlet property="delegate" destination="pdd-aM-sKl" id="3RN-az-SYU"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<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="686" y="-1245"/>
|
||||
</scene>
|
||||
<!--Hosts-->
|
||||
<scene sceneID="ZCV-Yx-jjW">
|
||||
<objects>
|
||||
<tableViewController id="WcC-nb-Vf5" customClass="TVCHosts" 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="nRF-dc-dC2">
|
||||
<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="HostCell" textLabel="Rnk-SP-UHm" detailTextLabel="ovQ-lJ-hWJ" style="IBUITableViewCellStyleSubtitle" id="uv0-9B-Zbb">
|
||||
<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" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="uv0-9B-Zbb" id="6vH-Du-gCg">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="55.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="Rnk-SP-UHm">
|
||||
<rect key="frame" x="16" y="10" width="33.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="ovQ-lJ-hWJ">
|
||||
<rect key="frame" x="16" y="31.5" width="44" height="14.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<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="h7Z-Qr-pJ5" kind="show" id="TPa-Zn-eOs"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="WcC-nb-Vf5" id="szM-iI-Jgi"/>
|
||||
<outlet property="delegate" destination="WcC-nb-Vf5" id="sBd-BW-Wg6"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<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="1391" y="-1245"/>
|
||||
</scene>
|
||||
<!--Occurrences-->
|
||||
<scene sceneID="ws3-sK-l8m">
|
||||
<objects>
|
||||
<tableViewController id="h7Z-Qr-pJ5" customClass="TVCHostDetails" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" allowsSelection="NO" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="4ms-FO-Fge">
|
||||
<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="HostDetailCell" textLabel="J2P-mU-Vad" style="IBUITableViewCellStyleDefault" id="ZCA-Dz-i92">
|
||||
<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="ZCA-Dz-i92" id="nxe-48-jAQ">
|
||||
<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" enabled="NO" adjustsFontSizeToFit="NO" id="J2P-mU-Vad">
|
||||
<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="h7Z-Qr-pJ5" id="fyW-Av-fWY"/>
|
||||
<outlet property="delegate" destination="h7Z-Qr-pJ5" id="gBq-jA-u5V"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Occurrences" prompt="com.domain.network.cdn" id="bys-2u-rHs"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="UxH-PH-KQy" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2096" y="-1245"/>
|
||||
</scene>
|
||||
<!--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"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="pdd-aM-sKl" kind="relationship" relationship="rootViewController" id="oMe-a0-xN7"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="8j4-AX-JBN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<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>
|
||||
237
main/Common Classes/CustomAlert.swift
Normal file
@@ -0,0 +1,237 @@
|
||||
import UIKit
|
||||
|
||||
class CustomAlert<CustomView: UIView>: UIViewController {
|
||||
|
||||
private let alertTitle: String?
|
||||
private let alertDetail: String?
|
||||
|
||||
private let customView: CustomView
|
||||
private var callback: ((CustomView) -> Void)?
|
||||
|
||||
/// Default: `[Cancel, Save]`
|
||||
lazy var buttonsBar: UIStackView = {
|
||||
let cancel = QuickUI.button("Cancel", target: self, action: #selector(didTapCancel))
|
||||
let save = QuickUI.button("Save", target: self, action: #selector(didTapSave))
|
||||
save.titleLabel?.font = save.titleLabel?.font.bold()
|
||||
let bar = UIStackView(arrangedSubviews: [cancel, save])
|
||||
bar.axis = .horizontal
|
||||
bar.distribution = .equalSpacing
|
||||
return bar
|
||||
}()
|
||||
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
init(title: String? = nil, detail: String? = nil, view custom: CustomView) {
|
||||
alertTitle = title
|
||||
alertDetail = detail
|
||||
customView = custom
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
override var isModalInPresentation: Bool { set{} get{true} }
|
||||
override var modalPresentationStyle: UIModalPresentationStyle { set{} get{.custom} }
|
||||
override var transitioningDelegate: UIViewControllerTransitioningDelegate? {
|
||||
set {} get {
|
||||
SlideInTransitioningDelegate(for: .bottom, modal: true)
|
||||
}
|
||||
}
|
||||
|
||||
internal override func loadView() {
|
||||
let control = UIView()
|
||||
control.backgroundColor = .sysBackground
|
||||
view = control
|
||||
|
||||
var tmpPrevivous: UIView? = nil
|
||||
|
||||
func adaptive(margin: CGFloat, _ fn: () -> NSLayoutConstraint) {
|
||||
regularConstraints.append(fn() + margin)
|
||||
compactConstraints.append(fn() + margin/2)
|
||||
}
|
||||
|
||||
func addLabel(_ lbl: UILabel) {
|
||||
lbl.numberOfLines = 0
|
||||
control.addSubview(lbl)
|
||||
lbl.anchor([.leading, .trailing], to: control.layoutMarginsGuide)
|
||||
if let p = tmpPrevivous {
|
||||
adaptive(margin: 16) { lbl.topAnchor =&= p.bottomAnchor }
|
||||
} else {
|
||||
adaptive(margin: 12) { lbl.topAnchor =&= control.layoutMarginsGuide.topAnchor }
|
||||
}
|
||||
tmpPrevivous = lbl
|
||||
}
|
||||
|
||||
// Alert title & description
|
||||
if let t = alertTitle {
|
||||
let lbl = QuickUI.label(t, align: .center, style: .subheadline)
|
||||
lbl.font = lbl.font.bold()
|
||||
addLabel(lbl)
|
||||
}
|
||||
|
||||
if let d = alertDetail {
|
||||
addLabel(QuickUI.label(d, align: .center, style: .footnote))
|
||||
}
|
||||
|
||||
// User content
|
||||
control.addSubview(customView)
|
||||
customView.anchor([.leading, .trailing], to: control)
|
||||
if let p = tmpPrevivous {
|
||||
customView.topAnchor =&= p.bottomAnchor | .defaultHigh
|
||||
} else {
|
||||
customView.topAnchor =&= control.layoutMarginsGuide.topAnchor
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
control.addSubview(buttonsBar)
|
||||
buttonsBar.anchor([.leading, .trailing], to: control.layoutMarginsGuide, margin: 8)
|
||||
buttonsBar.topAnchor =&= customView.bottomAnchor | .defaultHigh
|
||||
|
||||
adaptive(margin: 12) { control.layoutMarginsGuide.bottomAnchor =&= buttonsBar.bottomAnchor }
|
||||
|
||||
adaptToNewTraits(traitCollection)
|
||||
view.frame.size = view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Adaptive Traits
|
||||
|
||||
private var compactConstraints: [NSLayoutConstraint] = []
|
||||
private var regularConstraints: [NSLayoutConstraint] = []
|
||||
|
||||
private func adaptToNewTraits(_ traits: UITraitCollection) {
|
||||
let flag = traits.verticalSizeClass == .compact
|
||||
NSLayoutConstraint.deactivate(flag ? regularConstraints : compactConstraints)
|
||||
NSLayoutConstraint.activate(flag ? compactConstraints : regularConstraints)
|
||||
view.setNeedsLayout()
|
||||
}
|
||||
|
||||
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.willTransition(to: newCollection, with: coordinator)
|
||||
adaptToNewTraits(newCollection)
|
||||
}
|
||||
|
||||
// MARK: - User Interaction
|
||||
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
[UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(didTapCancel))]
|
||||
}
|
||||
|
||||
@objc private func didTapCancel() {
|
||||
callback = nil
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
@objc private func didTapSave() {
|
||||
dismiss(animated: true) {
|
||||
self.callback?(self.customView)
|
||||
self.callback = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Present & Dismiss
|
||||
|
||||
func present(in viewController: UIViewController, onSuccess: @escaping (CustomView) -> Void) {
|
||||
callback = onSuccess
|
||||
viewController.present(self, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// ###################################
|
||||
// #
|
||||
// # MARK: - Date Picker Alert
|
||||
// #
|
||||
// ###################################
|
||||
|
||||
class DatePickerAlert : CustomAlert<UIDatePicker> {
|
||||
|
||||
let datePicker = UIDatePicker()
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
init(title: String? = nil, detail: String? = nil, initial date: Date? = nil) {
|
||||
if let date = date {
|
||||
datePicker.setDate(date, animated: false)
|
||||
}
|
||||
super.init(title: title, detail: detail, view: datePicker)
|
||||
|
||||
let now = QuickUI.button("Now", target: self, action: #selector(didTapNow))
|
||||
now.titleLabel?.font = now.titleLabel?.font.bold()
|
||||
now.setTitleColor(.sysLabel, for: .normal)
|
||||
buttonsBar.insertArrangedSubview(now, at: 1)
|
||||
}
|
||||
|
||||
@objc private func didTapNow() {
|
||||
datePicker.date = Date()
|
||||
}
|
||||
|
||||
func present(in viewController: UIViewController, onSuccess: @escaping (UIDatePicker, Date) -> Void) {
|
||||
super.present(in: viewController) {
|
||||
onSuccess($0, $0.date)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #######################################
|
||||
// #
|
||||
// # MARK: - Duration Picker Alert
|
||||
// #
|
||||
// #######################################
|
||||
|
||||
class DurationPickerAlert: CustomAlert<UIPickerView>, UIPickerViewDataSource, UIPickerViewDelegate {
|
||||
|
||||
let pickerView = UIPickerView()
|
||||
private let dataSource: [[String]]
|
||||
private let compWidths: [CGFloat]
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
/// - Parameter options: [[List of labels] per component]
|
||||
/// - Parameter widths: If `nil` set all components to equal width
|
||||
init(title: String? = nil, detail: String? = nil, options: [[String]], widths: [CGFloat]? = nil) {
|
||||
assert(widths == nil || widths!.count == options.count, "widths.count != options.count")
|
||||
|
||||
dataSource = options
|
||||
compWidths = widths ?? options.map { _ in 1 / CGFloat(options.count) }
|
||||
|
||||
super.init(title: title, detail: detail, view: pickerView)
|
||||
|
||||
pickerView.dataSource = self
|
||||
pickerView.delegate = self
|
||||
}
|
||||
|
||||
func numberOfComponents(in _: UIPickerView) -> Int {
|
||||
dataSource.count
|
||||
}
|
||||
func pickerView(_: UIPickerView, numberOfRowsInComponent c: Int) -> Int {
|
||||
dataSource[c].count
|
||||
}
|
||||
func pickerView(_: UIPickerView, titleForRow r: Int, forComponent c: Int) -> String? {
|
||||
dataSource[c][r]
|
||||
}
|
||||
func pickerView(_ pickerView: UIPickerView, widthForComponent c: Int) -> CGFloat {
|
||||
compWidths[c] * pickerView.frame.width
|
||||
}
|
||||
|
||||
func present(in viewController: UIViewController, onSuccess: @escaping (UIPickerView, [Int]) -> Void) {
|
||||
super.present(in: viewController) {
|
||||
onSuccess($0, $0.selection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UIPickerView {
|
||||
var selection: [Int] {
|
||||
get { (0..<numberOfComponents).map { selectedRow(inComponent: $0) } }
|
||||
set { setSelection(newValue) }
|
||||
}
|
||||
/// - Warning: Does not check for boundaries!
|
||||
func setSelection(_ selection: [Int], animated: Bool = false) {
|
||||
assert(selection.count == numberOfComponents, "selection.count != components.count")
|
||||
for (c, i) in selection.enumerated() {
|
||||
selectRow(i, inComponent: c, animated: animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
418
main/Common Classes/FilterPipeline.swift
Normal file
@@ -0,0 +1,418 @@
|
||||
import UIKit
|
||||
|
||||
protocol FilterPipelineDelegate: AnyObject {
|
||||
/// Call `reloadData()`
|
||||
func filterPipelineDidReset()
|
||||
/// Call `safeDeleteRows()`
|
||||
func filterPipeline(delete rows: [Int])
|
||||
/// Call `safeInsertRow()`
|
||||
func filterPipeline(insert row: Int)
|
||||
/// Call `safeReloadRow()`
|
||||
func filterPipeline(update row: Int)
|
||||
/// Call `safeMoveRow()`
|
||||
func filterPipeline(move oldRow: Int, to newRow: Int)
|
||||
}
|
||||
|
||||
// MARK: - FilterPipeline
|
||||
|
||||
class FilterPipeline<T> {
|
||||
|
||||
private(set) fileprivate var dataSource: [T] = []
|
||||
|
||||
private var pipeline: [PipelineFilter<T>] = []
|
||||
private var display: PipelineSorting<T>!
|
||||
weak var delegate: FilterPipelineDelegate?
|
||||
|
||||
/// - Returns: Number of elements in `projection`
|
||||
@inline(__always) func displayObjectCount() -> Int { display.projection.count }
|
||||
|
||||
/// Dereference `projection` index to `dataSource` index
|
||||
/// - Complexity: O(1)
|
||||
@inline(__always) func displayObject(at index: Int) -> T { dataSource[display.projection[index]] }
|
||||
|
||||
/// Search and return first element in `dataSource` that matches `predicate`.
|
||||
/// - Returns: Index in `dataSource` and found object or `nil` if no matching item found.
|
||||
/// - Complexity: O(*n*), where *n* is the length of the `dataSource`.
|
||||
func dataSourceGet(where predicate: ((T) -> Bool)) -> (index: Int, object: T)? {
|
||||
// TODO: use sorted dataSource for binary lookup?
|
||||
// would require to shift filter and sorting indices for every new element
|
||||
guard let i = dataSource.firstIndex(where: predicate) else {
|
||||
return nil
|
||||
}
|
||||
return (i, dataSource[i])
|
||||
}
|
||||
|
||||
/// Set new data source and re-built filter and display sorting order.
|
||||
/// - Note: Will call `filterPipelineDidReset()`
|
||||
func reset(dataSource: [T]) {
|
||||
self.dataSource = dataSource
|
||||
self.resetFilters()
|
||||
delegate?.filterPipelineDidReset()
|
||||
}
|
||||
|
||||
/// Returns the index set of either the last filter layer, or `dataSource` if no filter is set.
|
||||
fileprivate func lastLayerIndices() -> [Int] {
|
||||
pipeline.last?.selection ?? dataSource.indices.arr()
|
||||
}
|
||||
|
||||
/// Get pipeline index of filter with given identifier
|
||||
private func indexOfFilter(_ identifier: String) -> Int? {
|
||||
pipeline.firstIndex(where: {$0.id == identifier})
|
||||
}
|
||||
|
||||
|
||||
// MARK: manage pipeline
|
||||
|
||||
/// Add new filter layer. Each layer is applied upon the previous layer. Therefore, each filter
|
||||
/// can only restrict the display further. A filter cannot introduce previously removed elements.
|
||||
/// - Warning: Use `[unowned self]` to prevent retain cycles!
|
||||
/// - Note: Will call `filterPipelineDidReset()`
|
||||
/// - Parameters:
|
||||
/// - identifier: Use this id to find the filter again. For reload and remove operations.
|
||||
/// - otherId: If `nil` or non-existent the new filter will be appended at the end.
|
||||
/// - predicate: Return `true` if you want to keep the element.
|
||||
func addFilter(_ identifier: String, before otherId: String? = nil, _ predicate: @escaping PipelineFilter<T>.Predicate) {
|
||||
guard indexOfFilter(identifier) == nil else { return }
|
||||
let newFilter = PipelineFilter(identifier, predicate)
|
||||
if let other = otherId, let i = indexOfFilter(other) {
|
||||
pipeline.insert(newFilter, at: i)
|
||||
resetFilters(startingAt: i)
|
||||
} else {
|
||||
newFilter.reset(to: dataSource, previous: lastLayerIndices())
|
||||
pipeline.append(newFilter)
|
||||
display?.apply(moreRestrictive: newFilter.selection)
|
||||
}
|
||||
delegate?.filterPipelineDidReset()
|
||||
}
|
||||
|
||||
/// Find and remove filter with given identifier. Will automatically update remaining filters and display sorting.
|
||||
/// - Note: Will call `filterPipelineDidReset()`
|
||||
func removeFilter(withId ident: String) {
|
||||
guard let i = indexOfFilter(ident) else { return }
|
||||
pipeline.remove(at: i)
|
||||
if i == pipeline.count {
|
||||
// only if we don't reset other layers we can assure `toLessRestrictive`
|
||||
display?.apply(lessRestrictive: lastLayerIndices())
|
||||
} else {
|
||||
resetFilters(startingAt: i)
|
||||
}
|
||||
delegate?.filterPipelineDidReset()
|
||||
}
|
||||
|
||||
/// Start filter evaluation on all entries from previous filter.
|
||||
/// - Note: Will call `filterPipelineDidReset()`
|
||||
func reloadFilter(withId ident: String) {
|
||||
guard let i = indexOfFilter(ident) else { return }
|
||||
resetFilters(startingAt: i)
|
||||
delegate?.filterPipelineDidReset()
|
||||
}
|
||||
|
||||
/// Sets the sort and display order. You should set the `delegate` to automatically update your `tableView`.
|
||||
/// - Warning: Use `[unowned self]` to prevent retain cycles!
|
||||
/// - Note: Will call `filterPipelineDidReset()`
|
||||
/// - Parameter predicate: Return `true` if first element should be sorted before second element.
|
||||
func setSorting(_ predicate: @escaping PipelineSorting<T>.Predicate) {
|
||||
display = .init(predicate, pipe: self)
|
||||
delegate?.filterPipelineDidReset()
|
||||
}
|
||||
|
||||
/// Will reverse the current display order without resorting. This is faster than setting a new sorting `predicate`.
|
||||
/// However, the `predicate` must be dynamic and support a sort order flag.
|
||||
/// - Note: Will call `filterPipelineDidReset()`
|
||||
/// - Warning: Make sure `predicate` does reflect the change or it will lead to data inconsistency!
|
||||
func reverseSorting() {
|
||||
// TODO: use semaphore to prevent concurrent edits
|
||||
display?.reverseOrder()
|
||||
delegate?.filterPipelineDidReset()
|
||||
}
|
||||
|
||||
/// Re-built filter and display sorting order.
|
||||
/// - Parameter index: Must be: `index <= pipeline.count`
|
||||
private func resetFilters(startingAt index: Int = 0) {
|
||||
for i in index..<pipeline.count {
|
||||
pipeline[i].reset(to: dataSource, previous: (i>0)
|
||||
? pipeline[i-1].selection : dataSource.indices.arr())
|
||||
}
|
||||
// Reset is NOT less-restrictive because filters are dynamic
|
||||
// Calling reset on a filter twice may yield different results
|
||||
// E.g. if filter uses variables outside of scope (current time, search term)
|
||||
display?.reset(to: lastLayerIndices())
|
||||
}
|
||||
|
||||
/// Push object through filter pipeline to check whether it survives all filters.
|
||||
/// - Parameter index: The index of the object in the original `dataSource`
|
||||
/// - Returns: `changed` is `true` if element persists or should be removed with this update.
|
||||
/// `display` indicates whther element should be shown (`true`) or hidden (`false`).
|
||||
/// - Complexity: O(*m* log *n*), where *m* is the number of filters and *n* the number of elements in each filter.
|
||||
private func processPipeline(with obj: T, at index: Int) -> (changed: Bool, display: Bool) {
|
||||
var keepGoing = true
|
||||
for filter in pipeline {
|
||||
let lastIndex: Int?
|
||||
if keepGoing {
|
||||
(keepGoing, lastIndex) = filter.update(obj, at: index)
|
||||
} else {
|
||||
lastIndex = filter.remove(dataSource: index)
|
||||
}
|
||||
// if it isnt in this layer, it wont appear in the following either
|
||||
if lastIndex == nil { return (false, false) }
|
||||
}
|
||||
return (true, keepGoing)
|
||||
}
|
||||
|
||||
|
||||
// MARK: data updates
|
||||
|
||||
/// Add new element to the original `dataSource` and immediately apply filter and sorting.
|
||||
/// - Note: Will call `filterPipeline(insert:)` if not filtered.
|
||||
/// - Complexity: O((*m*+1) log *n*), where *m* is the number of filters and *n* the number of elements in each filter.
|
||||
func addNew(_ obj: T) {
|
||||
let index = dataSource.count
|
||||
dataSource.append(obj)
|
||||
for filter in pipeline {
|
||||
if filter.add(obj, at: index) == nil { return }
|
||||
}
|
||||
// survived all filters
|
||||
let displayIndex = display.insertNew(index)
|
||||
delegate?.filterPipeline(insert: displayIndex)
|
||||
}
|
||||
|
||||
/// Update element at `index` in the original `dataSource` and immediately re-apply filter and sorting.
|
||||
/// - Note: Will call `filterPipeline(delete:)`, `(insert:)`, `(update:)`, or `(move:)`
|
||||
/// - Parameters:
|
||||
/// - obj: Element to be added. Will overwrite previous `dataSource` object.
|
||||
/// - index: Index in the original `dataSource`
|
||||
/// - Complexity: O(*n* + (*m*+1) log *n*), where *m* is the number of filters and *n* the number of elements in each filter / projection.
|
||||
func update(_ obj: T, at index: Int) {
|
||||
let status = processPipeline(with: obj, at: index)
|
||||
guard status.changed else {
|
||||
dataSource[index] = obj // we need to update anyway
|
||||
return
|
||||
}
|
||||
let oldPos = display.deleteOld(index)
|
||||
dataSource[index] = obj
|
||||
guard status.display else {
|
||||
if oldPos != -1 { delegate?.filterPipeline(delete: [oldPos]) }
|
||||
return
|
||||
}
|
||||
let newPos = display.insertNew(index, previousIndex: oldPos)
|
||||
if oldPos == -1 {
|
||||
delegate?.filterPipeline(insert: newPos)
|
||||
} else {
|
||||
if oldPos == newPos {
|
||||
delegate?.filterPipeline(update: oldPos)
|
||||
} else {
|
||||
delegate?.filterPipeline(move: oldPos, to: newPos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove elements from the original `dataSource`, from all filters, and from display sorting.
|
||||
/// - Note: Will call `filterPipeline(delete:)` if `sorted` array is not empty.
|
||||
/// - Parameter sorted: Indices in the original `dataSource`
|
||||
/// - Complexity: O(*t*(*m*+*n*) + *m* log *n*), where *t* is the number of filters,
|
||||
/// *m* the number of elements in each filter / projection, and *n* the length of `sorted` indices.
|
||||
func remove(indices sorted: [Int]) {
|
||||
guard sorted.count > 0 else { return }
|
||||
for i in sorted.reversed() {
|
||||
dataSource.remove(at: i)
|
||||
}
|
||||
for filter in pipeline {
|
||||
filter.shiftRemove(indices: sorted)
|
||||
}
|
||||
let indices = display.shiftRemove(indices: sorted)
|
||||
delegate?.filterPipeline(delete: indices)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Filter
|
||||
|
||||
class PipelineFilter<T>: CustomStringConvertible {
|
||||
var description: String { "\(Self.self)(id: \(id))" }
|
||||
|
||||
typealias Predicate = (T) -> Bool
|
||||
|
||||
let id: String
|
||||
private(set) var selection: [Int] = []
|
||||
private let shouldPersist: Predicate
|
||||
|
||||
/// - Parameter predicate: Return `true` if you want to keep the element
|
||||
required init(_ identifier: String, _ predicate: @escaping Predicate) {
|
||||
self.id = identifier
|
||||
shouldPersist = predicate
|
||||
}
|
||||
|
||||
/// Reset `selection` by copying the indices and applying the filter function
|
||||
fileprivate func reset(to dataSource: [T], previous filterIndices: [Int]) {
|
||||
selection = filterIndices
|
||||
selection.removeAll { !shouldPersist(dataSource[$0]) }
|
||||
}
|
||||
|
||||
/// Apply filter to `obj` and either insert or do nothing.
|
||||
/// - Parameters:
|
||||
/// - obj: Object that should be inserted if filter allows.
|
||||
/// - index: Index of object in original `dataSource`
|
||||
/// - Returns: Index in `selection` or `nil` if `obj` is removed by the filter.
|
||||
/// - Complexity:
|
||||
/// * O(1), if `index` is appended at end.
|
||||
/// * O(log *n*), where *n* is the length of the `selection`.
|
||||
fileprivate func add(_ obj: T, at index: Int) -> Int? {
|
||||
guard shouldPersist(obj) else {
|
||||
return nil
|
||||
}
|
||||
if selection.last ?? 0 < index { // in case we only append at end
|
||||
selection.append(index)
|
||||
return selection.count - 1
|
||||
}
|
||||
return selection.binTreeInsert(index, compare: (<))
|
||||
}
|
||||
|
||||
/// Search and remove original `dataSource` index
|
||||
/// - Parameter index: Index of object in original `dataSource`
|
||||
/// - Returns: Index of removed element in `selection` or `nil` if element does not exist
|
||||
/// - Complexity: O(log *n*), where *n* is the length of the `selection`.
|
||||
fileprivate func remove(dataSource index: Int) -> Int? {
|
||||
selection.binTreeRemove(index, compare: (<))
|
||||
}
|
||||
|
||||
/// Perform filter check and update internal `selection` indices.
|
||||
/// - Parameters:
|
||||
/// - obj: Object that was inserted or updated.
|
||||
/// - index: Index where the object is located after the update.
|
||||
/// - Returns: `keep` indicates whether the value should be displayed (`true`) or hidden (`false`).
|
||||
/// `idx` contains the selection filter index or `nil` if the value should be removed.
|
||||
/// - Complexity: O(log *n*), where *n* is the length of the `selection`.
|
||||
fileprivate func update(_ obj: T, at index: Int) -> (keep: Bool, idx: Int?) {
|
||||
let currentIndex = selection.binTreeIndex(of: index, compare: (<), mustExist: true)
|
||||
if shouldPersist(obj) {
|
||||
return (true, currentIndex ?? selection.binTreeInsert(index, compare: (<)))
|
||||
}
|
||||
if let i = currentIndex { selection.remove(at: i) }
|
||||
return (false, currentIndex)
|
||||
}
|
||||
|
||||
/// Instead of re-sorting we can decrement all remaining elements after X.
|
||||
/// - Parameter sorted: Elements to remove from collection
|
||||
/// - Complexity: O(*m*+*n*), where *m* is the length of the `selection`.
|
||||
/// *n* is equal to: *length of selection* `-` *index of first element* of `sorted` indices
|
||||
fileprivate func shiftRemove(indices sorted: [Int]) {
|
||||
guard sorted.count > 0 else {
|
||||
return
|
||||
}
|
||||
var list = sorted
|
||||
var del = list.popLast()
|
||||
for (i, val) in selection.enumerated().reversed() {
|
||||
while let d = del, d > val {
|
||||
del = list.popLast()
|
||||
}
|
||||
guard let d = del else { break }
|
||||
if d < val { selection[i] -= (list.count + 1) }
|
||||
else if d == val { selection.remove(at: i) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Sorting
|
||||
|
||||
class PipelineSorting<T> {
|
||||
typealias Predicate = (T, T) -> Bool
|
||||
|
||||
private(set) var projection: [Int] = []
|
||||
private let comperator: (Int, Int) -> Bool // links to pipeline.dataSource
|
||||
|
||||
/// Create a fresh, already sorted, display order projection.
|
||||
/// - Parameter predicate: Return `true` if first element should be sorted before second element.
|
||||
/// - Complexity: O(*n* log *n*), where *n* is the length of the `filter`.
|
||||
required init(_ predicate: @escaping Predicate, pipe: FilterPipeline<T>) {
|
||||
comperator = { [unowned pipe] in
|
||||
predicate(pipe.dataSource[$0], pipe.dataSource[$1])
|
||||
}
|
||||
reset(to: pipe.lastLayerIndices())
|
||||
}
|
||||
|
||||
/// - Warning: Make sure `predicate` does reflect the change. Or it will lead to data inconsistency.
|
||||
/// - Complexity: O(*n*), where *n* is the length of the `filter`.
|
||||
fileprivate func reverseOrder() {
|
||||
projection.reverse()
|
||||
}
|
||||
|
||||
/// Replace current `projection` with new filter indices and apply sorting.
|
||||
/// - Complexity: O(*n* log *n*), where *n* is the length of the `filter`.
|
||||
fileprivate func reset(to filterIndices: [Int]) {
|
||||
projection = filterIndices.sorted(by: comperator)
|
||||
}
|
||||
|
||||
/// After adding a new layer of filtering the new layer can only restrict the display even further.
|
||||
/// Therefore, indices that were removed in the last layer will be removed from the projection too.
|
||||
/// - Complexity: O(*m* log *n*), where *m* is the length of the `projection` and *n* the length of the `filter`.
|
||||
fileprivate func apply(moreRestrictive filterIndices: [Int]) {
|
||||
projection.removeAll { !filterIndices.binTreeExists($0, compare: (<)) }
|
||||
}
|
||||
|
||||
/// After removing a layer of filtering the previous layers are less restrictive and thus contain more indices.
|
||||
/// Therefore, the difference between both index sets will be inserted into the projection.
|
||||
/// - Complexity: O(*m* log *n*), where *m* is the difference to the previous layer and *n* is the length of the `projection`.
|
||||
fileprivate func apply(lessRestrictive filterIndices: [Int]) {
|
||||
for x in filterIndices.difference(toSubset: projection.sorted(), compare: (<)) {
|
||||
insertNew(x)
|
||||
}
|
||||
}
|
||||
|
||||
/// Add new element and automatically sort according to predicate
|
||||
/// - Parameters:
|
||||
/// - index: Index of the element position in the original `dataSource`
|
||||
/// - prev: If greater than `0`, try re-insert at the same position.
|
||||
/// - Returns: Index in the projection
|
||||
/// - Complexity: O(log *n*), where *n* is the length of the `projection`.
|
||||
@discardableResult fileprivate func insertNew(_ index: Int, previousIndex prev: Int = -1) -> Int {
|
||||
if prev >= 0, prev <= projection.count { // '<=' because previous delete removed one element
|
||||
if (prev == 0 || !comperator(index, projection[prev - 1])),
|
||||
(prev == projection.count || !comperator(projection[prev], index)) {
|
||||
// If element can be inserted at the same position without resorting, do that
|
||||
projection.insert(index, at: prev)
|
||||
return prev
|
||||
}
|
||||
}
|
||||
return projection.binTreeInsert(index, compare: comperator)
|
||||
}
|
||||
|
||||
/// Remove element from projection
|
||||
/// - Parameter index: Index of the element position in the original `dataSource`
|
||||
/// - Returns: Index in the projection or `-1` if element did not exist
|
||||
/// - Complexity: O(*n*), where *n* is the length of the `projection`.
|
||||
fileprivate func deleteOld(_ index: Int) -> Int {
|
||||
guard let i = projection.firstIndex(of: index) else {
|
||||
return -1
|
||||
}
|
||||
projection.remove(at: i)
|
||||
return i
|
||||
}
|
||||
|
||||
/// Instead of re-sorting we can decrement all remaining elements after X.
|
||||
/// - Parameter sorted: Elements to remove from collection
|
||||
/// - Returns: List of `projection` indices that were removed (reverse sort order)
|
||||
/// - Complexity: O(*m* log *n*), where *m* is the length of the `projection` and *n* is the length of `sorted`.
|
||||
@discardableResult fileprivate func shiftRemove(indices sorted: [Int]) -> [Int] {
|
||||
guard sorted.count > 0 else {
|
||||
return []
|
||||
}
|
||||
var listOfDeletes: [Int] = []
|
||||
let min = sorted.first!, max = sorted.last!
|
||||
for (i, val) in projection.enumerated().reversed() {
|
||||
guard val >= min else { continue }
|
||||
if val > max {
|
||||
projection[i] -= sorted.count
|
||||
} else {
|
||||
let c = sorted.binTreeIndex(of: val, compare: (<))!
|
||||
if val == sorted[c] {
|
||||
projection.remove(at: i)
|
||||
listOfDeletes.append(i)
|
||||
} else {
|
||||
projection[i] -= c
|
||||
}
|
||||
}
|
||||
}
|
||||
return listOfDeletes
|
||||
}
|
||||
}
|
||||
83
main/Common Classes/IBViews.swift
Normal file
@@ -0,0 +1,83 @@
|
||||
import UIKit
|
||||
import CoreGraphics
|
||||
|
||||
// MARK: White Triangle Popup Arrow
|
||||
|
||||
@IBDesignable
|
||||
class PopupTriangle: UIView {
|
||||
@IBInspectable var rotation: CGFloat = 0
|
||||
@IBInspectable var color: UIColor = .black
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
guard let c = UIGraphicsGetCurrentContext() else { return }
|
||||
let w = rect.width, h = rect.height
|
||||
switch rotation {
|
||||
case 90: // right
|
||||
c.lineFromTo(x1: 0, y1: 0, x2: w, y2: h/2)
|
||||
c.addLine(to: CGPoint(x: 0, y: h))
|
||||
case 180: // bottom
|
||||
c.lineFromTo(x1: w, y1: 0, x2: w/2, y2: h)
|
||||
c.addLine(to: CGPoint(x: 0, y: 0))
|
||||
case 270: // left
|
||||
c.lineFromTo(x1: w, y1: h, x2: 0, y2: h/2)
|
||||
c.addLine(to: CGPoint(x: w, y: 0))
|
||||
default: // top
|
||||
c.lineFromTo(x1: 0, y1: h, x2: w/2, y2: 0)
|
||||
c.addLine(to: CGPoint(x: w, y: h))
|
||||
}
|
||||
c.closePath()
|
||||
c.setFillColor(color.cgColor)
|
||||
c.fillPath()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Label as Tag Bubble
|
||||
|
||||
@IBDesignable
|
||||
class TagLabel: UILabel {
|
||||
private var em: CGFloat { font.pointSize }
|
||||
@IBInspectable var padTop: CGFloat = 0
|
||||
@IBInspectable var padLeft: CGFloat = 0
|
||||
@IBInspectable var padRight: CGFloat = 0
|
||||
@IBInspectable var padBottom: CGFloat = 0
|
||||
private var padding: UIEdgeInsets {
|
||||
.init(top: padTop + em/6, left: padLeft + em/3,
|
||||
bottom: padBottom + em/6, right: padRight + em/3)
|
||||
}
|
||||
|
||||
override func textRect(forBounds bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect {
|
||||
let i = padding
|
||||
let ii = UIEdgeInsets(top: -i.top, left: -i.left, bottom: -i.bottom, right: -i.right)
|
||||
return super.textRect(forBounds: bounds.inset(by: i),
|
||||
limitedToNumberOfLines: numberOfLines).inset(by: ii)
|
||||
}
|
||||
|
||||
override func drawText(in rect: CGRect) {
|
||||
layer.masksToBounds = true
|
||||
layer.cornerRadius = em/2.5
|
||||
super.drawText(in: rect.inset(by: padding))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Percentage meter
|
||||
|
||||
@IBDesignable
|
||||
class MeterBar: UIView {
|
||||
@IBInspectable var percent: CGFloat = 0 { didSet { setNeedsDisplay() } }
|
||||
@IBInspectable var barColor: UIColor = .sysLink
|
||||
@IBInspectable var horizontal: Bool = false
|
||||
|
||||
private var normPercent: CGFloat { 1 - max(0, min(percent, 1)) }
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
let c = UIGraphicsGetCurrentContext()
|
||||
c?.setFillColor(barColor.cgColor)
|
||||
if horizontal {
|
||||
c?.fill(rect.insetBy(dx: normPercent * (rect.width/2), dy: 0))
|
||||
} else {
|
||||
c?.fill(rect.insetBy(dx: 0, dy: normPercent * (rect.height/2)))
|
||||
}
|
||||
}
|
||||
}
|
||||
71
main/Common Classes/NotificationBanner.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
import UIKit
|
||||
|
||||
struct NotificationBanner {
|
||||
enum Style {
|
||||
case fail, ok
|
||||
}
|
||||
|
||||
let view: UIView
|
||||
|
||||
init(_ msg: String, style: Style) {
|
||||
let bg, fg: UIColor
|
||||
let imgName: String
|
||||
switch style {
|
||||
case .fail:
|
||||
bg = .systemRed
|
||||
fg = UIColor.black.withAlphaComponent(0.80)
|
||||
imgName = "circle-x"
|
||||
case .ok:
|
||||
bg = .systemGreen
|
||||
fg = UIColor.black.withAlphaComponent(0.65)
|
||||
imgName = "circle-check"
|
||||
}
|
||||
view = UIView()
|
||||
view.backgroundColor = bg
|
||||
let lbl = QuickUI.label(msg, style: .callout)
|
||||
lbl.textColor = fg
|
||||
lbl.numberOfLines = 0
|
||||
lbl.font = lbl.font.bold()
|
||||
let img = QuickUI.image(UIImage(named: imgName))
|
||||
img.tintColor = fg
|
||||
view.addSubview(lbl)
|
||||
view.addSubview(img)
|
||||
img.anchor([.centerY], to: lbl)
|
||||
lbl.anchor([.bottom, .trailing], to: view.layoutMarginsGuide)
|
||||
img.widthAnchor =&= 25
|
||||
img.heightAnchor =&= 25
|
||||
if #available(iOS 11, *) {
|
||||
img.leadingAnchor =&= view.layoutMarginsGuide.leadingAnchor
|
||||
lbl.topAnchor =&= view.layoutMarginsGuide.topAnchor
|
||||
} else {
|
||||
img.leadingAnchor =&= view.leadingAnchor + 8
|
||||
lbl.topAnchor =&= view.topAnchor + 8
|
||||
}
|
||||
lbl.leadingAnchor =&= img.trailingAnchor + 8
|
||||
img.bottomAnchor =<= view.bottomAnchor - 8 | .init(rawValue: 999)
|
||||
lbl.bottomAnchor =<= view.bottomAnchor - 8 | .init(rawValue: 999)
|
||||
}
|
||||
|
||||
/// Animate header banner from the top of the view. Show for `delay` seconds and then hide again.
|
||||
/// - Parameter onClose: Run after the close animation finishes.
|
||||
func present(in vc: UIViewController, hideAfter delay: TimeInterval = 3, onClose: (() -> Void)? = nil) {
|
||||
vc.view.addSubview(view)
|
||||
view.anchor([.leading, .trailing], to: vc.view!)
|
||||
view.widthAnchor =&= vc.view!.widthAnchor // Bug? left-right is not sufficient
|
||||
vc.view.layoutIfNeeded() // sets the height
|
||||
let h = view.frame.height
|
||||
let constraint = view.topAnchor =&= vc.view.topAnchor - h
|
||||
vc.view.layoutIfNeeded() // hide view
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
constraint.constant = 0
|
||||
vc.view.layoutIfNeeded() // animate view
|
||||
UIView.animate(withDuration: 0.3, delay: delay, options: .curveLinear, animations: {
|
||||
constraint.constant = -h
|
||||
vc.view.layoutIfNeeded() // hide again
|
||||
}, completion: { _ in
|
||||
self.view.removeFromSuperview()
|
||||
onClose?()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
126
main/Common Classes/Prefs.swift
Normal file
@@ -0,0 +1,126 @@
|
||||
import Foundation
|
||||
|
||||
enum Prefs {
|
||||
private static var suite: UserDefaults { UserDefaults.standard }
|
||||
|
||||
private static func Int(_ key: String) -> Int { suite.integer(forKey: key) }
|
||||
private static func Int(_ key: String, _ val: Int) { suite.set(val, forKey: key) }
|
||||
private static func Bool(_ key: String) -> Bool { suite.bool(forKey: key) }
|
||||
private static func Bool(_ key: String, _ val: Bool) { suite.set(val, forKey: key) }
|
||||
private static func Str(_ key: String) -> String? { suite.string(forKey: key) }
|
||||
private static func Str(_ key: String, _ val: String?) { suite.set(val, forKey: key) }
|
||||
private static func Obj(_ key: String) -> Any? { suite.object(forKey: key) }
|
||||
private static func Obj(_ key: String, _ val: Any?) { suite.set(val, forKey: key) }
|
||||
|
||||
static func registerDefaults() {
|
||||
suite.register(defaults: [
|
||||
"RecordingReminderEnabled" : true,
|
||||
"contextAnalyisCoOccurrenceTime" : 5,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Tutorial
|
||||
|
||||
extension Prefs {
|
||||
enum DidShowTutorial {
|
||||
static var Welcome: Bool {
|
||||
get { Prefs.Bool("didShowTutorialAppWelcome") }
|
||||
set { Prefs.Bool("didShowTutorialAppWelcome", newValue) }
|
||||
}
|
||||
static var Recordings: Bool {
|
||||
get { Prefs.Bool("didShowTutorialRecordings") }
|
||||
set { Prefs.Bool("didShowTutorialRecordings", newValue) }
|
||||
}
|
||||
static var RecordingHowTo: Bool {
|
||||
get { Prefs.Bool("didShowTutorialRecordingHowTo") }
|
||||
set { Prefs.Bool("didShowTutorialRecordingHowTo", newValue) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Date Filter
|
||||
|
||||
enum DateFilterKind: Int {
|
||||
case Off = 0, LastXMin = 1, ABRange = 2;
|
||||
}
|
||||
enum DateFilterOrderBy: Int {
|
||||
case Date = 0, Name = 1, Count = 2;
|
||||
}
|
||||
|
||||
extension Prefs {
|
||||
enum DateFilter {
|
||||
static var Kind: DateFilterKind {
|
||||
get { DateFilterKind(rawValue: Prefs.Int("dateFilterType"))! }
|
||||
set { Prefs.Int("dateFilterType", newValue.rawValue) }
|
||||
}
|
||||
/// Default: `0` (disabled)
|
||||
static var LastXMin: Int {
|
||||
get { Prefs.Int("dateFilterLastXMin") }
|
||||
set { Prefs.Int("dateFilterLastXMin", newValue) }
|
||||
}
|
||||
/// Default: `nil` (disabled)
|
||||
static var RangeA: Timestamp? {
|
||||
get { Prefs.Obj("dateFilterRangeA") as? Timestamp }
|
||||
set { Prefs.Obj("dateFilterRangeA", newValue) }
|
||||
}
|
||||
/// Default: `nil` (disabled)
|
||||
static var RangeB: Timestamp? {
|
||||
get { Prefs.Obj("dateFilterRangeB") as? Timestamp }
|
||||
set { Prefs.Obj("dateFilterRangeB", newValue) }
|
||||
}
|
||||
/// default: `.Date`
|
||||
static var OrderBy: DateFilterOrderBy {
|
||||
get { DateFilterOrderBy(rawValue: Prefs.Int("dateFilterOderType"))! }
|
||||
set { Prefs.Int("dateFilterOderType", newValue.rawValue) }
|
||||
}
|
||||
/// default: `false` (Desc)
|
||||
static var OrderAsc: Bool {
|
||||
get { Prefs.Bool("dateFilterOderAsc") }
|
||||
set { Prefs.Bool("dateFilterOderAsc", newValue) }
|
||||
}
|
||||
|
||||
/// - Returns: Timestamp restriction depending on current selected date filter.
|
||||
/// - `Off` : `(nil, nil)`
|
||||
/// - `LastXMin` : `(now-LastXMin, nil)`
|
||||
/// - `ABRange` : `(RangeA, RangeB)`
|
||||
static func restrictions() -> (type: DateFilterKind, earliest: Timestamp?, latest: Timestamp?) {
|
||||
let type = Kind
|
||||
switch type {
|
||||
case .Off: return (type, nil, nil)
|
||||
case .LastXMin: return (type, Timestamp.past(minutes: Prefs.DateFilter.LastXMin), nil)
|
||||
case .ABRange: return (type, Prefs.DateFilter.RangeA, Prefs.DateFilter.RangeB)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - ContextAnalyis
|
||||
|
||||
extension Prefs {
|
||||
enum ContextAnalyis {
|
||||
static var CoOccurrenceTime: Int {
|
||||
get { Prefs.Int("contextAnalyisCoOccurrenceTime") }
|
||||
set { Prefs.Int("contextAnalyisCoOccurrenceTime", newValue) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
extension Prefs {
|
||||
enum RecordingReminder {
|
||||
static var Enabled: Bool {
|
||||
get { Prefs.Bool("RecordingReminderEnabled") }
|
||||
set { Prefs.Bool("RecordingReminderEnabled", newValue) }
|
||||
}
|
||||
static var Sound: String {
|
||||
get { Prefs.Str("RecordingReminderSound") ?? "#default" }
|
||||
set { Prefs.Str("RecordingReminderSound", newValue) }
|
||||
}
|
||||
}
|
||||
}
|
||||
105
main/Common Classes/PrefsShared.swift
Normal file
@@ -0,0 +1,105 @@
|
||||
import Foundation
|
||||
|
||||
enum PrefsShared {
|
||||
private static var suite: UserDefaults { UserDefaults(suiteName: "group.de.uni-bamberg.psi.AppCheck")! }
|
||||
|
||||
private static func Int(_ key: String) -> Int { suite.integer(forKey: key) }
|
||||
private static func Int(_ key: String, _ val: Int) { suite.set(val, forKey: key); suite.synchronize() }
|
||||
private static func Bool(_ key: String) -> Bool { suite.bool(forKey: key) }
|
||||
private static func Bool(_ key: String, _ val: Bool) { suite.set(val, forKey: key); suite.synchronize() }
|
||||
private static func Str(_ key: String) -> String? { suite.string(forKey: key) }
|
||||
private static func Str(_ key: String, _ val: String?) { suite.set(val, forKey: key); suite.synchronize() }
|
||||
|
||||
static func registerDefaults() {
|
||||
suite.register(defaults: [
|
||||
"ForceDisconnectSWCD" : true,
|
||||
"RestartReminderEnabled" : true,
|
||||
"RestartReminderWithText" : true,
|
||||
"RestartReminderWithBadge" : true,
|
||||
"ConnectionAlertsListsElse" : true,
|
||||
])
|
||||
}
|
||||
|
||||
static var AutoDeleteLogsDays: Int {
|
||||
get { Int("AutoDeleteLogsDays") }
|
||||
set { Int("AutoDeleteLogsDays", newValue) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Recording State
|
||||
|
||||
enum CurrentRecordingState : Int {
|
||||
case Off = 0, App = 1, Background = 2
|
||||
}
|
||||
|
||||
extension PrefsShared {
|
||||
static var CurrentlyRecording: CurrentRecordingState {
|
||||
get { CurrentRecordingState(rawValue: Int("CurrentlyRecording")) ?? .Off }
|
||||
set { Int("CurrentlyRecording", newValue.rawValue) }
|
||||
}
|
||||
static var ForceDisconnectUnresolvableDNS: Bool {
|
||||
get { PrefsShared.Bool("ForceDisconnectUnresolvableDNS") }
|
||||
set { PrefsShared.Bool("ForceDisconnectUnresolvableDNS", newValue) }
|
||||
}
|
||||
static var ForceDisconnectSWCD: Bool {
|
||||
get { PrefsShared.Bool("ForceDisconnectSWCD") }
|
||||
set { PrefsShared.Bool("ForceDisconnectSWCD", newValue) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
extension PrefsShared {
|
||||
enum RestartReminder {
|
||||
static var Enabled: Bool {
|
||||
get { PrefsShared.Bool("RestartReminderEnabled") }
|
||||
set { PrefsShared.Bool("RestartReminderEnabled", newValue) }
|
||||
}
|
||||
static var WithText: Bool {
|
||||
get { PrefsShared.Bool("RestartReminderWithText") }
|
||||
set { PrefsShared.Bool("RestartReminderWithText", newValue) }
|
||||
}
|
||||
static var WithBadge: Bool {
|
||||
get { PrefsShared.Bool("RestartReminderWithBadge") }
|
||||
set { PrefsShared.Bool("RestartReminderWithBadge", newValue) }
|
||||
}
|
||||
static var Sound: String {
|
||||
get { PrefsShared.Str("RestartReminderSound") ?? "#default" }
|
||||
set { PrefsShared.Str("RestartReminderSound", newValue) }
|
||||
}
|
||||
}
|
||||
enum ConnectionAlerts {
|
||||
static var Enabled: Bool {
|
||||
get { PrefsShared.Bool("ConnectionAlertsEnabled") }
|
||||
set { PrefsShared.Bool("ConnectionAlertsEnabled", newValue) }
|
||||
}
|
||||
static var Sound: String {
|
||||
get { PrefsShared.Str("ConnectionAlertsSound") ?? "#default" }
|
||||
set { PrefsShared.Str("ConnectionAlertsSound", newValue) }
|
||||
}
|
||||
static var ExcludeMode: Bool {
|
||||
get { PrefsShared.Bool("ConnectionAlertsExcludeMode") }
|
||||
set { PrefsShared.Bool("ConnectionAlertsExcludeMode", newValue) }
|
||||
}
|
||||
enum Lists {
|
||||
static var CustomA: Bool {
|
||||
get { PrefsShared.Bool("ConnectionAlertsListsCustomA") }
|
||||
set { PrefsShared.Bool("ConnectionAlertsListsCustomA", newValue) }
|
||||
}
|
||||
static var CustomB: Bool {
|
||||
get { PrefsShared.Bool("ConnectionAlertsListsCustomB") }
|
||||
set { PrefsShared.Bool("ConnectionAlertsListsCustomB", newValue) }
|
||||
}
|
||||
static var Blocked: Bool {
|
||||
get { PrefsShared.Bool("ConnectionAlertsListsBlocked") }
|
||||
set { PrefsShared.Bool("ConnectionAlertsListsBlocked", newValue) }
|
||||
}
|
||||
static var Else: Bool {
|
||||
get { PrefsShared.Bool("ConnectionAlertsListsElse") }
|
||||
set { PrefsShared.Bool("ConnectionAlertsListsElse", newValue) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
58
main/Common Classes/QuickUI.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
import UIKit
|
||||
|
||||
struct QuickUI {
|
||||
|
||||
static func label(_ str: String, frame: CGRect = CGRect.zero, align: NSTextAlignment = .natural, style: UIFont.TextStyle = .body) -> UILabel {
|
||||
let x = UILabel(frame: frame)
|
||||
x.text = str
|
||||
x.textAlignment = align
|
||||
x.font = .preferredFont(forTextStyle: style)
|
||||
x.constrainHuggingCompression(.horizontal, .defaultLow)
|
||||
x.constrainHuggingCompression(.vertical, .defaultHigh)
|
||||
x.sizeToFit()
|
||||
if #available(iOS 10.0, *) {
|
||||
x.adjustsFontForContentSizeCategory = true
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
static func button(_ title: String, target: Any? = nil, action: Selector? = nil) -> UIButton {
|
||||
let x = UIButton(type: .roundedRect)
|
||||
x.setTitle(title, for: .normal)
|
||||
x.titleLabel?.font = .preferredFont(forTextStyle: .body)
|
||||
x.constrainHuggingCompression(.vertical, .defaultHigh)
|
||||
x.sizeToFit()
|
||||
if let a = action { x.addTarget(target, action: a, for: .touchUpInside) }
|
||||
if #available(iOS 10.0, *) {
|
||||
x.titleLabel?.adjustsFontForContentSizeCategory = true
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
static func image(_ img: UIImage?, frame: CGRect = CGRect.zero) -> UIImageView {
|
||||
let x = UIImageView(frame: frame)
|
||||
x.contentMode = .scaleAspectFit
|
||||
x.image = img
|
||||
return x
|
||||
}
|
||||
|
||||
static func text(_ str: String, frame: CGRect = CGRect.zero) -> UITextView {
|
||||
let x = UITextView(frame: frame)
|
||||
x.font = .preferredFont(forTextStyle: .body) // .systemFont(ofSize: UIFont.systemFontSize)
|
||||
x.isSelectable = false
|
||||
x.isEditable = false
|
||||
x.text = str
|
||||
if #available(iOS 10.0, *) {
|
||||
x.adjustsFontForContentSizeCategory = true
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
static func text(attributed: NSAttributedString, frame: CGRect = CGRect.zero) -> UITextView {
|
||||
let txt = self.text("", frame: frame)
|
||||
txt.attributedText = attributed
|
||||
txt.textContainerInset = .zero
|
||||
//txt.textContainer.lineFragmentPadding = 0 // remove left right padding
|
||||
return txt
|
||||
}
|
||||
}
|
||||
64
main/Common Classes/SearchBarManager.swift
Normal file
@@ -0,0 +1,64 @@
|
||||
import UIKit
|
||||
|
||||
class SearchBarManager: NSObject, UISearchResultsUpdating {
|
||||
|
||||
private(set) var isActive = false
|
||||
private(set) var term = ""
|
||||
private lazy var controller: UISearchController = {
|
||||
let x = UISearchController(searchResultsController: nil)
|
||||
x.searchBar.autocapitalizationType = .none
|
||||
x.searchBar.autocorrectionType = .no
|
||||
x.obscuresBackgroundDuringPresentation = false
|
||||
x.searchResultsUpdater = self
|
||||
return x
|
||||
}()
|
||||
private weak var tvc: UITableViewController?
|
||||
private let onChangeCallback: (String) -> Void
|
||||
|
||||
/// Prepare `UISearchBar` for user input
|
||||
/// - Parameter onChange: Code that will be executed every time the user changes the text (with 0.2s delay)
|
||||
required init(onChange: @escaping (String) -> Void) {
|
||||
onChangeCallback = onChange
|
||||
super.init()
|
||||
|
||||
UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self])
|
||||
.defaultTextAttributes = [.font: UIFont.preferredFont(forTextStyle: .body)]
|
||||
}
|
||||
|
||||
/// Assigns the `UISearchBar` to `tableView.tableHeaderView` (iOS 9) or `navigationItem.searchController` (iOS 11).
|
||||
func fuseWith(tableViewController: UITableViewController?) {
|
||||
guard tvc !== tableViewController else { return }
|
||||
tvc = tableViewController
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
tvc?.navigationItem.searchController = controller
|
||||
} else {
|
||||
let thv = tvc?.tableView.tableHeaderView
|
||||
guard thv == nil || thv is UISearchBar else {
|
||||
// Don't overwrite actions bar (co-occurrence, etc.)
|
||||
// FIXME: find alternative or iOS 9-10 users can't search in hosts
|
||||
tvc = nil
|
||||
return
|
||||
}
|
||||
controller.loadViewIfNeeded() // Fix: "Attempting to load the view of a view controller while it is deallocating"
|
||||
tvc?.definesPresentationContext = true // make search bar disappear if user changes scene (eg. select cell)
|
||||
//tvc?.tableView.backgroundView = UIView() // iOS 11+ bug: bright white background in dark mode
|
||||
tvc?.tableView.tableHeaderView = controller.searchBar
|
||||
tvc?.tableView.setContentOffset(.init(x: 0, y: controller.searchBar.frame.height), animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Search callback
|
||||
internal func updateSearchResults(for controller: UISearchController) {
|
||||
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil)
|
||||
perform(#selector(performSearch), with: nil, afterDelay: 0.2)
|
||||
}
|
||||
|
||||
/// Internal callback function for delayed text evaluation.
|
||||
/// This way we can avoid unnecessary searches while user is typing.
|
||||
@objc private func performSearch() {
|
||||
term = controller.searchBar.text?.lowercased() ?? ""
|
||||
isActive = term.count > 0
|
||||
onChangeCallback(term)
|
||||
}
|
||||
}
|
||||
182
main/Common Classes/SlideInAnimation.swift
Normal file
@@ -0,0 +1,182 @@
|
||||
import UIKit
|
||||
|
||||
enum PresentationEdge { case left, top, right, bottom }
|
||||
|
||||
// ########################################
|
||||
// #
|
||||
// # MARK: - Transitioning Delegate
|
||||
// #
|
||||
// ########################################
|
||||
|
||||
class SlideInTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
|
||||
private var edge: PresentationEdge
|
||||
private var modal: Bool
|
||||
private var dismissable: Bool
|
||||
private var shadow: UIColor?
|
||||
|
||||
init(for edge: PresentationEdge, modal: Bool, tapAnywhereToDismiss: Bool = false, modalBackgroundColor color: UIColor? = nil) {
|
||||
self.edge = edge
|
||||
self.dismissable = tapAnywhereToDismiss
|
||||
self.shadow = color
|
||||
self.modal = modal
|
||||
}
|
||||
|
||||
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
|
||||
StickyPresentationController(presented: presented, presenting: presenting, stickTo: edge, modal: modal, tapAnywhereToDismiss: dismissable, modalBackgroundColor: shadow)
|
||||
}
|
||||
|
||||
func animationController(forPresented _: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
SlideInAnimationController(from: edge, isPresentation: true)
|
||||
}
|
||||
|
||||
func animationController(forDismissed _: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
SlideInAnimationController(from: edge, isPresentation: false)
|
||||
}
|
||||
}
|
||||
|
||||
// ########################################
|
||||
// #
|
||||
// # MARK: - Animated Transitioning
|
||||
// #
|
||||
// ########################################
|
||||
|
||||
private final class SlideInAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
let edge: PresentationEdge
|
||||
let appear: Bool
|
||||
|
||||
init(from edge: PresentationEdge, isPresentation: Bool) {
|
||||
self.edge = edge
|
||||
self.appear = isPresentation
|
||||
super.init()
|
||||
}
|
||||
|
||||
func transitionDuration(using context: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||
(context?.isAnimated ?? true) ? 0.3 : 0.0
|
||||
}
|
||||
|
||||
func animateTransition(using context: UIViewControllerContextTransitioning) {
|
||||
guard let vc = context.viewController(forKey: appear ? .to : .from) else { return }
|
||||
|
||||
var to = context.finalFrame(for: vc)
|
||||
var from = to
|
||||
switch edge {
|
||||
case .left: from.origin.x = -to.width
|
||||
case .right: from.origin.x = context.containerView.frame.width
|
||||
case .top: from.origin.y = -to.height
|
||||
case .bottom: from.origin.y = context.containerView.frame.height
|
||||
}
|
||||
|
||||
if appear { context.containerView.addSubview(vc.view) }
|
||||
else { swap(&from, &to) }
|
||||
|
||||
vc.view.frame = from
|
||||
UIView.animate(withDuration: transitionDuration(using: context), animations: {
|
||||
vc.view.frame = to
|
||||
}, completion: { finished in
|
||||
if !self.appear { vc.view.removeFromSuperview() }
|
||||
context.completeTransition(finished)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// #########################################
|
||||
// #
|
||||
// # MARK: - Presentation Controller
|
||||
// #
|
||||
// #########################################
|
||||
|
||||
private class StickyPresentationController: UIPresentationController {
|
||||
private let stickTo: PresentationEdge
|
||||
private let isModal: Bool
|
||||
|
||||
private let bg = UIView()
|
||||
private var availableSize: CGSize = .zero // save original size when resizing the container
|
||||
|
||||
override var shouldPresentInFullscreen: Bool { false }
|
||||
override var frameOfPresentedViewInContainerView: CGRect { fittedContentFrame() }
|
||||
|
||||
required init(presented: UIViewController, presenting: UIViewController?, stickTo edge: PresentationEdge, modal: Bool = true, tapAnywhereToDismiss: Bool = false, modalBackgroundColor bgColor: UIColor? = nil) {
|
||||
self.stickTo = edge
|
||||
self.isModal = modal
|
||||
super.init(presentedViewController: presented, presenting: presenting)
|
||||
bg.backgroundColor = bgColor ?? .init(white: 0, alpha: 0.5)
|
||||
if modal, tapAnywhereToDismiss {
|
||||
bg.addGestureRecognizer(
|
||||
UITapGestureRecognizer(target: self, action: #selector(didTapBackground))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Present
|
||||
|
||||
override func presentationTransitionWillBegin() {
|
||||
availableSize = containerView!.frame.size
|
||||
|
||||
guard isModal else { return }
|
||||
containerView!.insertSubview(bg, at: 0)
|
||||
bg.alpha = 0.0
|
||||
if presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
|
||||
self.bg.alpha = 1.0
|
||||
}) != true { bg.alpha = 1.0 }
|
||||
}
|
||||
|
||||
@objc func didTapBackground(_ sender: UITapGestureRecognizer) {
|
||||
presentingViewController.dismiss(animated: true)
|
||||
}
|
||||
|
||||
// MARK: Dismiss
|
||||
|
||||
override func dismissalTransitionWillBegin() {
|
||||
if presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
|
||||
self.bg.alpha = 0.0
|
||||
}) != true { bg.alpha = 0.0 }
|
||||
}
|
||||
|
||||
override func dismissalTransitionDidEnd(_ completed: Bool) {
|
||||
if completed { bg.removeFromSuperview() }
|
||||
}
|
||||
|
||||
// MARK: Update
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
availableSize = size
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
}
|
||||
|
||||
override func containerViewDidLayoutSubviews() {
|
||||
super.containerViewDidLayoutSubviews()
|
||||
bg.frame = containerView!.bounds
|
||||
if isModal {
|
||||
presentedView!.frame = fittedContentFrame()
|
||||
} else {
|
||||
containerView!.frame = fittedContentFrame()
|
||||
presentedView!.frame = containerView!.bounds
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate `fittedContentSize()` then offset frame to sticky edge respecting *available* container size .
|
||||
func fittedContentFrame() -> CGRect {
|
||||
var frame = CGRect(origin: .zero, size: fittedContentSize())
|
||||
switch stickTo {
|
||||
case .right: frame.origin.x = availableSize.width - frame.width
|
||||
case .bottom: frame.origin.y = availableSize.height - frame.height
|
||||
default: break
|
||||
}
|
||||
return frame
|
||||
}
|
||||
|
||||
/// Calculate best fitting size for available container size and presentation sticky edge.
|
||||
func fittedContentSize() -> CGSize {
|
||||
guard let target = presentedView else { return availableSize }
|
||||
let full = availableSize
|
||||
let preferred = presentedViewController.preferredContentSize
|
||||
switch stickTo {
|
||||
case .left, .right:
|
||||
let fitted = target.fittingSize(fixedHeight: full.height, preferredWidth: preferred.width)
|
||||
return CGSize(width: min(fitted.width, full.width), height: full.height)
|
||||
case .top, .bottom:
|
||||
let fitted = target.fittingSize(fixedWidth: full.width, preferredHeight: preferred.height)
|
||||
return CGSize(width: full.width, height: min(fitted.height, full.height))
|
||||
}
|
||||
}
|
||||
}
|
||||
31
main/Common Classes/ThrottledBatchQueue.swift
Normal file
@@ -0,0 +1,31 @@
|
||||
import Foundation
|
||||
|
||||
class ThrottledBatchQueue<T> {
|
||||
private var cache: [T] = []
|
||||
private var scheduled: Bool = false
|
||||
private let queue: DispatchQueue
|
||||
private let delay: Double
|
||||
|
||||
init(_ delay: Double, using queue: DispatchQueue) {
|
||||
self.queue = queue
|
||||
self.delay = delay
|
||||
}
|
||||
|
||||
func addDelayed(_ elem: T, afterDelay closure: @escaping ([T]) -> Void) {
|
||||
queue.sync {
|
||||
cache.append(elem)
|
||||
guard !scheduled else {
|
||||
return
|
||||
}
|
||||
scheduled = true
|
||||
queue.asyncAfter(deadline: .now() + delay) {
|
||||
let aCopy = self.cache
|
||||
self.cache.removeAll(keepingCapacity: true)
|
||||
self.scheduled = false
|
||||
DispatchQueue.main.async {
|
||||
closure(aCopy)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
68
main/Common Classes/TinyMarkdown.swift
Normal file
@@ -0,0 +1,68 @@
|
||||
import UIKit
|
||||
|
||||
struct TinyMarkdown {
|
||||
/// Load markdown file and run through a (very) simple parser (see below).
|
||||
/// - Parameters:
|
||||
/// - filename: Will automatically append `.md` extension
|
||||
/// - replacements: Replace a single occurrence of search string with an attributed replacement.
|
||||
static func load(_ filename: String, replacements: [String : NSMutableAttributedString] = [:]) -> UITextView {
|
||||
let url = Bundle.main.url(forResource: filename, withExtension: "md")!
|
||||
let str = NSMutableAttributedString(withMarkdown: try! String(contentsOf: url))
|
||||
for (key, val) in replacements {
|
||||
guard let r = str.string.range(of: key) else {
|
||||
QLog.Debug("WARN: markdown key '\(key)' does not exist in \(filename)")
|
||||
continue
|
||||
}
|
||||
str.replaceCharacters(in: NSRange(r, in: str.string), with: val)
|
||||
}
|
||||
return QuickUI.text(attributed: str)
|
||||
}
|
||||
}
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
/// Supports only: `#h1`, `##h2`, `###h3`, `_italic_`, `__bold__`, `___boldItalic___`
|
||||
convenience init(withMarkdown content: String) {
|
||||
self.init()
|
||||
let emph = try! NSRegularExpression(pattern: #"(?<=(^|\W))(_{1,3})(\S|\S.*?\S)\2"#, options: [])
|
||||
beginEditing()
|
||||
content.enumerateLines { (line, _) in
|
||||
if line.starts(with: "#") {
|
||||
var h = 0
|
||||
for char in line {
|
||||
if char == "#" { h += 1 }
|
||||
else { break }
|
||||
}
|
||||
var line = line
|
||||
line.removeFirst(h)
|
||||
line = line.trimmingCharacters(in: CharacterSet(charactersIn: " "))
|
||||
switch h {
|
||||
case 1: self.h1(line + "\n")
|
||||
case 2: self.h2(line + "\n")
|
||||
default: self.h3(line + "\n")
|
||||
}
|
||||
} else {
|
||||
let nsline = line as NSString
|
||||
let range = NSRange(location: 0, length: nsline.length)
|
||||
var i = 0
|
||||
for x in emph.matches(in: line, options: [], range: range) {
|
||||
let r = x.range
|
||||
self.normal(nsline.substring(from: i, to: r.location))
|
||||
i = r.upperBound
|
||||
let before = nsline.substring(with: r)
|
||||
let after = before.trimmingCharacters(in: CharacterSet(charactersIn: "_"))
|
||||
switch (before.count - after.count) / 2 {
|
||||
case 1: self.italic(after)
|
||||
case 2: self.bold(after)
|
||||
default: self.boldItalic(after)
|
||||
}
|
||||
}
|
||||
if i < range.length {
|
||||
self.normal(nsline.substring(from: i, to: range.length) + "\n")
|
||||
} else {
|
||||
self.normal("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
endEditing()
|
||||
}
|
||||
}
|
||||
203
main/Common Classes/TutorialSheet.swift
Normal file
@@ -0,0 +1,203 @@
|
||||
import UIKit
|
||||
|
||||
fileprivate var margin: CGFloat { 20 }
|
||||
fileprivate var sheetInset: CGFloat { cornerRadius/2 }
|
||||
fileprivate var cornerRadius: CGFloat { 15 }
|
||||
fileprivate var uniRect: CGRect { CGRect(x: 0, y: 0, width: 500, height: 500) }
|
||||
|
||||
class TutorialSheet: UIViewController, UIScrollViewDelegate {
|
||||
|
||||
/// Maximum displayable width of a Tutorial Sheet in portrait mode.
|
||||
public static var verticalWidth: CGFloat {
|
||||
let s = UIScreen.main.bounds.size
|
||||
return min(s.width, s.height) - 2 * (margin + sheetInset)
|
||||
}
|
||||
|
||||
public var buttonTitleNext: String = "Next"
|
||||
public var buttonTitleDone: String = "Close"
|
||||
|
||||
private var priorIndex: Int?
|
||||
private var lastAnchor: NSLayoutConstraint?
|
||||
private var shouldAnimate: Bool = true
|
||||
private var shouldCloseBlock: (() -> Bool)? = nil
|
||||
private var didCloseBlock: (() -> Void)? = nil
|
||||
|
||||
private let sheetBg: UIView = {
|
||||
let x = UIView(frame: uniRect)
|
||||
x.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
x.backgroundColor = .sysBackground
|
||||
x.layer.cornerRadius = cornerRadius
|
||||
x.layer.shadowColor = UIColor.black.cgColor
|
||||
x.layer.shadowRadius = 10
|
||||
x.layer.shadowOpacity = 0.75
|
||||
x.layer.shadowOffset = CGSize(width: 0, height: 4)
|
||||
return x
|
||||
}()
|
||||
|
||||
private let pager: UIPageControl = {
|
||||
let x = UIPageControl(frame: uniRect)
|
||||
x.frame.size.height = x.size(forNumberOfPages: 1).height
|
||||
x.currentPageIndicatorTintColor = UIColor.sysLabel.withAlphaComponent(0.5)
|
||||
x.pageIndicatorTintColor = UIColor.sysLabel.withAlphaComponent(0.25)
|
||||
x.numberOfPages = 0
|
||||
x.hidesForSinglePage = true
|
||||
x.addTarget(self, action: #selector(pagerDidChange), for: .valueChanged)
|
||||
return x
|
||||
}()
|
||||
|
||||
private let pageScroll: UIScrollView = {
|
||||
let x = UIScrollView(frame: uniRect)
|
||||
x.bounces = false
|
||||
x.isPagingEnabled = true
|
||||
x.showsVerticalScrollIndicator = false
|
||||
x.showsHorizontalScrollIndicator = false
|
||||
|
||||
let content = UIView()
|
||||
x.addSubview(content)
|
||||
content.anchor([.left, .right, .top, .bottom], to: x)
|
||||
content.anchor([.width, .height], to: x) | .defaultLow
|
||||
return x
|
||||
}()
|
||||
|
||||
private lazy var button: UIButton = {
|
||||
let x = QuickUI.button("", target: self, action: #selector(buttonTapped))
|
||||
x.contentEdgeInsets = UIEdgeInsets(all: 8)
|
||||
return x
|
||||
}()
|
||||
|
||||
|
||||
// MARK: Init
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
required init() {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
view = makeControlUI()
|
||||
modalPresentationStyle = .custom
|
||||
if #available(iOS 13.0, *) {
|
||||
isModalInPresentation = true
|
||||
}
|
||||
UIDevice.orientationDidChangeNotification.observe(call: #selector(didChangeOrientation), on: self)
|
||||
}
|
||||
|
||||
/// Present Tutorial Sheet Controller
|
||||
/// - Parameter viewController: If set to `nil`, use main application as canvas. (Default: `nil`)
|
||||
/// - Parameter animate: Use `present` and `dismiss` animations. (Default: `true`)
|
||||
/// - Parameter shouldClose: Called before the view controller is dismissed. Return `false` to prevent the dismissal.
|
||||
/// Use this block to extract user data from input fields. (Default: `nil`)
|
||||
/// - Parameter didClose: Called after the view controller is completely dismissed (with animations).
|
||||
/// Use this block to update UI and visible changes. (Default: `nil`)
|
||||
func present(in viewController: UIViewController? = nil, animate: Bool = true, shouldClose: (() -> Bool)? = nil, didClose: (() -> Void)? = nil) {
|
||||
guard let vc = viewController ?? UIApplication.shared.keyWindow?.rootViewController else {
|
||||
return
|
||||
}
|
||||
shouldCloseBlock = shouldClose
|
||||
didCloseBlock = didClose
|
||||
shouldAnimate = animate
|
||||
vc.present(self, animated: animate)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Dynamic UI
|
||||
|
||||
@discardableResult func addSheet(_ closure: ((UIStackView) -> Void)? = nil) -> UIStackView {
|
||||
pager.numberOfPages += 1
|
||||
updateButtonTitle()
|
||||
let x = UIStackView(frame: pageScroll.bounds)
|
||||
x.axis = .vertical
|
||||
x.backgroundColor = UIColor.black
|
||||
x.isOpaque = true
|
||||
guard let content = pageScroll.subviews.first else {
|
||||
return x
|
||||
}
|
||||
let prev = content.subviews.last
|
||||
content.addSubview(x)
|
||||
x.anchor([.top, .height], to: pageScroll)
|
||||
x.widthAnchor =&= sheetBg.widthAnchor - 2 * sheetInset
|
||||
x.leadingAnchor =&= (prev==nil ? content.leadingAnchor : prev!.trailingAnchor)
|
||||
lastAnchor?.isActive = false
|
||||
lastAnchor = (x.trailingAnchor =&= pageScroll.trailingAnchor)
|
||||
closure?(x)
|
||||
return x
|
||||
}
|
||||
|
||||
|
||||
// MARK: Static UI
|
||||
|
||||
private func makeControlUI() -> UIView {
|
||||
pageScroll.delegate = self
|
||||
|
||||
sheetBg.addSubview(pager)
|
||||
sheetBg.addSubview(pageScroll)
|
||||
sheetBg.addSubview(button)
|
||||
|
||||
pager.anchor([.top, .left, .right], to: sheetBg)
|
||||
pageScroll.topAnchor =&= pager.bottomAnchor | .defaultHigh
|
||||
pageScroll.anchor([.left, .right, .top, .bottom], to: sheetBg, margin: sheetInset) | .defaultHigh
|
||||
button.topAnchor =&= pageScroll.bottomAnchor | .defaultHigh
|
||||
button.anchor([.bottom, .centerX], to: sheetBg)
|
||||
// button.bottomAnchor =&= sheetBg.bottomAnchor - 30
|
||||
// button.centerXAnchor =&= sheetBg.centerXAnchor
|
||||
|
||||
let bg = UIView(frame: uniRect)
|
||||
bg.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
bg.addSubview(sheetBg)
|
||||
|
||||
let h: CGFloat = UIApplication.shared.isStatusBarHidden ? 0 : UIApplication.shared.statusBarFrame.height
|
||||
sheetBg.frame = bg.frame.inset(by: UIEdgeInsets(all: margin, top: margin + h))
|
||||
return bg
|
||||
}
|
||||
|
||||
|
||||
// MARK: Delegates
|
||||
|
||||
override func viewWillLayoutSubviews() {
|
||||
priorIndex = pager.currentPage
|
||||
}
|
||||
|
||||
@objc private func didChangeOrientation() {
|
||||
if let i = priorIndex {
|
||||
priorIndex = nil
|
||||
switchToSheet(i, animated: false)
|
||||
}
|
||||
for case let x as UIStackView in pageScroll.subviews.first!.subviews {
|
||||
x.axis = (x.frame.width > x.frame.height) ? .horizontal : .vertical
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
let w = scrollView.frame.width
|
||||
let new = Int((scrollView.contentOffset.x + w/2) / w)
|
||||
if pager.currentPage != new {
|
||||
pager.currentPage = new
|
||||
updateButtonTitle()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func pagerDidChange(sender: UIPageControl) {
|
||||
switchToSheet(sender.currentPage, animated: true)
|
||||
}
|
||||
|
||||
private func switchToSheet(_ i: Int, animated: Bool) {
|
||||
pageScroll.setContentOffset(CGPoint(x: CGFloat(i) * pageScroll.bounds.width, y: 0), animated: animated)
|
||||
}
|
||||
|
||||
private func updateButtonTitle() {
|
||||
let last = (pager.currentPage == pager.numberOfPages - 1)
|
||||
let title = last ? buttonTitleDone : buttonTitleNext
|
||||
if button.title(for: .normal) != title {
|
||||
button.setTitle(title, for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func buttonTapped() {
|
||||
let next = pager.currentPage + 1
|
||||
if next < pager.numberOfPages {
|
||||
switchToSheet(next, animated: true)
|
||||
} else {
|
||||
if shouldCloseBlock?() ?? true {
|
||||
dismiss(animated: shouldAnimate, completion: didCloseBlock)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
505
main/DB/DBAppOnly.swift
Normal file
@@ -0,0 +1,505 @@
|
||||
import Foundation
|
||||
import SQLite3
|
||||
|
||||
typealias Timestamp = sqlite3_int64
|
||||
|
||||
extension SQLiteDatabase {
|
||||
func initAppOnlyScheme() {
|
||||
try? run(sql: CreateTable.heap)
|
||||
try? run(sql: CreateTable.rec)
|
||||
try? run(sql: CreateTable.recLog)
|
||||
do {
|
||||
try migrateDB()
|
||||
} catch {
|
||||
QLog.Error("during migration: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func migrateDB() throws {
|
||||
let version = try run(sql: "PRAGMA user_version;") { stmt -> Int32 in
|
||||
try ifStep(stmt, SQLITE_ROW)
|
||||
return sqlite3_column_int(stmt, 0)
|
||||
}
|
||||
if version != 2 {
|
||||
QLog.Info("migrate db \(version) -> 2")
|
||||
// version 0 -> 1: req(domain) -> heap(fqdn, domain)
|
||||
// version 1 -> 2: rec(+subtitle, +opt)
|
||||
if version == 1 {
|
||||
transaction("""
|
||||
ALTER TABLE rec ADD COLUMN subtitle TEXT;
|
||||
ALTER TABLE rec ADD COLUMN uploadkey TEXT;
|
||||
""")
|
||||
}
|
||||
try run(sql: "PRAGMA user_version = 2;")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum TableName: String {
|
||||
case heap, cache
|
||||
}
|
||||
|
||||
extension SQLiteDatabase {
|
||||
fileprivate func lastRowId(_ table: TableName) -> SQLiteRowID {
|
||||
(try? run(sql:"SELECT rowid FROM \(table.rawValue) ORDER BY rowid DESC LIMIT 1;") {
|
||||
try ifStep($0, SQLITE_ROW)
|
||||
return sqlite3_column_int64($0, 0)
|
||||
}) ?? 0
|
||||
}
|
||||
|
||||
fileprivate func col_ts(_ stmt: OpaquePointer, _ col: Int32) -> Timestamp {
|
||||
sqlite3_column_int64(stmt, col)
|
||||
}
|
||||
}
|
||||
|
||||
class WhereClauseBuilder: CustomStringConvertible {
|
||||
var description: String = ""
|
||||
private let prefix: String
|
||||
private(set) var bindings: [DBBinding] = []
|
||||
|
||||
init(prefix p: String = "WHERE") { prefix = "\(p) " }
|
||||
|
||||
/// Append new clause by either prepending `WHERE` prefix or placing `AND` between clauses.
|
||||
@discardableResult func and(_ clause: String, _ bind: DBBinding ...) -> Self {
|
||||
description.append((description=="" ? prefix : " AND ") + clause)
|
||||
bindings.append(contentsOf: bind)
|
||||
return self
|
||||
}
|
||||
/// Restrict to `rowid >= {range}.start AND rowid <= {range}.end`.
|
||||
/// Omitted if range is `nil` or individually if a value is `0`.
|
||||
@discardableResult func and(in range: SQLiteRowRange) -> Self {
|
||||
if range.start != 0 { and("rowid >= ?", BindInt64(range.start)) }
|
||||
if range.end != 0 { and("rowid <= ?", BindInt64(range.end)) }
|
||||
return self
|
||||
}
|
||||
/// Restrict to `ts >= {min} AND ts < {max}`. Omit one or the other if value is `0`.
|
||||
@discardableResult func and(min: Timestamp = 0, max: Timestamp = 0) -> Self {
|
||||
if min != 0 { and("ts >= ?", BindInt64(min)) }
|
||||
if max != 0 { and("ts < ?", BindInt64(max)) }
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - DNSLog
|
||||
|
||||
extension CreateTable {
|
||||
/// `ts`: Timestamp, `fqdn`: String, `domain`: String, `opt`: Int
|
||||
static var heap: String {"""
|
||||
CREATE TABLE IF NOT EXISTS heap(
|
||||
ts INTEGER DEFAULT (strftime('%s','now')),
|
||||
fqdn TEXT NOT NULL,
|
||||
domain TEXT NOT NULL,
|
||||
opt INTEGER
|
||||
);
|
||||
"""} // opt currently only used as "blocked" flag
|
||||
}
|
||||
|
||||
struct GroupedDomain {
|
||||
let domain: String, total: Int32, blocked: Int32, lastModified: Timestamp
|
||||
var options: FilterOptions? = nil
|
||||
}
|
||||
typealias GroupedTsOccurrence = (ts: Timestamp, total: Int32, blocked: Int32)
|
||||
typealias DomainTsPair = (domain: String, ts: Timestamp)
|
||||
|
||||
extension SQLiteDatabase {
|
||||
|
||||
// MARK: write
|
||||
|
||||
/// Move newest entries from `cache` to `heap` and return range (in `heap`) of newly inserted entries.
|
||||
/// - Returns: `nil` in case no entries were transmitted.
|
||||
@discardableResult func dnsLogsPersist() -> SQLiteRowRange? {
|
||||
guard lastRowId(.cache) > 0 else { return nil }
|
||||
let before = lastRowId(.heap) + 1
|
||||
createFunction("domainof") { ($0.first as! String).extractDomain() }
|
||||
transaction("""
|
||||
INSERT INTO heap(ts,fqdn,domain,opt) SELECT ts,dns,domainof(dns),nullif(opt&1,0) FROM cache;
|
||||
DELETE FROM cache;
|
||||
""")
|
||||
let after = lastRowId(.heap)
|
||||
return (before > after) ? nil : (before, after)
|
||||
}
|
||||
|
||||
/// `DELETE FROM cache; DELETE FROM heap;`
|
||||
func dnsLogsDeleteAll() throws {
|
||||
try? run(sql: "DELETE FROM cache; DELETE FROM heap;")
|
||||
vacuum()
|
||||
}
|
||||
|
||||
/// Delete rows matching `ts >= ? AND domain = ?`
|
||||
/// - Parameter strict: If `true`, use `fqdn` instead of `domain` column
|
||||
/// - Returns: Number of changes aka. Number of rows deleted
|
||||
@discardableResult func dnsLogsDelete(_ domain: String, strict: Bool, since ts: Timestamp = 0) -> Int32 {
|
||||
let Where = WhereClauseBuilder().and(min: ts)
|
||||
Where.and("\(strict ? "fqdn" : "domain") = ?", BindText(domain)) // (fqdn = ? OR fqdn LIKE '%.' || ?)
|
||||
return (try? run(sql: "DELETE FROM heap \(Where);", bind: Where.bindings) { stmt -> Int32 in
|
||||
try ifStep(stmt, SQLITE_DONE)
|
||||
return numberOfChanges
|
||||
}) ?? 0
|
||||
}
|
||||
|
||||
// MARK: read
|
||||
|
||||
/// `SELECT min(ts) FROM heap`
|
||||
func dnsLogsMinDate() -> Timestamp? {
|
||||
try? run(sql:"SELECT min(ts) FROM heap") {
|
||||
try ifStep($0, SQLITE_ROW)
|
||||
return col_ts($0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Select min and max row id with given condition `ts >= ? AND ts < ?`
|
||||
/// - Parameters:
|
||||
/// - ts1: Restrict min `rowid` to `ts >= ?`. Pass `0` to omit restriction.
|
||||
/// - ts2: Restrict max `rowid` to `ts < ?`. Pass `0` to omit restriction.
|
||||
/// - range: If set, only look at the specified range. Default: `(0,0)`
|
||||
/// - Returns: `nil` in case no rows are matching the condition
|
||||
func dnsLogsRowRange(between ts1: Timestamp, and ts2: Timestamp, within range: SQLiteRowRange = (0,0)) -> SQLiteRowRange? {
|
||||
let Where = WhereClauseBuilder().and(in: range).and(min: ts1, max: ts2)
|
||||
return try? run(sql:"SELECT min(rowid), max(rowid) FROM heap \(Where);", bind: Where.bindings) {
|
||||
try ifStep($0, SQLITE_ROW)
|
||||
let max = col_ts($0, 1)
|
||||
return (max == 0) ? nil : (col_ts($0, 0), max)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get raw logs between two timestamps. `ts >= ? AND ts <= ?`
|
||||
/// - Returns: List sorted by `ts` in descending order (newest entries first).
|
||||
func dnsLogs(between ts1: Timestamp, and ts2: Timestamp) -> [DomainTsPair]? {
|
||||
try? run(sql: "SELECT fqdn, ts FROM heap WHERE ts >= ? AND ts <= ? ORDER BY ts DESC, rowid ASC;",
|
||||
bind: [BindInt64(ts1), BindInt64(ts2)]) {
|
||||
allRows($0) {
|
||||
(col_text($0, 0) ?? "", col_ts($0, 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Group DNS logs by domain, count occurences and number of blocked requests.
|
||||
/// - Parameters:
|
||||
/// - range: Whenever possible set range to improve SQL lookup times. `start <= rowid <= end `
|
||||
/// - matchingDomain: Restrict `(fqdn|domain) = ?`. Which column is used is determined by `parentDomain`.
|
||||
/// - parentDomain: If `nil` returns `domain` column. Else returns `fqdn` column with restriction on `domain == parentDomain`.
|
||||
/// - Returns: List of grouped domains with no particular sorting order.
|
||||
func dnsLogsGrouped(range: SQLiteRowRange, matchingDomain: String? = nil, parentDomain: String? = nil) -> [GroupedDomain]? {
|
||||
let Where = WhereClauseBuilder().and(in: range)
|
||||
let col: String // fqdn or domain
|
||||
if let parent = parentDomain { // is subdomain
|
||||
col = "fqdn"
|
||||
Where.and("domain = ?", BindText(parent))
|
||||
} else {
|
||||
col = "domain"
|
||||
}
|
||||
if let matching = matchingDomain { // (fqdn = ? OR fqdn LIKE '%.' || ?)
|
||||
Where.and("\(col) = ?", BindText(matching))
|
||||
}
|
||||
return try? run(sql: "SELECT \(col), COUNT(*), COUNT(opt), MAX(ts) FROM heap \(Where) GROUP BY \(col);", bind: Where.bindings) {
|
||||
allRows($0) {
|
||||
GroupedDomain(domain: col_text($0, 0) ?? "",
|
||||
total: sqlite3_column_int($0, 1),
|
||||
blocked: sqlite3_column_int($0, 2),
|
||||
lastModified: col_ts($0, 3))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get list or individual DNS entries. Mutliple entries in the very same second are grouped.
|
||||
/// - Parameters:
|
||||
/// - fqdn: Exact match for domain name `fqdn = ?`
|
||||
/// - range: Whenever possible set range to improve SQL lookup times. `start <= rowid <= end `
|
||||
/// - Returns: List sorted by reverse timestamp order (newest first)
|
||||
func timesForDomain(_ fqdn: String, range: SQLiteRowRange) -> [GroupedTsOccurrence]? {
|
||||
let Where = WhereClauseBuilder().and(in: range).and("fqdn = ?", BindText(fqdn))
|
||||
return try? run(sql: "SELECT ts, COUNT(ts), COUNT(opt) FROM heap \(Where) GROUP BY ts ORDER BY ts DESC;", bind: Where.bindings) {
|
||||
allRows($0) {
|
||||
(col_ts($0, 0), sqlite3_column_int($0, 1), sqlite3_column_int($0, 2))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - Context Analysis
|
||||
|
||||
typealias ContextAnalysisResult = (domain: String, count: Int32, avg: Double, rank: Double)
|
||||
|
||||
extension SQLiteDatabase {
|
||||
/// Number of times how often given `fqdn` appears in the database
|
||||
func dnsLogsCount(fqdn: String) -> Int? {
|
||||
try? run(sql: "SELECT COUNT(*) FROM heap WHERE fqdn = ?;", bind: [BindText(fqdn)]) {
|
||||
try ifStep($0, SQLITE_ROW)
|
||||
return Int(sqlite3_column_int($0, 0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get sorted, unique list of `ts` with given `fqdn`.
|
||||
func dnsLogsUniqTs(_ domain: String, isFQDN flag: Bool) -> [Timestamp]? {
|
||||
try? run(sql: "SELECT DISTINCT ts FROM heap WHERE \(flag ? "fqdn" : "domain") = ? ORDER BY ts;",
|
||||
bind: [BindText(domain)]) {
|
||||
allRows($0) { col_ts($0, 0) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Find other domains occurring regularly at roughly the same time as `fqdn`.
|
||||
/// - Warning: `times` list must be **sorted** by time in ascending order.
|
||||
/// - Parameters:
|
||||
/// - times: List of `ts` from `dnsLogsUniqTs(fqdn)`
|
||||
/// - dt: Search for `ts - dt <= X <= ts + dt`
|
||||
/// - fqdn: Rows matching this domain will be excluded from the result set.
|
||||
/// - Returns: List of tuples ordered by rank (ASC).
|
||||
func contextAnalysis(coOccurrence times: [Timestamp], plusMinus dt: Timestamp, exclude domain: String, isFQDN flag: Bool) -> [ContextAnalysisResult]? {
|
||||
guard times.count > 0 else { return nil }
|
||||
createFunction("fnDist") {
|
||||
let x = $0.first as! Timestamp
|
||||
let i = times.binTreeIndex(of: x, compare: <)!
|
||||
let dist: Timestamp
|
||||
switch i {
|
||||
case 0: dist = times[0] - x
|
||||
case times.count: dist = x - times[i-1]
|
||||
default: dist = min(times[i] - x, x - times[i-1])
|
||||
}
|
||||
return dist
|
||||
}
|
||||
// `avg ^ 2`: prefer results that are closer to `times`
|
||||
// `_ / count`: prefer results with higher occurrence count
|
||||
// `time / 2`: Weighting factor (low: prefer close, high: prefer count)
|
||||
// `time` helpful esp. for smaller spans. `avg^2` will raise faster anyway.
|
||||
let fnRank = "(avg * avg + (? / 2.0) + 1) / count" // +1 in case time == 0 -> avg^2 == 0
|
||||
// improve query by excluding entries that are: before the first, or after the last ts
|
||||
let low = times.first! - dt
|
||||
let high = times.last! + dt
|
||||
return try? run(sql: """
|
||||
SELECT fqdn, count, avg, (\(fnRank)) rank FROM (
|
||||
SELECT fqdn, COUNT(*) count, AVG(dist) avg FROM (
|
||||
SELECT fqdn, fnDist(ts) dist FROM heap
|
||||
WHERE ts BETWEEN ? AND ? AND \(flag ? "fqdn" : "domain") != ? AND dist <= ?
|
||||
) GROUP BY fqdn
|
||||
) ORDER BY rank ASC LIMIT 99;
|
||||
""", bind: [BindInt64(dt), BindInt64(low), BindInt64(high), BindText(domain), BindInt64(dt)]) {
|
||||
allRows($0) {
|
||||
(col_text($0, 0) ?? "", sqlite3_column_int($0, 1), sqlite3_column_double($0, 2), sqlite3_column_double($0, 3))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - Recordings
|
||||
|
||||
extension CreateTable {
|
||||
/// `id`: Primary, `start`: Timestamp, `stop`: Timestamp, `appid`: String, `title`: String, `subtitle`: String, `notes`: String
|
||||
static var rec: String {"""
|
||||
CREATE TABLE IF NOT EXISTS rec(
|
||||
id INTEGER PRIMARY KEY,
|
||||
start INTEGER DEFAULT (strftime('%s','now')),
|
||||
stop INTEGER,
|
||||
appid TEXT,
|
||||
title TEXT,
|
||||
subtitle TEXT,
|
||||
notes TEXT,
|
||||
uploadkey TEXT
|
||||
);
|
||||
"""}
|
||||
}
|
||||
|
||||
let readRecordingSelect = "id, start, stop, appid, title, subtitle, notes, uploadkey"
|
||||
struct Recording {
|
||||
let id: sqlite3_int64
|
||||
let start: Timestamp
|
||||
let stop: Timestamp?
|
||||
var appId: String? = nil
|
||||
var title: String? = nil
|
||||
var subtitle: String? = nil
|
||||
var notes: String? = nil
|
||||
var uploadkey: String? = nil
|
||||
}
|
||||
typealias AppBundleInfo = (bundleId: String, name: String?, author: String?)
|
||||
|
||||
extension SQLiteDatabase {
|
||||
|
||||
// MARK: write
|
||||
|
||||
/// Create new recording with `stop` set to `NULL`.
|
||||
func recordingStartNew() throws -> Recording {
|
||||
try run(sql: "INSERT INTO rec (stop) VALUES (NULL);") { stmt -> Recording in
|
||||
try ifStep(stmt, SQLITE_DONE)
|
||||
return try recordingGet(withID: lastInsertedRow)
|
||||
}
|
||||
}
|
||||
|
||||
/// Update given recording by setting `stop` to current time.
|
||||
func recordingStop(_ r: inout Recording) {
|
||||
guard r.stop == nil else { return }
|
||||
let theID = r.id
|
||||
try? run(sql: "UPDATE rec SET stop = (strftime('%s','now')) WHERE id = ? LIMIT 1;",
|
||||
bind: [BindInt64(theID)]) { stmt -> Void in
|
||||
try ifStep(stmt, SQLITE_DONE)
|
||||
r = try recordingGet(withID: theID)
|
||||
}
|
||||
}
|
||||
|
||||
/// Update given recording by replacing `title`, `appid`, and `notes` with new values.
|
||||
func recordingUpdate(_ r: Recording) {
|
||||
try? run(sql: "UPDATE rec SET appid = ?, title = ?, subtitle = ?, notes = ?, uploadkey = ? WHERE id = ? LIMIT 1;",
|
||||
bind: [BindTextOrNil(r.appId), BindTextOrNil(r.title), BindTextOrNil(r.subtitle),
|
||||
BindTextOrNil(r.notes), BindTextOrNil(r.uploadkey), BindInt64(r.id)]) { stmt -> Void in
|
||||
sqlite3_step(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete recording and all of its entries.
|
||||
/// - Returns: `true` on success
|
||||
func recordingDelete(_ r: Recording) throws -> Bool {
|
||||
_ = try? recordingLogsDelete(r.id)
|
||||
return try run(sql: "DELETE FROM rec WHERE id = ? LIMIT 1;", bind: [BindInt64(r.id)]) {
|
||||
try ifStep($0, SQLITE_DONE)
|
||||
return numberOfChanges > 0
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: read
|
||||
|
||||
private func readRecording(_ stmt: OpaquePointer) -> Recording {
|
||||
let end = col_ts(stmt, 2)
|
||||
return Recording(id: sqlite3_column_int64(stmt, 0),
|
||||
start: col_ts(stmt, 1),
|
||||
stop: end == 0 ? nil : end,
|
||||
appId: col_text(stmt, 3),
|
||||
title: col_text(stmt, 4),
|
||||
subtitle: col_text(stmt, 5),
|
||||
notes: col_text(stmt, 6),
|
||||
uploadkey: col_text(stmt, 7))
|
||||
}
|
||||
|
||||
/// `WHERE stop IS NULL`
|
||||
func recordingGetOngoing() -> Recording? {
|
||||
try? run(sql: "SELECT \(readRecordingSelect) FROM rec WHERE stop IS NULL LIMIT 1;") {
|
||||
try ifStep($0, SQLITE_ROW)
|
||||
return readRecording($0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get `Timestamp` of last recording.
|
||||
func recordingLastTimestamp() -> Timestamp? {
|
||||
try? run(sql: "SELECT stop FROM rec WHERE stop IS NOT NULL ORDER BY rowid DESC LIMIT 1;") {
|
||||
try ifStep($0, SQLITE_ROW)
|
||||
return col_ts($0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// `WHERE stop IS NOT NULL`
|
||||
func recordingGetAll() -> [Recording]? {
|
||||
try? run(sql: "SELECT \(readRecordingSelect) FROM rec WHERE stop IS NOT NULL;") {
|
||||
allRows($0) { readRecording($0) }
|
||||
}
|
||||
}
|
||||
|
||||
/// `WHERE id = ?`
|
||||
private func recordingGet(withID: sqlite3_int64) throws -> Recording {
|
||||
try run(sql: "SELECT \(readRecordingSelect) FROM rec WHERE id = ? LIMIT 1;", bind: [BindInt64(withID)]) {
|
||||
try ifStep($0, SQLITE_ROW)
|
||||
return readRecording($0)
|
||||
}
|
||||
}
|
||||
|
||||
func appBundleList() -> [AppBundleInfo]? {
|
||||
try? run(sql: "SELECT appid, title, subtitle FROM rec WHERE appid IS NOT NULL GROUP BY appid ORDER BY lower(title) ASC;") {
|
||||
allRows($0) {
|
||||
AppBundleInfo(col_text($0, 0)!, col_text($0, 1), col_text($0, 2))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - RecordingLog
|
||||
|
||||
extension CreateTable {
|
||||
/// `rid`: Reference `rec(id)`, `ts`: Timestamp, `domain`: String
|
||||
static var recLog: String {"""
|
||||
CREATE TABLE IF NOT EXISTS recLog(
|
||||
rid INTEGER REFERENCES rec(id) ON DELETE CASCADE,
|
||||
ts INTEGER,
|
||||
domain TEXT
|
||||
);
|
||||
"""}
|
||||
}
|
||||
|
||||
extension SQLiteDatabase {
|
||||
|
||||
// MARK: write
|
||||
|
||||
/// Duplicate and copy all log entries for given recording to `recLog` table
|
||||
func recordingLogsPersist(_ r: Recording) {
|
||||
guard let end = r.stop else { return }
|
||||
// TODO: make sure cache entries get copied too.
|
||||
// either by copying them directly from cache or perform sync first
|
||||
try? run(sql: """
|
||||
INSERT INTO recLog (rid, ts, domain) SELECT ?, ts, fqdn FROM heap
|
||||
WHERE heap.ts >= ? AND heap.ts <= ?
|
||||
""", bind: [BindInt64(r.id), BindInt64(r.start), BindInt64(end)]) {
|
||||
try ifStep($0, SQLITE_DONE)
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete all log entries with given recording id. Optional: only delete entries for a single domain
|
||||
/// - Parameter d: If `nil` remove all entries for given recording
|
||||
/// - Returns: Number of deleted rows
|
||||
func recordingLogsDelete(_ recId: sqlite3_int64, matchingDomain d: String? = nil) throws -> Int32 {
|
||||
try run(sql: "DELETE FROM recLog WHERE rid = ? \(d==nil ? "" : "AND domain = ?");",
|
||||
bind: [BindInt64(recId), d==nil ? nil : BindText(d!)]) {
|
||||
try ifStep($0, SQLITE_DONE)
|
||||
return numberOfChanges
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete one recording log entry with given `recording id`, matching `domain`, and `ts`.
|
||||
/// - Returns: `true` if row was deleted
|
||||
func recordingLogsDelete(_ recId: sqlite3_int64, singleEntry ts: Timestamp, domain: String) throws -> Bool {
|
||||
try run(sql: "DELETE FROM recLog WHERE rid = ? AND ts = ? AND domain = ? LIMIT 1;",
|
||||
bind: [BindInt64(recId), BindInt64(ts), BindText(domain)]) {
|
||||
try ifStep($0, SQLITE_DONE)
|
||||
return numberOfChanges > 0
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: read
|
||||
|
||||
/// List of domains and count occurences for given recording.
|
||||
/// - Returns: List of `(domain, ts)` pairs. Sorted by `ts` in ascending order (oldest first)
|
||||
func recordingLogsGet(_ r: Recording) -> [DomainTsPair]? {
|
||||
try? run(sql: "SELECT domain, ts FROM recLog WHERE rid = ? ORDER BY ts ASC, rowid DESC;",
|
||||
bind: [BindInt64(r.id)]) {
|
||||
allRows($0) { (col_text($0, 0) ?? "", col_ts($0, 1)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - DBSettings
|
||||
|
||||
//extension CreateTable {
|
||||
// static var settings: String {
|
||||
// "CREATE TABLE IF NOT EXISTS settings(key TEXT UNIQUE NOT NULL, val TEXT);"
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension SQLiteDatabase {
|
||||
// func getSetting(for key: String) -> String? {
|
||||
// try? run(sql: "SELECT val FROM settings WHERE key = ?;",
|
||||
// bind: [BindText(key)]) { readText($0, 0) }
|
||||
// }
|
||||
// func setSetting(_ value: String?, for key: String) {
|
||||
// if let value = value {
|
||||
// try? run(sql: "INSERT OR REPLACE INTO settings (key, val) VALUES (?, ?);",
|
||||
// bind: [BindText(value), BindText(key)]) { step($0) }
|
||||
// } else {
|
||||
// try? run(sql: "DELETE FROM settings WHERE key = ?;",
|
||||
// bind: [BindText(key)]) { step($0) }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||