I’m posting this in the hopes that some time in the future this code will help someone with some struggles I have just been through. So if someone can benefit from this - great.
In OS X, in order to perform certain system level functions, you need utilize Apple’s Authorization Services API. This ends up elevating the user rights and allows execution of sudo or root commands. Using the AuthorizationMBS classes from MonkeyBread, this is fairly easily accomplished. Without too much effort you can do things like an an IP address to one of your NICs. Or you can copy files into places where authentication is needed. There are other actions though where the AuthorizationMBS class is not enough. You actually have to run code as root. One example of such an action is things using the launchctl command.
In my case, I wanted to take advantage of using OS X’s built in TFTP server to send firmware files to a device that communicates with my software. The TFTPd daemon is not normally running by default. You could start it by calling tftpd directly, but that is not the preferred way of doing it. Apple’s man documentation specifically says to use launchctl to control the startup and shutdown of the TFTP server. I want to detail for the community what I have done with TFTP as an example so that others may some day follow in my footsteps through this minefield.
Before I offer up my code and what I have done, I wish to thank several community members for their assistance: @Norman Palardy, @jim mckay, @Eduardo Gutierrez de Oliveira and @Tim Jones. Without their help, I would not have been able to get to where I am.
So let’s get started:
1.) Modify the plist file:
OS X launchctl services start up based on the contents of their plist file. These files are normally stored in /System/Library/LaunchDaemons. In order to properly run your service, you will need to edit the plist file prior to starting the service. Instead of actually modifying the copy of my plist file in question (tftp.plist), I decided to create an entirely new plist file in my temporary directory that had all the parameters that I wanted/needed. So here is my code for that:
Sub SetUpOSXPlist()
#If TargetMacOS Then
Dim f as FolderItem = App.ExecutableFile.Parent.Parent.Child("Resources") ' This is the path to where I will keep my firmware files.
' The TFTP root directory will be here
If f = Nil Then Return
Dim TempPath as FolderItem = SpecialFolder.Temporary
Dim f1 as FolderItem = SpecialFolder.Temporary.Child("tftp.plist")
If f1 = Nil Then Return
Dim bs as BinaryStream = BinaryStream.Create(f1,true)
bs.write("<?xml version=""1.0"" encoding=""UTF-8""?>"+EndOfLine.Unix)
bs.Write("<!DOCTYPE plist PUBLIC ""-//Apple//DTD PLIST 1.0//EN"" ""http://www.apple.com/DTDs/PropertyList-1.0.dtd"">"+EndOfLine.Unix)
bs.write("<plist version=""1.0"">"+EndOfLine.Unix)
bs.write("<dict>"+EndOfLine.Unix)
bs.write("<key>Disabled</key>"+EndOfLine.Unix)
bs.write("<true/>"+EndOfLine.Unix)
bs.write("<key>InitGroups</key>"+EndOfLine.Unix)
bs.write("<true/>"+EndOfLine.Unix)
bs.write("<key>Label</key>"+EndOfLine.Unix)
bs.write("<string>com.apple.tftpd</string>"+EndOfLine.Unix)
bs.write("<key>ProgramArguments</key>"+EndOfLine.Unix)
bs.write("<array>"+EndOfLine.Unix)
bs.write("<string>/usr/libexec/tftpd</string>"+EndOfLine.Unix)
bs.write("<string>-d</string>"+EndOfLine.Unix)
bs.write("<string>-i</string>"+EndOfLine.Unix)
bs.write("<string>-s</string>"+EndOfLine.Unix)
'bs.write("<string>"+path+"</string>"+EndOfLine.Unix)
bs.write("<string>"+f.nativepath+"</string>"+EndOfLine.Unix) ' Set the root directory here
bs.Write("<string>-u</string>"+EndOfLine.Unix)
bs.Write("<string>root</string>"+EndOfLine.Unix)
bs.write("</array>"+EndOfLine.Unix)
bs.write("<key>Sockets</key>"+EndOfLine.Unix)
bs.write("<dict>"+EndOfLine.Unix)
bs.write("<key>Listeners</key>"+EndOfLine.Unix)
bs.write("<dict>"+EndOfLine.Unix)
bs.write("<key>SockServiceName</key>"+EndOfLine.Unix)
bs.write("<string>tftp</string>"+EndOfLine.Unix)
bs.write("<key>SockType</key>"+EndOfLine.Unix)
bs.write("<string>dgram</string>"+EndOfLine.Unix)
bs.write("</dict>"+EndOfLine.Unix)
bs.write("</dict>"+EndOfLine.Unix)
bs.write("<key>inetdCompatibility</key>"+EndOfLine.Unix)
bs.write("<dict>"+EndOfLine.Unix)
bs.write("<key>Wait</key>"+EndOfLine.Unix)
bs.write("<true/>"+EndOfLine.Unix)
bs.write("</dict>"+EndOfLine.Unix)
bs.write("</dict>"+EndOfLine.Unix)
bs.write("</plist>"+EndOfLine.Unix)
#Endif
End Sub
2.) Stop the service in case it exists - We don’t necessarily know if another program or process has started the service in question. So we want to stop it first.
3.) Make a backup of the existing plist file.
4.) Copy the plist file we created to the /System/Library/LaunchDaemons folder
These 3 steps are carried out in the following code which is in the open event of my window where I need these functions. In the code below, there is a call to RunAuthShellOSX. This is a method that calls the AuthorizationMBS classes. It is documented in the MBS examples and will not be documented here.
Dim f as FolderItem = App.ExecutableFile.Parent.Parent.Child("Resources") ' Where I keep all my firmware files and helper apps
If f <> Nil and f.Exists Then
// Set up plistFile
SetUpOSXPlist
Dim ftmp as FolderItem = SpecialFolder.Temporary.child("tftp.plist")
If ftmp = Nil or Not ftmp.Exists Then Return
Dim MyScript as String
Dim MyParams as String =""
// The commands in MyScript below are the shell commands I want to execute using elevated permissions. I first set the permissions for my resources folder
// so that the TFTP service can serve them. Maybe this is not a good idea to change permissions inside the app bundle, but it works.
// I then unload the TFTP service using launchctl. This stops the service. This launchctl command CAN be called in normal elevated privileges.
// I then have commands to rename the existing tftp.plist file to tftp.bak. And then copy in the new plist file to the /System/Library/LaunchDaemons folder
MyScript = "#! /bin/sh"+endofline.unix+endofline.unix+"chmod 777 "+f.shellpath+EndOfLine.UNIX+ _
"launchctl unload -F /System/Library/LaunchDaemons/tftp.plist"+endofline.unix+ _
"mv -f /System/Library/LaunchDaemons/tftp.plist /System/Library/LaunchDaemons/tftp.bak"+EndOfLine.UNIX+_
"cp "+ftmp.nativepath+" /System/Library/LaunchDaemons/tftp.plist"+EndOfLine.UNIX
// Run the script
If RunAuthShellOSX(MyScript,MyParams) Then ' RunAuthShellOSX is a method that uses the AuthorizationMBS method.
MyScript = "#! /bin/sh"+endofline.unix+endofline.unix+f.ShellPath+"/TFTPStartHelper True"
MyParams = ""
If RunAuthShellOSX(MyScript,MyParams) Then
System.DebugLog("TFTP Server Started")
Return
End IF
End If
End If
Now after I run the first script in the AuthorizationMBS methods, there’s a line there referencing TFTPStartHelper. This is where the extra elevated privileges come into play. You cannot just be authorized and start a launchctl service. You have to actually be running as root. So TFTPStartHelper is a helper app that I wrote that has its permissions and setuid bit set to that of root. So the app actually runs as root and carries out its commands as root. Thanks to @jim mckay for this idea. This app is a small console app that is copied into the resources folder of my app bundle during a post compile copy step. The code for this app is below. After compiling the app, run the following commands on from a terminal window:
sudo chown root /path/to/help
sudo chgrp wheel /path/to/help
sudo chmod 6755 /path/to/help
/path/to/help is the path to your compiled helper application. This is a one time thing and should set things up so the helper runs as root. Then set up a post compile copy step to put the helper where you need it.
See next post for more as the forum won’t let me post a longer post…