source: trunk/PrefsPane/src/AuthHelper.m @ 1046

Revision 1017, 16.5 KB checked in by speck, 10 months ago (diff)

fix indents.

Line 
1/* Copyright (C) 2008-2009 Peter Speck
2 *
3 * This program is free software: you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation, either version 3 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
15 */
16
17/*
18    This file implements the setuid-root helper tool.
19*/
20
21#include <stdio.h>
22#include <sys/types.h>
23#include <sys/sysctl.h>
24#include <openssl/md5.h>
25#include <SystemConfiguration/SCDynamicStoreCopySpecific.h>
26#include "GBDebug.h"
27
28@interface AuthHelper : NSObject {
29@private
30    NSString* _selfRepairPlistPath;
31    NSString* _mainBundlePath;
32}
33
34- (id)init;
35- (void)dealloc;
36
37- (void)exitWithError:(NSString*)msg;
38
39@end
40
41@implementation AuthHelper
42
43static NSString* updateAppPath = @"/Library/PreferencePanes/GlimmerBlocker.prefPane/Contents/GlimmerBlockerUpdater.app";
44
45- (id)init
46{
47    if (!(self = [super init]))
48        return nil;
49    NSString *_selfRepairJobLabel = @"org.glimmerblocker.self-repair";
50    _selfRepairPlistPath = [[NSString stringWithFormat:@"/Library/LaunchDaemons/%@.plist", _selfRepairJobLabel] retain];
51    _mainBundlePath = @"/Library/PreferencePanes/GlimmerBlocker.prefPane";
52    return self;
53}
54
55- (void)dealloc
56{
57    [_selfRepairPlistPath release];
58    [_mainBundlePath release];
59    [super dealloc];
60}
61
62- (void)exitWithError:(NSString*)msg
63{
64    NSLog(@"AuthHelper failed: %@", msg);
65    [NSException raise:msg format:@"AuthHelperFormat"];
66}
67
68- (void)readBytes:(int)numBytesNeeded
69      readingWhat:(NSString*)what
70       withBuffer:(char*)buffer
71{
72    int numBytesRead = 0;
73    while (numBytesRead < numBytesNeeded) {
74        if (feof(stdin))
75            [self exitWithError:[NSString stringWithFormat:@"EOF on stdin while reading %@", what]];
76        int x = fread(buffer + numBytesRead, 1, numBytesNeeded - numBytesRead, stdin);
77        numBytesRead += x;
78        if (ferror(stdin))
79            [self exitWithError:[NSString stringWithFormat:@"error on stdin while reading %@", what]];
80    }
81}
82
83- (NSData*)readData:(NSString*)description
84{
85    long long len = 0;
86    NSAssert(sizeof(len) == 8, @"longlong != 64");
87    [self readBytes:sizeof(len) readingWhat:description withBuffer:(char*)&len];
88    if (len < 0 || len > 1024 * 1024) {
89        NSString* fmt = @"Invalid NSData chunk size in stdin: %lld bytes";
90        [self exitWithError:[NSString stringWithFormat:fmt, len]];
91    }
92    char* buffer = malloc(len);
93    if (!buffer)
94        [self exitWithError:[NSString stringWithFormat:@"Out of memory, can't malloc %d bytes", len]];
95    [self readBytes:len readingWhat:description withBuffer:buffer];
96    NSData* data = [NSData dataWithBytes:buffer length:len];
97    free(buffer);
98    return data;
99}
100
101- (void)readAuth
102{
103    NSData* data = [self readData:@"AuthorizationExternalForm"];
104    AuthorizationExternalForm extAuth;
105    NSAssert([data length] == sizeof(extAuth), @"AuthDataSize");
106    memcpy(&extAuth, [data bytes], sizeof(extAuth));
107    AuthorizationRef authRef = NULL;
108    OSStatus stat = AuthorizationCreateFromExternalForm(&extAuth, &authRef);
109    if (stat != errAuthorizationSuccess)
110        [self exitWithError:@"Authorization invalid/expired"];
111    DebugNSLog(@"AuthHelper.auth ok");
112}
113
114- (int)executeCommandlineTool:(NSString*)cmd
115                     withArgs:(NSArray*)args
116         withFatalDescription:(NSString*)fatal
117{
118    [args retain];
119    NSTask* task = [[NSTask alloc] init];
120    [task setLaunchPath:cmd];
121    if (args)
122        [task setArguments:args];
123    [task setCurrentDirectoryPath:@"/tmp"];
124    [task launch];
125    [task waitUntilExit];
126    int status = [task terminationStatus];
127    [task release];
128    [args release];
129    if (status && fatal) {
130        NSString* s = args ? [NSString stringWithFormat:@"%@ %@", cmd, [args componentsJoinedByString:@" "]] : cmd;
131        NSLog(@"Got exit code %d: %@", status, s);
132        [self exitWithError:fatal];
133    }
134    return status;
135}
136
137- (void)savePlist:(NSString*)xml
138           inPath:(NSString*)path
139{
140    NSFileManager* fm = [NSFileManager defaultManager];
141    NSData* data = [xml dataUsingEncoding:NSUTF8StringEncoding];
142    if ([[NSData dataWithContentsOfFile:path] isEqual:data]) {
143        DebugNSLog(@"AuthHelper.write-plist: no changed.");
144    } else {
145        NSDictionary* attrs = [NSDictionary dictionaryWithObjectsAndKeys:
146                               [NSNumber numberWithLong:0644], NSFilePosixPermissions,
147                               NULL];
148        BOOL b = [fm createFileAtPath:path contents:data attributes:attrs];
149        if (!b)
150            [self exitWithError:@"Can't create plist"];
151        DebugNSLog(@"AuthHelper.write-plist ok");
152    }
153    @try {
154        [self executeCommandlineTool:@"/bin/launchctl"
155                            withArgs:[NSArray arrayWithObjects:@"load", path, NULL]
156                withFatalDescription:@"launchctl won't load plist."];
157    }
158    @catch (NSException* ex) {
159        [fm removeItemAtPath:path error:NULL];
160        @throw ex;
161    }
162}
163
164- (void)zapPlist:(NSString*)path
165{
166    NSFileManager* fm = [NSFileManager defaultManager];
167    if (![fm fileExistsAtPath:path])
168        return;
169    [self executeCommandlineTool:@"/bin/launchctl"
170                        withArgs:[NSArray arrayWithObjects:@"unload", path, NULL]
171            withFatalDescription:NULL];
172    [fm removeItemAtPath:path error:NULL];
173    if ([fm fileExistsAtPath:path])
174        [self exitWithError:@"Failed deleting launchd plist file"];
175}
176
177- (NSString*)makeUpdaterPlistContents:(NSArray*)args
178{
179    NSFileManager* fm = [NSFileManager defaultManager];
180    return [NSString stringWithFormat:@""
181            "<?xml version='1.0' encoding='UTF-8'?>\n"
182            "<!DOCTYPE plist PUBLIC '-//Apple Computer//DTD PLIST 1.0//EN' 'http://www.apple.com/DTDs/PropertyList-1.0.dtd'>\n"
183            "<plist version='1.0'>\n"
184            "<dict>\n"
185            "   <key>Disabled</key>\n"
186            "   <false/>\n"
187            "   <key>Label</key>\n"
188            "   <string>%@</string>\n"
189            "   <key>InitGroups</key>\n"
190            "   <true/>\n"
191            "   <key>ProgramArguments</key>\n"
192            "   <array>\n"
193            "       <string>%@/Contents/MacOS/GlimmerBlockerUpdater</string>\n"
194            "       <string>--background</string>\n"
195            "%@"
196            "   </array>\n"
197            "   <key>Umask</key>\n"
198            "   <integer>18</integer>\n" // 022, files not writeable by group/other.
199            "   <key>StartInterval</key>\n"
200            "   <integer>%d</integer>\n"
201            "   <key>KeepAlive</key>\n"
202            "   <false/>\n"
203            "   <key>WorkingDirectory</key>\n"
204            "   <string>%@/Contents/GlimmerBlockerUpdater.app/Contents/Resources</string>\n"
205            "   <key>StandardErrorPath</key>\n"
206            "   <string>/Library/Logs/GlimmerBlocker/Launchd.log</string>\n"
207            "</dict>\n"
208            "</plist>\n",
209            UPDATER_JOB_LABEL,
210            [GBDebug updaterBundlePath],
211            [args containsObject:@"--beta"] ? @"    <string>--beta</string>\n" : @"",
212            [fm fileExistsAtPath:@"/Library/GlimmerBlocker/UseSparkleDevChannel"] ? 300 : 24 * 60 * 60,
213            _mainBundlePath];
214}
215
216- (NSString*)makeProxyPlistContents
217{
218    return [NSString stringWithFormat:@""
219            "<?xml version='1.0' encoding='UTF-8'?>\n"
220            "<!DOCTYPE plist PUBLIC '-//Apple Computer//DTD PLIST 1.0//EN' 'http://www.apple.com/DTDs/PropertyList-1.0.dtd'>\n"
221            "<plist version='1.0'>\n"
222            "<dict>\n"
223            "   <key>Disabled</key>\n"
224            "   <false/>\n"
225            "   <key>Label</key>\n"
226            "   <string>%@</string>\n"
227            "   <key>UserName</key>\n"
228            "   <string>_glimmerblocker</string>\n"
229            "   <key>InitGroups</key>\n"
230            "   <true/>\n"
231            "   <key>ProgramArguments</key>\n"
232            "   <array>\n"
233            "       <string>%@/Contents/GlimmerBlockerProxy.app/Contents/MacOS/GlimmerBlocker</string>\n"
234            "       %@\n"
235            "   </array>\n"
236            "   <key>OnDemand</key>\n"
237            "   <false/>\n"
238            "   <key>Umask</key>\n"
239            "   <integer>63</integer>\n" // 077, files only readable by _glimmerblocker
240            "   <key>KeepAlive</key>\n"
241            "   <true/>\n"
242            "   <key>WorkingDirectory</key>\n"
243            "   <string>%@/Contents/GlimmerBlockerProxy.app/Contents/Resources</string>\n"
244            "   <key>StandardErrorPath</key>\n"
245            "   <string>/Library/Logs/GlimmerBlocker/Launchd.log</string>\n"
246            "</dict>\n"
247            "</plist>\n",
248            PROXY_JOB_LABEL,
249            _mainBundlePath,
250            [GBDebug fingerprintOptionForInstalledPanel:[NSBundle mainBundle]],
251            _mainBundlePath];
252}
253
254- (void)stop:(NSString*)plistPath
255{
256    [self readAuth];
257    // launchctl doesn't return error code if unloading fails....
258    [self zapPlist:plistPath];
259    [self zapPlist:_selfRepairPlistPath];
260}
261
262- (void)start:(NSString*)plistPath  withContents:(NSString*)contents
263{
264    [self readAuth];
265    [self zapPlist:plistPath];
266    [self savePlist:contents inPath:plistPath];
267    //
268    [self zapPlist:_selfRepairPlistPath];
269}
270
271- (NSString*)makeDir:(NSString*)path
272           withPerms:(int)perms
273{
274    NSFileManager* fm = [NSFileManager defaultManager];
275    BOOL isDirectory = NO;
276    if ([fm fileExistsAtPath:path isDirectory:&isDirectory]) {
277        if (!isDirectory) {
278            NSString* msg = [NSString stringWithFormat:@"Has non-directory item when expected directory: %@", path];
279            [NSException raise:msg format:@"AuthHelperFormat"];
280        }
281        return path;
282    }
283    NSDictionary* attrs = [NSDictionary dictionaryWithObjectsAndKeys:
284                           [NSNumber numberWithLong:perms], NSFilePosixPermissions,
285                           NULL];
286    NSError *error = NULL;
287    if (![fm createDirectoryAtPath:path withIntermediateDirectories:YES attributes:attrs error:&error]) {
288        NSString* msg = [NSString stringWithFormat:@"Can't create directory (%@): %@", error, path];
289        [NSException raise:msg format:@"AuthHelperFormat"];
290    }
291    NSArray* args = [NSArray arrayWithObjects:@"_glimmerblocker:_glimmerblocker", path, NULL];
292    [self executeCommandlineTool:@"/usr/sbin/chown"
293                        withArgs:args
294            withFatalDescription:@"chmod of directory."];
295    return path;
296}
297
298- (NSString*)makePrefsDir
299{
300    return [self makeDir:@"/Library/GlimmerBlocker" withPerms:0755];
301}
302
303- (void)savePrefs
304{
305    [self readAuth];
306    NSData* prefsData = [self readData:@"Prefs data"];
307    NSData* expectedMD5 = [self readData:@"Prefs MD5"];
308    // checking MD5 makes sure we got all of the data.
309    NSData* actualMD5 = [NSData dataWithBytes:MD5([prefsData bytes], [prefsData length], NULL) length:MD5_DIGEST_LENGTH];
310    if (![actualMD5 isEqual:expectedMD5])
311        [self exitWithError:[NSString stringWithFormat:@"Prefs data MD5 = %@ but expected %@", actualMD5, expectedMD5]];
312    DebugNSLog(@"Got valid prefs");
313    NSString* path = [NSString stringWithFormat:@"%@/PanelSettings.xml", [self makePrefsDir]];
314    [GBDebug safeSaveFileAtPath:path contents:prefsData];
315    NSFileManager* fm = [NSFileManager defaultManager];
316    NSDictionary* attrs = [NSDictionary dictionaryWithObjectsAndKeys:
317                           [NSNumber numberWithLong:0644], NSFilePosixPermissions,
318                           NULL];
319    [fm setAttributes:attrs ofItemAtPath:path error:NULL];
320    [self executeCommandlineTool:@"/usr/sbin/chown"
321                        withArgs:[NSArray arrayWithObjects:@"_glimmerblocker:_glimmerblocker", path, NULL]
322            withFatalDescription:@"chmod of panel-prefs."];
323}
324
325- (BOOL)isUpdaterAlreadyRunning
326{
327    for (NSDictionary* dict in [[NSWorkspace sharedWorkspace] launchedApplications]) {
328        NSString* bid = [dict objectForKey:@"NSApplicationBundleIdentifier"];
329        if ([bid isEqual:@"org.glimmerblocker.upgradeApp"])
330            return YES;
331    }
332    return NO;
333}
334
335- (BOOL)isAdmin
336{
337    NSArray* args = [NSArray arrayWithObjects:@"-c", @"groups | perl -ne 'exit 42 if /\\badmin\\b/;'", NULL];
338    int status = [self executeCommandlineTool:@"/bin/sh" withArgs:args withFatalDescription:NULL];
339    //NSLog(@"isAdmin: %d", status);
340    return status == 42;
341}
342
343- (int)runNotificationHelper:(NSArray*)args
344{
345    if ([self isUpdaterAlreadyRunning]) {
346        NSLog(@"Ignores notification helper request as updater already is running.");
347        exit(501); // see UpdateDaemon.java for exit codes
348    }
349    // http://developer.apple.com/qa/qa2001/qa1133.html
350    // user-id, message
351    uid_t uid = -1;// = [userIdText intValue];
352    NSString* userName = (NSString*)SCDynamicStoreCopyConsoleUser(NULL, &uid, NULL);
353    [userName autorelease];
354    DebugNSLog(@"Current user: %d = %@", uid, userName);
355    if (uid <= 0 || ![userName length] || [userName isEqual:@"loginwindow"]) {
356        // loginwindow might be 10.4 stuff, no such user in 10.5
357        NSLog(@"Seems to have no logged-in user.");
358        exit(502); // see UpdateDaemon.java for exit codes
359    }
360    if (setuid(uid) < 0)
361        NSLog(@"Failed setting uid to %d", uid);
362    if (getuid() != uid)
363        [self exitWithError:[NSString stringWithFormat:@"Can't change uid to %d, is still %d", uid, getuid()]];
364    if (REQUIRED_VALID_CODESIGN && ![GBDebug verifyCodesign])
365        exit(503); // see UpdateDaemon.java for exit codes
366    if (![self isAdmin])
367        exit(504); // see UpdateDaemon.java for exit codes
368    //
369    // AuthorizationCopyRights succeeds only when the application is starting using the "open" command
370    // instead of being started directly.
371    //
372    NSMutableArray* openArgs = [NSMutableArray arrayWithCapacity:10];
373    [openArgs addObject:@"-n"]; // open new instance
374    [openArgs addObject:@"-W"]; // wait until app quit.
375    [openArgs addObject:@"-g"]; // open in background
376    [openArgs addObject:@"-a"]; // open with app
377    [openArgs addObject:[NSString stringWithFormat:@"%@/Contents/MacOS/GlimmerBlockerUpdater", updateAppPath]];
378    [openArgs addObjectsFromArray:args];
379    int status = [self executeCommandlineTool:@"/usr/bin/open" withArgs:openArgs withFatalDescription:NULL];
380    return status;
381}
382
383- (void)checkForUpdates:(NSArray*)args
384{
385    NSString* resourcesPath = [NSString stringWithFormat:@"%@/Contents/Resources", updateAppPath];
386    NSMutableArray* openArgs = [[NSMutableArray arrayWithCapacity:5] retain];
387    for (NSString* s in args) {
388        if ([s isEqual:@"--background"])
389            [openArgs addObject:[NSString stringWithFormat:@"%@/option-background.txt", resourcesPath]];
390        else if ([s isEqual:@"--beta"])
391            [openArgs addObject:[NSString stringWithFormat:@"%@/option-beta.txt", resourcesPath]];
392        else if (![s isEqual:@"--foreground"] && ![s isEqual:@"--final"])
393            NSLog(@"Unknown sub-option for --check-for-updates: '%@'", s);
394    }
395    int status = [self runNotificationHelper:openArgs];
396    exit(status);
397}
398
399- (void)askFilterUpdate:(NSArray*)args
400{
401    NSMutableArray* openArgs = [[NSMutableArray arrayWithCapacity:5] retain];
402    [openArgs addObject:[args objectAtIndex:0]];
403    int status = [self runNotificationHelper:openArgs];
404    NSLog(@"Ask filter return status: %d", status);
405    exit(status);
406}
407
408- (void)javaSettings
409{
410    [self readAuth];
411    NSURL* url = [NSURL fileURLWithPath:@"/Library/GlimmerBlocker/JavaSettings.xml"];
412    NSData* data = [NSData dataWithContentsOfURL:url];
413    NSFileHandle *stdoutHandle = [NSFileHandle fileHandleWithStandardOutput];
414    [stdoutHandle writeData:data];
415}
416
417- (void)writeStringToStdout:(NSString*)s
418{
419    NSData* data = [s dataUsingEncoding:NSUTF8StringEncoding];
420    NSFileHandle *stdoutHandle = [NSFileHandle fileHandleWithStandardOutput];
421    [stdoutHandle writeData:data];
422}
423
424- (void)runWithArgs:(NSArray*)args
425{
426    if (geteuid()) {
427        [self exitWithError:[NSString stringWithFormat:@"Not running as root but with uid = %d and euid = %d",
428                             getuid(), geteuid()]];
429    }
430    if (getuid()) {
431        if (setuid(0) < 0)
432            NSLog(@"Failed setting uid to 0");
433        if (getuid()) {
434            [self exitWithError:[NSString stringWithFormat:@"Failed to set uid to 0, is still %d, euid = %d",
435                                 getuid(), geteuid()]];
436        }
437    }
438    if ([args count] < 2)
439        [self exitWithError:@"No arguments given"];
440    NSString* cmd = [args objectAtIndex:1];
441    args = [args subarrayWithRange:NSMakeRange(2, [args count] - 2)];
442    if ([cmd isEqual:@"--plist-contents"])
443        [self writeStringToStdout:[self makeProxyPlistContents]];
444    else if ([cmd isEqual:@"--plist-path"])
445        [self writeStringToStdout:[GBDebug proxyPlistPath]];
446    else if ([cmd isEqual:@"--start"])
447        [self start:[GBDebug proxyPlistPath] withContents:[self makeProxyPlistContents]];
448    else if ([cmd isEqual:@"--stop"])
449        [self stop:[GBDebug proxyPlistPath]];
450    else if ([cmd isEqual:@"--updater-plist-path"])
451        [self writeStringToStdout:[GBDebug updaterPlistPath]];
452    else if ([cmd isEqual:@"--updater-plist-content"])
453        [self writeStringToStdout:[self makeUpdaterPlistContents:args]];
454    else if ([cmd isEqual:@"--updater-start"])
455        [self start:[GBDebug updaterPlistPath] withContents:[self makeUpdaterPlistContents:args]];
456    else if ([cmd isEqual:@"--updater-stop"])
457        [self stop:[GBDebug updaterPlistPath]];
458    else if ([cmd isEqual:@"--save-prefs"])
459        [self savePrefs];
460    else if ([cmd isEqual:@"--check-for-updates"])
461        [self checkForUpdates:args];
462    else if ([cmd isEqual:@"--ask-filter-update"])
463        [self askFilterUpdate:args];
464    else if ([cmd isEqual:@"--java-settings"])
465        [self javaSettings];
466    else
467        [self exitWithError:[NSString stringWithFormat:@"Unknown arg: %@", cmd]];
468}
469
470@end
471
472int main (int argc, const char * argv[])
473{
474    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
475    NSArray *args = [[NSProcessInfo processInfo] arguments];
476    AuthHelper* h = [[[AuthHelper alloc] init] retain];
477    @try {
478        [h runWithArgs:args];
479    }
480    @catch (NSException* ex) {
481        if (![[ex reason] isEqual:@"AuthHelperFormat"])
482            NSLog(@"Stops due to exception: '%@' / '%@'", [ex name], [ex reason]);
483        exit(1);
484    }
485    [h release];
486    [pool release];
487    return 0;
488}
Note: See TracBrowser for help on using the repository browser.