1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
|
// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/cocoa/install_from_dmg.h"
#include <ApplicationServices/ApplicationServices.h>
#import <AppKit/AppKit.h>
#include <CoreFoundation/CoreFoundation.h>
#include <CoreServices/CoreServices.h>
#include <IOKit/IOKitLib.h>
#include <string.h>
#include <sys/param.h>
#include <sys/mount.h>
#include "app/l10n_util.h"
#import "app/l10n_util_mac.h"
#include "base/basictypes.h"
#include "base/command_line.h"
#include "base/logging.h"
#import "base/mac_util.h"
#include "base/scoped_nsautorelease_pool.h"
#include "chrome/browser/cocoa/authorization_util.h"
#include "chrome/browser/cocoa/scoped_authorizationref.h"
#import "chrome/browser/cocoa/keystone_glue.h"
#include "grit/chromium_strings.h"
#include "grit/generated_resources.h"
// When C++ exceptions are disabled, the C++ library defines |try| and
// |catch| so as to allow exception-expecting C++ code to build properly when
// language support for exceptions is not present. These macros interfere
// with the use of |@try| and |@catch| in Objective-C files such as this one.
// Undefine these macros here, after everything has been #included, since
// there will be no C++ uses and only Objective-C uses from this point on.
#undef try
#undef catch
namespace {
// Just like ScopedCFTypeRef but for io_object_t and subclasses.
template<typename IOT>
class scoped_ioobject {
public:
typedef IOT element_type;
explicit scoped_ioobject(IOT object = NULL)
: object_(object) {
}
~scoped_ioobject() {
if (object_)
IOObjectRelease(object_);
}
void reset(IOT object = NULL) {
if (object_)
IOObjectRelease(object_);
object_ = object;
}
bool operator==(IOT that) const {
return object_ == that;
}
bool operator!=(IOT that) const {
return object_ != that;
}
operator IOT() const {
return object_;
}
IOT get() const {
return object_;
}
void swap(scoped_ioobject& that) {
IOT temp = that.object_;
that.object_ = object_;
object_ = temp;
}
IOT release() {
IOT temp = object_;
object_ = NULL;
return temp;
}
private:
IOT object_;
DISALLOW_COPY_AND_ASSIGN(scoped_ioobject);
};
// Returns true if |path| is located on a read-only filesystem of a disk
// image. Returns false if not, or in the event of an error.
bool IsPathOnReadOnlyDiskImage(const char path[]) {
struct statfs statfs_buf;
if (statfs(path, &statfs_buf) != 0) {
PLOG(ERROR) << "statfs " << path;
return false;
}
if (!(statfs_buf.f_flags & MNT_RDONLY)) {
// Not on a read-only filesystem.
return false;
}
const char dev_root[] = "/dev/";
const int dev_root_length = arraysize(dev_root) - 1;
if (strncmp(statfs_buf.f_mntfromname, dev_root, dev_root_length) != 0) {
// Not rooted at dev_root, no BSD name to search on.
return false;
}
// BSD names in IOKit don't include dev_root.
const char* bsd_device_name = statfs_buf.f_mntfromname + dev_root_length;
const mach_port_t master_port = kIOMasterPortDefault;
// IOBSDNameMatching gives ownership of match_dict to the caller, but
// IOServiceGetMatchingServices will assume that reference.
CFMutableDictionaryRef match_dict = IOBSDNameMatching(master_port,
0,
bsd_device_name);
if (!match_dict) {
LOG(ERROR) << "IOBSDNameMatching " << bsd_device_name;
return false;
}
io_iterator_t iterator_ref;
kern_return_t kr = IOServiceGetMatchingServices(master_port,
match_dict,
&iterator_ref);
if (kr != KERN_SUCCESS) {
LOG(ERROR) << "IOServiceGetMatchingServices " << bsd_device_name
<< ": kernel error " << kr;
return false;
}
scoped_ioobject<io_iterator_t> iterator(iterator_ref);
iterator_ref = NULL;
// There needs to be exactly one matching service.
scoped_ioobject<io_service_t> filesystem_service(IOIteratorNext(iterator));
if (!filesystem_service) {
LOG(ERROR) << "IOIteratorNext " << bsd_device_name << ": no service";
return false;
}
scoped_ioobject<io_service_t> unexpected_service(IOIteratorNext(iterator));
if (unexpected_service) {
LOG(ERROR) << "IOIteratorNext " << bsd_device_name << ": too many services";
return false;
}
iterator.reset();
const char disk_image_class[] = "IOHDIXController";
// This is highly unlikely. The filesystem service is expected to be of
// class IOMedia. Since the filesystem service's entire ancestor chain
// will be checked, though, check the filesystem service's class itself.
if (IOObjectConformsTo(filesystem_service, disk_image_class)) {
return true;
}
kr = IORegistryEntryCreateIterator(filesystem_service,
kIOServicePlane,
kIORegistryIterateRecursively |
kIORegistryIterateParents,
&iterator_ref);
if (kr != KERN_SUCCESS) {
LOG(ERROR) << "IORegistryEntryCreateIterator " << bsd_device_name
<< ": kernel error " << kr;
return false;
}
iterator.reset(iterator_ref);
iterator_ref = NULL;
// Look at each of the filesystem service's ancestor services, beginning
// with the parent, iterating all the way up to the device tree's root. If
// any ancestor service matches the class used for disk images, the
// filesystem resides on a disk image.
for(scoped_ioobject<io_service_t> ancestor_service(IOIteratorNext(iterator));
ancestor_service;
ancestor_service.reset(IOIteratorNext(iterator))) {
if (IOObjectConformsTo(ancestor_service, disk_image_class)) {
return true;
}
}
// The filesystem does not reside on a disk image.
return false;
}
// Returns true if the application is located on a read-only filesystem of a
// disk image. Returns false if not, or in the event of an error.
bool IsAppRunningFromReadOnlyDiskImage() {
return IsPathOnReadOnlyDiskImage(
[[[NSBundle mainBundle] bundlePath] fileSystemRepresentation]);
}
// Shows a dialog asking the user whether or not to install from the disk
// image. Returns true if the user approves installation.
bool ShouldInstallDialog() {
NSString* title = l10n_util::GetNSStringFWithFixup(
IDS_INSTALL_FROM_DMG_TITLE, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
NSString* prompt = l10n_util::GetNSStringFWithFixup(
IDS_INSTALL_FROM_DMG_PROMPT, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
NSString* yes = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_YES);
NSString* no = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_NO);
NSAlert* alert = [[[NSAlert alloc] init] autorelease];
[alert setAlertStyle:NSInformationalAlertStyle];
[alert setMessageText:title];
[alert setInformativeText:prompt];
[alert addButtonWithTitle:yes];
NSButton* cancel_button = [alert addButtonWithTitle:no];
[cancel_button setKeyEquivalent:@"\e"];
NSInteger result = [alert runModal];
return result == NSAlertFirstButtonReturn;
}
// Potentially shows an authorization dialog to request authentication to
// copy. If application_directory appears to be unwritable, attempts to
// obtain authorization, which may result in the display of the dialog.
// Returns NULL if authorization is not performed because it does not appear
// to be necessary because the user has permission to write to
// application_directory. Returns NULL if authorization fails.
AuthorizationRef MaybeShowAuthorizationDialog(NSString* application_directory) {
NSFileManager* file_manager = [NSFileManager defaultManager];
if ([file_manager isWritableFileAtPath:application_directory]) {
return NULL;
}
NSString* prompt = l10n_util::GetNSStringFWithFixup(
IDS_INSTALL_FROM_DMG_AUTHENTICATION_PROMPT,
l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
return authorization_util::AuthorizationCreateToRunAsRoot(
reinterpret_cast<CFStringRef>(prompt));
}
// Invokes the installer program at installer_path to copy source_path to
// target_path and perform any additional on-disk bookkeeping needed to be
// able to launch target_path properly. If authorization_arg is non-NULL,
// function will assume ownership of it, will invoke the installer with that
// authorization reference, and will attempt Keystone ticket promotion.
bool InstallFromDiskImage(AuthorizationRef authorization_arg,
NSString* installer_path,
NSString* source_path,
NSString* target_path) {
scoped_AuthorizationRef authorization(authorization_arg);
authorization_arg = NULL;
int exit_status;
if (authorization) {
const char* installer_path_c = [installer_path fileSystemRepresentation];
const char* source_path_c = [source_path fileSystemRepresentation];
const char* target_path_c = [target_path fileSystemRepresentation];
const char* arguments[] = {source_path_c, target_path_c, NULL};
OSStatus status = authorization_util::ExecuteWithPrivilegesAndWait(
authorization,
installer_path_c,
kAuthorizationFlagDefaults,
arguments,
NULL, // pipe
&exit_status);
if (status != errAuthorizationSuccess) {
LOG(ERROR) << "AuthorizationExecuteWithPrivileges install: " << status;
return false;
}
} else {
NSArray* arguments = [NSArray arrayWithObjects:source_path,
target_path,
nil];
NSTask* task;
@try {
task = [NSTask launchedTaskWithLaunchPath:installer_path
arguments:arguments];
} @catch(NSException* exception) {
LOG(ERROR) << "+[NSTask launchedTaskWithLaunchPath:arguments:]: "
<< [[exception description] UTF8String];
return false;
}
[task waitUntilExit];
exit_status = [task terminationStatus];
}
if (exit_status != 0) {
LOG(ERROR) << "install.sh: exit status " << exit_status;
return false;
}
if (authorization) {
// As long as an AuthorizationRef is available, promote the Keystone
// ticket. Inform KeystoneGlue of the new path to use.
KeystoneGlue* keystone_glue = [KeystoneGlue defaultKeystoneGlue];
[keystone_glue setAppPath:target_path];
[keystone_glue promoteTicketWithAuthorization:authorization.release()
synchronous:YES];
}
return true;
}
// Launches the application at app_path. The arguments passed to app_path
// will be the same as the arguments used to invoke this process, except any
// arguments beginning with -psn_ will be stripped.
bool LaunchInstalledApp(NSString* app_path) {
const UInt8* app_path_c =
reinterpret_cast<const UInt8*>([app_path fileSystemRepresentation]);
FSRef app_fsref;
OSStatus err = FSPathMakeRef(app_path_c, &app_fsref, NULL);
if (err != noErr) {
LOG(ERROR) << "FSPathMakeRef: " << err;
return false;
}
const std::vector<std::string>& argv =
CommandLine::ForCurrentProcess()->argv();
NSMutableArray* arguments =
[NSMutableArray arrayWithCapacity:argv.size() - 1];
// Start at argv[1]. LSOpenApplication adds its own argv[0] as the path of
// the launched executable.
for (size_t index = 1; index < argv.size(); ++index) {
std::string argument = argv[index];
const char psn_flag[] = "-psn_";
const int psn_flag_length = arraysize(psn_flag) - 1;
if (argument.compare(0, psn_flag_length, psn_flag) != 0) {
// Strip any -psn_ arguments, as they apply to a specific process.
[arguments addObject:[NSString stringWithUTF8String:argument.c_str()]];
}
}
struct LSApplicationParameters parameters = {0};
parameters.flags = kLSLaunchDefaults;
parameters.application = &app_fsref;
parameters.argv = reinterpret_cast<CFArrayRef>(arguments);
err = LSOpenApplication(¶meters, NULL);
if (err != noErr) {
LOG(ERROR) << "LSOpenApplication: " << err;
return false;
}
return true;
}
void ShowErrorDialog() {
NSString* title = l10n_util::GetNSStringWithFixup(
IDS_INSTALL_FROM_DMG_ERROR_TITLE);
NSString* error = l10n_util::GetNSStringFWithFixup(
IDS_INSTALL_FROM_DMG_ERROR, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
NSString* ok = l10n_util::GetNSStringWithFixup(IDS_OK);
NSAlert* alert = [[[NSAlert alloc] init] autorelease];
[alert setAlertStyle:NSWarningAlertStyle];
[alert setMessageText:title];
[alert setInformativeText:error];
[alert addButtonWithTitle:ok];
[alert runModal];
}
} // namespace
bool MaybeInstallFromDiskImage() {
base::ScopedNSAutoreleasePool autorelease_pool;
if (!IsAppRunningFromReadOnlyDiskImage()) {
return false;
}
NSArray* application_directories =
NSSearchPathForDirectoriesInDomains(NSApplicationDirectory,
NSLocalDomainMask,
YES);
if ([application_directories count] == 0) {
LOG(ERROR) << "NSSearchPathForDirectoriesInDomains: "
<< "no local application directories";
return false;
}
NSString* application_directory = [application_directories objectAtIndex:0];
NSFileManager* file_manager = [NSFileManager defaultManager];
BOOL is_directory;
if (![file_manager fileExistsAtPath:application_directory
isDirectory:&is_directory] ||
!is_directory) {
LOG(INFO) << "No application directory at "
<< [application_directory UTF8String];
return false;
}
NSString* source_path = [[NSBundle mainBundle] bundlePath];
NSString* application_name = [source_path lastPathComponent];
NSString* target_path =
[application_directory stringByAppendingPathComponent:application_name];
if ([file_manager fileExistsAtPath:target_path]) {
LOG(INFO) << "Something already exists at " << [target_path UTF8String];
return false;
}
NSString* installer_path =
[mac_util::MainAppBundle() pathForResource:@"install" ofType:@"sh"];
if (!installer_path) {
LOG(INFO) << "Could not locate install.sh";
return false;
}
if (!ShouldInstallDialog()) {
return false;
}
scoped_AuthorizationRef authorization(
MaybeShowAuthorizationDialog(application_directory));
// authorization will be NULL if it's deemed unnecessary or if
// authentication fails. In either case, try to install without privilege
// escalation.
if (!InstallFromDiskImage(authorization.release(),
installer_path,
source_path,
target_path) ||
!LaunchInstalledApp(target_path)) {
ShowErrorDialog();
return false;
}
return true;
}
|