pound.png

The tool chest available to widget developers is surprisingly robust. If you can put it on a web page, you can put it in a widget. AJAX and other Web 2.0 technologies are right at home inside Dashboard. If you need to do something slightly more radical, plugins can be written in Cocoa. And, of course, the wide wealth of unix command line tools are available to widget makers through the widget object’s system() method.

But having access to a command line tool and having the permissions needed to use it are two completely different things. I ran headlong into this when writing Share Switch. I wanted to be able to turn file sharing on and off inside Dashboard. Turns out there’s a command line tool called AppleFileServer that does just that! But there’s a catch: one must be root in order to run it.

You are probably all familiar with another command line tool called sudo. sudo’s sole purpose is to let you run commands as root. Sounds like it would be the perfect solution! Sadly, sudo is an interactive tool — you run it, it prompts for a password, you enter the password, and only then is your command run. This generally makes sudo unusable in the “fire and forget” world of scripting.

Thankfully, widgets can be much more than scripts. And Apple foresaw the need to provide for a certain amount of interactivity when calling the command line from Dashboard. The rest of this article details how to retrofit your widget to make system calls using sudo.

A Blank Slate

But first, we need a widget to modify. I’m going to start with foo, a mostly blank widget that implements the bare minimum Apple Classes and a few convenience functions. It also allows us to present debugging information right on the widget, which makes it great for screenshots. You playing along at home should feel free to use whatever you like, but my examples are going to be in the context of modifying foo.

To start, I’m going to get rid of everything on the front of the widget, and replace it with a simple button. I’ll open foo.html and replace the first content <div> with:

<div class="content">
    <p>
        <input type="button" value="who am i?" onclick="buttonClicked();" />
    </p>
</div>

Of course, now that we have that beautiful button, we need it to do something when clicked. Note that in the code snippet above, the button is wired to a function called buttonClicked(). We need to create that function.

To keep things orderly, we’ll put this function in the events.js file (located in the src folder inside the widget). Our implementation of buttonClicked() looks like:

var command;
function buttonClicked(){
    command = widget.system("/usr/bin/whoami", null);
    pdb(command.outputString);
}

A few things to note here. First, command is declared globally. While not necessary right now, this will be important later. Next is the widget.system() call. system() expects two arguments. The first is the command to be executed (as a string). The next is a handler to be called when execution is complete. Here we have passed null as the handler. This tells system() to execute the given command synchronously (system() will wait for the command to exit before returning, thus blocking execution in the widget). More on this later as well.

The command we are having the widget execute is whoami. This command very simply prints the name of the current user to stdout. The function pdb() is one that comes with foo and is defined in effects.js. It prints whatever string is passed to it on a semi-transparent window on top of the widget. Think of it as a more visual alert(). Finally, the outputString property of command holds whatever our command line utility writes to stdout.

If you’ve jumped ahead and tried to run the widget as it exists now, you may notice that nothing happens and a strange error has appeared in your console.log:

Value undefined (result of expression widget.system) is not object.

This is due to the fact that widget.system is not enabled by default. You have to explicitly tell Dashboard that your widget is going to use command line functionality before it lets you use this object. You can do so by adding:

<key>AllowSystem</key>
<true/>

To your info.plist dictionary. Once that change is saved, your widget is ready to roll.

So! Armed with all this knowledge, it should be pretty easy for you to predict what will happen when this widget is run and the button is pressed. Something like this:

sudo2.png

Of course, your widget will display your username instead of “jemmons”, but basics are the same. Clicking the button calls buttonClicked() which calls system() synchronously. system() (which was enabled by the change we made to info.plist) runs whoami, which writes the current username to stdout. stdout is recorded in outputString which is displayed on the widget by pdb().

With me so far? Good. Now that the foundations have been laid, we’re going to speed things up.

Out of Sync

Our widget runs, but we have some issues to deal with. First amongst these is that we’re calling the command line synchronously and that’s a big no-no. When a command is executed synchronously by system(), everything else in your widget is blocked. Your interface won’t update. Your buttons will become unresponsive. It’s a generally bad scene. And Apple has intimated that in the future such behavior may effect the performance of other widgets as well. The word as it’s come down from on high is clear. Don’t use system() synchronously.

