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

Skip to content

Commit 5b7c826

Browse files
committed
feat: add experimental privileged helper
1 parent 9f356e5 commit 5b7c826

15 files changed

+433
-45
lines changed

Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift

+3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ struct DesktopApp: App {
2525
SettingsView<CoderVPNService>()
2626
.environmentObject(appDelegate.vpn)
2727
.environmentObject(appDelegate.state)
28+
.environmentObject(appDelegate.helper)
2829
}
2930
.windowResizability(.contentSize)
3031
Window("Coder File Sync", id: Windows.fileSync.rawValue) {
@@ -45,10 +46,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
4546
let fileSyncDaemon: MutagenDaemon
4647
let urlHandler: URLHandler
4748
let notifDelegate: NotifDelegate
49+
let helper: HelperService
4850

4951
override init() {
5052
notifDelegate = NotifDelegate()
5153
vpn = CoderVPNService()
54+
helper = HelperService()
5255
let state = AppState(onChange: vpn.configureTunnelProviderProtocol)
5356
vpn.onStart = {
5457
// We don't need this to have finished before the VPN actually starts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import os
2+
import ServiceManagement
3+
4+
// Whilst the GUI app installs the helper, the System Extension communicates
5+
// with it over XPC
6+
@MainActor
7+
class HelperService: ObservableObject {
8+
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "HelperService")
9+
let plistName = "com.coder.Coder-Desktop.Helper.plist"
10+
@Published var state: HelperState = .uninstalled {
11+
didSet {
12+
logger.info("helper daemon state set: \(self.state.description, privacy: .public)")
13+
}
14+
}
15+
16+
init() {
17+
update()
18+
}
19+
20+
func update() {
21+
let daemon = SMAppService.daemon(plistName: plistName)
22+
state = HelperState(status: daemon.status)
23+
}
24+
25+
func install() {
26+
let daemon = SMAppService.daemon(plistName: plistName)
27+
do {
28+
try daemon.register()
29+
} catch let error as NSError {
30+
self.state = .failed(.init(error: error))
31+
} catch {
32+
state = .failed(.unknown(error.localizedDescription))
33+
}
34+
state = HelperState(status: daemon.status)
35+
}
36+
37+
func uninstall() {
38+
let daemon = SMAppService.daemon(plistName: plistName)
39+
do {
40+
try daemon.unregister()
41+
} catch let error as NSError {
42+
self.state = .failed(.init(error: error))
43+
} catch {
44+
state = .failed(.unknown(error.localizedDescription))
45+
}
46+
state = HelperState(status: daemon.status)
47+
}
48+
}
49+
50+
enum HelperState: Equatable {
51+
case uninstalled
52+
case installed
53+
case requiresApproval
54+
case failed(HelperError)
55+
56+
var description: String {
57+
switch self {
58+
case .uninstalled:
59+
"Uninstalled"
60+
case .installed:
61+
"Installed"
62+
case .requiresApproval:
63+
"Requires Approval"
64+
case let .failed(error):
65+
"Failed: \(error.localizedDescription)"
66+
}
67+
}
68+
69+
init(status: SMAppService.Status) {
70+
self = switch status {
71+
case .notRegistered:
72+
.uninstalled
73+
case .enabled:
74+
.installed
75+
case .requiresApproval:
76+
.requiresApproval
77+
case .notFound:
78+
// `Not found`` is the initial state, if `register` has never been called
79+
.uninstalled
80+
@unknown default:
81+
.failed(.unknown("Unknown status: \(status)"))
82+
}
83+
}
84+
}
85+
86+
enum HelperError: Error, Equatable {
87+
case alreadyRegistered
88+
case launchDeniedByUser
89+
case invalidSignature
90+
case unknown(String)
91+
92+
init(error: NSError) {
93+
self = switch error.code {
94+
case kSMErrorAlreadyRegistered:
95+
.alreadyRegistered
96+
case kSMErrorLaunchDeniedByUser:
97+
.launchDeniedByUser
98+
case kSMErrorInvalidSignature:
99+
.invalidSignature
100+
default:
101+
.unknown(error.localizedDescription)
102+
}
103+
}
104+
105+
var localizedDescription: String {
106+
switch self {
107+
case .alreadyRegistered:
108+
"Already registered"
109+
case .launchDeniedByUser:
110+
"Launch denied by user"
111+
case .invalidSignature:
112+
"Invalid signature"
113+
case let .unknown(message):
114+
message
115+
}
116+
}
117+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import LaunchAtLogin
2+
import SwiftUI
3+
4+
struct ExperimentalTab: View {
5+
var body: some View {
6+
Form {
7+
HelperSection()
8+
}.formStyle(.grouped)
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import LaunchAtLogin
2+
import ServiceManagement
3+
import SwiftUI
4+
5+
struct HelperSection: View {
6+
var body: some View {
7+
Section {
8+
HelperButton()
9+
Text("""
10+
Coder Connect executes a dynamic library downloaded from the Coder deployment.
11+
Administrator privileges are required when executing a copy of this library for the first time.
12+
Without this helper, these are granted by the user entering their password.
13+
With this helper, this is done automatically.
14+
This is useful if the Coder deployment updates frequently.
15+
16+
Coder Desktop will not execute code unless it has been signed by Coder.
17+
""")
18+
.font(.subheadline)
19+
.foregroundColor(.secondary)
20+
}
21+
}
22+
}
23+
24+
struct HelperButton: View {
25+
@EnvironmentObject var helperService: HelperService
26+
27+
var buttonText: String {
28+
switch helperService.state {
29+
case .uninstalled, .failed:
30+
"Install"
31+
case .installed:
32+
"Uninstall"
33+
case .requiresApproval:
34+
"Open Settings"
35+
}
36+
}
37+
38+
var buttonDescription: String {
39+
switch helperService.state {
40+
case .uninstalled, .installed:
41+
""
42+
case .requiresApproval:
43+
"Requires approval"
44+
case let .failed(err):
45+
err.localizedDescription
46+
}
47+
}
48+
49+
func buttonAction() {
50+
switch helperService.state {
51+
case .uninstalled, .failed:
52+
helperService.install()
53+
if helperService.state == .requiresApproval {
54+
SMAppService.openSystemSettingsLoginItems()
55+
}
56+
case .installed:
57+
helperService.uninstall()
58+
case .requiresApproval:
59+
SMAppService.openSystemSettingsLoginItems()
60+
}
61+
}
62+
63+
var body: some View {
64+
HStack {
65+
Text("Privileged Helper")
66+
Spacer()
67+
Text(buttonDescription)
68+
.foregroundColor(.secondary)
69+
Button(action: buttonAction) {
70+
Text(buttonText)
71+
}
72+
}.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
73+
helperService.update()
74+
}.onAppear {
75+
helperService.update()
76+
}
77+
}
78+
}
79+
80+
#Preview {
81+
HelperSection().environmentObject(HelperService())
82+
}

Coder-Desktop/Coder-Desktop/Views/Settings/Settings.swift

+6
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ struct SettingsView<VPN: VPNService>: View {
1313
.tabItem {
1414
Label("Network", systemImage: "dot.radiowaves.left.and.right")
1515
}.tag(SettingsTab.network)
16+
ExperimentalTab()
17+
.tabItem {
18+
Label("Experimental", systemImage: "gearshape.2")
19+
}.tag(SettingsTab.experimental)
20+
1621
}.frame(width: 600)
1722
.frame(maxHeight: 500)
1823
.scrollContentBackground(.hidden)
@@ -23,4 +28,5 @@ struct SettingsView<VPN: VPNService>: View {
2328
enum SettingsTab: Int {
2429
case general
2530
case network
31+
case experimental
2632
}

Coder-Desktop/Coder-Desktop/XPCInterface.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ import VPNLib
1414
}
1515

1616
func connect() {
17-
logger.debug("xpc connect called")
17+
logger.debug("VPN xpc connect called")
1818
guard xpc == nil else {
19-
logger.debug("xpc already exists")
19+
logger.debug("VPN xpc already exists")
2020
return
2121
}
2222
let networkExtDict = Bundle.main.object(forInfoDictionaryKey: "NetworkExtension") as? [String: Any]
@@ -34,14 +34,14 @@ import VPNLib
3434
xpcConn.exportedObject = self
3535
xpcConn.invalidationHandler = { [logger] in
3636
Task { @MainActor in
37-
logger.error("XPC connection invalidated.")
37+
logger.error("VPN XPC connection invalidated.")
3838
self.xpc = nil
3939
self.connect()
4040
}
4141
}
4242
xpcConn.interruptionHandler = { [logger] in
4343
Task { @MainActor in
44-
logger.error("XPC connection interrupted.")
44+
logger.error("VPN XPC connection interrupted.")
4545
self.xpc = nil
4646
self.connect()
4747
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Foundation
2+
3+
@objc protocol HelperXPCProtocol {
4+
func runCommand(command: String, withReply reply: @escaping (Int32, String) -> Void)
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>Label</key>
6+
<string>com.coder.Coder-Desktop.Helper</string>
7+
<key>BundleProgram</key>
8+
<string>Contents/MacOS/com.coder.Coder-Desktop.Helper</string>
9+
<key>MachServices</key>
10+
<dict>
11+
<!-- $(TeamIdentifierPrefix) isn't populated here, so this value is hardcoded -->
12+
<key>4399GN35BJ.com.coder.Coder-Desktop.Helper</key>
13+
<true/>
14+
</dict>
15+
<key>AssociatedBundleIdentifiers</key>
16+
<array>
17+
<string>com.coder.Coder-Desktop</string>
18+
</array>
19+
</dict>
20+
</plist>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import Foundation
2+
import os
3+
4+
class HelperToolDelegate: NSObject, NSXPCListenerDelegate, HelperXPCProtocol {
5+
private var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "HelperToolDelegate")
6+
7+
override init() {
8+
super.init()
9+
}
10+
11+
func listener(_: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
12+
newConnection.exportedInterface = NSXPCInterface(with: HelperXPCProtocol.self)
13+
newConnection.exportedObject = self
14+
newConnection.invalidationHandler = { [weak self] in
15+
self?.logger.info("Helper XPC connection invalidated")
16+
}
17+
newConnection.interruptionHandler = { [weak self] in
18+
self?.logger.debug("Helper XPC connection interrupted")
19+
}
20+
logger.info("new active connection")
21+
newConnection.resume()
22+
return true
23+
}
24+
25+
func runCommand(command: String, withReply reply: @escaping (Int32, String) -> Void) {
26+
let task = Process()
27+
let pipe = Pipe()
28+
29+
task.standardOutput = pipe
30+
task.standardError = pipe
31+
task.arguments = ["-c", command]
32+
task.executableURL = URL(fileURLWithPath: "/bin/bash")
33+
34+
do {
35+
try task.run()
36+
} catch {
37+
reply(1, "Failed to start command: \(error)")
38+
}
39+
40+
let data = pipe.fileHandleForReading.readDataToEndOfFile()
41+
let output = String(data: data, encoding: .utf8) ?? ""
42+
43+
task.waitUntilExit()
44+
reply(task.terminationStatus, output)
45+
}
46+
}
47+
48+
let delegate = HelperToolDelegate()
49+
let listener = NSXPCListener(machServiceName: "4399GN35BJ.com.coder.Coder-Desktop.Helper")
50+
listener.delegate = delegate
51+
listener.resume()
52+
RunLoop.main.run()

0 commit comments

Comments
 (0)