11import CryptoKit
22import Foundation
33
4- public protocol Validator : Sendable {
5- func validate( path: URL ) async throws
6- }
7-
84public 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 }
0 commit comments