Skip to content

feat: support RDP-specific deep links #147

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 12, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
Original file line number Diff line number Diff line change
@@ -85,7 +85,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
image: "MenuBarIcon",
onAppear: {
// If the VPN is enabled, it's likely the token isn't expired
guard case .disabled = self.vpn.state, self.state.hasSession else { return }
guard self.vpn.state != .connected, self.state.hasSession else { return }
Task { @MainActor in
await self.state.handleTokenExpiry()
}
64 changes: 57 additions & 7 deletions Coder-Desktop/Coder-Desktop/URLHandler.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import SwiftUI
import VPNLib

@MainActor
@@ -20,20 +21,69 @@ class URLHandler {
guard deployment.host() == url.host else {
throw .invalidAuthority(url.host() ?? "<none>")
}
let route: CoderRoute
do {
switch try router.match(url: url) {
case let .open(workspace, agent, type):
route = try router.match(url: url)
} catch {
throw .matchError(url: url)
}

switch route {
case let .open(workspace, agent, type):
do {
switch type {
case let .rdp(creds):
handleRDP(workspace: workspace, agent: agent, creds: creds)
try handleRDP(workspace: workspace, agent: agent, creds: creds)
}
} catch {
throw .openError(error)
}
} catch {
throw .matchError(url: url)
}
}

private func handleRDP(workspace: String, agent: String, creds: RDPCredentials) throws(OpenError) {
guard vpn.state == .connected else {
throw .coderConnectOffline
}

guard let workspace = vpn.menuState.findWorkspace(name: workspace) else {
throw .invalidWorkspace(workspace: workspace)
}

guard let agent = vpn.menuState.findAgent(workspaceID: workspace.id, name: agent) else {
throw .invalidAgent(workspace: workspace.name, agent: agent)
}

var rdpString = "rdp:full address=s:\(agent.primaryHost):3389"
if let username = creds.username {
rdpString += "&username=s:\(username)"
}
guard let url = URL(string: rdpString) else {
throw .couldNotCreateRDPURL(rdpString)
}

func handleRDP(workspace _: String, agent _: String, creds _: RDPCredentials) {
// TODO: Handle RDP
let alert = NSAlert()
alert.messageText = "Opening RDP"
alert.informativeText = "Connecting to \(agent.primaryHost)."
if let username = creds.username {
alert.informativeText += "\nUsername: \(username)"
}
if creds.password != nil {
alert.informativeText += "\nThe password will be copied to your clipboard."
}

alert.alertStyle = .informational
alert.addButton(withTitle: "Open")
alert.addButton(withTitle: "Cancel")
let response = alert.runModal()
if response == .alertFirstButtonReturn {
if let password = creds.password {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(password, forType: .string)
}
NSWorkspace.shared.open(url)
} else {
// User cancelled
}
}
}
9 changes: 9 additions & 0 deletions Coder-Desktop/Coder-Desktop/VPN/MenuState.swift
Original file line number Diff line number Diff line change
@@ -58,6 +58,15 @@ struct VPNMenuState {
// or have any invalid UUIDs.
var invalidAgents: [Vpn_Agent] = []

public func findAgent(workspaceID: UUID, name: String) -> Agent? {
agents.first(where: { $0.value.wsID == workspaceID && $0.value.name == name })?.value
}

public func findWorkspace(name: String) -> Workspace? {
workspaces
.first(where: { $0.value.name == name })?.value
}

mutating func upsertAgent(_ agent: Vpn_Agent) {
guard
let id = UUID(uuidData: agent.id),
26 changes: 26 additions & 0 deletions Coder-Desktop/VPNLib/CoderRouter.swift
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ import Foundation
import URLRouting

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

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

public var description: String {
switch self {
@@ -42,6 +44,30 @@ public enum RouterError: Error {
"Failed to handle \(url.absoluteString) because the format is unsupported."
case .noSession:
"Not logged in."
case let .openError(error):
error.description
}
}

public var localizedDescription: String { description }
}

public enum OpenError: Error {
case invalidWorkspace(workspace: String)
case invalidAgent(workspace: String, agent: String)
case coderConnectOffline
case couldNotCreateRDPURL(String)

public var description: String {
switch self {
case let .invalidWorkspace(ws):
"Could not find workspace '\(ws)'. Does it exist?"
case .coderConnectOffline:
"Coder Connect must be running."
case let .invalidAgent(workspace: workspace, agent: agent):
"Could not find agent '\(agent)' in workspace '\(workspace)'. Is the workspace running?"
case let .couldNotCreateRDPURL(rdpString):
"Could not construct RDP URL from '\(rdpString)'."
}
}

2 changes: 2 additions & 0 deletions Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
Original file line number Diff line number Diff line change
@@ -24,6 +24,8 @@ public protocol FileSyncDaemon: ObservableObject {
func resetSessions(ids: [String]) async throws(DaemonError)
}

// File Sync related code is in VPNLib to workaround a linking issue
// https://github.com/coder/coder-desktop-macos/issues/149
@MainActor
public class MutagenDaemon: FileSyncDaemon {
let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "mutagen")
2 changes: 1 addition & 1 deletion scripts/build.sh
Original file line number Diff line number Diff line change
@@ -131,7 +131,7 @@ xcodebuild \
CODE_SIGN_IDENTITY="$CODE_SIGN_IDENTITY" \
CODE_SIGN_INJECT_BASE_ENTITLEMENTS=NO \
CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION=YES \
OTHER_CODE_SIGN_FLAGS='--timestamp' | LC_ALL="en_US.UTF-8" xcpretty
OTHER_CODE_SIGN_FLAGS='--timestamp' | xcbeautify

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