Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit48afa7a

Browse files
feat: add experimental privileged helper (#160)
Closes#135.Closes#142.This PR adds an optional privileged `LaunchDaemon` capable of removing the quarantine flag on a downloaded `.dylib` without prompting the user to enter their password. This is most useful when the Coder deployment updates frequently.<img width="597" alt="image" src="https://github.com/user-attachments/assets/5f51b9a3-93ba-46b7-baa3-37c8bd817733" />The System Extension communicates directly with the `LaunchDaemon`, meaning a new `.dylib` can be downloaded and executed even if the app was closed, which was previously not possible. I've tested this in a fresh 15.4 VM.
1 parent9f356e5 commit48afa7a

15 files changed

+453
-45
lines changed

‎Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift

Lines changed: 3 additions & 0 deletions
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
letfileSyncDaemon:MutagenDaemon
4647
leturlHandler:URLHandler
4748
letnotifDelegate:NotifDelegate
49+
lethelper:HelperService
4850

4951
overrideinit(){
5052
notifDelegate=NotifDelegate()
5153
vpn=CoderVPNService()
54+
helper=HelperService()
5255
letstate=AppState(onChange: vpn.configureTunnelProviderProtocol)
5356
vpn.onStart={
5457
// We don't need this to have finished before the VPN actually starts
Lines changed: 117 additions & 0 deletions
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+
classHelperService:ObservableObject{
8+
privateletlogger=Logger(subsystem:Bundle.main.bundleIdentifier!, category:"HelperService")
9+
letplistName="com.coder.Coder-Desktop.Helper.plist"
10+
@Publishedvarstate: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+
letdaemon=SMAppService.daemon(plistName: plistName)
22+
state=HelperState(status: daemon.status)
23+
}
24+
25+
func install(){
26+
letdaemon=SMAppService.daemon(plistName: plistName)
27+
do{
28+
try daemon.register()
29+
}catchlet error asNSError{
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+
letdaemon=SMAppService.daemon(plistName: plistName)
39+
do{
40+
try daemon.unregister()
41+
}catchlet error asNSError{
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+
enumHelperState:Equatable{
51+
case uninstalled
52+
case installed
53+
case requiresApproval
54+
case failed(HelperError)
55+
56+
vardescription:String{
57+
switchself{
58+
case.uninstalled:
59+
"Uninstalled"
60+
case.installed:
61+
"Installed"
62+
case.requiresApproval:
63+
"Requires Approval"
64+
caselet.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+
@unknowndefault:
81+
.failed(.unknown("Unknown status:\(status)"))
82+
}
83+
}
84+
}
85+
86+
enumHelperError: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+
varlocalizedDescription:String{
106+
switchself{
107+
case.alreadyRegistered:
108+
"Already registered"
109+
case.launchDeniedByUser:
110+
"Launch denied by user"
111+
case.invalidSignature:
112+
"Invalid signature"
113+
caselet.unknown(message):
114+
message
115+
}
116+
}
117+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import LaunchAtLogin
2+
import SwiftUI
3+
4+
structExperimentalTab:View{
5+
varbody:someView{
6+
Form{
7+
HelperSection()
8+
}.formStyle(.grouped)
9+
}
10+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import LaunchAtLogin
2+
import ServiceManagement
3+
import SwiftUI
4+
5+
structHelperSection:View{
6+
varbody:someView{
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+
structHelperButton:View{
25+
@EnvironmentObjectvarhelperService:HelperService
26+
27+
varbuttonText: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+
varbuttonDescription:String{
39+
switch helperService.state{
40+
case.uninstalled,.installed:
41+
""
42+
case.requiresApproval:
43+
"Requires approval"
44+
caselet.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+
varbody:someView{
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

Lines changed: 6 additions & 0 deletions
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
enumSettingsTab:Int{
2429
case general
2530
case network
31+
case experimental
2632
}

‎Coder-Desktop/Coder-Desktop/XPCInterface.swift

Lines changed: 4 additions & 4 deletions
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("VPNxpc connect called")
1818
guard xpc==nilelse{
19-
logger.debug("xpc already exists")
19+
logger.debug("VPNxpc already exists")
2020
return
2121
}
2222
letnetworkExtDict=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{@MainActorin
37-
logger.error("XPC connection invalidated.")
37+
logger.error("VPNXPC connection invalidated.")
3838
self.xpc=nil
3939
self.connect()
4040
}
4141
}
4242
xpcConn.interruptionHandler={[logger]in
4343
Task{@MainActorin
44-
logger.error("XPC connection interrupted.")
44+
logger.error("VPNXPC connection interrupted.")
4545
self.xpc=nil
4646
self.connect()
4747
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Foundation
2+
3+
@objcprotocolHelperXPCProtocol{
4+
func removeQuarantine(path:String, withReply reply:@escaping(Int32,String)->Void)
5+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPEplist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plistversion="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>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import Foundation
2+
import os
3+
4+
classHelperToolDelegate:NSObject,NSXPCListenerDelegate,HelperXPCProtocol{
5+
privatevarlogger=Logger(subsystem:Bundle.main.bundleIdentifier!, category:"HelperToolDelegate")
6+
7+
overrideinit(){
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+
returntrue
23+
}
24+
25+
func removeQuarantine(path:String, withReply reply:@escaping(Int32,String)->Void){
26+
guardisCoderDesktopDylib(at: path)else{
27+
reply(1,"Path is not to a Coder Desktop dylib:\(path)")
28+
return
29+
}
30+
31+
lettask=Process()
32+
letpipe=Pipe()
33+
34+
task.standardOutput= pipe
35+
task.standardError= pipe
36+
task.arguments=["-d","com.apple.quarantine", path]
37+
task.executableURL=URL(fileURLWithPath:"/usr/bin/xattr")
38+
39+
do{
40+
try task.run()
41+
}catch{
42+
reply(1,"Failed to start command:\(error)")
43+
return
44+
}
45+
46+
letdata= pipe.fileHandleForReading.readDataToEndOfFile()
47+
letoutput=String(data: data, encoding:.utf8)??""
48+
49+
task.waitUntilExit()
50+
reply(task.terminationStatus, output)
51+
}
52+
}
53+
54+
func isCoderDesktopDylib(at rawPath:String)->Bool{
55+
leturl=URL(fileURLWithPath: rawPath)
56+
.standardizedFileURL
57+
.resolvingSymlinksInPath()
58+
59+
// *Must* be within the Coder Desktop System Extension sandbox
60+
letrequiredPrefix=["/","var","root","Library","Containers",
61+
"com.coder.Coder-Desktop.VPN"]
62+
guard url.pathComponents.starts(with: requiredPrefix)else{returnfalse}
63+
guard url.pathExtension.lowercased()=="dylib"else{returnfalse}
64+
guardFileManager.default.fileExists(atPath: url.path)else{returnfalse}
65+
returntrue
66+
}
67+
68+
letdelegate=HelperToolDelegate()
69+
letlistener=NSXPCListener(machServiceName:"4399GN35BJ.com.coder.Coder-Desktop.Helper")
70+
listener.delegate= delegate
71+
listener.resume()
72+
RunLoop.main.run()

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp