From ace4d5234613a8a88e6cbfe69665e9176d2a7637 Mon Sep 17 00:00:00 2001 From: Jamil Date: Thu, 30 Jan 2025 15:09:56 +0000 Subject: [PATCH] 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 --- swift/apple/.swiftlint.yml | 7 + .../apple/Firezone.xcodeproj/project.pbxproj | 49 +- .../xcshareddata/swiftpm/Package.resolved | 9 + swift/apple/FirezoneKit/Package.swift | 2 +- .../FirezoneKit/Helpers/DeviceMetadata.swift | 11 +- .../Sources/FirezoneKit/Helpers/Log.swift | 13 +- .../FirezoneKit/Helpers/SemanticVersion.swift | 131 ++---- .../FirezoneKit/Helpers/Telemetry.swift | 12 +- .../FirezoneKit/Keychain/Keychain.swift | 56 +-- .../FirezoneKit/Keychain/KeychainStatus.swift | 427 ------------------ .../Managers/SystemExtensionManager.swift | 22 +- .../Managers/VPNConfigurationManager.swift | 137 +++--- .../FirezoneKit/Models/AuthClient.swift | 1 - .../FirezoneKit/Models/FirezoneId.swift | 8 +- .../Sources/FirezoneKit/Models/Resource.swift | 31 +- .../Models/SessionNotification.swift | 6 +- .../Sources/FirezoneKit/Models/Settings.swift | 16 +- .../Sources/FirezoneKit/Models/Token.swift | 2 +- .../FirezoneKit/Models/WebAuthSession.swift | 6 +- .../Sources/FirezoneKit/Stores/Store.swift | 15 +- .../Sources/FirezoneKit/Views/AppView.swift | 3 +- .../FirezoneKit/Views/GrantVPNView.swift | 34 +- .../Sources/FirezoneKit/Views/MenuBar.swift | 87 ++-- .../FirezoneKit/Views/ResourceView.swift | 154 ++++--- .../FirezoneKit/Views/SessionView.swift | 8 +- .../FirezoneKit/Views/SettingsView.swift | 16 +- .../Views/UpdateNotification.swift | 164 ++++--- .../FirezoneKit/Views/iOSNavigationView.swift | 93 ++-- .../FirezoneKit/Views/macOSAlert.swift | 14 +- .../FirezoneNetworkExtension/Adapter.swift | 213 +++++---- .../BindResolvers.swift | 8 +- .../CallbackHandler.swift | 9 +- .../NetworkSettings.swift | 5 +- .../PacketTunnelProvider.swift | 21 +- .../SystemConfigurationResolvers.swift | 20 +- swift/apple/lint.sh | 17 - 36 files changed, 752 insertions(+), 1075 deletions(-) create mode 100644 swift/apple/.swiftlint.yml delete mode 100644 swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/KeychainStatus.swift delete mode 100755 swift/apple/lint.sh diff --git a/swift/apple/.swiftlint.yml b/swift/apple/.swiftlint.yml new file mode 100644 index 000000000..ed31c01ae --- /dev/null +++ b/swift/apple/.swiftlint.yml @@ -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 diff --git a/swift/apple/Firezone.xcodeproj/project.pbxproj b/swift/apple/Firezone.xcodeproj/project.pbxproj index 63bf236de..4dc6a20b1 100644 --- a/swift/apple/Firezone.xcodeproj/project.pbxproj +++ b/swift/apple/Firezone.xcodeproj/project.pbxproj @@ -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 */ diff --git a/swift/apple/Firezone.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/swift/apple/Firezone.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index dfceeff2b..cacdba912 100644 --- a/swift/apple/Firezone.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/swift/apple/Firezone.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -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 diff --git a/swift/apple/FirezoneKit/Package.swift b/swift/apple/FirezoneKit/Package.swift index 8b812f19a..4d3f5b55a 100644 --- a/swift/apple/FirezoneKit/Package.swift +++ b/swift/apple/FirezoneKit/Package.swift @@ -20,6 +20,6 @@ let package = Package( .testTarget( name: "FirezoneKitTests", dependencies: ["FirezoneKit"] - ), + ) ] ) diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/DeviceMetadata.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/DeviceMetadata.swift index 44bd8eb18..8d6b930ee 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/DeviceMetadata.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/DeviceMetadata.swift @@ -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 } diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/Log.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/Log.swift index f940437d8..a823b13b6 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/Log.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/Log.swift @@ -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) diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/SemanticVersion.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/SemanticVersion.swift index cf6e57db0..b174ac69f 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/SemanticVersion.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/SemanticVersion.swift @@ -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 } } diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/Telemetry.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/Telemetry.swift index b4498e384..c0f11197f 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/Telemetry.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/Telemetry.swift @@ -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() { diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/Keychain.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/Keychain.swift index 2c0b1656b..2f8031f8a 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/Keychain.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/Keychain.swift @@ -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) } } } diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/KeychainStatus.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/KeychainStatus.swift deleted file mode 100644 index f8fc8628a..000000000 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Keychain/KeychainStatus.swift +++ /dev/null @@ -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 - } -} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Managers/SystemExtensionManager.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Managers/SystemExtensionManager.swift index bb8a1e7c6..13cee461b 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Managers/SystemExtensionManager.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Managers/SystemExtensionManager.swift @@ -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 } diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Managers/VPNConfigurationManager.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Managers/VPNConfigurationManager.swift index 081144cfa..136b39791 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Managers/VPNConfigurationManager.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Managers/VPNConfigurationManager.swift @@ -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) diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/AuthClient.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/AuthClient.swift index 9afaad6b1..026cced2a 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/AuthClient.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/AuthClient.swift @@ -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 diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/FirezoneId.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/FirezoneId.swift index ee9b7bf11..ba00e6467 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/FirezoneId.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/FirezoneId.swift @@ -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) diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Resource.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Resource.swift index 531428cf8..927ab7ece 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Resource.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Resource.swift @@ -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 } diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/SessionNotification.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/SessionNotification.swift index 039d3d61f..f512addd5 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/SessionNotification.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/SessionNotification.swift @@ -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 diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Settings.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Settings.swift index 0530a2bb7..15d693ebe 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Settings.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Settings.swift @@ -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 ] } diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Token.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Token.swift index b4d528098..51a664ec3 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Token.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Token.swift @@ -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 diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/WebAuthSession.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/WebAuthSession.swift index 0d160503e..9a270c545 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/WebAuthSession.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/WebAuthSession.swift @@ -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 { diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift index f8c6b239f..20177221d 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift @@ -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) in + let status = + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) 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) in + self.systemExtensionStatus = + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) 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) } diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/AppView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/AppView.swift index 15dd421c0..5e9636593 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/AppView.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/AppView.swift @@ -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) } } } diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/GrantVPNView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/GrantVPNView.swift index 6f4999f15..94f576721 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/GrantVPNView.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/GrantVPNView.swift @@ -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) diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift index a4bc194b3..5868e2161 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift @@ -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 diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/ResourceView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/ResourceView.swift index 66b7274bd..ebda3d4c7 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/ResourceView.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/ResourceView.swift @@ -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() + } } - } + ) } } diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift index aa004e6d7..0a84470c3 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift @@ -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 } diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SettingsView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SettingsView.swift index 062ab2241..760ab24ae 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SettingsView.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SettingsView.swift @@ -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() } } diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/UpdateNotification.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/UpdateNotification.swift index 843d2ce62..e1dbf744a 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/UpdateNotification.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/UpdateNotification.swift @@ -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 diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/iOSNavigationView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/iOSNavigationView.swift index d702803a0..8e4204d3d 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/iOSNavigationView.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/iOSNavigationView.swift @@ -9,7 +9,7 @@ import SwiftUI #if os(iOS) -struct iOSNavigationView: View { +struct iOSNavigationView: 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: 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: 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") } diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/macOSAlert.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/macOSAlert.swift index 49effc14b..b95bd9cf8 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/macOSAlert.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/macOSAlert.swift @@ -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)" diff --git a/swift/apple/FirezoneNetworkExtension/Adapter.swift b/swift/apple/FirezoneNetworkExtension/Adapter.swift index 8c72f81aa..428746d52 100644 --- a/swift/apple/FirezoneNetworkExtension/Adapter.swift +++ b/swift/apple/FirezoneNetworkExtension/Adapter.swift @@ -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 = [] 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) } } diff --git a/swift/apple/FirezoneNetworkExtension/BindResolvers.swift b/swift/apple/FirezoneNetworkExtension/BindResolvers.swift index b47681b47..d750b131f 100644 --- a/swift/apple/FirezoneNetworkExtension/BindResolvers.swift +++ b/swift/apple/FirezoneNetworkExtension/BindResolvers.swift @@ -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, diff --git a/swift/apple/FirezoneNetworkExtension/CallbackHandler.swift b/swift/apple/FirezoneNetworkExtension/CallbackHandler.swift index c3a8f91cc..5ba9f458e 100644 --- a/swift/apple/FirezoneNetworkExtension/CallbackHandler.swift +++ b/swift/apple/FirezoneNetworkExtension/CallbackHandler.swift @@ -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(), diff --git a/swift/apple/FirezoneNetworkExtension/NetworkSettings.swift b/swift/apple/FirezoneNetworkExtension/NetworkSettings.swift index 3f03fd6b1..51e5e0b4d 100644 --- a/swift/apple/FirezoneNetworkExtension/NetworkSettings.swift +++ b/swift/apple/FirezoneNetworkExtension/NetworkSettings.swift @@ -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" ] } diff --git a/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift b/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift index c25254cf1..6ec1833de 100644 --- a/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift +++ b/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift @@ -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 diff --git a/swift/apple/FirezoneNetworkExtension/SystemConfigurationResolvers.swift b/swift/apple/FirezoneNetworkExtension/SystemConfigurationResolvers.swift index 7118c2fb4..d41f0b94b 100644 --- a/swift/apple/FirezoneNetworkExtension/SystemConfigurationResolvers.swift +++ b/swift/apple/FirezoneNetworkExtension/SystemConfigurationResolvers.swift @@ -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 diff --git a/swift/apple/lint.sh b/swift/apple/lint.sh deleted file mode 100755 index e5bf74bfa..000000000 --- a/swift/apple/lint.sh +++ /dev/null @@ -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