diff --git a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift index e2fe3ab..307e079 100644 --- a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift +++ b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift @@ -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() } diff --git a/Coder-Desktop/Coder-Desktop/URLHandler.swift b/Coder-Desktop/Coder-Desktop/URLHandler.swift index 191c19d..0dbc924 100644 --- a/Coder-Desktop/Coder-Desktop/URLHandler.swift +++ b/Coder-Desktop/Coder-Desktop/URLHandler.swift @@ -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() ?? "") } + 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 } } } diff --git a/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift b/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift index 59dfae0..c989c1d 100644 --- a/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift +++ b/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift @@ -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), diff --git a/Coder-Desktop/VPNLib/CoderRouter.swift b/Coder-Desktop/VPNLib/CoderRouter.swift index de849e7..d562e39 100644 --- a/Coder-Desktop/VPNLib/CoderRouter.swift +++ b/Coder-Desktop/VPNLib/CoderRouter.swift @@ -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)'." } } diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift index 01e1d6b..98807e3 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift @@ -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") diff --git a/scripts/build.sh b/scripts/build.sh index 227f04b..de6f34a 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -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"