forked from SelfControlApp/selfcontrol
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathHelperMain.m
executable file
·467 lines (402 loc) · 23.2 KB
/
HelperMain.m
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
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
//
// helpermain.m
// SelfControl
//
// Created by Charlie Stigler on 2/4/09.
// Copyright 2009 Eyebeam.
// This file is part of SelfControl.
//
// SelfControl is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#import "HelperMain.h"
int main(int argc, char* argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
if(geteuid()) {
NSLog(@"ERROR: SelfControl's helper tool must be run as root.");
printStatus(-201);
exit(EX_NOPERM);
}
setuid(0);
if(argc < 3 || argv[1] == NULL || argv[2] == NULL) {
NSLog(@"ERROR: Not enough arguments");
printStatus(-202);
exit(EX_USAGE);
}
NSString* modeString = [NSString stringWithUTF8String: argv[2]];
// We'll need the controlling UID to know what defaults database to search
// It's a signed long long int to avoid integer overflow with extra-long UIDs
signed long long int controllingUID = [[NSString stringWithUTF8String: argv[1]] longLongValue];
// First things first, let's set up our backup system -- give scheckup the SUID bit
NSDictionary* checkupAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt: 0], NSFileOwnerAccountID,
[NSNumber numberWithLongLong: controllingUID], NSFileGroupOwnerAccountID,
// 2541 (decimal) = 4755 (octal) = rwsr-xr-x
[NSNumber numberWithUnsignedLong: 2541], NSFilePosixPermissions,
nil];
NSString* scheckupPath = [[NSString stringWithUTF8String: argv[0]] stringByDeletingLastPathComponent];
scheckupPath = [scheckupPath stringByAppendingPathComponent: @"scheckup"];
if(![[NSFileManager defaultManager] changeFileAttributes: checkupAttributes atPath: scheckupPath]) {
// This is, uh, suuuuper messy. But pretty much it just checks whether the file already has the right attributes.
// If it does already, we don't bother to warn the user about being unable to change attributes.
NSDictionary* oldAttributes = [[NSFileManager defaultManager] fileAttributesAtPath: scheckupPath traverseLink: YES];
NSString* octalPerms = [NSString stringWithFormat: @"%o", [oldAttributes objectForKey: NSFilePosixPermissions]];
if(!([[oldAttributes objectForKey: NSFileOwnerAccountID] longLongValue] == 0 && [octalPerms characterAtIndex: 0] == '4' && [octalPerms characterAtIndex: 3] != 0 && [octalPerms characterAtIndex: 3] != 4))
NSLog(@"WARNING: Could not change file attributes on scheckup. Backup block-removal system may not work.");
}
// For proper security, we need to make sure that SelfControl files are owned
// by root and only writable by root. We'll define this here so we can use it
// throughout the main function.
NSDictionary* fileAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithUnsignedLong: 0], NSFileOwnerAccountID,
[NSNumber numberWithUnsignedLong: 0], NSFileGroupOwnerAccountID,
// 493 (decimal) = 755 (octal) = rwxr-xr-x
[NSNumber numberWithUnsignedLong: 493], NSFilePosixPermissions,
nil];
// This is where we get going with the lockfile system, saving a "lock" in /etc/SelfControl.lock
// to make a more reliable block detection system. For most of the program,
// the pattern exhibited here will be used: we attempt to use the lock file's
// contents, and revert to the user's defaults if the lock file has unreasonable
// contents.
NSDictionary* curLockDict = [NSDictionary dictionaryWithContentsOfFile: SelfControlLockFilePath];
if(!([[curLockDict objectForKey: @"HostBlacklist"] count] <= 0))
domainList = [curLockDict objectForKey: @"HostBlacklist"];
// You'll see this pattern several times in this file. The two resets and
// set of euid to the controlling UID are necessary in order to successfully
// return the NSUserDefaults object for the controlling user. Also note that
// the defaults object cannot be simply kept in a variable and repeatedly
// referred to, the resets will invalidate it. For now, we're just re-registering
// the default settings, since they don't carry over from the main application.
// TODO: Factor this code out into a functionf
[NSUserDefaults resetStandardUserDefaults];
seteuid(controllingUID);
defaults = [NSUserDefaults standardUserDefaults];
[defaults addSuiteNamed:@"org.eyebeam.SelfControl"];
NSDictionary* appDefaults = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt: 0], @"BlockDuration",
[NSDate distantFuture], @"BlockStartedDate",
[NSArray array], @"HostBlacklist",
[NSNumber numberWithBool: YES], @"EvaluateCommonSubdomains",
[NSNumber numberWithBool: YES], @"HighlightInvalidHosts",
[NSNumber numberWithBool: YES], @"VerifyInternetConnection",
[NSNumber numberWithBool: NO], @"TimerWindowFloats",
[NSNumber numberWithBool: NO], @"BlockSoundShouldPlay",
[NSNumber numberWithInt: 5], @"BlockSound",
[NSNumber numberWithBool: YES], @"ClearCaches",
[NSNumber numberWithBool: NO], @"BlockAsWhitelist",
[NSNumber numberWithBool: YES], @"BadgeApplicationIcon",
[NSNumber numberWithBool: YES], @"AllowLocalNetworks",
[NSNumber numberWithInt: 1440], @"MaxBlockLength",
[NSNumber numberWithInt: 15], @"BlockLengthInterval",
[NSNumber numberWithBool: NO], @"WhitelistAlertSuppress",
nil];
[defaults registerDefaults:appDefaults];
if(!domainList) {
domainList = [defaults objectForKey:@"HostBlacklist"];
if([domainList count] <= 0) {
NSLog(@"ERROR: Not enough block information.");
printStatus(-203);
exit(EX_CONFIG);
}
}
[defaults synchronize];
[NSUserDefaults resetStandardUserDefaults];
seteuid(0);
if([modeString isEqual: @"--install"]) {
NSFileManager* fileManager = [NSFileManager defaultManager];
// Initialize writeErr to nil so calling messages on it later don't cause
// crashes (it doesn't make sense we need to do this, but whatever).
NSError* writeErr = nil;
NSString* plistFormatPath = [[NSBundle mainBundle] pathForResource:@"org.eyebeam.SelfControl"
ofType:@"plist"];
NSString* plistFormatString = [NSString stringWithContentsOfFile: plistFormatPath];
NSString* plistString = [NSString stringWithFormat:
plistFormatString,
controllingUID];
[plistString writeToFile: @"/Library/LaunchDaemons/org.eyebeam.SelfControl.plist"
atomically: YES
encoding: NSUTF8StringEncoding
error: &writeErr];
if([writeErr code]) {
NSLog(@"ERROR: Could not write launchd plist file to LaunchDaemons folder.");
printStatus(-204);
exit(EX_IOERR);
}
if(![fileManager fileExistsAtPath: @"/Library/PrivilegedHelperTools"]) {
if(![fileManager createDirectoryAtPath: @"/Library/PrivilegedHelperTools"
attributes: fileAttributes]) {
NSLog(@"ERROR: Could not create PrivilegedHelperTools directory.");
printStatus(-205);
exit(EX_IOERR);
}
} else {
if(![fileManager changeFileAttributes: fileAttributes
atPath: @"/Library/PrivilegedHelperTools"]) {
NSLog(@"ERROR: Could not change permissions on PrivilegedHelperTools directory.");
printStatus(-206);
exit(EX_IOERR);
}
}
// We should delete the old file if it exists and copy the new binary in,
// because it might be a new version of the helper if we've upgraded SelfControl
if([fileManager fileExistsAtPath: @"/Library/PrivilegedHelperTools/org.eyebeam.SelfControl"]) {
if(![fileManager removeFileAtPath: @"/Library/PrivilegedHelperTools/org.eyebeam.SelfControl" handler: nil]) {
NSLog(@"ERROR: Could not delete old helper binary.");
printStatus(-207);
exit(EX_IOERR);
}
}
if(![fileManager copyPath: [NSString stringWithCString: argv[0]]
toPath: @"/Library/PrivilegedHelperTools/org.eyebeam.SelfControl"
handler: NULL]) {
NSLog(@"ERROR: Could not copy SelfControl's helper binary to PrivilegedHelperTools directory.");
printStatus(-208);
exit(EX_IOERR);
}
if([fileManager fileExistsAtPath: @"/Library/PrivilegedHelperTools/scheckup"]) {
if(![fileManager removeFileAtPath: @"/Library/PrivilegedHelperTools/scheckup" handler: nil]) {
NSLog(@"WARNING: Could not delete old scheckup binary.");
}
}
NSString* scheckupPath = [[NSString stringWithUTF8String: argv[0]] stringByDeletingLastPathComponent];
scheckupPath = [scheckupPath stringByAppendingPathComponent: @"scheckup"];
if(![fileManager copyPath: scheckupPath
toPath: @"/Library/PrivilegedHelperTools/scheckup"
handler: NULL]) {
NSLog(@"WARNING: Could not copy scheckup to PrivilegedHelperTools directory.");
}
// Let's set up our backup system -- give scheckup the SUID bit
NSDictionary* checkupAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt: 0], NSFileOwnerAccountID,
[NSNumber numberWithLongLong: controllingUID], NSFileGroupOwnerAccountID,
// 2541 (decimal) = 4755 (octal) = rwsr-xr-x
[NSNumber numberWithUnsignedLong: 2541], NSFilePosixPermissions,
nil];
if(![[NSFileManager defaultManager] changeFileAttributes: checkupAttributes atPath: scheckupPath]) {
NSLog(@"WARNING: Could not change file attributes on scheckup. Backup block-removal system may not work.");
}
if(![fileManager changeFileAttributes: fileAttributes
atPath: @"/Library/PrivilegedHelperTools/org.eyebeam.SelfControl"]) {
NSLog(@"ERROR: Could not change permissions on SelfControl's helper binary.");
printStatus(-209);
exit(EX_IOERR);
}
[NSUserDefaults resetStandardUserDefaults];
seteuid(controllingUID);
defaults = [NSUserDefaults standardUserDefaults];
[defaults addSuiteNamed:@"org.eyebeam.SelfControl"];
NSDate* d = [NSDate date];
[defaults setObject: d forKey: @"BlockStartedDate"];
// In this case it doesn't make any sense to use an existing lock file (in
// fact, one shouldn't exist), so we fail if the defaults system has unreasonable
// settings.
NSDictionary* lockDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
[defaults objectForKey: @"HostBlacklist"], @"HostBlacklist",
[defaults objectForKey: @"BlockDuration"], @"BlockDuration",
[defaults objectForKey: @"BlockStartedDate"], @"BlockStartedDate",
[defaults objectForKey: @"BlockAsWhitelist"], @"BlockAsWhitelist",
nil];
if([[lockDictionary objectForKey: @"HostBlacklist"] count] <= 0 || [[lockDictionary objectForKey: @"BlockDuration"] intValue] < 1
|| [lockDictionary objectForKey: @"BlockStartedDate"] == nil
|| [[lockDictionary objectForKey: @"BlockStartedDate"] isEqualToDate: [NSDate distantFuture]]) {
NSLog(@"ERROR: Not enough block information.");
printStatus(-210);
exit(EX_CONFIG);
}
[defaults synchronize];
[NSUserDefaults resetStandardUserDefaults];
seteuid(0);
// If perchance another lock is in existence already (which would be weird)
// we try to remove a block and continue as normal. This should definitely not be
// happening though.
if([fileManager fileExistsAtPath: SelfControlLockFilePath]) {
NSLog(@"ERROR: Lock already established. Attempting to stop block.");
removeBlock(controllingUID);
printStatus(-219);
exit(EX_CONFIG);
}
// And write out our lock...
if(![lockDictionary writeToFile: SelfControlLockFilePath atomically: YES]) {
NSLog(@"ERROR: Could not write lock file.");
printStatus(-216);
exit(EX_IOERR);
}
// Make sure the privileges are correct on our lock file
[fileManager changeFileAttributes: fileAttributes atPath: SelfControlLockFilePath];
addRulesToFirewall(controllingUID);
int result = [LaunchctlHelper loadLaunchdJobWithPlistAt: @"/Library/LaunchDaemons/org.eyebeam.SelfControl.plist"];
[[NSDistributedNotificationCenter defaultCenter] postNotificationName: @"SCConfigurationChangedNotification"
object: nil];
// Clear web browser caches if the user has the correct preference set, so
// that blocked pages are not loaded from a cache.
clearCachesIfRequested(controllingUID);
if(result) {
printStatus(-211);
exit(EX_UNAVAILABLE);
NSLog(@"WARNING: Launch daemon load returned a failure status code.");
} else NSLog(@"INFO: Block successfully added.");
}
if([modeString isEqual: @"--remove"]) {
// So you think you can rid yourself of SelfControl just like that?
NSLog(@"INFO: Nice try.");
printStatus(-212);
exit(EX_UNAVAILABLE);
} else if([modeString isEqual: @"--refresh"]) {
// Check what the current block is (based on the lock file) because if possible
// we want to keep most of its information.
NSDictionary* curDictionary = [NSDictionary dictionaryWithContentsOfFile: SelfControlLockFilePath];
NSDictionary* newLockDictionary;
[NSUserDefaults resetStandardUserDefaults];
seteuid(controllingUID);
defaults = [NSUserDefaults standardUserDefaults];
[defaults addSuiteNamed:@"org.eyebeam.SelfControl"];
if(curDictionary == nil) {
// If there is no block file we just use all information from defaults
if([[defaults objectForKey: @"BlockStartedDate"] isEqualToDate: [NSDate distantFuture]]) {
// But if the block is already over (which is going to happen if the user
// starts authentication for the host add and then the block expires before
// they authenticate), we shouldn't do anything at all.
NSLog(@"ERROR: Refreshing domain blacklist, but no block is currently ongoing.");
printStatus(-213);
exit(EX_SOFTWARE);
}
NSLog(@"WARNING: Refreshing domain blacklist, but no block is currently ongoing. Relaunching block.");
newLockDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
[defaults objectForKey: @"HostBlacklist"], @"HostBlacklist",
[defaults objectForKey: @"BlockDuration"], @"BlockDuration",
[defaults objectForKey: @"BlockStartedDate"], @"BlockStartedDate",
[defaults objectForKey: @"BlockAsWhitelist"], @"BlockAsWhitelist",
nil];
// And later on we'll be reloading the launchd daemon if curDictionary
// was nil, just in case. Watch for it.
} else {
// If there is an existing block file we can save most of it from the old file
newLockDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
[defaults objectForKey: @"HostBlacklist"], @"HostBlacklist",
[curDictionary objectForKey: @"BlockDuration"], @"BlockDuration",
[curDictionary objectForKey: @"BlockStartedDate"], @"BlockStartedDate",
[curDictionary objectForKey: @"BlockAsWhitelist"], @"BlockAsWhitelist",
nil];
}
[defaults synchronize];
[NSUserDefaults resetStandardUserDefaults];
seteuid(0);
if([[newLockDictionary objectForKey: @"HostBlacklist"] count] <= 0 || [[newLockDictionary objectForKey: @"BlockDuration"] intValue] < 1
|| [newLockDictionary objectForKey: @"BlockStartedDate"] == nil
|| [[newLockDictionary objectForKey: @"BlockStartedDate"] isEqualToDate: [NSDate distantFuture]]) {
NSLog(@"ERROR: Not enough block information.");
printStatus(-214);
exit(EX_CONFIG);
}
if(![newLockDictionary writeToFile: SelfControlLockFilePath atomically: YES]) {
NSLog(@"ERROR: Could not write lock file.");
printStatus(-217);
exit(EX_IOERR);
}
// Make sure the privileges are correct on our lock file
[[NSFileManager defaultManager] changeFileAttributes: fileAttributes atPath: SelfControlLockFilePath];
domainList = [newLockDictionary objectForKey: @"HostBlacklist"];
// Add and remove the rules to put in any new ones
removeRulesFromFirewall(controllingUID);
addRulesToFirewall(controllingUID);
if(curDictionary == nil) {
// aka if there was no lock file, and it's possible we're reloading the block,
// and we're sure the block is still on (that's checked earlier).
[LaunchctlHelper loadLaunchdJobWithPlistAt: @"/Library/LaunchDaemons/org.eyebeam.SelfControl.plist"];
}
// Clear web browser caches if the user has the correct preference set. We
// need to do this again even if it's only a refresh because there might be
// caches for the new host blocked.
clearCachesIfRequested(controllingUID);
} else if([modeString isEqual: @"--checkup"]) {
NSDictionary* curDictionary = [NSDictionary dictionaryWithContentsOfFile: SelfControlLockFilePath];
NSDate* blockStartedDate = [curDictionary objectForKey: @"BlockStartedDate"];
NSTimeInterval blockDuration = [[curDictionary objectForKey: @"BlockDuration"] intValue];
if(blockStartedDate == nil || [[NSDate distantFuture] isEqualToDate: blockStartedDate] || blockDuration < 1) {
// The lock file seems to be broken. Read from defaults, then write out a
// new lock file while we're at it.
[NSUserDefaults resetStandardUserDefaults];
seteuid(controllingUID);
defaults = [NSUserDefaults standardUserDefaults];
[defaults addSuiteNamed:@"org.eyebeam.SelfControl"];
blockStartedDate = [defaults objectForKey: @"BlockStartedDate"];
blockDuration = [[defaults objectForKey: @"BlockDuration"] intValue];
[defaults synchronize];
[NSUserDefaults resetStandardUserDefaults];
seteuid(0);
if(blockStartedDate == nil || blockDuration < 1) {
// Defaults is broken too! Let's get out of here!
NSLog(@"ERROR: Checkup ran but no block found. Attempting to remove block.");
// get rid of this block
removeBlock(controllingUID);
printStatus(-215);
exit(EX_SOFTWARE);
}
}
// convert to seconds
blockDuration *= 60;
NSTimeInterval timeSinceStarted = [[NSDate date] timeIntervalSinceDate: blockStartedDate];
// Note there are a few extra possible conditions on this if statement, this
// makes it more likely that an improperly applied block might come right
// off.
if( blockStartedDate == nil || blockDuration < 1 || [[NSDate distantFuture] isEqualToDate: blockStartedDate] || timeSinceStarted >= blockDuration) {
NSLog(@"INFO: Checkup ran, block expired, removing block.");
removeBlock(controllingUID);
// Execution should never reach this point. Launchd unloading the job in removeBlock()
// should have killed this process.
printStatus(-216);
exit(EX_SOFTWARE);
} else {
// The block is still on. Check if anybody removed our rules, and if so
// re-add them. Also make sure the user's defaults are set to the correct
// settings just in case.
IPFirewall* firewall = [[IPFirewall alloc] init];
if(![firewall containsSelfControlBlockSet]) {
// The firewall is missing at least the block header. Let's clear everything
// before we re-add to make sure everything goes smoothly.
HostFileBlocker* hostFileBlocker = [[[HostFileBlocker alloc] init] autorelease];
[hostFileBlocker removeSelfControlBlock];
BOOL success = [hostFileBlocker writeNewFileContents];
// Revert the host file blocker's file contents to disk so we can check
// whether or not it still contains the block (aka we messed up).
[hostFileBlocker revertFileContentsToDisk];
[firewall clearSelfControlBlockRuleSet];
if(!success || [hostFileBlocker containsSelfControlBlock]) {
NSLog(@"WARNING: Error removing host file block. Attempting to restore backup.");
if([hostFileBlocker restoreBackupHostsFile])
NSLog(@"INFO: Host file backup restored.");
else
NSLog(@"ERROR: Host file backup could not be restored. This may result in a permanent block.");
}
// Get rid of the backup file since we're about to make a new one.
[hostFileBlocker deleteBackupHostsFile];
// Perform the re-add of the rules
addRulesToFirewall(controllingUID);
NSLog(@"INFO: Checkup ran, readded block rules.");
} else NSLog(@"INFO: Checkup ran, no action needed.");
// Why would we make sure the defaults are correct even if we can get the
// info from the lock file? In case one goes down, we want to make sure
// we always have a backup.
[NSUserDefaults resetStandardUserDefaults];
seteuid(controllingUID);
defaults = [NSUserDefaults standardUserDefaults];
[defaults addSuiteNamed:@"org.eyebeam.SelfControl"];
[defaults setObject: blockStartedDate forKey: @"BlockStartedDate"];
[defaults setObject: [NSNumber numberWithInt: (blockDuration / 60)] forKey: @"BlockDuration"];
[defaults setObject: domainList forKey: @"HostBlacklist"];
[defaults synchronize];
[NSUserDefaults resetStandardUserDefaults];
seteuid(0);
}
}
[pool drain];
printStatus(0);
exit(EXIT_SUCCESS);
}