Skip to content

Commit e18f466

Browse files
chore: enforce minimum coder server version of v2.20.0 (#90)
This will cause Coder Desktop networking to fail to start unless the validated dylib is version `v2.20.0` or later. Obviously, using this build early would mean Coder Desktop would not work against our dogfood deployment.
1 parent 75f015c commit e18f466

File tree

4 files changed

+92
-3
lines changed

4 files changed

+92
-3
lines changed

Coder Desktop/Coder Desktop/Views/LoginForm.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import CoderSDK
22
import SwiftUI
3+
import VPNLib
34

45
struct LoginForm: View {
56
@EnvironmentObject var state: AppState
@@ -78,6 +79,22 @@ struct LoginForm: View {
7879
loginError = .failedAuth(error)
7980
return
8081
}
82+
let buildInfo: BuildInfoResponse
83+
do {
84+
buildInfo = try await client.buildInfo()
85+
} catch {
86+
loginError = .failedAuth(error)
87+
return
88+
}
89+
guard let semver = buildInfo.semver else {
90+
loginError = .missingServerVersion
91+
return
92+
}
93+
// x.compare(y) is .orderedDescending if x > y
94+
guard SignatureValidator.minimumCoderVersion.compare(semver, options: .numeric) != .orderedDescending else {
95+
loginError = .outdatedCoderVersion
96+
return
97+
}
8198
state.login(baseAccessURL: url, sessionToken: sessionToken)
8299
dismiss()
83100
}
@@ -190,6 +207,8 @@ enum LoginError: Error {
190207
case httpsRequired
191208
case noHost
192209
case invalidURL
210+
case outdatedCoderVersion
211+
case missingServerVersion
193212
case failedAuth(ClientError)
194213

195214
var description: String {
@@ -200,8 +219,15 @@ enum LoginError: Error {
200219
"URL must have a host"
201220
case .invalidURL:
202221
"Invalid URL"
222+
case .outdatedCoderVersion:
223+
"""
224+
The Coder deployment must be version \(SignatureValidator.minimumCoderVersion)
225+
or higher to use Coder Desktop.
226+
"""
203227
case let .failedAuth(err):
204228
"Could not authenticate with Coder deployment:\n\(err.localizedDescription)"
229+
case .missingServerVersion:
230+
"Coder deployment did not provide a server version"
205231
}
206232
}
207233

Coder Desktop/Coder DesktopTests/LoginFormTests.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ struct LoginTests {
7373
@Test
7474
func testFailedAuthentication() async throws {
7575
let url = URL(string: "https://testFailedAuthentication.com")!
76+
let buildInfo = BuildInfoResponse(
77+
version: "v2.20.0"
78+
)
79+
try Mock(
80+
url: url.appendingPathComponent("/api/v2/buildinfo"),
81+
statusCode: 200,
82+
data: [.get: Client.encoder.encode(buildInfo)]
83+
).register()
7684
Mock(url: url.appendingPathComponent("/api/v2/users/me"), statusCode: 401, data: [.get: Data()]).register()
7785

7886
try await ViewHosting.host(view) {
@@ -87,6 +95,30 @@ struct LoginTests {
8795
}
8896
}
8997

98+
@Test
99+
func testOutdatedServer() async throws {
100+
let url = URL(string: "https://testOutdatedServer.com")!
101+
let buildInfo = BuildInfoResponse(
102+
version: "v2.19.0"
103+
)
104+
try Mock(
105+
url: url.appendingPathComponent("/api/v2/buildinfo"),
106+
statusCode: 200,
107+
data: [.get: Client.encoder.encode(buildInfo)]
108+
).register()
109+
110+
try await ViewHosting.host(view) {
111+
try await sut.inspection.inspect { view in
112+
try view.find(ViewType.TextField.self).setInput(url.absoluteString)
113+
try view.find(button: "Next").tap()
114+
#expect(throws: Never.self) { try view.find(text: "Session Token") }
115+
try view.find(ViewType.SecureField.self).setInput("valid-token")
116+
try await view.actualView().submit()
117+
#expect(throws: Never.self) { try view.find(ViewType.Alert.self) }
118+
}
119+
}
120+
}
121+
90122
@Test
91123
func testSuccessfulLogin() async throws {
92124
let url = URL(string: "https://testSuccessfulLogin.com")!
@@ -95,13 +127,22 @@ struct LoginTests {
95127
id: UUID(),
96128
username: "admin"
97129
)
130+
let buildInfo = BuildInfoResponse(
131+
version: "v2.20.0"
132+
)
98133

99134
try Mock(
100135
url: url.appendingPathComponent("/api/v2/users/me"),
101136
statusCode: 200,
102137
data: [.get: Client.encoder.encode(user)]
103138
).register()
104139

140+
try Mock(
141+
url: url.appendingPathComponent("/api/v2/buildinfo"),
142+
statusCode: 200,
143+
data: [.get: Client.encoder.encode(buildInfo)]
144+
).register()
145+
105146
try await ViewHosting.host(view) {
106147
try await sut.inspection.inspect { view in
107148
try view.find(ViewType.TextField.self).setInput(url.absoluteString)

Coder Desktop/VPN/Manager.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ actor Manager {
3131
// The tunnel might be asked to start before the network interfaces have woken up from sleep
3232
sessionConfig.waitsForConnectivity = true
3333
// URLSession's waiting for connectivity sometimes hangs even when
34-
// the network is up so this is deliberately short (15s) to avoid a
34+
// the network is up so this is deliberately short (30s) to avoid a
3535
// poor UX where it appears stuck.
36-
sessionConfig.timeoutIntervalForResource = 15
36+
sessionConfig.timeoutIntervalForResource = 30
3737
try await download(src: dylibPath, dest: dest, urlSession: URLSession(configuration: sessionConfig))
3838
} catch {
3939
throw .download(error)

Coder Desktop/VPNLib/Download.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public enum ValidationError: Error {
1010
case invalidTeamIdentifier(identifier: String?)
1111
case missingInfoPList
1212
case invalidVersion(version: String?)
13+
case belowMinimumCoderVersion
1314

1415
public var description: String {
1516
switch self {
@@ -29,13 +30,21 @@ public enum ValidationError: Error {
2930
"Invalid team identifier: \(identifier ?? "unknown")."
3031
case .missingInfoPList:
3132
"Info.plist is not embedded within the dylib."
33+
case .belowMinimumCoderVersion:
34+
"""
35+
The Coder deployment must be version \(SignatureValidator.minimumCoderVersion)
36+
or higher to use Coder Desktop.
37+
"""
3238
}
3339
}
3440

3541
public var localizedDescription: String { description }
3642
}
3743

3844
public class SignatureValidator {
45+
// Whilst older dylibs exist, this app assumes v2.20 or later.
46+
public static let minimumCoderVersion = "2.20.0"
47+
3948
private static let expectedName = "CoderVPN"
4049
private static let expectedIdentifier = "com.coder.Coder-Desktop.VPN.dylib"
4150
private static let expectedTeamIdentifier = "4399GN35BJ"
@@ -87,6 +96,10 @@ public class SignatureValidator {
8796
throw .missingInfoPList
8897
}
8998

99+
try validateInfo(infoPlist: infoPlist, expectedVersion: expectedVersion)
100+
}
101+
102+
private static func validateInfo(infoPlist: [String: AnyObject], expectedVersion: String) throws(ValidationError) {
90103
guard let plistIdent = infoPlist[infoIdentifierKey] as? String, plistIdent == expectedIdentifier else {
91104
throw .invalidIdentifier(identifier: infoPlist[infoIdentifierKey] as? String)
92105
}
@@ -95,11 +108,20 @@ public class SignatureValidator {
95108
throw .invalidIdentifier(identifier: infoPlist[infoNameKey] as? String)
96109
}
97110

111+
// Downloaded dylib must match the version of the server
98112
guard let dylibVersion = infoPlist[infoShortVersionKey] as? String,
99-
expectedVersion.compare(dylibVersion, options: .numeric) != .orderedDescending
113+
expectedVersion == dylibVersion
100114
else {
101115
throw .invalidVersion(version: infoPlist[infoShortVersionKey] as? String)
102116
}
117+
118+
// Downloaded dylib must be at least the minimum Coder server version
119+
guard let dylibVersion = infoPlist[infoShortVersionKey] as? String,
120+
// x.compare(y) is .orderedDescending if x > y
121+
minimumCoderVersion.compare(dylibVersion, options: .numeric) != .orderedDescending
122+
else {
123+
throw .belowMinimumCoderVersion
124+
}
103125
}
104126
}
105127

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy