mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
style(apple): Add SwiftLint (#7909)
Adds SwiftLint using (mostly) default rules to our build pipeline. Unfortunately, this results in quite a lot of errors / warnings. All fixes included in this PR are simply refactoring. No functionality has been changed. Fixes: #4302
This commit is contained in:
7
swift/apple/.swiftlint.yml
Normal file
7
swift/apple/.swiftlint.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
disabled_rules:
|
||||
# We use the TODO convention throughout the Firezone codebase. It doesn't
|
||||
# necessarily imply immediate action needs to be taken.
|
||||
- todo
|
||||
excluded:
|
||||
- FirezoneNetworkExtension/Connlib/Generated/connlib-client-apple/connlib-client-apple.swift
|
||||
- FirezoneNetworkExtension/Connlib/Generated/SwiftBridgeCore.swift
|
||||
@@ -303,8 +303,8 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 8DCC024328D512AE007E12D2 /* Build configuration list for PBXNativeTarget "Firezone" */;
|
||||
buildPhases = (
|
||||
8DE721892ACA295200E395D5 /* swift-format */,
|
||||
8DCC021528D512AC007E12D2 /* Sources */,
|
||||
8D70FAE62D4971E900216473 /* SwiftLint */,
|
||||
8DCC021628D512AC007E12D2 /* Frameworks */,
|
||||
8DCC021728D512AC007E12D2 /* Resources */,
|
||||
8DC169A32CFF77D1006801B5 /* Embed Foundation Extensions */,
|
||||
@@ -359,6 +359,7 @@
|
||||
mainGroup = 8DCC021028D512AC007E12D2;
|
||||
packageReferences = (
|
||||
8D4087C72D2464D6005B2BAF /* XCRemoteSwiftPackageReference "sentry-cocoa" */,
|
||||
8D70FAE52D49714500216473 /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */,
|
||||
);
|
||||
productRefGroup = 8DCC021A28D512AC007E12D2 /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -399,6 +400,25 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
8D70FAE62D4971E900216473 /* SwiftLint */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = SwiftLint;
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if [[ \"$(uname -m)\" == arm64 ]]\nthen\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif command -v swiftlint >/dev/null 2>&1\nthen\n swiftlint --autocorrect\n swiftlint --strict\nelse\n echo \"warning: `swiftlint` command not found - See https://github.com/realm/SwiftLint#installation for installation instructions.\"\nfi\n";
|
||||
};
|
||||
8D8555822D0A796100A1EA09 /* Build Connlib */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
@@ -439,25 +459,6 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "cd ../../rust/connlib/clients/apple\n./build-rust.sh\n";
|
||||
};
|
||||
8DE721892ACA295200E395D5 /* swift-format */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "swift-format";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "./lint.sh\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -951,6 +952,14 @@
|
||||
minimumVersion = 8.42.1;
|
||||
};
|
||||
};
|
||||
8D70FAE52D49714500216473 /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/SimplyDanny/SwiftLintPlugins";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.58.2;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
|
||||
@@ -8,6 +8,15 @@
|
||||
"revision" : "f45e9c62d7a4d9258ac3cf35a3acf9dbab4481d1",
|
||||
"version" : "8.43.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftlintplugins",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SimplyDanny/SwiftLintPlugins",
|
||||
"state" : {
|
||||
"revision" : "7a3d77f3dd9f91d5cea138e52c20cfceabf352de",
|
||||
"version" : "0.58.2"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
|
||||
@@ -20,6 +20,6 @@ let package = Package(
|
||||
.testTarget(
|
||||
name: "FirezoneKitTests",
|
||||
dependencies: ["FirezoneKit"]
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@@ -27,9 +27,9 @@ public class DeviceMetadata {
|
||||
// Returns the OS version. Must be valid ASCII.
|
||||
// See https://github.com/firezone/firezone/issues/3034
|
||||
// See https://github.com/firezone/firezone/issues/5467
|
||||
let os = ProcessInfo.processInfo.operatingSystemVersion
|
||||
let osVersion = ProcessInfo.processInfo.operatingSystemVersion
|
||||
|
||||
return "\(os.majorVersion).\(os.minorVersion).\(os.patchVersion)"
|
||||
return "\(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)"
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
@@ -71,7 +71,12 @@ func getDeviceInfo(key: CFString) -> String? {
|
||||
let platformExpert = IOServiceGetMatchingService(kIOMainPortDefault, matchingDict)
|
||||
defer { IOObjectRelease(platformExpert) }
|
||||
|
||||
if let serial = IORegistryEntryCreateCFProperty(platformExpert, key, kCFAllocatorDefault, 0)?.takeUnretainedValue() as? String {
|
||||
if let serial = IORegistryEntryCreateCFProperty(
|
||||
platformExpert,
|
||||
key,
|
||||
kCFAllocatorDefault,
|
||||
0
|
||||
)?.takeUnretainedValue() as? String {
|
||||
return serial
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ public final class Log {
|
||||
including: [
|
||||
.totalFileAllocatedSizeKey,
|
||||
.totalFileSizeKey,
|
||||
.isRegularFileKey,
|
||||
.isRegularFileKey
|
||||
]
|
||||
) { url, resourceValues in
|
||||
taskGroup.addTask {
|
||||
@@ -161,9 +161,9 @@ private final class LogWriter {
|
||||
.appendingPathExtension("jsonl")
|
||||
|
||||
// Create log file
|
||||
guard fileManager.createFile(atPath: logFileURL.path, contents: "".data(using: .utf8)),
|
||||
guard fileManager.createFile(atPath: logFileURL.path, contents: Data()),
|
||||
let handle = try? FileHandle(forWritingTo: logFileURL),
|
||||
let _ = try? handle.seekToEnd()
|
||||
(try? handle.seekToEnd()) != nil
|
||||
else {
|
||||
logger.error("Could not create log file: \(logFileURL.path)")
|
||||
return nil
|
||||
@@ -187,14 +187,13 @@ private final class LogWriter {
|
||||
severity: severity,
|
||||
message: message)
|
||||
|
||||
guard var jsonData = try? jsonEncoder.encode(logEntry),
|
||||
let newLineData = "\n".data(using: .utf8)
|
||||
else {
|
||||
guard var jsonData = try? jsonEncoder.encode(logEntry)
|
||||
else {
|
||||
logger.error("Could not encode log message to JSON!")
|
||||
return
|
||||
}
|
||||
|
||||
jsonData.append(newLineData)
|
||||
jsonData.append(Data("\n".utf8))
|
||||
|
||||
workQueue.async { [weak self] in
|
||||
self?.handle.write(jsonData)
|
||||
|
||||
@@ -6,111 +6,60 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
public struct VersionInfo: Decodable {
|
||||
let apple: SemVerString
|
||||
|
||||
public static func from(data: Data?) -> VersionInfo? {
|
||||
guard let data = data,
|
||||
let versionString = String(data: data, encoding: .utf8),
|
||||
let versionString = versionString.data(using: .utf8),
|
||||
let versionInfo = try? JSONDecoder().decode(VersionInfo.self, from: versionString) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return versionInfo
|
||||
struct SemanticVersion: Comparable, CustomStringConvertible {
|
||||
var description: String {
|
||||
return "\(major).\(minor).\(patch)"
|
||||
}
|
||||
}
|
||||
|
||||
struct SemVerString: Decodable, Comparable {
|
||||
private let originalString: String
|
||||
private let semVer: SemanticVersion
|
||||
|
||||
private init(originalString: String, semVer: SemanticVersion) {
|
||||
self.originalString = originalString
|
||||
self.semVer = semVer
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let versionString = try container.decode(String.self)
|
||||
|
||||
guard let parsed = SemanticVersion.from(string: versionString) else {
|
||||
throw DecodingError.dataCorruptedError(in: container,
|
||||
debugDescription: "Invalid SemVer string format")
|
||||
}
|
||||
|
||||
originalString = versionString
|
||||
semVer = parsed
|
||||
}
|
||||
|
||||
public static func from(string: String) -> SemVerString? {
|
||||
guard let parsed = SemanticVersion.from(string: string) else { return nil }
|
||||
return SemVerString(originalString: string, semVer: parsed)
|
||||
}
|
||||
|
||||
public func versionString() -> String {
|
||||
originalString
|
||||
}
|
||||
|
||||
static func < (lhs: SemVerString, rhs: SemVerString) -> Bool {
|
||||
lhs.semVer < rhs.semVer
|
||||
}
|
||||
|
||||
static func == (lhs: SemVerString, rhs: SemVerString) -> Bool {
|
||||
lhs.semVer == rhs.semVer
|
||||
}
|
||||
}
|
||||
|
||||
private struct SemanticVersion: Comparable {
|
||||
let major: Int
|
||||
let minor: Int
|
||||
let patch: Int
|
||||
|
||||
init(major: Int, minor: Int, patch: Int) {
|
||||
self.major = major
|
||||
self.minor = minor
|
||||
self.patch = patch
|
||||
enum Error: Swift.Error {
|
||||
case invalidVersionString
|
||||
}
|
||||
|
||||
private let major: Int
|
||||
private let minor: Int
|
||||
private let patch: Int
|
||||
|
||||
// This doesn't conform to the full semver spec but it's enough for our use-case
|
||||
static func parse(versionString: String) -> (major: Int, minor: Int, patch: Int)? {
|
||||
guard let coreVersion = versionString.components(separatedBy: ["+", "-"]).first else {
|
||||
return nil
|
||||
}
|
||||
init(_ version: String) throws {
|
||||
guard let coreVersion = version.components(separatedBy: ["+", "-"]).first
|
||||
else {
|
||||
throw Error.invalidVersionString
|
||||
}
|
||||
|
||||
let components = coreVersion.split(separator: ".")
|
||||
guard components.count == 3,
|
||||
let major = Int(components[0]),
|
||||
let minor = Int(components[1]),
|
||||
let patch = Int(components[2]) else {
|
||||
return nil
|
||||
}
|
||||
return (major, minor, patch)
|
||||
}
|
||||
let components = coreVersion.split(separator: ".")
|
||||
|
||||
static func from(string: String) -> SemanticVersion? {
|
||||
guard let parsed = parse(versionString: string) else {
|
||||
return nil
|
||||
}
|
||||
return SemanticVersion(major: parsed.major, minor: parsed.minor, patch: parsed.patch)
|
||||
guard components.count == 3
|
||||
else {
|
||||
throw Error.invalidVersionString
|
||||
}
|
||||
|
||||
guard let major = Int(components[0]),
|
||||
let minor = Int(components[1]),
|
||||
let patch = Int(components[2])
|
||||
else {
|
||||
throw Error.invalidVersionString
|
||||
}
|
||||
|
||||
self.major = major
|
||||
self.minor = minor
|
||||
self.patch = patch
|
||||
}
|
||||
|
||||
static func < (lhs: SemanticVersion, rhs: SemanticVersion) -> Bool {
|
||||
if lhs.major != rhs.major {
|
||||
return lhs.major < rhs.major
|
||||
}
|
||||
if lhs.major != rhs.major {
|
||||
return lhs.major < rhs.major
|
||||
}
|
||||
|
||||
if lhs.minor != rhs.minor {
|
||||
return lhs.minor < rhs.minor
|
||||
}
|
||||
if lhs.minor != rhs.minor {
|
||||
return lhs.minor < rhs.minor
|
||||
}
|
||||
|
||||
return lhs.patch < rhs.patch
|
||||
return lhs.patch < rhs.patch
|
||||
}
|
||||
|
||||
static func == (lhs: SemanticVersion, rhs: SemanticVersion) -> Bool {
|
||||
return lhs.major == rhs.major &&
|
||||
lhs.minor == rhs.minor &&
|
||||
lhs.patch == rhs.patch
|
||||
return lhs.major == rhs.major &&
|
||||
lhs.minor == rhs.minor &&
|
||||
lhs.patch == rhs.patch
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,22 +14,22 @@ public enum Telemetry {
|
||||
private static var _firezoneId: String?
|
||||
private static var _accountSlug: String?
|
||||
public static var firezoneId: String? {
|
||||
get {
|
||||
return self._firezoneId
|
||||
}
|
||||
set {
|
||||
self._firezoneId = newValue
|
||||
updateUser(id: self._firezoneId, slug: self._accountSlug)
|
||||
}
|
||||
get {
|
||||
return self._firezoneId
|
||||
}
|
||||
}
|
||||
public static var accountSlug: String? {
|
||||
get {
|
||||
return self._accountSlug
|
||||
}
|
||||
set {
|
||||
self._accountSlug = newValue
|
||||
updateUser(id: self._firezoneId, slug: self._accountSlug)
|
||||
}
|
||||
get {
|
||||
return self._accountSlug
|
||||
}
|
||||
}
|
||||
|
||||
public static func start() {
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
import Foundation
|
||||
|
||||
public enum KeychainError: Error {
|
||||
case securityError(KeychainStatus)
|
||||
case appleSecError(call: String, status: Keychain.SecStatus)
|
||||
case securityError(OSStatus)
|
||||
case appleSecError(call: String, status: OSStatus)
|
||||
case nilResultFromAppleSecCall(call: String)
|
||||
case resultFromAppleSecCallIsInvalid(call: String)
|
||||
case unableToFindSavedItem
|
||||
@@ -20,28 +20,18 @@ public enum KeychainError: Error {
|
||||
public enum Keychain {
|
||||
public typealias PersistentRef = Data
|
||||
|
||||
public enum SecStatus: Equatable {
|
||||
case status(KeychainStatus)
|
||||
case unknownStatus(OSStatus)
|
||||
|
||||
init(_ osStatus: OSStatus) {
|
||||
if let status = KeychainStatus(rawValue: osStatus) {
|
||||
self = .status(status)
|
||||
} else {
|
||||
self = .unknownStatus(osStatus)
|
||||
}
|
||||
}
|
||||
|
||||
var isSuccess: Bool {
|
||||
return self == .status(.success)
|
||||
}
|
||||
enum Result: Int32 {
|
||||
case success = 0
|
||||
case itemNotFound = -25300
|
||||
}
|
||||
|
||||
public static func add(query: [CFString: Any]) throws {
|
||||
var ref: CFTypeRef?
|
||||
let ret = SecStatus(SecItemAdd(query as CFDictionary, &ref))
|
||||
guard ret.isSuccess else {
|
||||
throw KeychainError.appleSecError(call: "SecItemAdd", status: ret)
|
||||
let status = SecItemAdd(query as CFDictionary, &ref)
|
||||
|
||||
guard status == Result.success.rawValue
|
||||
else {
|
||||
throw KeychainError.appleSecError(call: "SecItemAdd", status: status)
|
||||
}
|
||||
|
||||
return
|
||||
@@ -52,11 +42,11 @@ public enum Keychain {
|
||||
attributesToUpdate: [CFString: Any]
|
||||
) throws {
|
||||
|
||||
let ret = SecStatus(
|
||||
SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary))
|
||||
let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary)
|
||||
|
||||
guard ret.isSuccess else {
|
||||
throw KeychainError.appleSecError(call: "SecItemUpdate", status: ret)
|
||||
guard status == Result.success.rawValue
|
||||
else {
|
||||
throw KeychainError.appleSecError(call: "SecItemUpdate", status: status)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,13 +54,13 @@ public enum Keychain {
|
||||
let query = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecValuePersistentRef: persistentRef,
|
||||
kSecReturnData: true,
|
||||
kSecReturnData: true
|
||||
] as [CFString: Any]
|
||||
|
||||
var result: CFTypeRef?
|
||||
let ret = SecStatus(SecItemCopyMatching(query as CFDictionary, &result))
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
|
||||
guard ret.isSuccess,
|
||||
guard status == Result.success.rawValue,
|
||||
let resultData = result as? Data
|
||||
else {
|
||||
return nil
|
||||
@@ -82,13 +72,13 @@ public enum Keychain {
|
||||
public static func search(query: [CFString: Any]) -> PersistentRef? {
|
||||
let query = query.merging([
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecReturnPersistentRef: true,
|
||||
kSecReturnPersistentRef: true
|
||||
]) { (_, new) in new }
|
||||
|
||||
var result: CFTypeRef?
|
||||
let ret = SecStatus(SecItemCopyMatching(query as CFDictionary, &result))
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
|
||||
guard ret.isSuccess,
|
||||
guard status == Result.success.rawValue,
|
||||
let persistentRef = result as? Data
|
||||
else {
|
||||
return nil
|
||||
@@ -99,11 +89,11 @@ public enum Keychain {
|
||||
|
||||
public static func delete(persistentRef: PersistentRef) throws {
|
||||
let query = [kSecValuePersistentRef: persistentRef] as [CFString: Any]
|
||||
let ret = SecStatus(SecItemDelete(query as CFDictionary))
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
|
||||
guard ret.isSuccess || ret == .status(.itemNotFound)
|
||||
guard status == Result.success.rawValue || status == Result.itemNotFound.rawValue
|
||||
else {
|
||||
throw KeychainError.appleSecError(call: "SecItemDelete", status: ret)
|
||||
throw KeychainError.appleSecError(call: "SecItemDelete", status: status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,427 +0,0 @@
|
||||
//
|
||||
// Status.swift
|
||||
// (c) 2024 Firezone, Inc.
|
||||
// LICENSE: Apache-2.0
|
||||
//
|
||||
|
||||
// Copyright (c) 2014 kishikawa katsumi
|
||||
// The MIT License (MIT)
|
||||
// https://github.com/kishikawakatsumi/KeychainAccess
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum KeychainStatus: OSStatus, Error {
|
||||
case success = 0
|
||||
case unimplemented = -4
|
||||
case diskFull = -34
|
||||
case io = -36
|
||||
case opWr = -49
|
||||
case param = -50
|
||||
case wrPerm = -61
|
||||
case allocate = -108
|
||||
case userCanceled = -128
|
||||
case badReq = -909
|
||||
case internalComponent = -2070
|
||||
case notAvailable = -25291
|
||||
case readOnly = -25292
|
||||
case authFailed = -25293
|
||||
case noSuchKeychain = -25294
|
||||
case invalidKeychain = -25295
|
||||
case duplicateKeychain = -25296
|
||||
case duplicateCallback = -25297
|
||||
case invalidCallback = -25298
|
||||
case duplicateItem = -25299
|
||||
case itemNotFound = -25300
|
||||
case bufferTooSmall = -25301
|
||||
case dataTooLarge = -25302
|
||||
case noSuchAttr = -25303
|
||||
case invalidItemRef = -25304
|
||||
case invalidSearchRef = -25305
|
||||
case noSuchClass = -25306
|
||||
case noDefaultKeychain = -25307
|
||||
case interactionNotAllowed = -25308
|
||||
case readOnlyAttr = -25309
|
||||
case wrongSecVersion = -25310
|
||||
case keySizeNotAllowed = -25311
|
||||
case noStorageModule = -25312
|
||||
case noCertificateModule = -25313
|
||||
case noPolicyModule = -25314
|
||||
case interactionRequired = -25315
|
||||
case dataNotAvailable = -25316
|
||||
case dataNotModifiable = -25317
|
||||
case createChainFailed = -25318
|
||||
case invalidPrefsDomain = -25319
|
||||
case inDarkWake = -25320
|
||||
case aclNotSimple = -25240
|
||||
case policyNotFound = -25241
|
||||
case invalidTrustSetting = -25242
|
||||
case noAccessForItem = -25243
|
||||
case invalidOwnerEdit = -25244
|
||||
case trustNotAvailable = -25245
|
||||
case unsupportedFormat = -25256
|
||||
case unknownFormat = -25257
|
||||
case keyIsSensitive = -25258
|
||||
case multiplePrivKeys = -25259
|
||||
case passphraseRequired = -25260
|
||||
case invalidPasswordRef = -25261
|
||||
case invalidTrustSettings = -25262
|
||||
case noTrustSettings = -25263
|
||||
case pkcs12VerifyFailure = -25264
|
||||
case invalidCertificate = -26265
|
||||
case notSigner = -26267
|
||||
case policyDenied = -26270
|
||||
case invalidKey = -26274
|
||||
case decode = -26275
|
||||
case `internal` = -26276
|
||||
case unsupportedAlgorithm = -26268
|
||||
case unsupportedOperation = -26271
|
||||
case unsupportedPadding = -26273
|
||||
case itemInvalidKey = -34000
|
||||
case itemInvalidKeyType = -34001
|
||||
case itemInvalidValue = -34002
|
||||
case itemClassMissing = -34003
|
||||
case itemMatchUnsupported = -34004
|
||||
case useItemListUnsupported = -34005
|
||||
case useKeychainUnsupported = -34006
|
||||
case useKeychainListUnsupported = -34007
|
||||
case returnDataUnsupported = -34008
|
||||
case returnAttributesUnsupported = -34009
|
||||
case returnRefUnsupported = -34010
|
||||
case returnPersitentRefUnsupported = -34011
|
||||
case valueRefUnsupported = -34012
|
||||
case valuePersistentRefUnsupported = -34013
|
||||
case returnMissingPointer = -34014
|
||||
case matchLimitUnsupported = -34015
|
||||
case itemIllegalQuery = -34016
|
||||
case waitForCallback = -34017
|
||||
case missingEntitlement = -34018
|
||||
case upgradePending = -34019
|
||||
case mpSignatureInvalid = -25327
|
||||
case otrTooOld = -25328
|
||||
case otrIDTooNew = -25329
|
||||
case serviceNotAvailable = -67585
|
||||
case insufficientClientID = -67586
|
||||
case deviceReset = -67587
|
||||
case deviceFailed = -67588
|
||||
case appleAddAppACLSubject = -67589
|
||||
case applePublicKeyIncomplete = -67590
|
||||
case appleSignatureMismatch = -67591
|
||||
case appleInvalidKeyStartDate = -67592
|
||||
case appleInvalidKeyEndDate = -67593
|
||||
case conversionError = -67594
|
||||
case appleSSLv2Rollback = -67595
|
||||
case quotaExceeded = -67596
|
||||
case fileTooBig = -67597
|
||||
case invalidDatabaseBlob = -67598
|
||||
case invalidKeyBlob = -67599
|
||||
case incompatibleDatabaseBlob = -67600
|
||||
case incompatibleKeyBlob = -67601
|
||||
case hostNameMismatch = -67602
|
||||
case unknownCriticalExtensionFlag = -67603
|
||||
case noBasicConstraints = -67604
|
||||
case noBasicConstraintsCA = -67605
|
||||
case invalidAuthorityKeyID = -67606
|
||||
case invalidSubjectKeyID = -67607
|
||||
case invalidKeyUsageForPolicy = -67608
|
||||
case invalidExtendedKeyUsage = -67609
|
||||
case invalidIDLinkage = -67610
|
||||
case pathLengthConstraintExceeded = -67611
|
||||
case invalidRoot = -67612
|
||||
case crlExpired = -67613
|
||||
case crlNotValidYet = -67614
|
||||
case crlNotFound = -67615
|
||||
case crlServerDown = -67616
|
||||
case crlBadURI = -67617
|
||||
case unknownCertExtension = -67618
|
||||
case unknownCRLExtension = -67619
|
||||
case crlNotTrusted = -67620
|
||||
case crlPolicyFailed = -67621
|
||||
case idpFailure = -67622
|
||||
case smimeEmailAddressesNotFound = -67623
|
||||
case smimeBadExtendedKeyUsage = -67624
|
||||
case smimeBadKeyUsage = -67625
|
||||
case smimeKeyUsageNotCritical = -67626
|
||||
case smimeNoEmailAddress = -67627
|
||||
case smimeSubjAltNameNotCritical = -67628
|
||||
case sslBadExtendedKeyUsage = -67629
|
||||
case ocspBadResponse = -67630
|
||||
case ocspBadRequest = -67631
|
||||
case ocspUnavailable = -67632
|
||||
case ocspStatusUnrecognized = -67633
|
||||
case endOfData = -67634
|
||||
case incompleteCertRevocationCheck = -67635
|
||||
case networkFailure = -67636
|
||||
case ocspNotTrustedToAnchor = -67637
|
||||
case recordModified = -67638
|
||||
case ocspSignatureError = -67639
|
||||
case ocspNoSigner = -67640
|
||||
case ocspResponderMalformedReq = -67641
|
||||
case ocspResponderInternalError = -67642
|
||||
case ocspResponderTryLater = -67643
|
||||
case ocspResponderSignatureRequired = -67644
|
||||
case ocspResponderUnauthorized = -67645
|
||||
case ocspResponseNonceMismatch = -67646
|
||||
case codeSigningBadCertChainLength = -67647
|
||||
case codeSigningNoBasicConstraints = -67648
|
||||
case codeSigningBadPathLengthConstraint = -67649
|
||||
case codeSigningNoExtendedKeyUsage = -67650
|
||||
case codeSigningDevelopment = -67651
|
||||
case resourceSignBadCertChainLength = -67652
|
||||
case resourceSignBadExtKeyUsage = -67653
|
||||
case trustSettingDeny = -67654
|
||||
case invalidSubjectName = -67655
|
||||
case unknownQualifiedCertStatement = -67656
|
||||
case mobileMeRequestQueued = -67657
|
||||
case mobileMeRequestRedirected = -67658
|
||||
case mobileMeServerError = -67659
|
||||
case mobileMeServerNotAvailable = -67660
|
||||
case mobileMeServerAlreadyExists = -67661
|
||||
case mobileMeServerServiceErr = -67662
|
||||
case mobileMeRequestAlreadyPending = -67663
|
||||
case mobileMeNoRequestPending = -67664
|
||||
case mobileMeCSRVerifyFailure = -67665
|
||||
case mobileMeFailedConsistencyCheck = -67666
|
||||
case notInitialized = -67667
|
||||
case invalidHandleUsage = -67668
|
||||
case pvcReferentNotFound = -67669
|
||||
case functionIntegrityFail = -67670
|
||||
case internalError = -67671
|
||||
case memoryError = -67672
|
||||
case invalidData = -67673
|
||||
case mdsError = -67674
|
||||
case invalidPointer = -67675
|
||||
case selfCheckFailed = -67676
|
||||
case functionFailed = -67677
|
||||
case moduleManifestVerifyFailed = -67678
|
||||
case invalidGUID = -67679
|
||||
case invalidHandle = -67680
|
||||
case invalidDBList = -67681
|
||||
case invalidPassthroughID = -67682
|
||||
case invalidNetworkAddress = -67683
|
||||
case crlAlreadySigned = -67684
|
||||
case invalidNumberOfFields = -67685
|
||||
case verificationFailure = -67686
|
||||
case unknownTag = -67687
|
||||
case invalidSignature = -67688
|
||||
case invalidName = -67689
|
||||
case invalidCertificateRef = -67690
|
||||
case invalidCertificateGroup = -67691
|
||||
case tagNotFound = -67692
|
||||
case invalidQuery = -67693
|
||||
case invalidValue = -67694
|
||||
case callbackFailed = -67695
|
||||
case aclDeleteFailed = -67696
|
||||
case aclReplaceFailed = -67697
|
||||
case aclAddFailed = -67698
|
||||
case aclChangeFailed = -67699
|
||||
case invalidAccessCredentials = -67700
|
||||
case invalidRecord = -67701
|
||||
case invalidACL = -67702
|
||||
case invalidSampleValue = -67703
|
||||
case incompatibleVersion = -67704
|
||||
case privilegeNotGranted = -67705
|
||||
case invalidScope = -67706
|
||||
case pvcAlreadyConfigured = -67707
|
||||
case invalidPVC = -67708
|
||||
case emmLoadFailed = -67709
|
||||
case emmUnloadFailed = -67710
|
||||
case addinLoadFailed = -67711
|
||||
case invalidKeyRef = -67712
|
||||
case invalidKeyHierarchy = -67713
|
||||
case addinUnloadFailed = -67714
|
||||
case libraryReferenceNotFound = -67715
|
||||
case invalidAddinFunctionTable = -67716
|
||||
case invalidServiceMask = -67717
|
||||
case moduleNotLoaded = -67718
|
||||
case invalidSubServiceID = -67719
|
||||
case attributeNotInContext = -67720
|
||||
case moduleManagerInitializeFailed = -67721
|
||||
case moduleManagerNotFound = -67722
|
||||
case eventNotificationCallbackNotFound = -67723
|
||||
case inputLengthError = -67724
|
||||
case outputLengthError = -67725
|
||||
case privilegeNotSupported = -67726
|
||||
case deviceError = -67727
|
||||
case attachHandleBusy = -67728
|
||||
case notLoggedIn = -67729
|
||||
case algorithmMismatch = -67730
|
||||
case keyUsageIncorrect = -67731
|
||||
case keyBlobTypeIncorrect = -67732
|
||||
case keyHeaderInconsistent = -67733
|
||||
case unsupportedKeyFormat = -67734
|
||||
case unsupportedKeySize = -67735
|
||||
case invalidKeyUsageMask = -67736
|
||||
case unsupportedKeyUsageMask = -67737
|
||||
case invalidKeyAttributeMask = -67738
|
||||
case unsupportedKeyAttributeMask = -67739
|
||||
case invalidKeyLabel = -67740
|
||||
case unsupportedKeyLabel = -67741
|
||||
case invalidKeyFormat = -67742
|
||||
case unsupportedVectorOfBuffers = -67743
|
||||
case invalidInputVector = -67744
|
||||
case invalidOutputVector = -67745
|
||||
case invalidContext = -67746
|
||||
case invalidAlgorithm = -67747
|
||||
case invalidAttributeKey = -67748
|
||||
case missingAttributeKey = -67749
|
||||
case invalidAttributeInitVector = -67750
|
||||
case missingAttributeInitVector = -67751
|
||||
case invalidAttributeSalt = -67752
|
||||
case missingAttributeSalt = -67753
|
||||
case invalidAttributePadding = -67754
|
||||
case missingAttributePadding = -67755
|
||||
case invalidAttributeRandom = -67756
|
||||
case missingAttributeRandom = -67757
|
||||
case invalidAttributeSeed = -67758
|
||||
case missingAttributeSeed = -67759
|
||||
case invalidAttributePassphrase = -67760
|
||||
case missingAttributePassphrase = -67761
|
||||
case invalidAttributeKeyLength = -67762
|
||||
case missingAttributeKeyLength = -67763
|
||||
case invalidAttributeBlockSize = -67764
|
||||
case missingAttributeBlockSize = -67765
|
||||
case invalidAttributeOutputSize = -67766
|
||||
case missingAttributeOutputSize = -67767
|
||||
case invalidAttributeRounds = -67768
|
||||
case missingAttributeRounds = -67769
|
||||
case invalidAlgorithmParms = -67770
|
||||
case missingAlgorithmParms = -67771
|
||||
case invalidAttributeLabel = -67772
|
||||
case missingAttributeLabel = -67773
|
||||
case invalidAttributeKeyType = -67774
|
||||
case missingAttributeKeyType = -67775
|
||||
case invalidAttributeMode = -67776
|
||||
case missingAttributeMode = -67777
|
||||
case invalidAttributeEffectiveBits = -67778
|
||||
case missingAttributeEffectiveBits = -67779
|
||||
case invalidAttributeStartDate = -67780
|
||||
case missingAttributeStartDate = -67781
|
||||
case invalidAttributeEndDate = -67782
|
||||
case missingAttributeEndDate = -67783
|
||||
case invalidAttributeVersion = -67784
|
||||
case missingAttributeVersion = -67785
|
||||
case invalidAttributePrime = -67786
|
||||
case missingAttributePrime = -67787
|
||||
case invalidAttributeBase = -67788
|
||||
case missingAttributeBase = -67789
|
||||
case invalidAttributeSubprime = -67790
|
||||
case missingAttributeSubprime = -67791
|
||||
case invalidAttributeIterationCount = -67792
|
||||
case missingAttributeIterationCount = -67793
|
||||
case invalidAttributeDLDBHandle = -67794
|
||||
case missingAttributeDLDBHandle = -67795
|
||||
case invalidAttributeAccessCredentials = -67796
|
||||
case missingAttributeAccessCredentials = -67797
|
||||
case invalidAttributePublicKeyFormat = -67798
|
||||
case missingAttributePublicKeyFormat = -67799
|
||||
case invalidAttributePrivateKeyFormat = -67800
|
||||
case missingAttributePrivateKeyFormat = -67801
|
||||
case invalidAttributeSymmetricKeyFormat = -67802
|
||||
case missingAttributeSymmetricKeyFormat = -67803
|
||||
case invalidAttributeWrappedKeyFormat = -67804
|
||||
case missingAttributeWrappedKeyFormat = -67805
|
||||
case stagedOperationInProgress = -67806
|
||||
case stagedOperationNotStarted = -67807
|
||||
case verifyFailed = -67808
|
||||
case querySizeUnknown = -67809
|
||||
case blockSizeMismatch = -67810
|
||||
case publicKeyInconsistent = -67811
|
||||
case deviceVerifyFailed = -67812
|
||||
case invalidLoginName = -67813
|
||||
case alreadyLoggedIn = -67814
|
||||
case invalidDigestAlgorithm = -67815
|
||||
case invalidCRLGroup = -67816
|
||||
case certificateCannotOperate = -67817
|
||||
case certificateExpired = -67818
|
||||
case certificateNotValidYet = -67819
|
||||
case certificateRevoked = -67820
|
||||
case certificateSuspended = -67821
|
||||
case insufficientCredentials = -67822
|
||||
case invalidAction = -67823
|
||||
case invalidAuthority = -67824
|
||||
case verifyActionFailed = -67825
|
||||
case invalidCertAuthority = -67826
|
||||
case invaldCRLAuthority = -67827
|
||||
case invalidCRLEncoding = -67828
|
||||
case invalidCRLType = -67829
|
||||
case invalidCRL = -67830
|
||||
case invalidFormType = -67831
|
||||
case invalidID = -67832
|
||||
case invalidIdentifier = -67833
|
||||
case invalidIndex = -67834
|
||||
case invalidPolicyIdentifiers = -67835
|
||||
case invalidTimeString = -67836
|
||||
case invalidReason = -67837
|
||||
case invalidRequestInputs = -67838
|
||||
case invalidResponseVector = -67839
|
||||
case invalidStopOnPolicy = -67840
|
||||
case invalidTuple = -67841
|
||||
case multipleValuesUnsupported = -67842
|
||||
case notTrusted = -67843
|
||||
case noDefaultAuthority = -67844
|
||||
case rejectedForm = -67845
|
||||
case requestLost = -67846
|
||||
case requestRejected = -67847
|
||||
case unsupportedAddressType = -67848
|
||||
case unsupportedService = -67849
|
||||
case invalidTupleGroup = -67850
|
||||
case invalidBaseACLs = -67851
|
||||
case invalidTupleCredendtials = -67852
|
||||
case invalidEncoding = -67853
|
||||
case invalidValidityPeriod = -67854
|
||||
case invalidRequestor = -67855
|
||||
case requestDescriptor = -67856
|
||||
case invalidBundleInfo = -67857
|
||||
case invalidCRLIndex = -67858
|
||||
case noFieldValues = -67859
|
||||
case unsupportedFieldFormat = -67860
|
||||
case unsupportedIndexInfo = -67861
|
||||
case unsupportedLocality = -67862
|
||||
case unsupportedNumAttributes = -67863
|
||||
case unsupportedNumIndexes = -67864
|
||||
case unsupportedNumRecordTypes = -67865
|
||||
case fieldSpecifiedMultiple = -67866
|
||||
case incompatibleFieldFormat = -67867
|
||||
case invalidParsingModule = -67868
|
||||
case databaseLocked = -67869
|
||||
case datastoreIsOpen = -67870
|
||||
case missingValue = -67871
|
||||
case unsupportedQueryLimits = -67872
|
||||
case unsupportedNumSelectionPreds = -67873
|
||||
case unsupportedOperator = -67874
|
||||
case invalidDBLocation = -67875
|
||||
case invalidAccessRequest = -67876
|
||||
case invalidIndexInfo = -67877
|
||||
case invalidNewOwner = -67878
|
||||
case invalidModifyMode = -67879
|
||||
case missingRequiredExtension = -67880
|
||||
case extendedKeyUsageNotCritical = -67881
|
||||
case timestampMissing = -67882
|
||||
case timestampInvalid = -67883
|
||||
case timestampNotTrusted = -67884
|
||||
case timestampServiceNotAvailable = -67885
|
||||
case timestampBadAlg = -67886
|
||||
case timestampBadRequest = -67887
|
||||
case timestampBadDataFormat = -67888
|
||||
case timestampTimeNotAvailable = -67889
|
||||
case timestampUnacceptedPolicy = -67890
|
||||
case timestampUnacceptedExtension = -67891
|
||||
case timestampAddInfoNotAvailable = -67892
|
||||
case timestampSystemFailure = -67893
|
||||
case signingTimeMissing = -67894
|
||||
case timestampRejection = -67895
|
||||
case timestampWaiting = -67896
|
||||
case timestampRevocationWarning = -67897
|
||||
case timestampRevocationNotification = -67898
|
||||
case unexpectedError = -99999
|
||||
}
|
||||
|
||||
extension KeychainStatus {
|
||||
static func == (lhs: OSStatus, rhs: KeychainStatus) -> Bool {
|
||||
lhs == rhs.rawValue
|
||||
}
|
||||
|
||||
static func != (lhs: OSStatus, rhs: KeychainStatus) -> Bool {
|
||||
lhs != rhs.rawValue
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,10 @@ public class SystemExtensionManager: NSObject, OSSystemExtensionRequestDelegate,
|
||||
// MARK: - OSSystemExtensionRequestDelegate
|
||||
|
||||
// Result of system extension installation
|
||||
public func request(_ request: OSSystemExtensionRequest, didFinishWithResult result: OSSystemExtensionRequest.Result) {
|
||||
public func request(
|
||||
_ request: OSSystemExtensionRequest,
|
||||
didFinishWithResult result: OSSystemExtensionRequest.Result
|
||||
) {
|
||||
guard result == .completed else {
|
||||
resume(throwing: SystemExtensionError.unknownResult(result))
|
||||
|
||||
@@ -83,12 +86,11 @@ public class SystemExtensionManager: NSObject, OSSystemExtensionRequestDelegate,
|
||||
foundProperties properties: [OSSystemExtensionProperties]
|
||||
) {
|
||||
// Standard keys in any bundle. If missing, we've got bigger issues.
|
||||
let ourBundleVersion = Bundle.main.object(
|
||||
forInfoDictionaryKey: "CFBundleVersion"
|
||||
) as! String
|
||||
let ourBundleShortVersion = Bundle.main.object(
|
||||
forInfoDictionaryKey: "CFBundleShortVersionString"
|
||||
) as! String
|
||||
guard let ourBundleVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String,
|
||||
let ourBundleShortVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
|
||||
else {
|
||||
fatalError("Version should exist in bundle")
|
||||
}
|
||||
|
||||
// Up to date if version and build number match
|
||||
let isCurrentVersionInstalled = properties.contains { sysex in
|
||||
@@ -122,7 +124,11 @@ public class SystemExtensionManager: NSObject, OSSystemExtensionRequestDelegate,
|
||||
// We assume this state until we receive a success response.
|
||||
}
|
||||
|
||||
public func request(_ request: OSSystemExtensionRequest, actionForReplacingExtension existing: OSSystemExtensionProperties, withExtension ext: OSSystemExtensionProperties) -> OSSystemExtensionRequest.ReplacementAction {
|
||||
public func request(
|
||||
_ request: OSSystemExtensionRequest,
|
||||
actionForReplacingExtension existing: OSSystemExtensionProperties,
|
||||
withExtension ext: OSSystemExtensionProperties
|
||||
) -> OSSystemExtensionRequest.ReplacementAction {
|
||||
return .replace
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
// Abstracts the nitty gritty of loading and saving to our
|
||||
// VPN configuration in system preferences.
|
||||
|
||||
// TODO: Refactor to fix file length
|
||||
// swiftlint:disable file_length
|
||||
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
import NetworkExtension
|
||||
@@ -68,17 +71,17 @@ public enum TunnelMessage: Codable {
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let type = try container.decode(MessageType.self, forKey: .type)
|
||||
switch type {
|
||||
case .internetResourceEnabled:
|
||||
let value = try container.decode(Bool.self, forKey: .value)
|
||||
self = .internetResourceEnabled(value)
|
||||
case .getResourceList:
|
||||
let value = try container.decode(Data.self, forKey: .value)
|
||||
self = .getResourceList(value)
|
||||
case .signOut:
|
||||
self = .signOut
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let type = try container.decode(MessageType.self, forKey: .type)
|
||||
switch type {
|
||||
case .internetResourceEnabled:
|
||||
let value = try container.decode(Bool.self, forKey: .value)
|
||||
self = .internetResourceEnabled(value)
|
||||
case .getResourceList:
|
||||
let value = try container.decode(Data.self, forKey: .value)
|
||||
self = .getResourceList(value)
|
||||
case .signOut:
|
||||
self = .signOut
|
||||
case .clearLogs:
|
||||
self = .clearLogs
|
||||
case .getLogFolderSize:
|
||||
@@ -87,19 +90,19 @@ public enum TunnelMessage: Codable {
|
||||
self = .exportLogs
|
||||
case .consumeStopReason:
|
||||
self = .consumeStopReason
|
||||
}
|
||||
}
|
||||
}
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
switch self {
|
||||
case .internetResourceEnabled(let value):
|
||||
try container.encode(MessageType.internetResourceEnabled, forKey: .type)
|
||||
try container.encode(value, forKey: .value)
|
||||
case .getResourceList(let value):
|
||||
try container.encode(MessageType.getResourceList, forKey: .type)
|
||||
try container.encode(value, forKey: .value)
|
||||
case .signOut:
|
||||
try container.encode(MessageType.signOut, forKey: .type)
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
switch self {
|
||||
case .internetResourceEnabled(let value):
|
||||
try container.encode(MessageType.internetResourceEnabled, forKey: .type)
|
||||
try container.encode(value, forKey: .value)
|
||||
case .getResourceList(let value):
|
||||
try container.encode(MessageType.getResourceList, forKey: .type)
|
||||
try container.encode(value, forKey: .value)
|
||||
case .signOut:
|
||||
try container.encode(MessageType.signOut, forKey: .type)
|
||||
case .clearLogs:
|
||||
try container.encode(MessageType.clearLogs, forKey: .type)
|
||||
case .getLogFolderSize:
|
||||
@@ -108,10 +111,12 @@ public enum TunnelMessage: Codable {
|
||||
try container.encode(MessageType.exportLogs, forKey: .type)
|
||||
case .consumeStopReason:
|
||||
try container.encode(MessageType.consumeStopReason, forKey: .type)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Refactor this to remove the lint ignore
|
||||
// swiftlint:disable:next type_body_length
|
||||
public class VPNConfigurationManager {
|
||||
|
||||
// Connect status updates with our listeners
|
||||
@@ -133,10 +138,10 @@ public class VPNConfigurationManager {
|
||||
|
||||
// Encoder used to send messages to the tunnel
|
||||
private let encoder = {
|
||||
let _encoder = PropertyListEncoder()
|
||||
_encoder.outputFormat = .binary
|
||||
let encoder = PropertyListEncoder()
|
||||
encoder.outputFormat = .binary
|
||||
|
||||
return _encoder
|
||||
return encoder
|
||||
}()
|
||||
|
||||
public static let bundleIdentifier: String = "\(Bundle.main.bundleIdentifier!).network-extension"
|
||||
@@ -174,43 +179,44 @@ public class VPNConfigurationManager {
|
||||
}
|
||||
}
|
||||
|
||||
func loadFromPreferences(vpnStateUpdateHandler: @escaping @MainActor (NEVPNStatus, Settings?, String?) -> Void) async throws {
|
||||
func loadFromPreferences(
|
||||
vpnStateUpdateHandler: @escaping @MainActor (NEVPNStatus, Settings?, String?) -> Void
|
||||
) async throws {
|
||||
// loadAllFromPreferences() returns list of VPN configurations created by our main app's bundle ID.
|
||||
// Since our bundle ID can change (by us), find the one that's current and ignore the others.
|
||||
let managers = try await NETunnelProviderManager.loadAllFromPreferences()
|
||||
|
||||
Log.log("\(#function): \(managers.count) tunnel managers found")
|
||||
for manager in managers {
|
||||
if manager.localizedDescription == bundleDescription { // Found it
|
||||
|
||||
guard let protocolConfiguration = manager.protocolConfiguration as? NETunnelProviderProtocol,
|
||||
let providerConfiguration = protocolConfiguration.providerConfiguration as? [String: String]
|
||||
else {
|
||||
throw VPNConfigurationManagerError.cannotLoad
|
||||
}
|
||||
|
||||
// Update our state
|
||||
self.manager = manager
|
||||
|
||||
let settings = Settings.fromProviderConfiguration(providerConfiguration)
|
||||
let actorName = providerConfiguration[VPNConfigurationManagerKeys.actorName]
|
||||
if let internetResourceEnabled = providerConfiguration[VPNConfigurationManagerKeys.internetResourceEnabled]?.data(using: .utf8) {
|
||||
|
||||
self.internetResourceEnabled = (try? JSONDecoder().decode(Bool.self, from: internetResourceEnabled)) ?? false
|
||||
|
||||
}
|
||||
let status = manager.connection.status
|
||||
|
||||
// Configure our Telemetry environment
|
||||
Telemetry.setEnvironmentOrClose(settings.apiURL)
|
||||
Telemetry.accountSlug = providerConfiguration[VPNConfigurationManagerKeys.accountSlug]
|
||||
|
||||
// Share what we found with our caller
|
||||
await vpnStateUpdateHandler(status, settings, actorName)
|
||||
|
||||
// Stop looking for our tunnel
|
||||
break
|
||||
for manager in managers where manager.localizedDescription == bundleDescription {
|
||||
guard let protocolConfiguration = manager.protocolConfiguration as? NETunnelProviderProtocol,
|
||||
let providerConfiguration = protocolConfiguration.providerConfiguration as? [String: String]
|
||||
else {
|
||||
throw VPNConfigurationManagerError.cannotLoad
|
||||
}
|
||||
|
||||
// Update our state
|
||||
self.manager = manager
|
||||
|
||||
let settings = Settings.fromProviderConfiguration(providerConfiguration)
|
||||
let actorName = providerConfiguration[VPNConfigurationManagerKeys.actorName]
|
||||
if let internetResourceEnabled = providerConfiguration[
|
||||
VPNConfigurationManagerKeys.internetResourceEnabled
|
||||
]?.data(using: .utf8) {
|
||||
|
||||
self.internetResourceEnabled = (try? JSONDecoder().decode(Bool.self, from: internetResourceEnabled)) ?? false
|
||||
|
||||
}
|
||||
let status = manager.connection.status
|
||||
|
||||
// Configure our Telemetry environment
|
||||
Telemetry.setEnvironmentOrClose(settings.apiURL)
|
||||
Telemetry.accountSlug = providerConfiguration[VPNConfigurationManagerKeys.accountSlug]
|
||||
|
||||
// Share what we found with our caller
|
||||
await vpnStateUpdateHandler(status, settings, actorName)
|
||||
|
||||
// Stop looking for our tunnel
|
||||
break
|
||||
}
|
||||
|
||||
// If no tunnel configuration was found, update state to
|
||||
@@ -255,7 +261,8 @@ public class VPNConfigurationManager {
|
||||
var newProviderConfiguration = settings.toProviderConfiguration()
|
||||
|
||||
// Don't clobber existing actorName
|
||||
newProviderConfiguration[VPNConfigurationManagerKeys.actorName] = providerConfiguration[VPNConfigurationManagerKeys.actorName]
|
||||
newProviderConfiguration[VPNConfigurationManagerKeys.actorName] =
|
||||
providerConfiguration[VPNConfigurationManagerKeys.actorName]
|
||||
protocolConfiguration.providerConfiguration = newProviderConfiguration
|
||||
protocolConfiguration.serverAddress = settings.apiURL
|
||||
manager.protocolConfiguration = protocolConfiguration
|
||||
@@ -276,13 +283,13 @@ public class VPNConfigurationManager {
|
||||
|
||||
// Pass token if provided
|
||||
if let token = token {
|
||||
options.merge(["token": token as NSObject]) { _, n in n }
|
||||
options.merge(["token": token as NSObject]) { _, new in new }
|
||||
}
|
||||
|
||||
// Pass pre-1.4.0 Firezone ID if it exists. Pre 1.4.0 clients will have this
|
||||
// persisted to the app side container URL.
|
||||
if let id = FirezoneId.load(.Pre_1_4_0) {
|
||||
options.merge(["id": id as NSObject]) { _, n in n }
|
||||
if let id = FirezoneId.load(.pre140) {
|
||||
options.merge(["id": id as NSObject]) { _, new in new }
|
||||
}
|
||||
|
||||
try session()?.startTunnel(options: options)
|
||||
@@ -322,7 +329,13 @@ public class VPNConfigurationManager {
|
||||
self.resourceListHash = Data(SHA256.hash(data: data))
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
self.resourcesListCache = ResourceList.loaded(try! decoder.decode([Resource].self, from: data))
|
||||
|
||||
guard let decoded = try? decoder.decode([Resource].self, from: data)
|
||||
else {
|
||||
fatalError("Should be able to decode ResourceList")
|
||||
}
|
||||
|
||||
self.resourcesListCache = ResourceList.loaded(decoded)
|
||||
}
|
||||
|
||||
callback(self.resourcesListCache)
|
||||
|
||||
@@ -80,7 +80,6 @@ struct AuthClient {
|
||||
return bytes.map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
|
||||
// TODO: Use a cryptography lib that the compiler can't optimize out
|
||||
private func areStringsEqualConstantTime(_ string1: String, _ string2: String) -> Bool {
|
||||
let charArray1 = string1.utf8CString
|
||||
let charArray2 = string2.utf8CString
|
||||
|
||||
@@ -17,8 +17,8 @@ import Foundation
|
||||
/// Can be refactored to remove the Version enum all clients >= 1.4.0
|
||||
public struct FirezoneId {
|
||||
public enum Version {
|
||||
case Pre_1_4_0
|
||||
case Post_1_4_0
|
||||
case pre140
|
||||
case post140
|
||||
}
|
||||
|
||||
public static func save(_ id: String) {
|
||||
@@ -39,9 +39,9 @@ public struct FirezoneId {
|
||||
|
||||
public static func load(_ version: Version) -> String? {
|
||||
let appGroupId = switch version {
|
||||
case .Post_1_4_0:
|
||||
case .post140:
|
||||
BundleHelper.appGroupId
|
||||
case .Pre_1_4_0:
|
||||
case .pre140:
|
||||
#if os(macOS)
|
||||
"47R2M6779T.group.dev.firezone.firezone"
|
||||
#elseif os(iOS)
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
import Foundation
|
||||
|
||||
class StatusSymbol {
|
||||
static var on: String = "<->"
|
||||
static var off: String = "—"
|
||||
static var enabled: String = "<->"
|
||||
static var disabled: String = "—"
|
||||
}
|
||||
|
||||
public enum ResourceList {
|
||||
@@ -21,8 +21,8 @@ public enum ResourceList {
|
||||
switch self {
|
||||
case .loading:
|
||||
[]
|
||||
case .loaded(let x):
|
||||
x
|
||||
case .loaded(let ele):
|
||||
ele
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,15 @@ public struct Resource: Decodable, Identifiable, Equatable {
|
||||
public var sites: [Site]
|
||||
public var type: ResourceType
|
||||
|
||||
public init(id: String, name: String, address: String?, addressDescription: String?, status: ResourceStatus, sites: [Site], type: ResourceType) {
|
||||
public init(
|
||||
id: String,
|
||||
name: String,
|
||||
address: String?,
|
||||
addressDescription: String?,
|
||||
status: ResourceStatus,
|
||||
sites: [Site],
|
||||
type: ResourceType
|
||||
) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.address = address
|
||||
@@ -75,14 +83,17 @@ public enum ResourceStatus: String, Decodable {
|
||||
case .online:
|
||||
return "You're connected to a healthy Gateway in this Site."
|
||||
case .unknown:
|
||||
return "No connection has been attempted to Resources in this Site. Access a Resource to establish a Gateway connection."
|
||||
return """
|
||||
No connection has been attempted to Resources in this Site.
|
||||
Access a Resource to establish a Gateway connection.
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ResourceType: String, Decodable {
|
||||
case dns = "dns"
|
||||
case cidr = "cidr"
|
||||
case ip = "ip"
|
||||
case internet = "internet"
|
||||
case dns
|
||||
case cidr
|
||||
case ip // swiftlint:disable:this identifier_name
|
||||
case internet
|
||||
}
|
||||
|
||||
@@ -114,7 +114,11 @@ public class SessionNotification: NSObject {
|
||||
|
||||
#if os(iOS)
|
||||
extension SessionNotification: UNUserNotificationCenterDelegate {
|
||||
public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
public func userNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
didReceive response: UNNotificationResponse,
|
||||
withCompletionHandler completionHandler: @escaping () -> Void
|
||||
) {
|
||||
Log.log("\(#function): 'Sign In' clicked in notification")
|
||||
let actionId = response.actionIdentifier
|
||||
let categoryId = response.notification.request.content.categoryIdentifier
|
||||
|
||||
@@ -27,7 +27,6 @@ struct Settings: Equatable {
|
||||
&& !logFilter.isEmpty
|
||||
}
|
||||
|
||||
|
||||
// Convert provider configuration (which may have empty fields if it was tampered with) to Settings
|
||||
static func fromProviderConfiguration(_ providerConfiguration: [String: Any]?) -> Settings {
|
||||
if let providerConfiguration = providerConfiguration as? [String: String] {
|
||||
@@ -38,7 +37,8 @@ struct Settings: Equatable {
|
||||
?? Settings.defaultValue.apiURL,
|
||||
logFilter: providerConfiguration[VPNConfigurationManagerKeys.logFilter]
|
||||
?? Settings.defaultValue.logFilter,
|
||||
internetResourceEnabled: getInternetResourceEnabled(internetResourceEnabled: providerConfiguration[VPNConfigurationManagerKeys.internetResourceEnabled])
|
||||
internetResourceEnabled: getInternetResourceEnabled(
|
||||
internetResourceEnabled: providerConfiguration[VPNConfigurationManagerKeys.internetResourceEnabled])
|
||||
)
|
||||
} else {
|
||||
return Settings.defaultValue
|
||||
@@ -46,18 +46,26 @@ struct Settings: Equatable {
|
||||
}
|
||||
|
||||
static private func getInternetResourceEnabled(internetResourceEnabled: String?) -> Bool? {
|
||||
guard let internetResourceEnabled = internetResourceEnabled, let jsonData = internetResourceEnabled.data(using: .utf8) else { return nil }
|
||||
guard let internetResourceEnabled = internetResourceEnabled,
|
||||
let jsonData = internetResourceEnabled.data(using: .utf8)
|
||||
else { return nil }
|
||||
|
||||
return try? JSONDecoder().decode(Bool?.self, from: jsonData)
|
||||
}
|
||||
|
||||
// Used for initializing a new providerConfiguration from Settings
|
||||
func toProviderConfiguration() -> [String: String] {
|
||||
guard let data = try? JSONEncoder().encode(internetResourceEnabled),
|
||||
let string = String(data: data, encoding: .utf8)
|
||||
else {
|
||||
fatalError("internetResourceEnabled should be encodable")
|
||||
}
|
||||
|
||||
return [
|
||||
VPNConfigurationManagerKeys.authBaseURL: authBaseURL,
|
||||
VPNConfigurationManagerKeys.apiURL: apiURL,
|
||||
VPNConfigurationManagerKeys.logFilter: logFilter,
|
||||
VPNConfigurationManagerKeys.internetResourceEnabled: String(data: try! JSONEncoder().encode(internetResourceEnabled) , encoding: .utf8)!,
|
||||
VPNConfigurationManagerKeys.internetResourceEnabled: string
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ public struct Token: CustomStringConvertible {
|
||||
kSecAttrLabel: "Firezone token",
|
||||
kSecAttrAccount: "1",
|
||||
kSecAttrService: BundleHelper.appGroupId,
|
||||
kSecAttrDescription: "Firezone access token",
|
||||
kSecAttrDescription: "Firezone access token"
|
||||
]
|
||||
|
||||
private var data: Data
|
||||
|
||||
@@ -22,10 +22,8 @@ struct WebAuthSession {
|
||||
|
||||
let anchor = PresentationAnchor()
|
||||
|
||||
let authResponse = try await withCheckedThrowingContinuation() { continuation in
|
||||
let session = ASWebAuthenticationSession(url: url, callbackURLScheme: scheme) {
|
||||
returnedURL,
|
||||
error in
|
||||
let authResponse = try await withCheckedThrowingContinuation { continuation in
|
||||
let session = ASWebAuthenticationSession(url: url, callbackURLScheme: scheme) { returnedURL, error in
|
||||
do {
|
||||
if let error = error as? ASWebAuthenticationSessionError,
|
||||
error.code == .canceledLogin {
|
||||
|
||||
@@ -10,7 +10,7 @@ import UserNotifications
|
||||
import OSLog
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
@@ -81,8 +81,7 @@ public final class Store: ObservableObject {
|
||||
if let savedValue = try await self.vpnConfigurationManager.consumeStopReason(),
|
||||
let rawValue = Int(savedValue),
|
||||
let reason = NEProviderStopReason(rawValue: rawValue),
|
||||
case .authenticationCanceled = reason
|
||||
{
|
||||
case .authenticationCanceled = reason {
|
||||
#if os(macOS)
|
||||
await self.sessionNotification.showSignedOutAlertmacOS()
|
||||
#endif
|
||||
@@ -97,8 +96,8 @@ public final class Store: ObservableObject {
|
||||
func checkedSystemExtensionStatus() async throws -> SystemExtensionStatus {
|
||||
let checker = SystemExtensionManager()
|
||||
|
||||
let status = try await withCheckedThrowingContinuation {
|
||||
(continuation: CheckedContinuation<SystemExtensionStatus, Error>) in
|
||||
let status =
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<SystemExtensionStatus, Error>) in
|
||||
|
||||
checker.checkStatus(
|
||||
identifier: VPNConfigurationManager.bundleIdentifier,
|
||||
@@ -122,8 +121,8 @@ public final class Store: ObservableObject {
|
||||
|
||||
// Apple recommends installing the system extension as early as possible after app launch.
|
||||
// See https://developer.apple.com/documentation/systemextensions/installing-system-extensions-and-drivers
|
||||
self.systemExtensionStatus = try await withCheckedThrowingContinuation {
|
||||
(continuation: CheckedContinuation<SystemExtensionStatus, Error>) in
|
||||
self.systemExtensionStatus =
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<SystemExtensionStatus, Error>) in
|
||||
|
||||
installer.installSystemExtension(
|
||||
identifier: VPNConfigurationManager.bundleIdentifier,
|
||||
@@ -184,7 +183,7 @@ public final class Store: ObservableObject {
|
||||
Log.log("\(#function)")
|
||||
|
||||
// Define the Timer's closure
|
||||
let updateResources: @Sendable (Timer) -> Void = { _timer in
|
||||
let updateResources: @Sendable (Timer) -> Void = { _ in
|
||||
Task.detached { [weak self] in
|
||||
await self?.vpnConfigurationManager.fetchResources(callback: callback)
|
||||
}
|
||||
|
||||
@@ -38,8 +38,7 @@ public class AppViewModel: ObservableObject {
|
||||
Task.detached { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
do { try await WebAuthSession.signIn(store: self.store) }
|
||||
catch { Log.error(error) }
|
||||
do { try await WebAuthSession.signIn(store: self.store) } catch { Log.error(error) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
|
||||
@MainActor
|
||||
final class GrantVPNViewModel: ObservableObject {
|
||||
@Published var isInstalled: Bool = false
|
||||
@@ -107,9 +106,10 @@ struct GrantVPNView: View {
|
||||
.frame(maxWidth: 320)
|
||||
.padding(.horizontal, 10)
|
||||
Spacer()
|
||||
Text(
|
||||
"Firezone requires your permission to create VPN configurations. Until it has that permission, all functionality will be disabled."
|
||||
)
|
||||
Text("""
|
||||
Firezone requires your permission to create VPN configurations.
|
||||
Until it has that permission, all functionality will be disabled.
|
||||
""")
|
||||
.font(.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 5))
|
||||
@@ -165,11 +165,14 @@ struct GrantVPNView: View {
|
||||
.padding(.vertical, 10)
|
||||
.opacity(model.isInstalled ? 0.5 : 1.0)
|
||||
Spacer()
|
||||
Button(action: {
|
||||
model.installSystemExtensionButtonTapped()
|
||||
}) {
|
||||
Label("Enable System Extension", systemImage: "gearshape")
|
||||
}
|
||||
Button(
|
||||
action: {
|
||||
model.installSystemExtensionButtonTapped()
|
||||
},
|
||||
label: {
|
||||
Label("Enable System Extension", systemImage: "gearshape")
|
||||
}
|
||||
)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
.disabled(model.isInstalled)
|
||||
@@ -185,11 +188,14 @@ struct GrantVPNView: View {
|
||||
.font(.body)
|
||||
.padding(.vertical, 10)
|
||||
Spacer()
|
||||
Button(action: {
|
||||
model.grantPermissionButtonTapped()
|
||||
}) {
|
||||
Label("Grant VPN Permission", systemImage: "network.badge.shield.half.filled")
|
||||
}
|
||||
Button(
|
||||
action: {
|
||||
model.grantPermissionButtonTapped()
|
||||
},
|
||||
label: {
|
||||
Label("Grant VPN Permission", systemImage: "network.badge.shield.half.filled")
|
||||
}
|
||||
)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
.disabled(!model.isInstalled)
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
// Created by Jamil Bou Kheir on 4/2/24.
|
||||
//
|
||||
|
||||
// TODO: Refactor to fix file length
|
||||
// swiftlint:disable file_length
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import NetworkExtension
|
||||
@@ -15,6 +18,7 @@ import SwiftUI
|
||||
@MainActor
|
||||
// TODO: Refactor to MenuBarExtra for macOS 13+
|
||||
// https://developer.apple.com/documentation/swiftui/menubarextra
|
||||
// swiftlint:disable:next type_body_length
|
||||
public final class MenuBar: NSObject, ObservableObject {
|
||||
private var statusItem: NSStatusItem
|
||||
|
||||
@@ -41,7 +45,7 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
private lazy var connectingAnimationImages = [
|
||||
NSImage(named: "MenuBarIconConnecting1"),
|
||||
NSImage(named: "MenuBarIconConnecting2"),
|
||||
NSImage(named: "MenuBarIconConnecting3"),
|
||||
NSImage(named: "MenuBarIconConnecting3")
|
||||
]
|
||||
private var connectingAnimationImageIndex: Int = 0
|
||||
private var connectingAnimationTimer: Timer?
|
||||
@@ -65,17 +69,17 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
private func setupObservers() {
|
||||
model.favorites.$ids
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] ids in
|
||||
.sink(receiveValue: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
// When the user clicks to add or remove a favorite, the menu will close anyway, so just recreate the whole menu.
|
||||
// This avoids complex logic when changing in and out of the "nothing is favorited" special case
|
||||
// When the user clicks to add or remove a favorite, the menu will close anyway, so just recreate the whole
|
||||
// menu. This avoids complex logic when changing in and out of the "nothing is favorited" special case.
|
||||
self.populateResourceMenus([])
|
||||
self.populateResourceMenus(model.resources.asArray())
|
||||
}).store(in: &cancellables)
|
||||
|
||||
model.$resources
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] resources in
|
||||
.sink(receiveValue: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.populateResourceMenus(model.resources.asArray())
|
||||
self.handleTunnelStatusOrResourcesChanged()
|
||||
@@ -83,7 +87,7 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
|
||||
model.$status
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] status in
|
||||
.sink(receiveValue: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.vpnStatus = model.status
|
||||
self.updateStatusItemIcon()
|
||||
@@ -227,7 +231,7 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
menu.addItem(resourcesUnavailableReasonMenuItem)
|
||||
menu.addItem(resourcesSeparatorMenuItem)
|
||||
|
||||
if (!model.favorites.ids.isEmpty) {
|
||||
if !model.favorites.ids.isEmpty {
|
||||
menu.addItem(otherResourcesMenuItem)
|
||||
menu.addItem(otherResourcesSeparatorMenuItem)
|
||||
}
|
||||
@@ -272,7 +276,7 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
alert.messageText = "Error signing in"
|
||||
alert.informativeText = error.localizedDescription
|
||||
alert.alertStyle = .warning
|
||||
let _ = alert.runModal()
|
||||
_ = alert.runModal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -407,9 +411,7 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
(connectingAnimationImageIndex + 1) % connectingAnimationImages.count
|
||||
}
|
||||
|
||||
private func handleTunnelStatusOrResourcesChanged() {
|
||||
let resources = model.resources
|
||||
let status = model.status
|
||||
private func updateSignInMenuItems(status: NEVPNStatus?) {
|
||||
// Update "Sign In" / "Sign Out" menu items
|
||||
switch status {
|
||||
case nil:
|
||||
@@ -446,6 +448,9 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func updateResourcesMenuItems(status: NEVPNStatus?, resources: ResourceList) {
|
||||
// Update resources "header" menu items
|
||||
switch status {
|
||||
case .connecting:
|
||||
@@ -487,6 +492,15 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func handleTunnelStatusOrResourcesChanged() {
|
||||
let resources = model.resources
|
||||
let status = model.status
|
||||
|
||||
updateSignInMenuItems(status: status)
|
||||
updateResourcesMenuItems(status: status, resources: resources)
|
||||
|
||||
quitMenuItem.title = {
|
||||
switch status {
|
||||
case .connected, .connecting:
|
||||
@@ -501,8 +515,8 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
switch resources {
|
||||
case .loading:
|
||||
return "Loading Resources..."
|
||||
case .loaded(let x):
|
||||
if x.isEmpty {
|
||||
case .loaded(let list):
|
||||
if list.isEmpty {
|
||||
return "No Resources"
|
||||
} else {
|
||||
return "Resources"
|
||||
@@ -513,7 +527,7 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
private func populateResourceMenus(_ newResources: [Resource]) {
|
||||
// If we have no favorites, then everything is a favorite
|
||||
let hasAnyFavorites = newResources.contains { model.favorites.contains($0.id) }
|
||||
let newFavorites = if (hasAnyFavorites) {
|
||||
let newFavorites = if hasAnyFavorites {
|
||||
newResources.filter { model.favorites.contains($0.id) || $0.isInternetResource() }
|
||||
} else {
|
||||
newResources
|
||||
@@ -540,7 +554,7 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
// We don't ever need to remove this as the whole menu will be recreated
|
||||
// if the user updates, and there's no reason for the update to no longer be available
|
||||
// versions should be monotonically increased.
|
||||
if (updateChecker.updateAvailable && !updateMenuDisplayed) {
|
||||
if updateChecker.updateAvailable && !updateMenuDisplayed {
|
||||
updateMenuDisplayed = true
|
||||
let index = menu.index(of: settingsMenuItem) + 1
|
||||
menu.insertItem(NSMenuItem.separator(), at: index)
|
||||
@@ -570,13 +584,13 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
}
|
||||
|
||||
private func populateOtherResourcesMenu(_ newOthers: [Resource]) {
|
||||
if (newOthers.isEmpty) {
|
||||
if newOthers.isEmpty {
|
||||
removeItemFromMenu(menu: menu, item: otherResourcesMenuItem)
|
||||
removeItemFromMenu(menu: menu, item: otherResourcesSeparatorMenuItem)
|
||||
} else {
|
||||
let i = menu.index(of: aboutMenuItem)
|
||||
addItemToMenu(menu: menu, item: otherResourcesMenuItem, at: i)
|
||||
addItemToMenu(menu: menu, item: otherResourcesSeparatorMenuItem, at: i + 1)
|
||||
let idx = menu.index(of: aboutMenuItem)
|
||||
addItemToMenu(menu: menu, item: otherResourcesMenuItem, location: idx)
|
||||
addItemToMenu(menu: menu, item: otherResourcesSeparatorMenuItem, location: idx + 1)
|
||||
}
|
||||
|
||||
// Update the menu in place so everything won't vanish if it's open when it updates
|
||||
@@ -598,20 +612,20 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
|
||||
}
|
||||
|
||||
private func addItemToMenu(menu: NSMenu, item: NSMenuItem, at: Int) {
|
||||
private func addItemToMenu(menu: NSMenu, item: NSMenuItem, location: Int) {
|
||||
// Adding an item that already exists will crash the process, so check for it first.
|
||||
let i = menu.index(of: otherResourcesMenuItem)
|
||||
if (i != -1) {
|
||||
let idx = menu.index(of: otherResourcesMenuItem)
|
||||
if idx != -1 {
|
||||
// Item's already in the menu, do nothing
|
||||
return
|
||||
}
|
||||
menu.insertItem(otherResourcesMenuItem, at: at)
|
||||
menu.insertItem(otherResourcesMenuItem, at: location)
|
||||
}
|
||||
|
||||
private func removeItemFromMenu(menu: NSMenu, item: NSMenuItem) {
|
||||
// Removing an item that doesn't exist will crash the process, so check for it first.
|
||||
let i = menu.index(of: item)
|
||||
if (i == -1) {
|
||||
let idx = menu.index(of: item)
|
||||
if idx == -1 {
|
||||
// Item's already not in the menu, do nothing
|
||||
return
|
||||
}
|
||||
@@ -619,7 +633,7 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
}
|
||||
|
||||
private func internetResourceTitle(resource: Resource) -> String {
|
||||
let status = model.store.internetResourceEnabled() ? StatusSymbol.on : StatusSymbol.off
|
||||
let status = model.store.internetResourceEnabled() ? StatusSymbol.enabled : StatusSymbol.disabled
|
||||
|
||||
return status + " " + resource.name
|
||||
}
|
||||
@@ -645,6 +659,8 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
model.isInternetResourceEnabled() ? "Disable this resource" : "Enable this resource"
|
||||
}
|
||||
|
||||
// TODO: Refactor this when refactoring for macOS 13
|
||||
// swiftlint:disable:next function_body_length
|
||||
private func nonInternetResourceHeader(resource: Resource) -> NSMenu {
|
||||
let subMenu = NSMenu()
|
||||
|
||||
@@ -654,13 +670,14 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
resourceAddressDescriptionItem.title = addressDescription
|
||||
|
||||
if let url = URL(string: addressDescription),
|
||||
let _ = url.host {
|
||||
url.host != nil {
|
||||
// Looks like a URL, so allow opening it
|
||||
resourceAddressDescriptionItem.action = #selector(resourceURLTapped(_:))
|
||||
resourceAddressDescriptionItem.toolTip = "Click to open"
|
||||
|
||||
// Using Markdown here only to highlight the URL
|
||||
resourceAddressDescriptionItem.attributedTitle = try? NSAttributedString(markdown: "[\(addressDescription)](\(addressDescription))")
|
||||
resourceAddressDescriptionItem.attributedTitle =
|
||||
try? NSAttributedString(markdown: "[\(addressDescription)](\(addressDescription))")
|
||||
} else {
|
||||
resourceAddressDescriptionItem.title = addressDescription
|
||||
resourceAddressDescriptionItem.action = #selector(resourceValueTapped(_:))
|
||||
@@ -813,12 +830,16 @@ public final class MenuBar: NSObject, ObservableObject {
|
||||
}
|
||||
|
||||
@objc private func addFavoriteTapped(_ sender: NSMenuItem) {
|
||||
let id = sender.representedObject as! String
|
||||
guard let id = sender.representedObject as? String
|
||||
else { fatalError("Expected to receive a String") }
|
||||
|
||||
setFavorited(id: id, favorited: true)
|
||||
}
|
||||
|
||||
@objc private func removeFavoriteTapped(_ sender: NSMenuItem) {
|
||||
let id = sender.representedObject as! String
|
||||
guard let id = sender.representedObject as? String
|
||||
else { fatalError("Expected to receive a String") }
|
||||
|
||||
setFavorited(id: id, favorited: false)
|
||||
}
|
||||
|
||||
@@ -855,7 +876,11 @@ extension NSImage {
|
||||
func resized(to newSize: NSSize) -> NSImage {
|
||||
let newImage = NSImage(size: newSize)
|
||||
newImage.lockFocus()
|
||||
self.draw(in: NSRect(origin: .zero, size: newSize), from: NSRect(origin: .zero, size: self.size), operation: .copy, fraction: 1.0)
|
||||
self.draw(
|
||||
in: NSRect(origin: .zero, size: newSize),
|
||||
from: NSRect(origin: .zero, size: self.size),
|
||||
operation: .copy, fraction: 1.0
|
||||
)
|
||||
newImage.unlockFocus()
|
||||
newImage.size = newSize
|
||||
return newImage
|
||||
|
||||
@@ -37,12 +37,15 @@ struct ResourceView: View {
|
||||
Text(site.name)
|
||||
}
|
||||
.contextMenu {
|
||||
Button(action: {
|
||||
copyToClipboard(site.name)
|
||||
}) {
|
||||
Text("Copy name")
|
||||
Image(systemName: "doc.on.doc")
|
||||
}
|
||||
Button(
|
||||
action: {
|
||||
copyToClipboard(site.name)
|
||||
},
|
||||
label: {
|
||||
Text("Copy name")
|
||||
Image(systemName: "doc.on.doc")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
HStack {
|
||||
@@ -56,12 +59,15 @@ struct ResourceView: View {
|
||||
.padding(.leading, 5)
|
||||
}
|
||||
.contextMenu {
|
||||
Button(action: {
|
||||
copyToClipboard(resource.status.toSiteStatus())
|
||||
}) {
|
||||
Text("Copy status")
|
||||
Image(systemName: "doc.on.doc")
|
||||
}
|
||||
Button(
|
||||
action: {
|
||||
copyToClipboard(resource.status.toSiteStatus())
|
||||
},
|
||||
label: {
|
||||
Text("Copy status")
|
||||
Image(systemName: "doc.on.doc")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,12 +113,15 @@ struct NonInternetResourceHeader: View {
|
||||
Text(resource.name)
|
||||
}
|
||||
.contextMenu {
|
||||
Button(action: {
|
||||
copyToClipboard(resource.name)
|
||||
}) {
|
||||
Text("Copy name")
|
||||
Image(systemName: "doc.on.doc")
|
||||
}
|
||||
Button(
|
||||
action: {
|
||||
copyToClipboard(resource.name)
|
||||
},
|
||||
label: {
|
||||
Text("Copy name")
|
||||
Image(systemName: "doc.on.doc")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
HStack {
|
||||
@@ -122,56 +131,70 @@ struct NonInternetResourceHeader: View {
|
||||
.foregroundColor(.secondary)
|
||||
.frame(width: 80, alignment: .leading)
|
||||
if let url = URL(string: resource.addressDescription ?? resource.address!),
|
||||
let _ = url.host {
|
||||
Button(action: {
|
||||
openURL(url)
|
||||
}) {
|
||||
Text(resource.addressDescription ?? resource.address!)
|
||||
.foregroundColor(.blue)
|
||||
.underline()
|
||||
.font(.system(size: 16))
|
||||
.contextMenu {
|
||||
Button(action: {
|
||||
copyToClipboard(resource.addressDescription ?? resource.address!)
|
||||
}) {
|
||||
Text("Copy address")
|
||||
Image(systemName: "doc.on.doc")
|
||||
url.host != nil {
|
||||
Button(
|
||||
action: {
|
||||
openURL(url)
|
||||
},
|
||||
label: {
|
||||
Text(resource.addressDescription ?? resource.address!)
|
||||
.foregroundColor(.blue)
|
||||
.underline()
|
||||
.font(.system(size: 16))
|
||||
.contextMenu {
|
||||
Button(
|
||||
action: {
|
||||
copyToClipboard(resource.addressDescription ?? resource.address!)
|
||||
},
|
||||
label: {
|
||||
Text("Copy address")
|
||||
Image(systemName: "doc.on.doc")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Text(resource.addressDescription ?? resource.address!)
|
||||
.contextMenu {
|
||||
Button(action: {
|
||||
copyToClipboard(resource.addressDescription ?? resource.address!)
|
||||
}) {
|
||||
Text("Copy address")
|
||||
Image(systemName: "doc.on.doc")
|
||||
}
|
||||
Button(
|
||||
action: {
|
||||
copyToClipboard(resource.addressDescription ?? resource.address!)
|
||||
},
|
||||
label: {
|
||||
Text("Copy address")
|
||||
Image(systemName: "doc.on.doc")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(model.favorites.ids.contains(resource.id)) {
|
||||
Button(action: {
|
||||
model.favorites.remove(resource.id)
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "star")
|
||||
Text("Remove from favorites")
|
||||
Spacer()
|
||||
if model.favorites.ids.contains(resource.id) {
|
||||
Button(
|
||||
action: {
|
||||
model.favorites.remove(resource.id)
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
Image(systemName: "star")
|
||||
Text("Remove from favorites")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Button(action: {
|
||||
model.favorites.add(resource.id)
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "star.fill")
|
||||
Text("Add to favorites")
|
||||
Spacer()
|
||||
Button(
|
||||
action: {
|
||||
model.favorites.add(resource.id)
|
||||
}, label: {
|
||||
HStack {
|
||||
Image(systemName: "star.fill")
|
||||
Text("Add to favorites")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,14 +243,17 @@ struct ToggleInternetResourceButton: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
model.store.toggleInternetResource(enabled: !model.isInternetResourceEnabled())
|
||||
}) {
|
||||
HStack {
|
||||
Text(toggleResourceEnabledText())
|
||||
Spacer()
|
||||
Button(
|
||||
action: {
|
||||
model.store.toggleInternetResource(enabled: !model.isInternetResourceEnabled())
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
Text(toggleResourceEnabledText())
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public final class SessionViewModel: ObservableObject {
|
||||
@Published private(set) var actorName: String? = nil
|
||||
@Published private(set) var actorName: String?
|
||||
@Published private(set) var favorites: Favorites
|
||||
@Published private(set) var resources: ResourceList = ResourceList.loading
|
||||
@Published private(set) var status: NEVPNStatus?
|
||||
@@ -26,7 +26,7 @@ public final class SessionViewModel: ObservableObject {
|
||||
|
||||
favorites.$ids
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] ids in
|
||||
.sink(receiveValue: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.objectWillChange.send()
|
||||
})
|
||||
@@ -48,7 +48,7 @@ public final class SessionViewModel: ObservableObject {
|
||||
self.status = status
|
||||
|
||||
if status == .connected {
|
||||
store.beginUpdatingResources() { resources in
|
||||
store.beginUpdatingResources { resources in
|
||||
self.resources = resources
|
||||
}
|
||||
} else {
|
||||
@@ -130,7 +130,7 @@ struct ResourceSection: View {
|
||||
@ObservedObject var model: SessionViewModel
|
||||
|
||||
private func internetResourceTitle(resource: Resource) -> String {
|
||||
let status = model.store.internetResourceEnabled() ? StatusSymbol.on : StatusSymbol.off
|
||||
let status = model.store.internetResourceEnabled() ? StatusSymbol.enabled : StatusSymbol.disabled
|
||||
|
||||
return status + " " + resource.name
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
// LICENSE: Apache-2.0
|
||||
//
|
||||
|
||||
// TODO: Refactor to fix file length
|
||||
// swiftlint:disable file_length
|
||||
|
||||
import Combine
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
@@ -160,6 +163,8 @@ extension FileManager {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Refactor body length
|
||||
// swiftlint:disable:next type_body_length
|
||||
public struct SettingsView: View {
|
||||
@ObservedObject var favorites: Favorites
|
||||
@ObservedObject var model: SettingsViewModel
|
||||
@@ -203,11 +208,12 @@ public struct SettingsView: View {
|
||||
}
|
||||
|
||||
struct FootnoteText {
|
||||
static let forAdvanced = try! AttributedString(
|
||||
static let forAdvanced = try? AttributedString(
|
||||
markdown: """
|
||||
**WARNING:** These settings are intended for internal debug purposes **only**. \
|
||||
Changing these will disrupt access to your Firezone resources.
|
||||
""")
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
public init(favorites: Favorites, model: SettingsViewModel) {
|
||||
@@ -342,7 +348,7 @@ public struct SettingsView: View {
|
||||
prompt: Text(PlaceholderText.logFilter)
|
||||
)
|
||||
|
||||
Text(FootnoteText.forAdvanced)
|
||||
Text(FootnoteText.forAdvanced ?? "")
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
HStack(spacing: 30) {
|
||||
@@ -445,7 +451,7 @@ public struct SettingsView: View {
|
||||
}
|
||||
},
|
||||
header: { Text("Advanced Settings") },
|
||||
footer: { Text(FootnoteText.forAdvanced) }
|
||||
footer: { Text(FootnoteText.forAdvanced ?? "") }
|
||||
)
|
||||
}
|
||||
Spacer()
|
||||
@@ -633,7 +639,7 @@ public struct SettingsView: View {
|
||||
await MainActor.run {
|
||||
alert.messageText = "Error exporting logs: \(error.localizedDescription)"
|
||||
alert.alertStyle = .critical
|
||||
let _ = alert.runModal()
|
||||
_ = alert.runModal()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,70 +24,86 @@ class UpdateChecker {
|
||||
|
||||
private var timer: Timer?
|
||||
private let notificationAdapter: NotificationAdapter = NotificationAdapter()
|
||||
private let versionCheckUrl: URL = URL(string: "https://www.firezone.dev/api/releases")!
|
||||
private let marketingVersion = SemVerString.from(string: Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String)!
|
||||
private let versionCheckUrl: URL
|
||||
private let marketingVersion: SemanticVersion
|
||||
|
||||
@Published public var updateAvailable: Bool = false
|
||||
|
||||
init() {
|
||||
startCheckingForUpdates()
|
||||
guard let versionCheckUrl = URL(string: "https://www.firezone.dev/api/releases"),
|
||||
let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
|
||||
let marketingVersion = try? SemanticVersion(versionString)
|
||||
else {
|
||||
fatalError("Should be able to initialize the UpdateChecker")
|
||||
}
|
||||
|
||||
self.versionCheckUrl = versionCheckUrl
|
||||
self.marketingVersion = marketingVersion
|
||||
|
||||
startCheckingForUpdates()
|
||||
}
|
||||
|
||||
private func startCheckingForUpdates() {
|
||||
timer = Timer.scheduledTimer(timeInterval: 6 * 60 * 60, target: self, selector: #selector(checkForUpdates), userInfo: nil, repeats: true)
|
||||
checkForUpdates()
|
||||
}
|
||||
private func startCheckingForUpdates() {
|
||||
timer = Timer.scheduledTimer(
|
||||
timeInterval: 6 * 60 * 60,
|
||||
target: self,
|
||||
selector: #selector(checkForUpdates),
|
||||
userInfo: nil,
|
||||
repeats: true
|
||||
)
|
||||
checkForUpdates()
|
||||
}
|
||||
|
||||
deinit {
|
||||
timer?.invalidate()
|
||||
}
|
||||
deinit {
|
||||
timer?.invalidate()
|
||||
}
|
||||
|
||||
@objc private func checkForUpdates() {
|
||||
let task = URLSession.shared.dataTask(with: versionCheckUrl) { [weak self] data, response, error in
|
||||
guard let self = self else { return }
|
||||
@objc private func checkForUpdates() {
|
||||
let task = URLSession.shared.dataTask(with: versionCheckUrl) { [weak self] data, _, error in
|
||||
guard let self = self else { return }
|
||||
|
||||
if let error = error as NSError?,
|
||||
error.domain == NSURLErrorDomain,
|
||||
[
|
||||
NSURLErrorTimedOut,
|
||||
NSURLErrorCannotFindHost,
|
||||
NSURLErrorCannotConnectToHost,
|
||||
NSURLErrorNetworkConnectionLost,
|
||||
NSURLErrorDNSLookupFailed,
|
||||
NSURLErrorNotConnectedToInternet
|
||||
].contains(error.code) // Don't capture transient errors
|
||||
{
|
||||
Log.warning("\(#function): Update check failed: \(error)")
|
||||
if let error = error as NSError?,
|
||||
error.domain == NSURLErrorDomain,
|
||||
[
|
||||
NSURLErrorTimedOut,
|
||||
NSURLErrorCannotFindHost,
|
||||
NSURLErrorCannotConnectToHost,
|
||||
NSURLErrorNetworkConnectionLost,
|
||||
NSURLErrorDNSLookupFailed,
|
||||
NSURLErrorNotConnectedToInternet
|
||||
].contains(error.code) { // Don't capture transient errors
|
||||
Log.warning("\(#function): Update check failed: \(error)")
|
||||
|
||||
return
|
||||
} else if let error = error {
|
||||
Log.error(error)
|
||||
return
|
||||
} else if let error = error {
|
||||
Log.error(error)
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard let versionInfo = VersionInfo.from(data: data) else {
|
||||
let attemptedVersion = String(data: data ?? Data(), encoding: .utf8) ?? ""
|
||||
Log.error(UpdateError.invalidVersion(attemptedVersion))
|
||||
guard let data = data,
|
||||
let versions = try? JSONDecoder().decode([String: String].self, from: data),
|
||||
let versionString = versions["apple"],
|
||||
let latestVersion = try? SemanticVersion(versionString)
|
||||
else {
|
||||
Log.error(UpdateError.invalidVersion("data was invalid or 'apple' key not found"))
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let latestVersion = versionInfo.apple
|
||||
if latestVersion > marketingVersion {
|
||||
self.updateAvailable = true
|
||||
|
||||
if latestVersion > marketingVersion {
|
||||
self.updateAvailable = true
|
||||
|
||||
if let lastDismissedVersion = getLastDismissedVersion(), lastDismissedVersion >= latestVersion {
|
||||
return
|
||||
}
|
||||
|
||||
self.notificationAdapter.showUpdateNotification(version: latestVersion)
|
||||
}
|
||||
if let lastDismissedVersion = getLastDismissedVersion(), lastDismissedVersion >= latestVersion {
|
||||
return
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
self.notificationAdapter.showUpdateNotification(version: latestVersion)
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
static func downloadURL() -> URL {
|
||||
if BundleHelper.isAppStore() {
|
||||
@@ -99,8 +115,8 @@ class UpdateChecker {
|
||||
}
|
||||
|
||||
private class NotificationAdapter: NSObject, UNUserNotificationCenterDelegate {
|
||||
private var lastNotifiedVersion: SemVerString?
|
||||
private var lastDismissedVersion: SemVerString?
|
||||
private var lastNotifiedVersion: SemanticVersion?
|
||||
private var lastDismissedVersion: SemanticVersion?
|
||||
static let notificationIdentifier = "UPDATE_CATEGORY"
|
||||
static let dismissIdentifier = "DISMISS_ACTION"
|
||||
|
||||
@@ -114,9 +130,9 @@ private class NotificationAdapter: NSObject, UNUserNotificationCenterDelegate {
|
||||
options: [])
|
||||
|
||||
let notificationCategory = UNNotificationCategory(identifier: NotificationAdapter.notificationIdentifier,
|
||||
actions: [dismissAction],
|
||||
intentIdentifiers: [],
|
||||
options: [])
|
||||
actions: [dismissAction],
|
||||
intentIdentifiers: [],
|
||||
options: [])
|
||||
|
||||
notificationCenter.setNotificationCategories([notificationCategory])
|
||||
|
||||
@@ -137,8 +153,7 @@ private class NotificationAdapter: NSObject, UNUserNotificationCenterDelegate {
|
||||
|
||||
}
|
||||
|
||||
|
||||
func showUpdateNotification(version: SemVerString) {
|
||||
func showUpdateNotification(version: SemanticVersion) {
|
||||
let content = UNMutableNotificationContent()
|
||||
lastNotifiedVersion = version
|
||||
content.title = "Update Firezone"
|
||||
@@ -146,58 +161,57 @@ private class NotificationAdapter: NSObject, UNUserNotificationCenterDelegate {
|
||||
content.sound = .default
|
||||
content.categoryIdentifier = NotificationAdapter.notificationIdentifier
|
||||
|
||||
|
||||
let request = UNNotificationRequest(
|
||||
identifier: UUID().uuidString,
|
||||
content: content,
|
||||
trigger: UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||
)
|
||||
content: content,
|
||||
trigger: UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||
)
|
||||
|
||||
UNUserNotificationCenter.current().add(request) { error in
|
||||
if let error = error {
|
||||
Log.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter,
|
||||
didReceive response: UNNotificationResponse,
|
||||
withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
if response.actionIdentifier == NotificationAdapter.dismissIdentifier {
|
||||
try? setLastDismissedVersion(version: lastNotifiedVersion!)
|
||||
return
|
||||
}
|
||||
if response.actionIdentifier == NotificationAdapter.dismissIdentifier {
|
||||
setLastDismissedVersion(version: lastNotifiedVersion!)
|
||||
return
|
||||
}
|
||||
|
||||
Task.detached {
|
||||
NSWorkspace.shared.open(UpdateChecker.downloadURL())
|
||||
}
|
||||
|
||||
completionHandler()
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
// Show the notification even when the app is in the foreground
|
||||
func userNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
willPresent notification: UNNotification,
|
||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions
|
||||
) -> Void) {
|
||||
// Show the notification even when the app is in the foreground
|
||||
completionHandler([.badge, .banner, .sound])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let lastDismissedVersionKey = "lastDismissedVersion"
|
||||
private let lastDismissedVersionKey = "lastDismissedVersion"
|
||||
|
||||
private func setLastDismissedVersion(version: SemVerString) throws {
|
||||
guard let data = version.versionString().data(using: .utf8) else { return }
|
||||
UserDefaults.standard.setValue(String(data: data, encoding: .utf8), forKey: lastDismissedVersionKey)
|
||||
private func setLastDismissedVersion(version: SemanticVersion) {
|
||||
UserDefaults.standard.setValue(version, forKey: lastDismissedVersionKey)
|
||||
}
|
||||
|
||||
private func getLastDismissedVersion() -> SemVerString? {
|
||||
guard let versionString = UserDefaults.standard.string(forKey: lastDismissedVersionKey) else { return nil }
|
||||
return SemVerString.from(string: versionString)
|
||||
private func getLastDismissedVersion() -> SemanticVersion? {
|
||||
return UserDefaults.standard.object(forKey: lastDismissedVersionKey) as? SemanticVersion
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import SwiftUI
|
||||
|
||||
#if os(iOS)
|
||||
struct iOSNavigationView<Content: View>: View {
|
||||
struct iOSNavigationView<Content: View>: View { // swiftlint:disable:this type_name
|
||||
@State private var isSettingsPresented = false
|
||||
@ObservedObject var model: AppViewModel
|
||||
@Environment(\.openURL) var openURL
|
||||
@@ -26,7 +26,7 @@ struct iOSNavigationView<Content: View>: View {
|
||||
NavigationView {
|
||||
content
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarItems(leading: AuthMenu, trailing: SettingsButton)
|
||||
.navigationBarItems(leading: authMenu, trailing: settingsButton)
|
||||
.alert(
|
||||
item: $errorHandler.currentAlert,
|
||||
content: { alert in
|
||||
@@ -46,57 +46,72 @@ struct iOSNavigationView<Content: View>: View {
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
|
||||
private var SettingsButton: some View {
|
||||
Button(action: {
|
||||
isSettingsPresented = true
|
||||
}) {
|
||||
Label("Settings", systemImage: "gear")
|
||||
}
|
||||
private var settingsButton: some View {
|
||||
Button(
|
||||
action: {
|
||||
isSettingsPresented = true
|
||||
},
|
||||
label: {
|
||||
Label("Settings", systemImage: "gear")
|
||||
}
|
||||
)
|
||||
.disabled(model.status == .invalid)
|
||||
}
|
||||
|
||||
private var AuthMenu: some View {
|
||||
private var authMenu: some View {
|
||||
Menu {
|
||||
if model.status == .connected {
|
||||
Text("Signed in as \(model.store.actorName ?? "Unknown user")")
|
||||
Button(action: {
|
||||
signOutButtonTapped()
|
||||
}) {
|
||||
Label("Sign out", systemImage: "rectangle.portrait.and.arrow.right")
|
||||
}
|
||||
Button(
|
||||
action: {
|
||||
signOutButtonTapped()
|
||||
},
|
||||
label: {
|
||||
Label("Sign out", systemImage: "rectangle.portrait.and.arrow.right")
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Button(action: {
|
||||
Task.detached {
|
||||
do {
|
||||
try await WebAuthSession.signIn(store: model.store)
|
||||
} catch {
|
||||
Log.error(error)
|
||||
Button(
|
||||
action: {
|
||||
Task.detached {
|
||||
do {
|
||||
try await WebAuthSession.signIn(store: model.store)
|
||||
} catch {
|
||||
Log.error(error)
|
||||
|
||||
await MainActor.run {
|
||||
self.errorHandler.handle(
|
||||
ErrorAlert(
|
||||
title: "Error signing in",
|
||||
error: error
|
||||
await MainActor.run {
|
||||
self.errorHandler.handle(
|
||||
ErrorAlert(
|
||||
title: "Error signing in",
|
||||
error: error
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
label: {
|
||||
Label("Sign in", systemImage: "person.crop.circle.fill.badge.plus")
|
||||
}
|
||||
}) {
|
||||
Label("Sign in", systemImage: "person.crop.circle.fill.badge.plus")
|
||||
}
|
||||
)
|
||||
}
|
||||
Divider()
|
||||
Button(action: {
|
||||
openURL(URL(string: "https://www.firezone.dev/support?utm_source=ios-client")!)
|
||||
}) {
|
||||
Label("Support...", systemImage: "safari")
|
||||
}
|
||||
Button(action: {
|
||||
openURL(URL(string: "https://www.firezone.dev/kb?utm_source=ios=client")!)
|
||||
}) {
|
||||
Label("Documentation...", systemImage: "safari")
|
||||
}
|
||||
Button(
|
||||
action: {
|
||||
openURL(URL(string: "https://www.firezone.dev/support?utm_source=ios-client")!)
|
||||
},
|
||||
label: {
|
||||
Label("Support...", systemImage: "safari")
|
||||
}
|
||||
)
|
||||
Button(
|
||||
action: {
|
||||
openURL(URL(string: "https://www.firezone.dev/kb?utm_source=ios=client")!)
|
||||
},
|
||||
label: {
|
||||
Label("Documentation...", systemImage: "safari")
|
||||
}
|
||||
)
|
||||
} label: {
|
||||
Image(systemName: "person.circle")
|
||||
}
|
||||
|
||||
@@ -11,14 +11,13 @@ import AppKit
|
||||
import NetworkExtension
|
||||
|
||||
@MainActor
|
||||
struct macOSAlert {
|
||||
struct macOSAlert { // swiftlint:disable:this type_name
|
||||
static func show(for error: Error) {
|
||||
if let error = error as? OSSystemExtensionError,
|
||||
// Expected in normal operation
|
||||
error.code != .requestCanceled,
|
||||
error.code != .requestSuperseded,
|
||||
error.code != .authorizationRequired
|
||||
{
|
||||
error.code != .authorizationRequired {
|
||||
alert(message(for: error))
|
||||
}
|
||||
|
||||
@@ -31,7 +30,7 @@ struct macOSAlert {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = messageText
|
||||
alert.alertStyle = .critical
|
||||
let _ = alert.runModal()
|
||||
_ = alert.runModal()
|
||||
}
|
||||
|
||||
// NEVPNError
|
||||
@@ -87,6 +86,7 @@ struct macOSAlert {
|
||||
}
|
||||
|
||||
// OSSystemExtensionError
|
||||
// swiftlint:disable:next cyclomatic_complexity function_body_length
|
||||
private static func message(for error: OSSystemExtensionError) -> String {
|
||||
return {
|
||||
switch error.code {
|
||||
@@ -171,18 +171,18 @@ struct macOSAlert {
|
||||
// Code 11
|
||||
case .requestCanceled:
|
||||
// This will happen if the user cancels
|
||||
fallthrough
|
||||
return "\(error)"
|
||||
|
||||
// Code 12
|
||||
case .requestSuperseded:
|
||||
// This will happen if the user repeatedly clicks "Enable ..."
|
||||
fallthrough
|
||||
return "\(error)"
|
||||
|
||||
// Code 13
|
||||
case .authorizationRequired:
|
||||
// This happens the first time we try to install the system extension.
|
||||
// The user is prompted but we still get this.
|
||||
fallthrough
|
||||
return "\(error)"
|
||||
|
||||
@unknown default:
|
||||
return "\(error)"
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import CryptoKit
|
||||
//
|
||||
// Adapter.swift
|
||||
// (c) 2024 Firezone, Inc.
|
||||
// LICENSE: Apache-2.0
|
||||
//
|
||||
|
||||
// TODO: Refactor to fix file length
|
||||
// swiftlint:disable file_length
|
||||
|
||||
import CryptoKit
|
||||
import FirezoneKit
|
||||
import Foundation
|
||||
import NetworkExtension
|
||||
@@ -54,9 +59,9 @@ class Adapter {
|
||||
private var networkMonitor: NWPathMonitor?
|
||||
|
||||
/// Used to avoid path update callback cycles on iOS
|
||||
#if os(iOS)
|
||||
private var gateways: [Network.NWEndpoint] = []
|
||||
#endif
|
||||
#if os(iOS)
|
||||
private var gateways: [Network.NWEndpoint] = []
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
/// Used for finding system DNS resolvers on macOS when network conditions have changed.
|
||||
@@ -67,7 +72,8 @@ class Adapter {
|
||||
private var lastFetchedResolvers: [String] = []
|
||||
|
||||
/// Remembers the last _relevant_ path update.
|
||||
/// A path update is considered relevant if certain properties change that require us to reset connlib's network state.
|
||||
/// A path update is considered relevant if certain properties change that require us to reset connlib's
|
||||
/// network state.
|
||||
private var lastRelevantPath: Network.NWPath?
|
||||
|
||||
/// Private queue used to ensure consistent ordering among path update and connlib callbacks
|
||||
@@ -140,10 +146,8 @@ class Adapter {
|
||||
interfaceName: path.availableInterfaces.first?.name)
|
||||
|
||||
if self.lastFetchedResolvers != resolvers,
|
||||
let jsonResolvers = try? String(
|
||||
decoding: JSONEncoder().encode(resolvers), as: UTF8.self
|
||||
).intoRustString()
|
||||
{
|
||||
let encoded = try? JSONEncoder().encode(resolvers),
|
||||
let jsonResolvers = String(data: encoded, encoding: .utf8)?.intoRustString() {
|
||||
|
||||
// Update connlib DNS
|
||||
session.setDns(jsonResolvers)
|
||||
@@ -232,18 +236,18 @@ class Adapter {
|
||||
|
||||
// Grab a session pointer
|
||||
let session =
|
||||
try WrappedSession.connect(
|
||||
apiURL,
|
||||
"\(token)",
|
||||
"\(id)",
|
||||
"\(Telemetry.accountSlug!)",
|
||||
DeviceMetadata.getDeviceName(),
|
||||
DeviceMetadata.getOSVersion(),
|
||||
connlibLogFolderPath,
|
||||
logFilter,
|
||||
callbackHandler,
|
||||
String(data: jsonEncoder.encode(DeviceMetadata.deviceInfo()), encoding: .utf8)!
|
||||
)
|
||||
try WrappedSession.connect(
|
||||
apiURL,
|
||||
"\(token)",
|
||||
"\(id)",
|
||||
"\(Telemetry.accountSlug!)",
|
||||
DeviceMetadata.getDeviceName(),
|
||||
DeviceMetadata.getOSVersion(),
|
||||
connlibLogFolderPath,
|
||||
logFilter,
|
||||
callbackHandler,
|
||||
String(data: jsonEncoder.encode(DeviceMetadata.deviceInfo()), encoding: .utf8)!
|
||||
)
|
||||
|
||||
// Start listening for network change events. The first few will be our
|
||||
// tunnel interface coming up, but that's ok -- it will trigger a `set_dns`
|
||||
@@ -253,9 +257,9 @@ class Adapter {
|
||||
// Update state in case everything succeeded
|
||||
self.state = .tunnelStarted(session: session)
|
||||
} catch let error {
|
||||
let msg = error as! RustString
|
||||
// `toString` needed to deep copy the string and avoid a possible dangling pointer
|
||||
throw AdapterError.connlibConnectError(msg.toString())
|
||||
let msg = (error as? RustString)?.toString() ?? "Unknown error"
|
||||
throw AdapterError.connlibConnectError(msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,7 +294,7 @@ class Adapter {
|
||||
// This is async to avoid blocking the main UI thread
|
||||
workQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard case .tunnelStarted(_) = self.state
|
||||
guard case .tunnelStarted = self.state
|
||||
else {
|
||||
Log.debug("\(#function): Invalid state \(self.state)")
|
||||
return
|
||||
@@ -315,7 +319,7 @@ class Adapter {
|
||||
public func setInternetResourceEnabled(_ enabled: Bool) {
|
||||
workQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard case .tunnelStarted(_) = self.state
|
||||
guard case .tunnelStarted = self.state
|
||||
else {
|
||||
Log.debug("\(#function): Invalid state \(self.state)")
|
||||
return
|
||||
@@ -332,16 +336,20 @@ class Adapter {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
internetResource = resources().filter{ $0.isInternetResource() }.first
|
||||
internetResource = resources().filter { $0.isInternetResource() }.first
|
||||
|
||||
var disablingResources: Set<String> = []
|
||||
if let internetResource = internetResource, !internetResourceEnabled {
|
||||
disablingResources.insert(internetResource.id)
|
||||
}
|
||||
|
||||
guard let currentlyDisabled = try? JSONEncoder().encode(disablingResources),
|
||||
let toSet = String(data: currentlyDisabled, encoding: .utf8)
|
||||
else {
|
||||
fatalError("Should be able to encode 'disablingResources'")
|
||||
}
|
||||
|
||||
let currentlyDisabled = try! JSONEncoder().encode(disablingResources)
|
||||
session.setDisabledResources(String(data: currentlyDisabled, encoding: .utf8)!)
|
||||
session.setDisabledResources(toSet)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,32 +363,36 @@ extension Adapter {
|
||||
networkMonitor.start(queue: self.workQueue)
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
private func shouldFetchSystemResolvers(path: Network.NWPath) -> Bool {
|
||||
if path.gateways != gateways {
|
||||
gateways = path.gateways
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
#else
|
||||
private func shouldFetchSystemResolvers(path _: Network.NWPath) -> Bool {
|
||||
#if os(iOS)
|
||||
private func shouldFetchSystemResolvers(path: Network.NWPath) -> Bool {
|
||||
if path.gateways != gateways {
|
||||
gateways = path.gateways
|
||||
return true
|
||||
}
|
||||
#endif
|
||||
|
||||
return false
|
||||
}
|
||||
#else
|
||||
private func shouldFetchSystemResolvers(path _: Network.NWPath) -> Bool {
|
||||
return true
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: Implementing CallbackHandlerDelegate
|
||||
|
||||
extension Adapter: CallbackHandlerDelegate {
|
||||
public func onSetInterfaceConfig(
|
||||
tunnelAddressIPv4: String, tunnelAddressIPv6: String, dnsAddresses: [String], routeListv4: String, routeListv6: String
|
||||
tunnelAddressIPv4: String,
|
||||
tunnelAddressIPv6: String,
|
||||
dnsAddresses: [String],
|
||||
routeListv4: String,
|
||||
routeListv6: String
|
||||
) {
|
||||
// This is a queued callback to ensure ordering
|
||||
workQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard case .tunnelStarted(_) = self.state
|
||||
guard case .tunnelStarted = self.state
|
||||
else {
|
||||
Log.debug("\(#function): Invalid state \(self.state)")
|
||||
return
|
||||
@@ -392,15 +404,22 @@ extension Adapter: CallbackHandlerDelegate {
|
||||
Log.log(
|
||||
"\(#function): \(tunnelAddressIPv4) \(tunnelAddressIPv6) \(dnsAddresses) \(routeListv4) \(routeListv6)")
|
||||
|
||||
guard let data4 = routeListv4.data(using: .utf8),
|
||||
let data6 = routeListv6.data(using: .utf8),
|
||||
let decoded4 = try? JSONDecoder().decode([NetworkSettings.Cidr].self, from: data4),
|
||||
let decoded6 = try? JSONDecoder().decode([NetworkSettings.Cidr].self, from: data6)
|
||||
else {
|
||||
fatalError("Could not decode route list from connlib")
|
||||
}
|
||||
|
||||
let routes4 = decoded4.compactMap({ $0.asNEIPv4Route })
|
||||
let routes6 = decoded6.compactMap({ $0.asNEIPv6Route })
|
||||
|
||||
networkSettings.tunnelAddressIPv4 = tunnelAddressIPv4
|
||||
networkSettings.tunnelAddressIPv6 = tunnelAddressIPv6
|
||||
networkSettings.dnsAddresses = dnsAddresses
|
||||
networkSettings.routes4 = try! JSONDecoder().decode(
|
||||
[NetworkSettings.Cidr].self, from: routeListv4.data(using: .utf8)!
|
||||
).compactMap { $0.asNEIPv4Route }
|
||||
networkSettings.routes6 = try! JSONDecoder().decode(
|
||||
[NetworkSettings.Cidr].self, from: routeListv6.data(using: .utf8)!
|
||||
).compactMap { $0.asNEIPv6Route }
|
||||
networkSettings.routes4 = routes4
|
||||
networkSettings.routes6 = routes6
|
||||
|
||||
networkSettings.apply()
|
||||
}
|
||||
@@ -410,7 +429,7 @@ extension Adapter: CallbackHandlerDelegate {
|
||||
// This is a queued callback to ensure ordering
|
||||
workQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard case .tunnelStarted(_) = self.state
|
||||
guard case .tunnelStarted = self.state
|
||||
else {
|
||||
Log.debug("Tried to call \(#function) while state is \(self.state)")
|
||||
return
|
||||
@@ -430,7 +449,7 @@ extension Adapter: CallbackHandlerDelegate {
|
||||
// to ensure that we can clean up even if connlib exits before we are done.
|
||||
workQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard case .tunnelStarted(_) = self.state
|
||||
guard case .tunnelStarted = self.state
|
||||
else {
|
||||
Log.debug("\(#function): Invalid state \(self.state)")
|
||||
return
|
||||
@@ -457,12 +476,12 @@ extension Adapter: CallbackHandlerDelegate {
|
||||
}
|
||||
|
||||
private func getSystemDefaultResolvers(interfaceName: String?) -> [String] {
|
||||
#if os(macOS)
|
||||
let resolvers = self.systemConfigurationResolvers.getDefaultDNSServers(
|
||||
interfaceName: interfaceName)
|
||||
#elseif os(iOS)
|
||||
let resolvers = resetToSystemDNSGettingBindResolvers()
|
||||
#endif
|
||||
#if os(macOS)
|
||||
let resolvers = self.systemConfigurationResolvers.getDefaultDNSServers(
|
||||
interfaceName: interfaceName)
|
||||
#elseif os(iOS)
|
||||
let resolvers = resetToSystemDNSGettingBindResolvers()
|
||||
#endif
|
||||
|
||||
return resolvers
|
||||
}
|
||||
@@ -470,54 +489,54 @@ extension Adapter: CallbackHandlerDelegate {
|
||||
|
||||
// MARK: Getting System Resolvers on iOS
|
||||
#if os(iOS)
|
||||
extension Adapter {
|
||||
// When the tunnel is up, we can only get the system's default resolvers
|
||||
// by reading /etc/resolv.conf when matchDomains is set to a non-empty string.
|
||||
// If matchDomains is an empty string, /etc/resolv.conf will contain connlib's
|
||||
// sentinel, which isn't helpful to us.
|
||||
private func resetToSystemDNSGettingBindResolvers() -> [String] {
|
||||
guard let networkSettings = networkSettings
|
||||
else {
|
||||
// Network Settings hasn't been applied yet, so our sentinel isn't
|
||||
// the system's resolver and we can grab the system resolvers directly.
|
||||
// If we try to continue below without valid tunnel addresses assigned
|
||||
// to the interface, we'll crash.
|
||||
return BindResolvers().getservers().map(BindResolvers.getnameinfo)
|
||||
}
|
||||
|
||||
var resolvers: [String] = []
|
||||
|
||||
// The caller is in an async context, so it's ok to block this thread here.
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
|
||||
// Set tunnel's matchDomains to a dummy string that will never match any name
|
||||
networkSettings.matchDomains = ["firezone-fd0020211111"]
|
||||
|
||||
// Call apply to populate /etc/resolv.conf with the system's default resolvers
|
||||
networkSettings.apply {
|
||||
guard let networkSettings = self.networkSettings else { return }
|
||||
|
||||
// Only now can we get the system resolvers
|
||||
resolvers = BindResolvers().getservers().map(BindResolvers.getnameinfo)
|
||||
|
||||
// Restore connlib's DNS resolvers
|
||||
networkSettings.matchDomains = [""]
|
||||
networkSettings.apply { semaphore.signal() }
|
||||
}
|
||||
|
||||
semaphore.wait()
|
||||
return resolvers
|
||||
extension Adapter {
|
||||
// When the tunnel is up, we can only get the system's default resolvers
|
||||
// by reading /etc/resolv.conf when matchDomains is set to a non-empty string.
|
||||
// If matchDomains is an empty string, /etc/resolv.conf will contain connlib's
|
||||
// sentinel, which isn't helpful to us.
|
||||
private func resetToSystemDNSGettingBindResolvers() -> [String] {
|
||||
guard let networkSettings = networkSettings
|
||||
else {
|
||||
// Network Settings hasn't been applied yet, so our sentinel isn't
|
||||
// the system's resolver and we can grab the system resolvers directly.
|
||||
// If we try to continue below without valid tunnel addresses assigned
|
||||
// to the interface, we'll crash.
|
||||
return BindResolvers().getservers().map(BindResolvers.getnameinfo)
|
||||
}
|
||||
|
||||
var resolvers: [String] = []
|
||||
|
||||
// The caller is in an async context, so it's ok to block this thread here.
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
|
||||
// Set tunnel's matchDomains to a dummy string that will never match any name
|
||||
networkSettings.matchDomains = ["firezone-fd0020211111"]
|
||||
|
||||
// Call apply to populate /etc/resolv.conf with the system's default resolvers
|
||||
networkSettings.apply {
|
||||
guard let networkSettings = self.networkSettings else { return }
|
||||
|
||||
// Only now can we get the system resolvers
|
||||
resolvers = BindResolvers().getservers().map(BindResolvers.getnameinfo)
|
||||
|
||||
// Restore connlib's DNS resolvers
|
||||
networkSettings.matchDomains = [""]
|
||||
networkSettings.apply { semaphore.signal() }
|
||||
}
|
||||
|
||||
semaphore.wait()
|
||||
return resolvers
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
extension Network.NWPath {
|
||||
func connectivityDifferentFrom(path: Network.NWPath) -> Bool {
|
||||
// We define a path as different from another if the following properties change
|
||||
return path.supportsIPv4 != self.supportsIPv4 ||
|
||||
path.supportsIPv6 != self.supportsIPv6 ||
|
||||
path.availableInterfaces.first?.name != self.availableInterfaces.first?.name ||
|
||||
// Apple provides no documentation on whether order is meaningful, so assume it isn't.
|
||||
Set(self.gateways) != Set(path.gateways)
|
||||
path.supportsIPv6 != self.supportsIPv6 ||
|
||||
path.availableInterfaces.first?.name != self.availableInterfaces.first?.name ||
|
||||
// Apple provides no documentation on whether order is meaningful, so assume it isn't.
|
||||
Set(self.gateways) != Set(path.gateways)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,12 +28,12 @@ public class BindResolvers {
|
||||
}
|
||||
|
||||
extension BindResolvers {
|
||||
public static func getnameinfo(_ s: res_9_sockaddr_union) -> String {
|
||||
var s = s
|
||||
public static func getnameinfo(_ sock: res_9_sockaddr_union) -> String {
|
||||
var sockUnion = sock
|
||||
var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
||||
|
||||
let sinlen = socklen_t(s.sin.sin_len)
|
||||
let _ = withUnsafePointer(to: &s) {
|
||||
let sinlen = socklen_t(sockUnion.sin.sin_len)
|
||||
_ = withUnsafePointer(to: &sockUnion) {
|
||||
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
|
||||
Darwin.getnameinfo(
|
||||
$0, sinlen,
|
||||
|
||||
@@ -12,7 +12,7 @@ import OSLog
|
||||
// shall get updated.
|
||||
// This is so that the app stays buildable even when the FFI changes.
|
||||
|
||||
// TODO: https://github.com/chinedufn/swift-bridge/issues/150
|
||||
// See https://github.com/chinedufn/swift-bridge/issues/150
|
||||
extension RustString: @unchecked Sendable {}
|
||||
extension RustString: Error {}
|
||||
|
||||
@@ -48,8 +48,11 @@ public class CallbackHandler {
|
||||
IPv6 routes: \(routeListv6.toString())
|
||||
""")
|
||||
|
||||
let dnsData = dnsAddresses.toString().data(using: .utf8)!
|
||||
let dnsArray = try! JSONDecoder().decode([String].self, from: dnsData)
|
||||
guard let dnsData = dnsAddresses.toString().data(using: .utf8),
|
||||
let dnsArray = try? JSONDecoder().decode([String].self, from: dnsData)
|
||||
else {
|
||||
fatalError("Should be able to decode DNS Addresses from connlib")
|
||||
}
|
||||
|
||||
delegate?.onSetInterfaceConfig(
|
||||
tunnelAddressIPv4: tunnelAddressIPv4.toString(),
|
||||
|
||||
@@ -39,7 +39,8 @@ class NetworkSettings {
|
||||
// Set tunnel addresses and routes
|
||||
let ipv4Settings = NEIPv4Settings(addresses: [tunnelAddressIPv4!], subnetMasks: ["255.255.255.255"])
|
||||
// This is a hack since macos routing table ignores, for full route, any prefix smaller than 120.
|
||||
// Without this, adding a full route, remove the previous default route and leaves the system with none, breaking completely IPv6 on the user's system.
|
||||
// Without this, adding a full route, remove the previous default route and leaves the system with none,
|
||||
// completely breaking IPv6 on the user's system.
|
||||
let ipv6Settings = NEIPv6Settings(addresses: [tunnelAddressIPv6!], networkPrefixLengths: [120])
|
||||
let dnsSettings = NEDNSSettings(servers: dnsAddresses)
|
||||
ipv4Settings.includedRoutes = routes4
|
||||
@@ -96,7 +97,7 @@ enum IPv4SubnetMaskLookup {
|
||||
29: "255.255.255.248",
|
||||
30: "255.255.255.252",
|
||||
31: "255.255.255.254",
|
||||
32: "255.255.255.255",
|
||||
32: "255.255.255.255"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
super.init()
|
||||
}
|
||||
|
||||
// TODO: Refactor this to shorten function body
|
||||
// swiftlint:disable:next function_body_length
|
||||
override func startTunnel(
|
||||
options: [String: NSObject]?,
|
||||
completionHandler: @escaping (Error?) -> Void
|
||||
@@ -84,7 +86,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
Telemetry.accountSlug = accountSlug
|
||||
|
||||
let internetResourceEnabled: Bool = if let internetResourceEnabledJSON = providerConfiguration[VPNConfigurationManagerKeys.internetResourceEnabled]?.data(using: .utf8) {
|
||||
let internetResourceEnabled: Bool =
|
||||
if let internetResourceEnabledJSON = providerConfiguration[
|
||||
VPNConfigurationManagerKeys.internetResourceEnabled]?.data(using: .utf8) {
|
||||
(try? JSONDecoder().decode(Bool.self, from: internetResourceEnabledJSON )) ?? false
|
||||
} else {
|
||||
false
|
||||
@@ -147,10 +151,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
)
|
||||
}
|
||||
}
|
||||
#if os(iOS)
|
||||
// iOS notifications should be shown from the tunnel process
|
||||
SessionNotification.showSignedOutNotificationiOS()
|
||||
#endif
|
||||
#if os(iOS)
|
||||
// iOS notifications should be shown from the tunnel process
|
||||
SessionNotification.showSignedOutNotificationiOS()
|
||||
#endif
|
||||
}
|
||||
|
||||
// handles both connlib-initiated and user-initiated stops
|
||||
@@ -161,7 +165,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
// TODO: It would be helpful to be able to encapsulate Errors here. To do that
|
||||
// It would be helpful to be able to encapsulate Errors here. To do that
|
||||
// we need to update TunnelMessage to encode/decode Result to and from Data.
|
||||
override func handleAppMessage(_ message: Data, completionHandler: ((Data?) -> Void)? = nil) {
|
||||
guard let tunnelMessage = try? PropertyListDecoder().decode(TunnelMessage.self, from: message) else { return }
|
||||
@@ -176,8 +180,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
Log.error(error)
|
||||
}
|
||||
case .getResourceList(let value):
|
||||
adapter?.getResourcesIfVersionDifferentFrom(hash: value) {
|
||||
resourceListJSON in
|
||||
adapter?.getResourcesIfVersionDifferentFrom(hash: value) { resourceListJSON in
|
||||
completionHandler?(resourceListJSON?.data(using: .utf8))
|
||||
}
|
||||
case .clearLogs:
|
||||
@@ -208,7 +211,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
func loadAndSaveFirezoneId(from options: [String: NSObject]?) -> String {
|
||||
let passedId = options?["id"] as? String
|
||||
let persistedId = FirezoneId.load(.Post_1_4_0)
|
||||
let persistedId = FirezoneId.load(.post140)
|
||||
|
||||
let id = passedId ?? persistedId ?? UUID().uuidString
|
||||
|
||||
|
||||
@@ -31,20 +31,18 @@ class SystemConfigurationResolvers {
|
||||
/// can fail in some circumstances to initialize, like because of allocation failures.
|
||||
private var _dynamicStore: SCDynamicStore?
|
||||
private var dynamicStore: SCDynamicStore? {
|
||||
get {
|
||||
if self._dynamicStore == nil {
|
||||
guard let dynamicStore = SCDynamicStoreCreate(nil, storeName, nil, nil)
|
||||
else {
|
||||
let code = SCError()
|
||||
Log.error(SystemConfigurationError.failedToCreateDynamicStore(code: code))
|
||||
return nil
|
||||
}
|
||||
|
||||
self._dynamicStore = dynamicStore
|
||||
if self._dynamicStore == nil {
|
||||
guard let dynamicStore = SCDynamicStoreCreate(nil, storeName, nil, nil)
|
||||
else {
|
||||
let code = SCError()
|
||||
Log.error(SystemConfigurationError.failedToCreateDynamicStore(code: code))
|
||||
return nil
|
||||
}
|
||||
|
||||
return self._dynamicStore
|
||||
self._dynamicStore = dynamicStore
|
||||
}
|
||||
|
||||
return self._dynamicStore
|
||||
}
|
||||
|
||||
// Arbitrary name for the connection to the store
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
##################################################
|
||||
# We call this from an Xcode run script.
|
||||
##################################################
|
||||
|
||||
set -e
|
||||
|
||||
if [[ "$(uname -m)" == arm64 ]]; then
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
fi
|
||||
|
||||
if which swift-format >/dev/null; then
|
||||
find . -name "*.swift" -not -path "./FirezoneNetworkExtension/Connlib/Generated/*" -exec xargs swift-format lint --strict \;
|
||||
else
|
||||
echo "warning: swift-format not installed, install with 'brew install swift-format'"
|
||||
fi
|
||||
Reference in New Issue
Block a user