1
1
import CryptoKit
2
2
import Foundation
3
3
4
- public protocol Validator : Sendable {
5
- func validate( path: URL ) async throws
6
- }
7
-
8
4
public enum ValidationError : Error {
9
5
case fileNotFound
10
6
case unableToCreateStaticCode
@@ -37,114 +33,114 @@ public enum ValidationError: Error {
37
33
}
38
34
}
39
35
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 "
45
41
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 "
49
45
50
- private let signInfoFlags : SecCSFlags = . init( rawValue: kSecCSSigningInformation)
46
+ private static let signInfoFlags : SecCSFlags = . init( rawValue: kSecCSSigningInformation)
51
47
52
- public init ( ) { }
53
-
54
- public func validate( path: URL ) throws {
48
+ public static func validate( path: URL ) throws ( ValidationError) {
55
49
guard FileManager . default. fileExists ( atPath: path. path) else {
56
- throw ValidationError . fileNotFound
50
+ throw . fileNotFound
57
51
}
58
52
59
53
var staticCode : SecStaticCode ?
60
54
let status = SecStaticCodeCreateWithPath ( path as CFURL , SecCSFlags ( ) , & staticCode)
61
55
guard status == errSecSuccess, let code = staticCode else {
62
- throw ValidationError . unableToCreateStaticCode
56
+ throw . unableToCreateStaticCode
63
57
}
64
58
65
59
let validateStatus = SecStaticCodeCheckValidity ( code, SecCSFlags ( ) , nil )
66
60
guard validateStatus == errSecSuccess else {
67
- throw ValidationError . invalidSignature
61
+ throw . invalidSignature
68
62
}
69
63
70
64
var information : CFDictionary ?
71
65
let infoStatus = SecCodeCopySigningInformation ( code, signInfoFlags, & information)
72
66
guard infoStatus == errSecSuccess, let info = information as? [ String : Any ] else {
73
- throw ValidationError . unableToRetrieveInfo
67
+ throw . unableToRetrieveInfo
74
68
}
75
69
76
70
guard let identifier = info [ kSecCodeInfoIdentifier as String ] as? String ,
77
71
identifier == expectedIdentifier
78
72
else {
79
- throw ValidationError . invalidIdentifier ( identifier: info [ kSecCodeInfoIdentifier as String ] as? String )
73
+ throw . invalidIdentifier( identifier: info [ kSecCodeInfoIdentifier as String ] as? String )
80
74
}
81
75
82
76
guard let teamIdentifier = info [ kSecCodeInfoTeamIdentifier as String ] as? String ,
83
77
teamIdentifier == expectedTeamIdentifier
84
78
else {
85
- throw ValidationError . invalidTeamIdentifier (
79
+ throw . invalidTeamIdentifier(
86
80
identifier: info [ kSecCodeInfoTeamIdentifier as String ] as? String
87
81
)
88
82
}
89
83
90
84
guard let infoPlist = info [ kSecCodeInfoPList as String ] as? [ String : AnyObject ] else {
91
- throw ValidationError . missingInfoPList
85
+ throw . missingInfoPList
92
86
}
93
87
94
88
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 )
96
90
}
97
91
98
92
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 )
100
94
}
101
95
102
96
guard let dylibVersion = infoPlist [ infoShortVersionKey] as? String ,
103
97
minDylibVersion. compare ( dylibVersion, options: . numeric) != . orderedDescending
104
98
else {
105
- throw ValidationError . invalidVersion ( version: infoPlist [ infoShortVersionKey] as? String )
99
+ throw . invalidVersion( version: infoPlist [ infoShortVersionKey] as? String )
106
100
}
107
101
}
108
102
}
109
103
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 " )
122
109
}
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)
129
122
}
123
+ }
130
124
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
+ }
138
132
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
+ }
142
136
137
+ do {
143
138
if FileManager . default. fileExists ( atPath: dest. path) {
144
139
try FileManager . default. removeItem ( at: dest)
145
140
}
146
141
try FileManager . default. moveItem ( at: tempURL, to: dest)
147
- try await validator. validate ( path: dest)
142
+ } catch {
143
+ throw . fileOpError( error)
148
144
}
149
145
}
150
146
@@ -154,14 +150,20 @@ func etag(data: Data) -> String {
154
150
return " \" \( etag) \" "
155
151
}
156
152
157
- enum DownloadError : Error {
153
+ public enum DownloadError : Error {
158
154
case unexpectedStatusCode( Int )
159
155
case invalidResponse
156
+ case networkError( any Error )
157
+ case fileOpError( any Error )
160
158
161
159
var localizedDescription : String {
162
160
switch self {
163
161
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) "
165
167
case . invalidResponse:
166
168
return " Received non-HTTP response "
167
169
}
0 commit comments