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