Skip to content

Commit 844df27

Browse files
committed
download and validator to functions
1 parent 4eac98b commit 844df27

File tree

3 files changed

+66
-72
lines changed

3 files changed

+66
-72
lines changed

Coder Desktop/VPN/Manager.swift

-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import VPNLib
44

55
actor Manager {
66
let ptp: PacketTunnelProvider
7-
let downloader: Downloader
87

98
var tunnelHandle: TunnelHandle?
109
var speaker: Speaker<Vpn_ManagerMessage, Vpn_TunnelMessage>?
@@ -16,6 +15,5 @@ actor Manager {
1615

1716
init(with: PacketTunnelProvider) {
1817
ptp = with
19-
downloader = Downloader()
2018
}
2119
}

Coder Desktop/VPNLib/Downloader.swift renamed to Coder Desktop/VPNLib/Download.swift

+59-57
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import CryptoKit
22
import Foundation
33

4-
public protocol Validator: Sendable {
5-
func validate(path: URL) async throws
6-
}
7-
84
public enum ValidationError: Error {
95
case fileNotFound
106
case unableToCreateStaticCode
@@ -37,114 +33,114 @@ public enum ValidationError: Error {
3733
}
3834
}
3935

40-
public struct SignatureValidator: Validator {
41-
private let expectedName = "CoderVPN"
42-
private let expectedIdentifier = "com.coder.Coder-Desktop.VPN.dylib"
43-
private let expectedTeamIdentifier = "4399GN35BJ"
44-
private let minDylibVersion = "2.18.1"
36+
public class SignatureValidator {
37+
private static let expectedName = "CoderVPN"
38+
private static let expectedIdentifier = "com.coder.Coder-Desktop.VPN.dylib"
39+
private static let expectedTeamIdentifier = "4399GN35BJ"
40+
private static let minDylibVersion = "2.18.1"
4541

46-
private let infoIdentifierKey = "CFBundleIdentifier"
47-
private let infoNameKey = "CFBundleName"
48-
private let infoShortVersionKey = "CFBundleShortVersionString"
42+
private static let infoIdentifierKey = "CFBundleIdentifier"
43+
private static let infoNameKey = "CFBundleName"
44+
private static let infoShortVersionKey = "CFBundleShortVersionString"
4945

50-
private let signInfoFlags: SecCSFlags = .init(rawValue: kSecCSSigningInformation)
46+
private static let signInfoFlags: SecCSFlags = .init(rawValue: kSecCSSigningInformation)
5147

52-
public init() {}
53-
54-
public func validate(path: URL) throws {
48+
public static func validate(path: URL) throws(ValidationError) {
5549
guard FileManager.default.fileExists(atPath: path.path) else {
56-
throw ValidationError.fileNotFound
50+
throw .fileNotFound
5751
}
5852

5953
var staticCode: SecStaticCode?
6054
let status = SecStaticCodeCreateWithPath(path as CFURL, SecCSFlags(), &staticCode)
6155
guard status == errSecSuccess, let code = staticCode else {
62-
throw ValidationError.unableToCreateStaticCode
56+
throw .unableToCreateStaticCode
6357
}
6458

6559
let validateStatus = SecStaticCodeCheckValidity(code, SecCSFlags(), nil)
6660
guard validateStatus == errSecSuccess else {
67-
throw ValidationError.invalidSignature
61+
throw .invalidSignature
6862
}
6963

7064
var information: CFDictionary?
7165
let infoStatus = SecCodeCopySigningInformation(code, signInfoFlags, &information)
7266
guard infoStatus == errSecSuccess, let info = information as? [String: Any] else {
73-
throw ValidationError.unableToRetrieveInfo
67+
throw .unableToRetrieveInfo
7468
}
7569

7670
guard let identifier = info[kSecCodeInfoIdentifier as String] as? String,
7771
identifier == expectedIdentifier
7872
else {
79-
throw ValidationError.invalidIdentifier(identifier: info[kSecCodeInfoIdentifier as String] as? String)
73+
throw .invalidIdentifier(identifier: info[kSecCodeInfoIdentifier as String] as? String)
8074
}
8175

8276
guard let teamIdentifier = info[kSecCodeInfoTeamIdentifier as String] as? String,
8377
teamIdentifier == expectedTeamIdentifier
8478
else {
85-
throw ValidationError.invalidTeamIdentifier(
79+
throw .invalidTeamIdentifier(
8680
identifier: info[kSecCodeInfoTeamIdentifier as String] as? String
8781
)
8882
}
8983

9084
guard let infoPlist = info[kSecCodeInfoPList as String] as? [String: AnyObject] else {
91-
throw ValidationError.missingInfoPList
85+
throw .missingInfoPList
9286
}
9387

9488
guard let plistIdent = infoPlist[infoIdentifierKey] as? String, plistIdent == expectedIdentifier else {
95-
throw ValidationError.invalidIdentifier(identifier: infoPlist[infoIdentifierKey] as? String)
89+
throw .invalidIdentifier(identifier: infoPlist[infoIdentifierKey] as? String)
9690
}
9791

9892
guard let plistName = infoPlist[infoNameKey] as? String, plistName == expectedName else {
99-
throw ValidationError.invalidIdentifier(identifier: infoPlist[infoNameKey] as? String)
93+
throw .invalidIdentifier(identifier: infoPlist[infoNameKey] as? String)
10094
}
10195

10296
guard let dylibVersion = infoPlist[infoShortVersionKey] as? String,
10397
minDylibVersion.compare(dylibVersion, options: .numeric) != .orderedDescending
10498
else {
105-
throw ValidationError.invalidVersion(version: infoPlist[infoShortVersionKey] as? String)
99+
throw .invalidVersion(version: infoPlist[infoShortVersionKey] as? String)
106100
}
107101
}
108102
}
109103

110-
public struct Downloader: Sendable {
111-
let validator: Validator
112-
public init(validator: Validator = SignatureValidator()) {
113-
self.validator = validator
114-
}
115-
116-
public func download(src: URL, dest: URL) async throws {
117-
var req = URLRequest(url: src)
118-
if FileManager.default.fileExists(atPath: dest.path) {
119-
if let existingFileData = try? Data(contentsOf: dest, options: .mappedIfSafe) {
120-
req.setValue(etag(data: existingFileData), forHTTPHeaderField: "If-None-Match")
121-
}
104+
public func download(src: URL, dest: URL) async throws(DownloadError) {
105+
var req = URLRequest(url: src)
106+
if FileManager.default.fileExists(atPath: dest.path) {
107+
if let existingFileData = try? Data(contentsOf: dest, options: .mappedIfSafe) {
108+
req.setValue(etag(data: existingFileData), forHTTPHeaderField: "If-None-Match")
122109
}
123-
// TODO: Add Content-Length headers to coderd, add download progress delegate
124-
let (tempURL, response) = try await URLSession.shared.download(for: req)
125-
defer {
126-
if FileManager.default.fileExists(atPath: tempURL.path) {
127-
do { try FileManager.default.removeItem(at: tempURL) } catch {}
128-
}
110+
}
111+
// TODO: Add Content-Length headers to coderd, add download progress delegate
112+
let tempURL: URL
113+
let response: URLResponse
114+
do {
115+
(tempURL, response) = try await URLSession.shared.download(for: req)
116+
} catch {
117+
throw .networkError(error)
118+
}
119+
defer {
120+
if FileManager.default.fileExists(atPath: tempURL.path) {
121+
try? FileManager.default.removeItem(at: tempURL)
129122
}
123+
}
130124

131-
guard let httpResponse = response as? HTTPURLResponse else {
132-
throw DownloadError.invalidResponse
133-
}
134-
guard httpResponse.statusCode != 304 else {
135-
// We already have the latest dylib downloaded on disk
136-
return
137-
}
125+
guard let httpResponse = response as? HTTPURLResponse else {
126+
throw .invalidResponse
127+
}
128+
guard httpResponse.statusCode != 304 else {
129+
// We already have the latest dylib downloaded on disk
130+
return
131+
}
138132

139-
guard httpResponse.statusCode == 200 else {
140-
throw DownloadError.unexpectedStatusCode(httpResponse.statusCode)
141-
}
133+
guard httpResponse.statusCode == 200 else {
134+
throw .unexpectedStatusCode(httpResponse.statusCode)
135+
}
142136

137+
do {
143138
if FileManager.default.fileExists(atPath: dest.path) {
144139
try FileManager.default.removeItem(at: dest)
145140
}
146141
try FileManager.default.moveItem(at: tempURL, to: dest)
147-
try await validator.validate(path: dest)
142+
} catch {
143+
throw .fileOpError(error)
148144
}
149145
}
150146

@@ -154,14 +150,20 @@ func etag(data: Data) -> String {
154150
return "\"\(etag)\""
155151
}
156152

157-
enum DownloadError: Error {
153+
public enum DownloadError: Error {
158154
case unexpectedStatusCode(Int)
159155
case invalidResponse
156+
case networkError(any Error)
157+
case fileOpError(any Error)
160158

161159
var localizedDescription: String {
162160
switch self {
163161
case let .unexpectedStatusCode(code):
164-
return "Unexpected status code: \(code)"
162+
return "Unexpected HTTP status code: \(code)"
163+
case let .networkError(error):
164+
return "Network error: \(error.localizedDescription)"
165+
case let .fileOpError(error):
166+
return "File operation error: \(error.localizedDescription)"
165167
case .invalidResponse:
166168
return "Received non-HTTP response"
167169
}

Coder Desktop/VPNLibTests/DownloaderTests.swift renamed to Coder Desktop/VPNLibTests/DownloadTests.swift

+7-13
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,8 @@ import Mocker
33
import Testing
44
@testable import VPNLib
55

6-
struct NoopValidator: Validator {
7-
func validate(path _: URL) async throws {}
8-
}
9-
10-
@Suite
11-
struct DownloaderTests {
12-
let downloader = Downloader(validator: NoopValidator())
13-
6+
@Suite(.timeLimit(.minutes(1)))
7+
struct DownloadTests {
148
@Test
159
func downloadFile() async throws {
1610
let destinationURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
@@ -19,7 +13,7 @@ struct DownloaderTests {
1913
let fileURL = URL(string: "http://example.com/test1.txt")!
2014
Mock(url: fileURL, contentType: .html, statusCode: 200, data: [.get: testData]).register()
2115

22-
try await downloader.download(src: fileURL, dest: destinationURL)
16+
try await download(src: fileURL, dest: destinationURL)
2317

2418
try #require(FileManager.default.fileExists(atPath: destinationURL.path))
2519
defer { try? FileManager.default.removeItem(at: destinationURL) }
@@ -38,7 +32,7 @@ struct DownloaderTests {
3832

3933
Mock(url: fileURL, contentType: .html, statusCode: 200, data: [.get: testData]).register()
4034

41-
try await downloader.download(src: fileURL, dest: destinationURL)
35+
try await download(src: fileURL, dest: destinationURL)
4236
try #require(FileManager.default.fileExists(atPath: destinationURL.path))
4337
let downloadedData = try Data(contentsOf: destinationURL)
4438
#expect(downloadedData == testData)
@@ -50,7 +44,7 @@ struct DownloaderTests {
5044
}
5145
mock.register()
5246

53-
try await downloader.download(src: fileURL, dest: destinationURL)
47+
try await download(src: fileURL, dest: destinationURL)
5448
let unchangedData = try Data(contentsOf: destinationURL)
5549
#expect(unchangedData == testData)
5650
#expect(etagIncluded)
@@ -67,7 +61,7 @@ struct DownloaderTests {
6761

6862
Mock(url: fileURL, contentType: .html, statusCode: 200, data: [.get: ogData]).register()
6963

70-
try await downloader.download(src: fileURL, dest: destinationURL)
64+
try await download(src: fileURL, dest: destinationURL)
7165
try #require(FileManager.default.fileExists(atPath: destinationURL.path))
7266
var downloadedData = try Data(contentsOf: destinationURL)
7367
#expect(downloadedData == ogData)
@@ -79,7 +73,7 @@ struct DownloaderTests {
7973
}
8074
mock.register()
8175

82-
try await downloader.download(src: fileURL, dest: destinationURL)
76+
try await download(src: fileURL, dest: destinationURL)
8377
downloadedData = try Data(contentsOf: destinationURL)
8478
#expect(downloadedData == newData)
8579
#expect(etagIncluded)

0 commit comments

Comments
 (0)