So how do we make it asynchronous? By passing it a target action instead of null:

function buttonClicked(){
    command = widget.system("/usr/bin/whoami", commandDone);
}

Now system() will return immediately, not waiting for whoami to finish executing. This frees it up to do other things. When whoami does complete, commandDone() will be called and passed an instance of the system object which contains, among other things, the outputString property that we’re interested in. So a possible commandDone() implementation might look like:

function commandDone(cmd){
    pdb(cmd.outputString);
}

The Root of the Problem

That takes care of our synchronicity problem. The current username is printed on the widget, and nothing is blocked in the process. The thing is, we really don’t want “jemmons” (or whatever your name is) to be displayed on the widget. The point of this exercise is to run a command with root privileges. If we do that, whoami ought to print “root” not <your_name_here>. So let’s change our command again, this time to incorporate sudo:

function buttonClicked(){
    command = widget.system("/usr/bin/sudo /usr/bin/whoami", commandDone);
}

Save your widget, run it, click the button and… nothing! It doesn’t display “root”, but then it doesn’t display anything else either. What happened?

If you open up your Activity Monitor and search for “sudo” it should start to become clear. sudo is running. In fact, it’s still running. It’s just sitting there. And it will stay sitting there until you actually close your widget by clicking on its X (you can go ahead and do that now).

This is the problem with interactive commands. When you run sudo, it prompts you for your password and then waits there patiently until it gets it. We need the widget to detect when sudo is asking for a password, and then send said password to it. system.onreaderror and system.write() are going to help us out with this.

onreaderror is a handler that gets executed whenever system detects data has been written to stderr. It reads the text off the stream, packages it in a string, and passes it as the sole argument to whatever function onreaderror points to. Lucky for us, when sudo prompts for a password, it sends that prompt to stderr. So all we have to do is assign a handler:

function buttonClicked(){
    command = widget.system("/usr/bin/sudo /usr/bin/whoami", commandDone);
    command.onreaderror = commandError;
}

And we’re set. Of course we’ll need to write an implementation for that handler. And in that implementation, we will want to pass a string to the command’s stdin (remember, sudo has just prompted for a password. We want to give it one). That’s exactly what write() does — sends a string to its command’s stdin:

function commandError(stderr){
    command.write("your_password" +"\n");
}

And now we see why we had to make command a global var. commandError() doesn’t get passed a system instance. It only gets a string. If we want to do anything with our command, we need to use the global instance of it we set up when we first instantiated it in buttonClicked().

Go ahead, save these changes (making sure to replace “your_password” with your password in the code above), and run the widget.

sudo3.png

Success!

A Cautionary Tale

Now of course, you’ll never want to hard-code the password as I’m doing in this example. I’ll leave the creation of an “Enter Password” interface as an exercise for the reader (you can use Share Switch as an example, if you like). For the most part it’s as easy as popping an input field into a div and using the display style to hide and show it. But there are a few gotchas to keep in mind:

  1. The input field you use should probably be of type password. These fields offer extra security, not the least of which is preventing the password from actually being displayed.

  2. For security reasons, you do not want to ever put the password in a variable. When you need it, always grab it directly from the input field.

  3. Never store the password using setPreferenceForKey() or anything else that writes to plain text files without using some sort of strong encryption.

  4. Unlike a web page where after a password is submitted a new page is loaded, your widget will never load a “new page”. Therefore, once text is entered into your widget’s password field, it will stay there until Dashboard is restarted or you explicitly clear it. This is probably not desired, so you should clear it whenever you are finished reading from it by setting its value property to "".

That Voodoo that sudo

We now have a widget working with sudo! We’re done, right?

In a word, no. Our widget works, but it is not very robust. What happens, for example, if a user enters the wrong password? sudo prompts for the password again. Shouldn’t our widget?

I’m so glad you asked! Here’s how it works. When sudo asks for a password, it writes “Password:” to stderr. When it reads a bad password from stdin it writes “Sorry, try again.” also to stderr. So really, when our widget is notified that there is something to read from stderr, we should be checking to see what type of situation we are in:

