Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 314edbe

Browse files
feat: support RDP-specific deep links (#147)
Closes #96. If a `coder_app` exists on the workspace, where the URL is of the form: ```coder://dev.coder.com/v0/open/ws/<workspace>/agent/<agent>/rdp?username=administrator&password=password``` the URL will be parsed, validated, and an alert opened. If `Open` is clicked on the alert, the password will be written to the clipboard, where it can be pasted when prompted. https://github.com/user-attachments/assets/da8410c7-d656-4bf7-936a-8d465953e195 We're unable to avoid the entering of the password, as the `password` field in an `.rdp` file, even if encrypted properly, is ignored by the macOS Windows RDP app. The app supports reading credentials from the macOS keychain, and whilst we could create keychain entries, they have to be associated with an RDP config in the app, and there's no way to automate the creation of that config, and then run that config. Further reading: https://stackoverflow.com/questions/48713606/how-to-create-rdp-file-on-mac-os-that-allows-auto-login https://techcommunity.microsoft.com/discussions/azurevirtualdesktopforum/macos-remote-desktop-client-app---automatic-logon-no-credential-prompt/2596451 The above demo was done by adding this app to the template: ``` resource "coder_app" "connectrdp" { agent_id = coder_agent.main.id slug = "connectrdp" display_name = "Coder Connect RDP" url = "coder://dev.coder.com/v0/open/ws/${data.coder_workspace.me.name}/agent/main/rdp?username=Administrator&password=coderRDP!" icon = "/icon/terminal.svg" external = true } ```
1 parent 2198d3e commit 314edbe

File tree

6 files changed

+96
-9
lines changed

6 files changed

+96
-9
lines changed

Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
8585
image: "MenuBarIcon",
8686
onAppear: {
8787
// If the VPN is enabled, it's likely the token isn't expired
88-
guard case .disabled = self.vpn.state, self.state.hasSession else { return }
88+
guard self.vpn.state != .connected, self.state.hasSession else { return }
8989
Task { @MainActor in
9090
await self.state.handleTokenExpiry()
9191
}

Coder-Desktop/Coder-Desktop/URLHandler.swift

+57-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import SwiftUI
23
import VPNLib
34

45
@MainActor
@@ -20,20 +21,69 @@ class URLHandler {
2021
guard deployment.host() == url.host else {
2122
throw .invalidAuthority(url.host() ?? "<none>")
2223
}
24+
let route: CoderRoute
2325
do {
24-
switch try router.match(url: url) {
25-
case let .open(workspace, agent, type):
26+
route = try router.match(url: url)
27+
} catch {
28+
throw .matchError(url: url)
29+
}
30+
31+
switch route {
32+
case let .open(workspace, agent, type):
33+
do {
2634
switch type {
2735
case let .rdp(creds):
28-
handleRDP(workspace: workspace, agent: agent, creds: creds)
36+
try handleRDP(workspace: workspace, agent: agent, creds: creds)
2937
}
38+
} catch {
39+
throw .openError(error)
3040
}
31-
} catch {
32-
throw .matchError(url: url)
41+
}
42+
}
43+
44+
private func handleRDP(workspace: String, agent: String, creds: RDPCredentials) throws(OpenError) {
45+
guard vpn.state == .connected else {
46+
throw .coderConnectOffline
47+
}
48+
49+
guard let workspace = vpn.menuState.findWorkspace(name: workspace) else {
50+
throw .invalidWorkspace(workspace: workspace)
51+
}
52+
53+
guard let agent = vpn.menuState.findAgent(workspaceID: workspace.id, name: agent) else {
54+
throw .invalidAgent(workspace: workspace.name, agent: agent)
55+
}
56+
57+
var rdpString = "rdp:full address=s:\(agent.primaryHost):3389"
58+
if let username = creds.username {
59+
rdpString += "&username=s:\(username)"
60+
}
61+
guard let url = URL(string: rdpString) else {
62+
throw .couldNotCreateRDPURL(rdpString)
3363
}
3464

35-
func handleRDP(workspace _: String, agent _: String, creds _: RDPCredentials) {
36-
// TODO: Handle RDP
65+
let alert = NSAlert()
66+
alert.messageText = "Opening RDP"
67+
alert.informativeText = "Connecting to \(agent.primaryHost)."
68+
if let username = creds.username {
69+
alert.informativeText += "\nUsername: \(username)"
70+
}
71+
if creds.password != nil {
72+
alert.informativeText += "\nThe password will be copied to your clipboard."
73+
}
74+
75+
alert.alertStyle = .informational
76+
alert.addButton(withTitle: "Open")
77+
alert.addButton(withTitle: "Cancel")
78+
let response = alert.runModal()
79+
if response == .alertFirstButtonReturn {
80+
if let password = creds.password {
81+
NSPasteboard.general.clearContents()
82+
NSPasteboard.general.setString(password, forType: .string)
83+
}
84+
NSWorkspace.shared.open(url)
85+
} else {
86+
// User cancelled
3787
}
3888
}
3989
}

Coder-Desktop/Coder-Desktop/VPN/MenuState.swift

+9
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ struct VPNMenuState {
5858
// or have any invalid UUIDs.
5959
var invalidAgents: [Vpn_Agent] = []
6060

61+
public func findAgent(workspaceID: UUID, name: String) -> Agent? {
62+
agents.first(where: { $0.value.wsID == workspaceID && $0.value.name == name })?.value
63+
}
64+
65+
public func findWorkspace(name: String) -> Workspace? {
66+
workspaces
67+
.first(where: { $0.value.name == name })?.value
68+
}
69+
6170
mutating func upsertAgent(_ agent: Vpn_Agent) {
6271
guard
6372
let id = UUID(uuidData: agent.id),

Coder-Desktop/VPNLib/CoderRouter.swift

+26
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Foundation
22
import URLRouting
33

44
// This is in VPNLib to avoid depending on `swift-collections` in both the app & extension.
5+
// https://github.com/coder/coder-desktop-macos/issues/149
56
public struct CoderRouter: ParserPrinter {
67
public init() {}
78

@@ -33,6 +34,7 @@ public enum RouterError: Error {
3334
case invalidAuthority(String)
3435
case matchError(url: URL)
3536
case noSession
37+
case openError(OpenError)
3638

3739
public var description: String {
3840
switch self {
@@ -42,6 +44,30 @@ public enum RouterError: Error {
4244
"Failed to handle \(url.absoluteString) because the format is unsupported."
4345
case .noSession:
4446
"Not logged in."
47+
case let .openError(error):
48+
error.description
49+
}
50+
}
51+
52+
public var localizedDescription: String { description }
53+
}
54+
55+
public enum OpenError: Error {
56+
case invalidWorkspace(workspace: String)
57+
case invalidAgent(workspace: String, agent: String)
58+
case coderConnectOffline
59+
case couldNotCreateRDPURL(String)
60+
61+
public var description: String {
62+
switch self {
63+
case let .invalidWorkspace(ws):
64+
"Could not find workspace '\(ws)'. Does it exist?"
65+
case .coderConnectOffline:
66+
"Coder Connect must be running."
67+
case let .invalidAgent(workspace: workspace, agent: agent):
68+
"Could not find agent '\(agent)' in workspace '\(workspace)'. Is the workspace running?"
69+
case let .couldNotCreateRDPURL(rdpString):
70+
"Could not construct RDP URL from '\(rdpString)'."
4571
}
4672
}
4773

Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public protocol FileSyncDaemon: ObservableObject {
2424
func resetSessions(ids: [String]) async throws(DaemonError)
2525
}
2626

27+
// File Sync related code is in VPNLib to workaround a linking issue
28+
// https://github.com/coder/coder-desktop-macos/issues/149
2729
@MainActor
2830
public class MutagenDaemon: FileSyncDaemon {
2931
let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "mutagen")

scripts/build.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ xcodebuild \
131131
CODE_SIGN_IDENTITY="$CODE_SIGN_IDENTITY" \
132132
CODE_SIGN_INJECT_BASE_ENTITLEMENTS=NO \
133133
CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION=YES \
134-
OTHER_CODE_SIGN_FLAGS='--timestamp' | LC_ALL="en_US.UTF-8" xcpretty
134+
OTHER_CODE_SIGN_FLAGS='--timestamp' | xcbeautify
135135

136136
# Create exportOptions.plist
137137
EXPORT_OPTIONS_PATH="./build/exportOptions.plist"

0 commit comments

Comments
 (0)