gcapi.mm revision 68043e1e95eeb07d5cae7aca370b26518b0867d6
1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/installer/gcapi_mac/gcapi.h"
6
7#import <Cocoa/Cocoa.h>
8#include <grp.h>
9#include <pwd.h>
10#include <sys/stat.h>
11#include <sys/types.h>
12#include <sys/utsname.h>
13
14namespace {
15
16// The "~~" prefixes are replaced with the home directory of the
17// console owner (i.e. not the home directory of the euid).
18NSString* const kChromeInstallPath = @"/Applications/Google Chrome.app";
19
20NSString* const kBrandKey = @"KSBrandID";
21NSString* const kUserBrandPath = @"~~/Library/Google/Google Chrome Brand.plist";
22
23NSString* const kSystemKsadminPath =
24    @"/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/"
25     "Contents/MacOS/ksadmin";
26
27NSString* const kUserKsadminPath =
28    @"~~/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/"
29     "Contents/MacOS/ksadmin";
30
31NSString* const kSystemMasterPrefsPath =
32    @"/Library/Google/Google Chrome Master Preferences";
33NSString* const kUserMasterPrefsPath =
34    @"~~/Library/Application Support/Google/Chrome/"
35     "Google Chrome Master Preferences";
36
37// Condensed from chromium's base/mac/mac_util.mm.
38bool IsOSXVersionSupported() {
39  // On 10.6, Gestalt() was observed to be able to spawn threads (see
40  // http://crbug.com/53200). Don't call Gestalt().
41  struct utsname uname_info;
42  if (uname(&uname_info) != 0)
43    return false;
44  if (strcmp(uname_info.sysname, "Darwin") != 0)
45    return false;
46
47  char* dot = strchr(uname_info.release, '.');
48  if (!dot)
49    return false;
50
51  int darwin_major_version = atoi(uname_info.release);
52  if (darwin_major_version < 6)
53    return false;
54
55  // The Darwin major version is always 4 greater than the Mac OS X minor
56  // version for Darwin versions beginning with 6, corresponding to Mac OS X
57  // 10.2.
58  int mac_os_x_minor_version = darwin_major_version - 4;
59
60  // Chrome is known to work on 10.6 - 10.9.
61  return mac_os_x_minor_version >= 6 && mac_os_x_minor_version <= 9;
62}
63
64// Returns the pid/gid of the logged-in user, even if getuid() claims that the
65// current user is root.
66// Returns NULL on error.
67passwd* GetRealUserId() {
68  CFDictionaryRef session_info_dict = CGSessionCopyCurrentDictionary();
69  [NSMakeCollectable(session_info_dict) autorelease];
70  if (!session_info_dict)
71    return NULL;  // Possibly no screen plugged in.
72
73  CFNumberRef ns_uid = (CFNumberRef)CFDictionaryGetValue(session_info_dict,
74                                                         kCGSessionUserIDKey);
75  if (CFGetTypeID(ns_uid) != CFNumberGetTypeID())
76    return NULL;
77
78  uid_t uid;
79  BOOL success = CFNumberGetValue(ns_uid, kCFNumberSInt32Type, &uid);
80  if (!success)
81    return NULL;
82
83  return getpwuid(uid);
84}
85
86enum TicketKind {
87  kSystemTicket, kUserTicket
88};
89
90// Replaces "~~" with |home_dir|.
91NSString* AdjustHomedir(NSString* s, const char* home_dir) {
92  if (![s hasPrefix:@"~~"])
93    return s;
94  NSString* ns_home_dir = [NSString stringWithUTF8String:home_dir];
95  return [ns_home_dir stringByAppendingString:[s substringFromIndex:2]];
96}
97
98// If |chrome_path| is not 0, |*chrome_path| is set to the path where chrome
99// is according to keystone. It's only set if that path exists on disk.
100BOOL FindChromeTicket(TicketKind kind, const passwd* user,
101                      NSString** chrome_path) {
102  if (chrome_path)
103    *chrome_path = nil;
104
105  // Don't use Objective-C 2 loop syntax, in case an installer runs on 10.4.
106  NSMutableArray* keystone_paths =
107      [NSMutableArray arrayWithObject:kSystemKsadminPath];
108  if (kind == kUserTicket) {
109    [keystone_paths insertObject:AdjustHomedir(kUserKsadminPath, user->pw_dir)
110                        atIndex:0];
111  }
112  NSEnumerator* e = [keystone_paths objectEnumerator];
113  id ks_path;
114  while ((ks_path = [e nextObject])) {
115    if (![[NSFileManager defaultManager] fileExistsAtPath:ks_path])
116      continue;
117
118    NSTask* task = nil;
119    NSString* string = nil;
120    bool ksadmin_ran_successfully = false;
121
122    @try {
123      task = [[NSTask alloc] init];
124      [task setLaunchPath:ks_path];
125
126      NSArray* arguments = @[
127          kind == kUserTicket ? @"--user-store" : @"--system-store",
128          @"--print-tickets",
129          @"--productid",
130          @"com.google.Chrome",
131      ];
132      if (geteuid() == 0 && kind == kUserTicket) {
133        NSString* run_as = [NSString stringWithUTF8String:user->pw_name];
134        [task setLaunchPath:@"/usr/bin/sudo"];
135        arguments = [@[@"-u", run_as, ks_path]
136            arrayByAddingObjectsFromArray:arguments];
137      }
138      [task setArguments:arguments];
139
140      NSPipe* pipe = [NSPipe pipe];
141      [task setStandardOutput:pipe];
142
143      NSFileHandle* file = [pipe fileHandleForReading];
144
145      [task launch];
146
147      NSData* data = [file readDataToEndOfFile];
148      [task waitUntilExit];
149
150      ksadmin_ran_successfully = [task terminationStatus] == 0;
151      string = [[[NSString alloc] initWithData:data
152                                    encoding:NSUTF8StringEncoding] autorelease];
153    }
154    @catch (id exception) {
155      // Most likely, ks_path didn't exist.
156    }
157    [task release];
158
159    if (ksadmin_ran_successfully && [string length] > 0) {
160      // If the user deleted chrome, it doesn't get unregistered in keystone.
161      // Check if the path keystone thinks chrome is at still exists, and if not
162      // treat this as "chrome isn't installed". Sniff for
163      //   xc=<KSPathExistenceChecker:1234 path=/Applications/Google Chrome.app>
164      // in the output. But don't mess with system tickets, since reinstalling
165      // a user chrome on top of a system ticket produces a non-autoupdating
166      // chrome.
167      NSRange start = [string rangeOfString:@"\n\txc=<KSPathExistenceChecker:"];
168      if (start.location == NSNotFound && start.length == 0)
169        return YES;  // Err on the cautious side.
170      string = [string substringFromIndex:start.location];
171
172      start = [string rangeOfString:@"path="];
173      if (start.location == NSNotFound && start.length == 0)
174        return YES;  // Err on the cautious side.
175      string = [string substringFromIndex:start.location];
176
177      NSRange end = [string rangeOfString:@".app>\n\t"];
178      if (end.location == NSNotFound && end.length == 0)
179        return YES;
180
181      string = [string substringToIndex:NSMaxRange(end) - [@">\n\t" length]];
182      string = [string substringFromIndex:start.length];
183
184      BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:string];
185      if (exists && chrome_path)
186        *chrome_path = string;
187      // Don't allow reinstallation over a system ticket, even if chrome doesn't
188      // exist on disk.
189      if (kind == kSystemTicket)
190        return YES;
191      return exists;
192    }
193  }
194
195  return NO;
196}
197
198// File permission mask for files created by gcapi.
199const mode_t kUserPermissions = 0755;
200const mode_t kAdminPermissions = 0775;
201
202BOOL CreatePathToFile(NSString* path, const passwd* user) {
203  path = [path stringByDeletingLastPathComponent];
204
205  // Default owner, group, permissions:
206  // * Permissions are set according to the umask of the current process. For
207  //   more information, see umask.
208  // * The owner ID is set to the effective user ID of the process.
209  // * The group ID is set to that of the parent directory.
210  // The default group ID is fine. Owner ID is fine if creating a system path,
211  // but when creating a user path explicitly set the owner in case euid is 0.
212  // Do set permissions explicitly; for admin paths all admins can write, for
213  // user paths just the owner may.
214  NSMutableDictionary* attributes = [NSMutableDictionary dictionary];
215  if (user) {
216    [attributes setObject:[NSNumber numberWithShort:kUserPermissions]
217                   forKey:NSFilePosixPermissions];
218    [attributes setObject:[NSNumber numberWithInt:user->pw_uid]
219                   forKey:NSFileOwnerAccountID];
220  } else {
221    [attributes setObject:[NSNumber numberWithShort:kAdminPermissions]
222                   forKey:NSFilePosixPermissions];
223    [attributes setObject:@"admin" forKey:NSFileGroupOwnerAccountName];
224  }
225
226  NSFileManager* manager = [NSFileManager defaultManager];
227  return [manager createDirectoryAtPath:path
228            withIntermediateDirectories:YES
229                             attributes:attributes
230                                  error:nil];
231}
232
233// Tries to write |data| at |user_path|.
234// Returns the path where it wrote, or nil on failure.
235NSString* WriteUserData(NSData* data,
236                        NSString* user_path,
237                        const passwd* user) {
238  user_path = AdjustHomedir(user_path, user->pw_dir);
239  if (CreatePathToFile(user_path, user) &&
240      [data writeToFile:user_path atomically:YES]) {
241    chmod([user_path fileSystemRepresentation], kUserPermissions & ~0111);
242    chown([user_path fileSystemRepresentation], user->pw_uid, user->pw_gid);
243    return user_path;
244  }
245  return nil;
246}
247
248// Tries to write |data| at |system_path| or if that fails at |user_path|.
249// Returns the path where it wrote, or nil on failure.
250NSString* WriteData(NSData* data,
251                    NSString* system_path,
252                    NSString* user_path,
253                    const passwd* user) {
254  // Try system first.
255  if (CreatePathToFile(system_path, NULL) &&
256      [data writeToFile:system_path atomically:YES]) {
257    chmod([system_path fileSystemRepresentation], kAdminPermissions & ~0111);
258    // Make sure the file is owned by group admin.
259    if (group* group = getgrnam("admin"))
260      chown([system_path fileSystemRepresentation], 0, group->gr_gid);
261    return system_path;
262  }
263
264  // Failed, try user.
265  return WriteUserData(data, user_path, user);
266}
267
268NSString* WriteBrandCode(const char* brand_code, const passwd* user) {
269  NSDictionary* brand_dict = @{
270      kBrandKey: [NSString stringWithUTF8String:brand_code],
271  };
272  NSData* contents = [NSPropertyListSerialization
273      dataFromPropertyList:brand_dict
274                    format:NSPropertyListBinaryFormat_v1_0
275          errorDescription:nil];
276
277  return WriteUserData(contents, kUserBrandPath, user);
278}
279
280BOOL WriteMasterPrefs(const char* master_prefs_contents,
281                      size_t master_prefs_contents_size,
282                      const passwd* user) {
283  NSData* contents = [NSData dataWithBytes:master_prefs_contents
284                                    length:master_prefs_contents_size];
285  return WriteData(
286      contents, kSystemMasterPrefsPath, kUserMasterPrefsPath, user) != nil;
287}
288
289NSString* PathToFramework(NSString* app_path, NSDictionary* info_plist) {
290  NSString* version = [info_plist objectForKey:@"CFBundleShortVersionString"];
291  if (!version)
292    return nil;
293  return [[[app_path
294      stringByAppendingPathComponent:@"Contents/Versions"]
295      stringByAppendingPathComponent:version]
296      stringByAppendingPathComponent:@"Google Chrome Framework.framework"];
297}
298
299NSString* PathToInstallScript(NSString* app_path, NSDictionary* info_plist) {
300  return [PathToFramework(app_path, info_plist) stringByAppendingPathComponent:
301      @"Resources/install.sh"];
302}
303
304bool isbrandchar(int c) {
305  // Always four upper-case alpha chars.
306  return c >= 'A' && c <= 'Z';
307}
308
309}  // namespace
310
311int GoogleChromeCompatibilityCheck(unsigned* reasons) {
312  unsigned local_reasons = 0;
313  @autoreleasepool {
314    passwd* user = GetRealUserId();
315    if (!user)
316      return GCCC_ERROR_ACCESSDENIED;
317
318    if (!IsOSXVersionSupported())
319      local_reasons |= GCCC_ERROR_OSNOTSUPPORTED;
320
321    NSString* path;
322    if (FindChromeTicket(kSystemTicket, NULL, &path)) {
323      local_reasons |= GCCC_ERROR_ALREADYPRESENT;
324      if (!path)  // Ticket points to nothingness.
325        local_reasons |= GCCC_ERROR_ACCESSDENIED;
326    }
327
328    if (FindChromeTicket(kUserTicket, user, NULL))
329      local_reasons |= GCCC_ERROR_ALREADYPRESENT;
330
331    if ([[NSFileManager defaultManager] fileExistsAtPath:kChromeInstallPath])
332      local_reasons |= GCCC_ERROR_ALREADYPRESENT;
333
334    if ((local_reasons & GCCC_ERROR_ALREADYPRESENT) == 0) {
335      if (![[NSFileManager defaultManager]
336              isWritableFileAtPath:@"/Applications"])
337      local_reasons |= GCCC_ERROR_ACCESSDENIED;
338    }
339
340  }
341  if (reasons != NULL)
342    *reasons = local_reasons;
343  return local_reasons == 0;
344}
345
346int InstallGoogleChrome(const char* source_path,
347                        const char* brand_code,
348                        const char* master_prefs_contents,
349                        unsigned master_prefs_contents_size) {
350  if (!GoogleChromeCompatibilityCheck(NULL))
351    return 0;
352
353  @autoreleasepool {
354    passwd* user = GetRealUserId();
355    if (!user)
356      return 0;
357
358    NSString* app_path = [NSString stringWithUTF8String:source_path];
359    NSString* info_plist_path =
360        [app_path stringByAppendingPathComponent:@"Contents/Info.plist"];
361    NSDictionary* info_plist =
362        [NSDictionary dictionaryWithContentsOfFile:info_plist_path];
363
364    // Use install.sh from the Chrome app bundle to copy Chrome to its
365    // destination.
366    NSString* install_script = PathToInstallScript(app_path, info_plist);
367    if (!install_script) {
368      return 0;
369    }
370
371    @try {
372      NSTask* task = [[[NSTask alloc] init] autorelease];
373
374      // install.sh tries to make the installed app admin-writable, but
375      // only when it's not run as root.
376      if (geteuid() == 0) {
377        // Use |su $(whoami)| instead of sudo -u. If the current user is in more
378        // than 16 groups, |sudo -u $(whoami)| will drop all but the first 16
379        // groups, which can lead to problems (e.g. if "admin" is one of the
380        // dropped groups).
381        // Since geteuid() is 0, su won't prompt for a password.
382        NSString* run_as = [NSString stringWithUTF8String:user->pw_name];
383        [task setLaunchPath:@"/usr/bin/su"];
384
385        NSString* single_quote_escape = @"'\"'\"'";
386        NSString* install_script_quoted = [install_script
387            stringByReplacingOccurrencesOfString:@"'"
388                                      withString:single_quote_escape];
389        NSString* app_path_quoted =
390            [app_path stringByReplacingOccurrencesOfString:@"'"
391                                                withString:single_quote_escape];
392        NSString* install_path_quoted = [kChromeInstallPath
393            stringByReplacingOccurrencesOfString:@"'"
394                                      withString:single_quote_escape];
395
396        NSString* install_script_execution =
397            [NSString stringWithFormat:@"exec '%@' '%@' '%@'",
398                                       install_script_quoted,
399                                       app_path_quoted,
400                                       install_path_quoted];
401        [task setArguments:
402            @[run_as, @"-c", install_script_execution]];
403      } else {
404        [task setLaunchPath:install_script];
405        [task setArguments:@[app_path, kChromeInstallPath]];
406      }
407
408      [task launch];
409      [task waitUntilExit];
410      if ([task terminationStatus] != 0) {
411        return 0;
412      }
413    }
414    @catch (id exception) {
415      return 0;
416    }
417
418    // Set brand code. If Chrome's Info.plist contains a brand code, use that.
419    NSString* info_plist_brand = [info_plist objectForKey:kBrandKey];
420    if (info_plist_brand &&
421        [info_plist_brand respondsToSelector:@selector(UTF8String)])
422      brand_code = [info_plist_brand UTF8String];
423
424    BOOL valid_brand_code = brand_code && strlen(brand_code) == 4 &&
425        isbrandchar(brand_code[0]) && isbrandchar(brand_code[1]) &&
426        isbrandchar(brand_code[2]) && isbrandchar(brand_code[3]);
427
428    NSString* brand_path = nil;
429    if (valid_brand_code)
430      brand_path = WriteBrandCode(brand_code, user);
431
432    // Write master prefs.
433    if (master_prefs_contents)
434      WriteMasterPrefs(master_prefs_contents, master_prefs_contents_size, user);
435
436    // TODO Set default browser if requested.
437  }
438  return 1;
439}
440
441int LaunchGoogleChrome() {
442  @autoreleasepool {
443    passwd* user = GetRealUserId();
444    if (!user)
445      return 0;
446
447    NSString* app_path;
448
449    NSString* path;
450    if (FindChromeTicket(kUserTicket, user, &path) && path)
451      app_path = path;
452    else if (FindChromeTicket(kSystemTicket, NULL, &path) && path)
453      app_path = path;
454    else
455      app_path = kChromeInstallPath;
456
457    // NSWorkspace launches processes as the current console owner,
458    // even when running with euid of 0.
459    return [[NSWorkspace sharedWorkspace] launchApplication:app_path];
460  }
461}
462