function commandError(stderr){
    if(thing.match(/Password:/)){
        //Asking for the password...
        command.write("bad_password" +"\n");

    }else if(stderr.match(/Sorry, try again./)){
        //The wrong password was entered.
        pdbln("Oops!");
        command.write("good_password" +"\n");
    }
}

Replace “good_password” with your password. Replace “bad_password” with anything but. When sudo is first run, it will prompt for a password. This function purposefully sends it the wrong one, causing sudo to send its error message. This causes the proper password to be sent. So you should expect to see something like this:

sudo4.png

To Cache or Not to Cache

Ah, but depending on how fast of a reader you are, you may just see “root” displayed. No “Oops!”. The reason is that sudo doesn’t always prompt for a password. Once sudo authenticates your password the first time, it won’t ask you for the password again for (by default) five minutes. If you just ran your widget within five minutes of the last time you used it, sudo won’t prompt for a password and commandError() will never be called.

Users often find this to be a convenience. They appreciate only having to enter their password once during a session of using your widget. sudo’s caching behavior does have security ramifications, however. After a user authenticates with your widget using sudo, anyone who comes up to the computer in the next five minutes has free reign to run anything from the terminal with the permissions of root.

There is a way to tell sudo to reset this five minute timestamp to 0, however. Simply call it with the -K option. If you feel you would rather play it safe than sorry, make another system() call in your commandDone() function:

function commandDone(cmd){
    pdb(cmd.outputString);
    widget.system("/usr/bin/sudo -K", sudoKilled);
}

function sudoKilled(cmd){
    pdb("sudo timestamp removed.")
}

Once, Twice, Three Times Too Many

Our widget is really shaping up. It processes correct passwords. It can kill sudo’s timestamp. It even recognizes incorrect passwords and gives users another chance to right their wrongs. But sudo doesn’t have infinite patience for people typing in bad passwords. After the third attempt, it lets its displeasure be known by quitting with an error code. It is therefore important that our widget is able to distinguish between the error codes sudo can have when it exits:

function commandDone(cmd){
    if(cmd.status == 0){
        //The password was accepted and sudo ran properly.
        pdb(cmd.outputString);
        command = widget.system("/usr/bin/sudo -K", sudoKilled);

    }else if(cmd.status == 1){
        //sudo exited due to too many password attempts.
        pdb("Too many tries!");
    }
}

As you can see above, one of the properties of the system instance passed in to commandDone() is status. This property holds the exit value of the command line program that just finished running. sudo returns an exit value of 1 if there’s a problem, and returns the value of whatever command it executed as root if there is not (whoami has an exit value of 0 when everything is ok).

To see this in action, make both passwords incorrect in commandError, save your files, run sudo -K to clear the timestamp, and run the widget.

sudo5.png

Our widget reports three incorrect password attempts and then signals that sudo has given up. Just what we expected! But note when you run this yourself that there is a significant delay between password attempts (the “Oops!” messages don’t appear all at once). Also note that this delay gets longer the more times try to enter a password. If you run this version of the widget again right away, you may be waiting 10-20 seconds between password attempts!

This is a security feature of sudo. The more password attempts you make, the longer it makes you wait between password prompts. This functionality is there to discourage brute-force dictionary attacks. But what it means to you as a widget maker is that you should probably pop up some sort of “please wait…” banner or spinning gear between initially calling sudo or rejecting a password, and prompting for a password. That way the user knows that your widget isn’t “broken” or “hung”, it’s just waiting on sudo to get its act together.

With Great Power…

The asynchronous command line functionality Apple has built into Dashboard is a leatherman in the belt of the well-rounded widget maker. We’ve seen just how flexible it can be. And when applied to the sudo command line utility, flexibility isn’t the only thing your widget gains. It gets power, too. As all power can be abused, I leave you with some words of wisdom from sudo itself:

We trust you have received the usual lecture from the local System
Administrator. It usually boils down to these three things:

    #1) Respect the privacy of others.
    #2) Think before you type.
    #3) With great power comes great responsibility.
A version of the widget created in this article can be found at http://www.skia.net/SampleWidgets/sudo.zip.