Mon, 16 Jan 2012 23:08:14 +0100
Inconclusively complete possibly missing fields. This change introduces
inconsistencies difficult to correct given incomplete documentation of
IPKG and OPKG packaging standards.
1 /*
2 * Copyright (c) 2004 Angelo Laub
3 * Contributions by Dirk Theisen <dirk@objectpark.org>,
4 * Jens Ohlig,
5 * Waldemar Brodkorb
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License version 2
9 * as published by the Free Software Foundation.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program (see the file COPYING included with this
18 * distribution); if not, write to the Free Software Foundation, Inc.,
19 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 */
23 #import "MenuController.h"
24 #import "NSApplication+NetworkNotifications.h"
25 #import "helper.h"
28 #define NSAppKitVersionNumber10_0 577
29 #define NSAppKitVersionNumber10_1 620
30 #define NSAppKitVersionNumber10_2 663
31 #define NSAppKitVersionNumber10_3 743
35 BOOL systemIsTigerOrNewer()
36 {
37 return (floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_3) ;
38 }
40 @interface NSStatusBar (NSStatusBar_Private)
41 - (id)_statusItemWithLength:(float)l withPriority:(int)p;
42 @end
45 @implementation MenuController
47 - (void) createStatusItem
48 {
49 NSStatusBar *bar = [NSStatusBar systemStatusBar];
50 int priority = INT32_MAX;
51 if (systemIsTigerOrNewer()) {
52 priority = MIN(priority, 2147483646); // found by experimenting - dirk
53 }
55 if (!theItem) {
56 theItem = [[bar statusItemWithLength: NSVariableStatusItemLength] retain];
57 //theItem = [[bar _statusItemWithLength: NSVariableStatusItemLength withPriority: 0] retain];
58 }
59 // Dirk: For Tiger and up, re-insert item to place it correctly.
60 if ([bar respondsToSelector: @selector(_insertStatusItem:withPriority:)]) {
61 [bar removeStatusItem: theItem];
62 [bar _insertStatusItem: theItem withPriority: priority];
63 }
64 }
66 -(id) init
67 {
68 if (self = [super init]) {
69 [self dmgCheck];
71 errorImage = [[NSImage imageNamed: @"error.tif"] retain];
72 mainImage = [[NSImage imageNamed: @"00_closed.tif"] retain];
73 connectedImage = [[NSImage imageNamed: @"connected.png"] retain];
76 transitionalImage1 = [[NSImage imageNamed: @"01.tif"] retain];
77 transitionalImage2 = [[NSImage imageNamed: @"02.tif"] retain];
78 transitionalImage3 = [[NSImage imageNamed: @"03.tif"] retain];
79 [NSApp setDelegate:self];
81 myVPNConnectionDictionary = [[NSMutableDictionary alloc] init];
82 myVPNConnectionArray = [[[NSMutableArray alloc] init] retain];
83 userDefaults = [[NSMutableDictionary alloc] init];
85 connectionArray = [[[NSMutableArray alloc] init] retain];
86 appDefaults = [NSUserDefaults standardUserDefaults];
87 [appDefaults registerDefaults:userDefaults];
90 detailsItem = [[NSMenuItem alloc] init];
91 [detailsItem setTitle: @"Details..."];
92 [detailsItem setTarget: self];
93 [detailsItem setAction: @selector(openLogWindow:)];
95 quitItem = [[NSMenuItem alloc] init];
96 [quitItem setTitle: @"Quit"];
97 [quitItem setTarget: self];
98 [quitItem setAction: @selector(quit:)];
100 [self createStatusItem];
102 [self updateMenu];
103 [self setState: @"EXITING"]; // synonym for "Disconnected"
105 [[NSNotificationCenter defaultCenter] addObserver: self
106 selector: @selector(logNeedsScrolling:)
107 name: @"LogDidChange"
108 object: nil];
110 // In case the systemUIServer restarts, we observed this notification.
111 // We use it to prevent to end up with a statusItem right of Spotlight:
112 [[NSDistributedNotificationCenter defaultCenter] addObserver: self
113 selector: @selector(menuExtrasWereAdded:)
114 name: @"com.apple.menuextra.added"
115 object: nil];
116 [[[NSWorkspace sharedWorkspace] notificationCenter] addObserver: self
117 selector: @selector(willGoToSleep)
118 name: @"NSWorkspaceWillSleepNotification"
119 object:nil];
121 [[[NSWorkspace sharedWorkspace] notificationCenter] addObserver: self
122 selector: @selector(wokeUpFromSleep)
123 name: @"NSWorkspaceDidWakeNotification"
124 object:nil];
126 NSString* vpnDirectory = [NSHomeDirectory() stringByAppendingPathComponent:@"Library/openvpn/"];
128 UKKQueue* myQueue = [UKKQueue sharedQueue];
129 [myQueue addPathToQueue: vpnDirectory];
130 [myQueue setDelegate: self];
131 [myQueue setAlwaysNotify: YES];
133 [NSThread detachNewThreadSelector:@selector(moveAllWindowsToForegroundThread) toTarget:self withObject:nil];
135 updater = [[SUUpdater alloc] init];
137 }
138 return self;
139 }
141 -(void)moveAllWindowsToForegroundThread {
142 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
143 sleep(3);
144 [self moveAllWindowsToForeground];
145 // [NSTimer scheduledTimerWithTimeInterval: 1.0 target: self selector: @selector(moveAllWindowsToForeground) userInfo: nil repeats: YES];
146 [pool release];
148 }
150 - (void) menuExtrasWereAdded: (NSNotification*) n
151 {
152 [self createStatusItem];
153 }
157 - (IBAction) quit: (id) sender
158 {
159 // Remove us from the login items if terminates manually...
160 [NSApp setAutoLaunchOnLogin: NO];
161 [NSApp terminate: sender];
162 }
166 - (void) awakeFromNib
167 {
168 [self createDefaultConfig];
169 [self initialiseAnim];
170 }
172 - (void) initialiseAnim
173 {
174 NSAnimationProgress progMarks[] = {
175 1.0/8.0, 2.0/8.0, 3.0/8.0, 4.0/8.0, 5.0/8.0, 6.0/8.0, 7.0/8.0, 8.0/8.0
176 };
178 int i, count = 8;
179 // theAnim is an NSAnimation instance variable
180 theAnim = [[NSAnimation alloc] initWithDuration:2.0
181 animationCurve:NSAnimationLinear];
182 [theAnim setFrameRate:8.0];
183 [theAnim setDelegate:self];
185 for (i=0; i<count; i++)
186 [theAnim addProgressMark:progMarks[i]];
188 [theAnim setAnimationBlockingMode: NSAnimationNonblocking];
189 }
191 -(void) updateMenu
192 {
193 [theItem setHighlightMode:YES];
194 [theItem setMenu:nil];
195 [myVPNMenu dealloc]; myVPNMenu = nil;
196 [[myVPNConnectionDictionary allValues] makeObjectsPerformSelector:@selector(disconnect:) withObject:self];
197 [myVPNConnectionDictionary removeAllObjects];
199 myVPNMenu = [[NSMenu alloc] init];
200 [myVPNMenu setDelegate:self];
202 [theItem setMenu: myVPNMenu];
204 statusMenuItem = [[NSMenuItem alloc] init];
205 [myVPNMenu addItem:statusMenuItem];
206 [myVPNMenu addItem:[NSMenuItem separatorItem]];
208 [myConfigArray release];
209 myConfigArray = [[[self getConfigs] sortedArrayUsingSelector:@selector(compare:)] retain];
210 [myConfigModDatesArray release];
211 myConfigModDatesArray = [[self getModDates:myConfigArray] retain];
213 NSEnumerator *m = [myConfigArray objectEnumerator];
214 NSString *configString;
215 int i = 2; // we start at MenuItem #2
217 while (configString = [m nextObject])
218 {
219 NSMenuItem *connectionItem = [[[NSMenuItem alloc] init] autorelease];
221 // configure connection object:
222 VPNConnection* myConnection = [[VPNConnection alloc] initWithConfig: configString]; // initialize VPN Connection with config
223 [myConnection setState:@"EXITING"];
224 [myConnection setDelegate:self];
226 // handle autoconnection:
227 NSString *autoConnectKey = [[myConnection configName] stringByAppendingString: @"autoConnect"];
228 if([[NSUserDefaults standardUserDefaults] boolForKey:autoConnectKey])
229 {
230 if(![myConnection isConnected]) [myConnection connect:self];
231 }
233 [myVPNConnectionDictionary setObject: myConnection forKey:configString];
235 // Note: The item's title will be set on demand in -validateMenuItem
236 [connectionItem setTarget:myConnection];
237 [connectionItem setAction:@selector(toggle:)];
239 [myVPNMenu insertItem:connectionItem atIndex:i];
240 i++;
241 }
243 [myVPNMenu addItem: [NSMenuItem separatorItem]];
244 [myVPNMenu addItem: detailsItem];
245 [myVPNMenu addItem: quitItem];
247 // Localize all menu items:
248 NSMenuItem *item;
249 NSEnumerator *e = [[myVPNMenu itemArray] objectEnumerator];
251 while (item = [e nextObject])
252 {
253 [item setTitle:NSLocalizedString([item title], nil)];
254 }
255 }
257 - (void)activateStatusMenu
258 {
259 //[theItem retain];
260 [self updateUI];
262 // If any config files were changed/added/deleted, update the menu
263 // We don't do it UNLESS files were changed/added/deleted because all connections are reset when the menu is updated.
264 // activateStatusMenu is called whenever anything changes in the config directory, even the file-accessed date,
265 // so a backup of the directory, for example, would cause disconnects if we always updated the menu.
266 NSArray * curConfigsArray = [[self getConfigs] sortedArrayUsingSelector:@selector(compare:)];
267 NSArray * curModDatesArray = [self getModDates:curConfigsArray];
269 if ( ! ( [myConfigArray isEqualToArray:curConfigsArray]
270 && [myConfigModDatesArray isEqualToArray:curModDatesArray] ) ) {
271 NSLog(@"One or more configuration files were changed, added, or deleted. All connections will be closed.\n");
272 [self updateMenu];
273 }
274 }
276 - (void)connectionStateDidChange:(id)connection
277 {
278 [self updateTabLabels];
279 if (connection == [self selectedConnection])
280 {
281 [self validateLogButtons];
282 }
283 }
285 -(NSArray *)getConfigs {
286 int i = 0;
287 NSMutableArray *array = [[[NSMutableArray alloc] init] autorelease];
288 NSString *file;
289 NSString *confDir = [NSHomeDirectory() stringByAppendingPathComponent: @"/Library/openvpn"];
290 NSDirectoryEnumerator *dirEnum = [[NSFileManager defaultManager] enumeratorAtPath: confDir];
291 while (file = [dirEnum nextObject]) {
292 if ([[file pathExtension] isEqualToString: @"conf"] || [[file pathExtension] isEqualToString: @"ovpn"]) {
293 [array insertObject:file atIndex:i];
294 //if(NSDebugEnabled) NSLog(@"Object: %@ atIndex: %d\n",file,i);
295 i++;
296 }
297 }
298 return array;
299 }
301 // Returns an array of modification date strings
302 // Each entry in the array is the modification date of the file in the corresponding entry in fileArray
303 -(NSArray *)getModDates:(NSArray *)fileArray {
304 int i;
305 NSMutableArray *array = [[[NSMutableArray alloc] init] autorelease];
306 NSString *file;
307 NSString *cfgDirSlash = [NSHomeDirectory() stringByAppendingString: @"/Library/openvpn/"];
308 NSString *filePath;
309 NSDate *modDate;
310 NSString *modDateS;
311 NSFileManager *fileManager = [NSFileManager defaultManager];
312 for (i=0; i<[fileArray count]; i++) {
313 file = [fileArray objectAtIndex:i];
314 filePath = [cfgDirSlash stringByAppendingString:file];
315 modDate = [[fileManager fileAttributesAtPath:filePath traverseLink:YES] fileModificationDate];
316 if (modDate == nil) {
317 modDateS = @"";
318 } else if ( (modDateS = [modDate description]) == nil ) {
319 modDateS = @"";
320 }
321 [array insertObject:modDateS atIndex:i];
322 }
323 return array;
324 }
326 - (IBAction)validateLogButtons
327 {
328 //NSLog(@"validating log buttons");
329 VPNConnection* connection = [self selectedConnection];
330 [connectButton setEnabled:[connection isDisconnected]];
331 [disconnectButton setEnabled:(![connection isDisconnected])];
332 [[NSUserDefaults standardUserDefaults] synchronize];
333 NSString *autoConnectKey = [[connection configName] stringByAppendingString:@"autoConnect"];
334 if([[NSUserDefaults standardUserDefaults] boolForKey:autoConnectKey]) {
335 [autoLaunchCheckbox setState:NSOnState];
336 } else {
337 [autoLaunchCheckbox setState:NSOffState];
338 }
340 BOOL lol = useDNSStatus(connection);
341 if(lol) {
342 [useNameserverCheckbox setState:NSOnState];
343 } else {
344 [useNameserverCheckbox setState:NSOffState];
345 }
346 }
348 -(void)updateTabLabels
349 {
350 NSArray *keyArray = [[myVPNConnectionDictionary allKeys] sortedArrayUsingSelector: @selector(compare:)];
351 NSArray *myConnectionArray = [myVPNConnectionDictionary objectsForKeys:keyArray notFoundMarker:[NSNull null]];
352 NSEnumerator *connectionEnumerator = [myConnectionArray objectEnumerator];
353 VPNConnection *myConnection;
355 // Get preferences for showing duration times
356 BOOL showAllDurations = FALSE;
357 BOOL showConnectedDurations = TRUE;
358 id tmp = [[NSUserDefaults standardUserDefaults] objectForKey:@"showAllDurations"];
359 if(tmp != nil) {
360 showAllDurations = [[NSUserDefaults standardUserDefaults] boolForKey:@"showAllDurations"];
361 }
362 tmp = [[NSUserDefaults standardUserDefaults] objectForKey:@"showConnectedDurations"];
363 if(tmp != nil) {
364 showConnectedDurations = [[NSUserDefaults standardUserDefaults] boolForKey:@"showConnectedDurations"];
365 }
367 int i = 0;
368 while(myConnection = [connectionEnumerator nextObject]) {
369 //NSLog(@"configName: %@\nconnectionState: %@\n",[myConnection configName],[myConnection state]);
370 NSString * cState = [myConnection state];
371 NSString * cTimeS = @"";
373 // Get connection duration if preferences say to
374 if ( showAllDurations || ( showConnectedDurations && [cState isEqualToString: @"CONNECTED"] ) ) {
375 NSDate * csd = [myConnection connectedSinceDate];
376 NSTimeInterval ti = [csd timeIntervalSinceNow];
377 long cTimeL = (long) round(-ti);
378 if ( cTimeL >= 0 ) {
379 if ( cTimeL < 3600 ) {
380 cTimeS = [NSString stringWithFormat:@" %li:%02li", cTimeL/60, cTimeL%60];
381 } else {
382 cTimeS = [NSString stringWithFormat:@" %li:%02li:%02li", cTimeL/3600, (cTimeL/60) % 60, cTimeL%60];
383 }
384 }
385 }
386 NSString *label = [NSString stringWithFormat:@"%@ (%@%@)",[myConnection configName],NSLocalizedString(cState, nil), cTimeS];
387 [[tabView tabViewItemAtIndex:i] setLabel:label];
388 i++;
389 }
390 }
393 - (void) updateUI
394 {
395 unsigned connectionNumber = [connectionArray count];
396 NSString *myState;
397 if(connectionNumber == 1) {
398 myState = local(@"OpenVPN: 1 connection active.");
399 } else {
400 myState = [NSString stringWithFormat:NSLocalizedString(@"OpenVPN: %d connections active.", nil),connectionNumber];
401 }
403 [statusMenuItem setTitle: myState];
404 [theItem setToolTip: myState];
406 if( (![lastState isEqualToString:@"EXITING"]) && (![lastState isEqualToString:@"CONNECTED"]) ) {
407 // override while in transitional state
408 // Any other state shows "transitional" image:
409 //[theItem setImage: transitionalImage];
410 if (![theAnim isAnimating])
411 {
412 //NSLog(@"Starting Animation");
413 [theAnim startAnimation];
414 }
415 } else
416 {
417 if ([theAnim isAnimating])
418 {
419 [theAnim stopAnimation];
420 }
421 }
422 if (connectionNumber > 0 ) {
423 [theItem setImage: connectedImage];
424 } else {
425 [theItem setImage: mainImage];
426 }
427 }
429 - (void)animationDidEnd:(NSAnimation*)animation
430 {
431 if ((![lastState isEqualToString:@"EXITING"]) && (![lastState isEqualToString:@"CONNECTED"]))
432 {
433 // NSLog(@"Starting Animation (2)");
434 [theAnim startAnimation];
435 }
436 if ([connectionArray count] > 0 ) {
437 [theItem setImage: connectedImage];
438 } else {
439 [theItem setImage: mainImage];
440 }
441 }
443 - (void)animation:(NSAnimation *)animation
444 didReachProgressMark:(NSAnimationProgress)progress
445 {
446 if (animation == theAnim)
447 {
448 // NSLog(@"progress is %f %i", progress, lround(progress * 8));
449 // Do an effect appropriate to progress mark.
450 switch(lround(progress * 8))
451 {
452 case 1:
453 [theItem performSelectorOnMainThread:@selector(setImage:) withObject:mainImage waitUntilDone:YES];
454 break;
456 case 2:
457 [theItem performSelectorOnMainThread:@selector(setImage:) withObject:transitionalImage1 waitUntilDone:YES];
458 break;
460 case 3:
461 [theItem performSelectorOnMainThread:@selector(setImage:) withObject:transitionalImage2 waitUntilDone:YES];
462 break;
464 case 4:
465 [theItem performSelectorOnMainThread:@selector(setImage:) withObject:transitionalImage3 waitUntilDone:YES];
466 break;
468 case 5:
469 [theItem performSelectorOnMainThread:@selector(setImage:) withObject:connectedImage waitUntilDone:YES];
470 break;
472 case 6:
473 [theItem performSelectorOnMainThread:@selector(setImage:) withObject:transitionalImage3 waitUntilDone:YES];
474 break;
476 case 7:
477 [theItem performSelectorOnMainThread:@selector(setImage:) withObject:transitionalImage2 waitUntilDone:YES];
478 break;
480 case 8:
481 [theItem performSelectorOnMainThread:@selector(setImage:) withObject:transitionalImage1 waitUntilDone:YES];
482 break;
484 default:
485 NSLog(@"Unknown progress mark %f selected by Tunnelblick animation", progress);
486 }
487 }
488 }
490 - (void) tabView: (NSTabView*) inTabView willSelectTabViewItem: (NSTabViewItem*) tabViewItem
491 {
492 NSView* view = [[inTabView selectedTabViewItem] view];
493 [tabViewItem setView: view];
494 [[[self selectedLogView] textStorage] setDelegate: nil];
495 }
497 - (void) tabView: (NSTabView*) inTabView didSelectTabViewItem: (NSTabViewItem*) tabViewItem
498 {
499 VPNConnection* newConnection = [self selectedConnection];
500 NSTextView* logView = [self selectedLogView];
501 [[logView layoutManager] replaceTextStorage: [newConnection logStorage]];
502 //[logView setSelectedRange: NSMakeRange([[logView textStorage] length],[[logView textStorage] length])];
503 [logView scrollRangeToVisible: NSMakeRange([[logView string] length]-1, 0)];
505 [[logView textStorage] setDelegate: self];
507 [self validateLogButtons];
508 }
509 - (void) textStorageDidProcessEditing: (NSNotification*) aNotification
510 {
511 NSNotification *notification = [NSNotification notificationWithName: @"LogDidChange"
512 object: [self selectedLogView]];
513 [[NSNotificationQueue defaultQueue] enqueueNotification: notification
514 postingStyle: NSPostWhenIdle
515 coalesceMask: NSNotificationCoalescingOnName | NSNotificationCoalescingOnSender
516 forModes: nil];
517 }
519 - (void) logNeedsScrolling: (NSNotification*) aNotification
520 {
521 NSTextView* textView = [aNotification object];
522 [textView scrollRangeToVisible: NSMakeRange([[textView string] length]-1, 0)];
523 }
525 - (NSTextView*) selectedLogView
526 {
527 NSTextView* result = [[[[[tabView selectedTabViewItem] view] subviews] lastObject] documentView];
528 return result;
529 }
531 - (IBAction) clearLog: (id) sender
532 {
533 NSString * versionInfo = [NSString stringWithFormat:local(@"Tunnelblick version %@"),[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]];
534 NSCalendarDate* date = [NSCalendarDate date];
535 NSString *dateText = [NSString stringWithFormat:@"%@ %@\n",[date descriptionWithCalendarFormat:@"%Y-%m-%d %H:%M:%S"],versionInfo];
536 [[self selectedLogView] setString: [[[NSString alloc] initWithString: dateText] autorelease]];
537 }
539 - (VPNConnection*) selectedConnection
540 /*" Returns the connection associated with the currently selected log tab or nil on error. "*/
541 {
542 if (![tabView selectedTabViewItem]) {
543 [tabView selectFirstTabViewItem: nil];
544 }
546 NSString* configPath = [[tabView selectedTabViewItem] identifier];
547 VPNConnection* connection = [myVPNConnectionDictionary objectForKey:configPath];
548 NSArray *allConnections = [myVPNConnectionDictionary allValues];
549 if(connection) return connection;
550 else if([allConnections count]) return [allConnections objectAtIndex:0] ;
551 else return nil;
552 }
557 - (IBAction)connect:(id)sender
558 {
559 [[self selectedConnection] connect: sender];
560 }
562 - (IBAction)disconnect:(id)sender
563 {
564 [[self selectedConnection] disconnect: sender];
565 }
568 - (IBAction) openLogWindow: (id) sender
569 {
570 if (logWindow != nil) {
571 [logWindow makeKeyAndOrderFront: self];
572 [NSApp activateIgnoringOtherApps:YES];
573 return;
574 }
576 [NSBundle loadNibNamed: @"LogWindow" owner: self]; // also sets tabView etc.
578 // Set the window's size and position from preferences (saved when window is closed)
579 // But only if the preference's version matches the TB version (since window size could be different in different versions of TB)
580 NSString * tbVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
581 id tmp = [[NSUserDefaults standardUserDefaults] objectForKey:@"detailsWindowFrameVersion"];
582 if (tmp != nil) {
583 if ( [tbVersion isEqualToString: [[NSUserDefaults standardUserDefaults] stringForKey:@"detailsWindowFrameVersion"]] ) {
584 tmp = [[NSUserDefaults standardUserDefaults] objectForKey:@"detailsWindowFrame"];
585 if(tmp != nil) {
586 NSString * frame = [[NSUserDefaults standardUserDefaults] stringForKey:@"detailsWindowFrame"];
587 [logWindow setFrameFromString:frame];
588 }
589 }
590 }
592 [logWindow setDelegate:self];
593 VPNConnection *myConnection = [self selectedConnection];
594 NSTextStorage* store = [myConnection logStorage];
595 [[[self selectedLogView] layoutManager] replaceTextStorage: store];
597 NSEnumerator* e = [[[myVPNConnectionDictionary allKeys] sortedArrayUsingSelector: @selector(compare:)] objectEnumerator];
598 //id test = [[myVPNConnectionDictionary allKeys] sortedArrayUsingSelector: @selector(compare:)];
599 NSTabViewItem* initialItem;
600 VPNConnection* connection = [myVPNConnectionDictionary objectForKey: [e nextObject]];
601 if (connection) {
602 initialItem = [tabView tabViewItemAtIndex: 0];
603 [initialItem setIdentifier: [connection configPath]];
604 [initialItem setLabel: [connection configName]];
606 int curTabIndex = 0;
607 [tabView selectTabViewItemAtIndex:0];
608 BOOL haveOpenConnection = ! [connection isDisconnected];
609 while (connection = [myVPNConnectionDictionary objectForKey: [e nextObject]]) {
610 NSTabViewItem* newItem = [[NSTabViewItem alloc] init];
611 [newItem setIdentifier: [connection configPath]];
612 [newItem setLabel: [connection configName]];
613 [tabView addTabViewItem: newItem];
614 ++curTabIndex;
615 if ( ( ! haveOpenConnection ) && ( ! [connection isDisconnected] ) ) {
616 [tabView selectTabViewItemAtIndex:curTabIndex];
617 haveOpenConnection = YES;
618 }
619 }
620 }
621 [self tabView:tabView didSelectTabViewItem:initialItem];
622 [self validateLogButtons];
623 [self updateTabLabels];
625 // Set up a timer to update the tab labels with connections' duration times
626 BOOL showAllDurations = FALSE;
627 BOOL showConnectedDurations = TRUE;
628 tmp = [[NSUserDefaults standardUserDefaults] objectForKey:@"showAllDurations"];
629 if(tmp != nil) {
630 showAllDurations = [[NSUserDefaults standardUserDefaults] boolForKey:@"showAllDurations"];
631 }
632 tmp = [[NSUserDefaults standardUserDefaults] objectForKey:@"showConnectedDurations"];
633 if(tmp != nil) {
634 showConnectedDurations = [[NSUserDefaults standardUserDefaults] boolForKey:@"showConnectedDurations"];
635 }
637 if ( (showDurationsTimer == nil) && (showAllDurations || showConnectedDurations) ) {
638 showDurationsTimer = [[NSTimer scheduledTimerWithTimeInterval:1.0
639 target:self
640 selector:@selector(updateTabLabels)
641 userInfo:nil
642 repeats:YES] retain];
643 }
645 // Localize Buttons
646 [clearButton setTitle:local([clearButton title])];
647 [editButton setTitle:local([editButton title])];
648 [connectButton setTitle:local([connectButton title])];
649 [disconnectButton setTitle:local([disconnectButton title])];
650 [useNameserverCheckbox setTitle:local([useNameserverCheckbox title])];
651 [autoLaunchCheckbox setTitle:local([autoLaunchCheckbox title])];
653 [logWindow makeKeyAndOrderFront: self];
654 [NSApp activateIgnoringOtherApps:YES];
655 }
657 // Invoked when the Details... window (logWindow) will close
658 - (void)windowWillClose:(NSNotification *)n
659 {
660 if ( [n object] == logWindow ) {
661 // Stop and release the timer used to update the duration displays
662 if (showDurationsTimer != nil) {
663 [showDurationsTimer invalidate];
664 [showDurationsTimer release];
665 showDurationsTimer = nil;
666 }
668 // Save the window's size and position in the preferences and save the TB version that saved them, BUT ONLY IF anything has changed
669 NSString * frame = [logWindow stringWithSavedFrame];
670 NSString * tbVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
671 BOOL saveIt = TRUE;
672 id tmp = [[NSUserDefaults standardUserDefaults] objectForKey:@"detailsWindowFrame"];
673 if(tmp != nil) {
674 tmp = [[NSUserDefaults standardUserDefaults] objectForKey:@"detailsWindowFrameVersion"];
675 if (tmp != nil) {
676 if ( [tbVersion isEqualToString: [[NSUserDefaults standardUserDefaults] stringForKey:@"detailsWindowFrameVersion"]] ) {
677 if ( [frame isEqualToString: [[NSUserDefaults standardUserDefaults] stringForKey:@"detailsWindowFrame"]] ) {
678 saveIt = FALSE;
679 }
680 }
681 }
682 }
684 if (saveIt) {
685 [[NSUserDefaults standardUserDefaults] setObject: frame forKey: @"detailsWindowFrame"];
686 [[NSUserDefaults standardUserDefaults] setObject: [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]
687 forKey: @"detailsWindowFrameVersion"];
688 [[NSUserDefaults standardUserDefaults] synchronize];
689 }
690 }
691 }
693 - (void) dealloc
694 {
695 [lastState release];
696 [theItem release];
697 [myConfigArray release];
699 #warning todo: release non-IB ivars here!
700 [statusMenuItem release];
701 [myVPNMenu release];
702 [userDefaults release];
703 [appDefaults release];
704 [theItem release];
706 [mainImage release];
707 [connectedImage release];
708 [errorImage release];
709 [transitionalImage release];
710 [connectionArray release];
713 [super dealloc];
714 }
717 -(void)killAllConnections
718 {
719 id connection;
720 NSEnumerator* e = [connectionArray objectEnumerator];
722 while (connection = [e nextObject]) {
723 [connection disconnect:self];
724 if(NSDebugEnabled) NSLog(@"Killing connection.\n");
725 }
726 }
728 -(void)killAllOpenVPN
729 {
730 NSString* path = [[NSBundle mainBundle] pathForResource: @"openvpnstart"
731 ofType: nil];
732 NSTask* task = [[[NSTask alloc] init] autorelease];
733 [task setLaunchPath: path];
735 NSArray *arguments = [NSArray arrayWithObjects:@"killall", nil];
736 [task setArguments:arguments];
737 [task launch];
738 [task waitUntilExit];
739 }
741 -(void)resetActiveConnections {
742 VPNConnection *connection;
743 NSEnumerator* e = [connectionArray objectEnumerator];
745 while (connection = [e nextObject]) {
746 if (NSDebugEnabled) NSLog(@"Connection %@ is connected for %f seconds\n",[connection configName],[[connection connectedSinceDate] timeIntervalSinceNow]);
747 if ([[connection connectedSinceDate] timeIntervalSinceNow] < -5) {
748 if (NSDebugEnabled) NSLog(@"Resetting connection: %@\n",[connection configName]);
749 [connection disconnect:self];
750 [connection connect:self];
751 }
752 else {
753 if (NSDebugEnabled) NSLog(@"Not Resetting connection: %@\n, waiting...",[connection configName]);
754 }
755 }
756 }
758 -(void)createDefaultConfig
759 {
760 NSFileManager *fileManager = [NSFileManager defaultManager];
761 NSString *directoryPath = [NSHomeDirectory() stringByAppendingPathComponent:@"Library/openvpn"];
762 NSString *confResource = [[NSBundle mainBundle] pathForResource: @"openvpn"
763 ofType: @"conf"];
765 if([[self getConfigs] count] == 0) { // if there are no config files, create a default one
766 [NSApp activateIgnoringOtherApps:YES];
767 if(NSRunCriticalAlertPanel(local(@"Welcome to OpenVPN on Mac OS X: Please put your config file (e.g. openvpn.conf) to '~/Library/openvpn/'."), local(@"You can also continue and Tunnelblick will create an example config at the right place that you can customize or replace."),local(@"Quit"),local(@"Continue"),nil) == NSAlertDefaultReturn) {
768 exit (1);
769 }
770 else {
771 [fileManager createDirectoryAtPath:directoryPath attributes:nil];
772 [fileManager copyPath:confResource toPath:[directoryPath stringByAppendingPathComponent:@"/openvpn.conf"] handler:nil];
773 [self editConfig:self];
774 }
777 }
778 }
780 -(IBAction)editConfig:(id)sender
781 {
782 VPNConnection *connection = [self selectedConnection];
783 NSString *directoryPath = [NSHomeDirectory() stringByAppendingPathComponent:@"Library/openvpn"];
784 NSString *configPath = [connection configPath];
785 if(configPath == nil) configPath = @"/openvpn.conf";
787 // NSString *helper = @"/usr/sbin/chown";
788 // NSString *userString = [NSString stringWithFormat:@"%d",getuid()];
789 // NSArray *arguments = [NSArray arrayWithObjects:userString,configPath,nil];
790 // AuthorizationRef authRef = [NSApplication getAuthorizationRef];
791 // [NSApplication executeAuthorized:helper withArguments:arguments withAuthorizationRef:authRef];
792 // AuthorizationFree(authRef,kAuthorizationFlagDefaults);
794 [[NSWorkspace sharedWorkspace] openFile:[directoryPath stringByAppendingPathComponent:configPath] withApplication:@"TextEdit"];
795 }
798 - (void) networkConfigurationDidChange
799 {
800 if (NSDebugEnabled) NSLog(@"Got networkConfigurationDidChange notification!!");
801 [self resetActiveConnections];
802 }
804 - (void) applicationWillTerminate: (NSNotification*) notification
805 {
806 if (NSDebugEnabled) NSLog(@"App will terminate...\n");
807 [self cleanup];
808 }
810 -(void)cleanup
811 {
812 [NSApp callDelegateOnNetworkChange: NO];
813 [self killAllConnections];
814 [self killAllOpenVPN];
815 [[NSStatusBar systemStatusBar] removeStatusItem:theItem];
816 }
818 -(void)saveUseNameserverCheckboxState:(BOOL)inBool
819 {
820 VPNConnection* connection = [self selectedConnection];
821 if(connection != nil) {
822 NSString* key = [[connection configName] stringByAppendingString: @"useDNS"];
823 [[NSUserDefaults standardUserDefaults] setObject: [NSNumber numberWithBool: inBool] forKey: key];
824 [[NSUserDefaults standardUserDefaults] synchronize];
825 }
827 }
828 -(void)saveAutoLaunchCheckboxState:(BOOL)inBool
829 {
830 VPNConnection* connection = [self selectedConnection];
831 if(connection != nil) {
832 NSString* autoConnectKey = [[connection configName] stringByAppendingString: @"autoConnect"];
833 [[NSUserDefaults standardUserDefaults] setObject: [NSNumber numberWithBool: inBool] forKey: autoConnectKey];
834 [[NSUserDefaults standardUserDefaults] synchronize];
835 }
837 }
839 -(BOOL)getCurrentAutoLaunchSetting
840 {
841 VPNConnection *connection = [self selectedConnection];
842 NSString *autoConnectKey = [[connection configName] stringByAppendingString:@"autoConnect"];
843 return [[NSUserDefaults standardUserDefaults] boolForKey:autoConnectKey];
844 }
846 - (void) setState: (NSString*) newState
847 // Be sure to call this in main thread only
848 {
849 [newState retain];
850 [lastState release];
851 lastState = newState;
852 //[self updateUI];
853 [self performSelectorOnMainThread:@selector(updateUI) withObject:nil waitUntilDone:NO];
854 }
856 -(void)addConnection:(id)sender
857 {
858 if(sender != nil) {
859 [connectionArray removeObject:sender];
860 [connectionArray addObject:sender];
861 }
862 }
864 -(void)removeConnection:(id)sender
865 {
866 if(sender != nil) [connectionArray removeObject:sender];
867 }
869 static void signal_handler(int signalNumber)
870 {
871 printf("signal %d caught!\n",signalNumber);
873 if (signalNumber == SIGHUP) {
874 printf("SIGHUP received. Restarting active connections...\n");
875 [[NSApp delegate] resetActiveConnections];
876 } else {
877 printf("Received fatal signal. Cleaning up...\n");
878 [[NSApp delegate] cleanup];
879 exit(0);
880 }
881 }
884 - (void) installSignalHandler
885 {
886 struct sigaction action;
888 action.sa_handler = signal_handler;
889 sigemptyset(&action.sa_mask);
890 action.sa_flags = 0;
892 if (sigaction(SIGHUP, &action, NULL) ||
893 sigaction(SIGQUIT, &action, NULL) ||
894 sigaction(SIGTERM, &action, NULL) ||
895 sigaction(SIGBUS, &action, NULL) ||
896 sigaction(SIGSEGV, &action, NULL) ||
897 sigaction(SIGPIPE, &action, NULL)) {
898 NSLog(@"Warning: setting signal handler failed: %s", strerror(errno));
899 }
900 }
901 - (void) applicationDidFinishLaunching: (NSNotification *)notification
902 {
903 [NSApp callDelegateOnNetworkChange: NO];
904 [self installSignalHandler];
905 [NSApp setAutoLaunchOnLogin: YES];
906 [self activateStatusMenu];
907 if(needsRepair()){
908 if ([self repairPermissions] != TRUE) {
909 [NSApp terminate:self];
910 }
911 }
913 [updater checkForUpdatesInBackground];
914 }
916 -(void) dmgCheck
917 {
918 NSString *path = [[NSBundle mainBundle] bundlePath];
919 if([path hasPrefix:@"/Volumes/Tunnelblick"]) {
920 NSPanel *panel = NSGetAlertPanel(local(@"You're trying to launch Tunnelblick from the disk image"),local(@"Please copy Tunnelblick.app to your Harddisk before launching it."),local(@"Cancel"),nil,nil);
921 [panel setLevel:NSStatusWindowLevel];
922 [panel makeKeyAndOrderFront:nil];
923 [NSApp runModalForWindow:panel];
924 exit(2);
925 }
926 }
928 -(void)moveAllWindowsToForeground
929 {
930 NSArray *windows = [NSApp windows];
931 NSEnumerator *e = [windows objectEnumerator];
932 NSWindow *window = nil;
933 while(window = [e nextObject]) {
934 [window setLevel:NSStatusWindowLevel];
935 }
936 }
938 -(void) fileSystemHasChanged: (NSNotification*) n
939 {
940 if(NSDebugEnabled) NSLog(@"FileSystem has changed.");
941 [self performSelectorOnMainThread: @selector(activateStatusMenu) withObject: nil waitUntilDone: YES];
942 }
943 -(void) kqueue: (UKKQueue*) kq receivedNotification: (NSString*) nm forFile: (NSString*) fpath {
945 [self fileSystemHasChanged: nil];
946 }
948 -(BOOL)repairPermissions
949 {
950 NSBundle *thisBundle = [NSBundle mainBundle];
951 NSString *installer = [thisBundle pathForResource:@"installer" ofType:nil];
953 AuthorizationRef authRef= [NSApplication getAuthorizationRef];
955 if(authRef == nil)
956 return FALSE;
958 while(needsRepair()) {
959 NSLog(@"Repairing Application...\n");
960 [NSApplication executeAuthorized:installer withArguments:nil withAuthorizationRef:authRef];
961 sleep(1);
962 }
963 AuthorizationFree(authRef, kAuthorizationFlagDefaults);
964 return TRUE;
965 }
970 BOOL needsRepair()
971 {
972 NSBundle *thisBundle = [NSBundle mainBundle];
973 NSString *helperPath = [thisBundle pathForResource:@"openvpnstart" ofType:nil];
974 NSString *tunPath = [thisBundle pathForResource:@"tun.kext" ofType:nil];
975 NSString *tapPath = [thisBundle pathForResource:@"tap.kext" ofType:nil];
977 NSString *tunExecutable = [tunPath stringByAppendingPathComponent:@"/Contents/MacOS/tun"];
978 NSString *tapExecutable = [tapPath stringByAppendingPathComponent:@"/Contents/MacOS/tap"];
979 NSString *openvpnPath = [thisBundle pathForResource:@"openvpn" ofType:nil];
982 // check setuid helper
983 const char *path = [helperPath UTF8String];
984 struct stat sb;
985 if(stat(path,&sb)) runUnrecoverableErrorPanel();
987 if (!( (sb.st_mode & S_ISUID) // set uid bit is set
988 && (sb.st_mode & S_IXUSR) // owner may execute it
989 && (sb.st_uid == 0) // is owned by root
990 )) {
991 NSLog(@"openvpnstart helper has missing set uid bit");
992 return YES;
993 }
995 // check files which should be only accessible by root
996 NSArray *inaccessibleObjects = [NSArray arrayWithObjects:tunExecutable,tapExecutable,openvpnPath,nil];
997 NSEnumerator *e = [inaccessibleObjects objectEnumerator];
998 NSString *currentPath;
999 NSFileManager *fileManager = [NSFileManager defaultManager];
1000 while(currentPath = [e nextObject]) {
1001 NSDictionary *fileAttributes = [fileManager fileAttributesAtPath:currentPath traverseLink:YES];
1002 unsigned long perms = [fileAttributes filePosixPermissions];
1003 NSString *octalString = [NSString stringWithFormat:@"%o",perms];
1004 NSNumber *fileOwner = [fileAttributes fileOwnerAccountID];
1006 if ( (![octalString isEqualToString:@"755"]) || (![fileOwner isEqualToNumber:[NSNumber numberWithInt:0]])) {
1007 NSLog(@"File %@ has permissions: %@, is owned by %@ and needs repair...\n",currentPath,octalString,fileOwner);
1008 return YES;
1009 }
1010 }
1012 // check tun and tap driver packages
1013 NSArray *filesToCheck = [NSArray arrayWithObjects:tunPath,tapPath,nil];
1014 NSEnumerator *enumerator = [filesToCheck objectEnumerator];
1015 NSString *file;
1016 while(file = [enumerator nextObject]) {
1017 NSDictionary *fileAttributes = [fileManager fileAttributesAtPath:file traverseLink:YES];
1018 unsigned long perms = [fileAttributes filePosixPermissions];
1019 NSString *octalString = [NSString stringWithFormat:@"%o",perms];
1020 if ( (![octalString isEqualToString:@"755"]) ) {
1021 NSLog(@"File %@ has permissions: %@ and needs repair...\n",currentPath,octalString);
1022 return YES;
1023 }
1024 }
1025 return NO;
1026 }
1028 -(void)willGoToSleep
1029 {
1030 if(NSDebugEnabled) NSLog(@"Computer will go to sleep...\n");
1031 connectionsToRestore = [connectionArray mutableCopy];
1032 [self killAllConnections];
1033 }
1034 -(void)wokeUpFromSleep
1035 {
1036 if(NSDebugEnabled) NSLog(@"Computer just woke up from sleep...\n");
1038 NSEnumerator *e = [connectionsToRestore objectEnumerator];
1039 VPNConnection *connection;
1040 while(connection = [e nextObject]) {
1041 if(NSDebugEnabled) NSLog(@"Restoring Connection %@",[connection configName]);
1042 [connection connect:self];
1043 }
1044 }
1045 int runUnrecoverableErrorPanel(void)
1046 {
1047 NSPanel *panel = NSGetAlertPanel(local(@"Tunnelblick Error"),local(@"It seems like you need to reinstall Tunnelblick. Please move Tunnelblick to the Trash and download a fresh copy."),local(@"Download"),local(@"Quit"),nil);
1048 [panel setLevel:NSStatusWindowLevel];
1049 [panel makeKeyAndOrderFront:nil];
1050 if( [NSApp runModalForWindow:panel] != NSAlertDefaultReturn ) {
1051 exit(2);
1052 } else {
1053 [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://tunnelblick.net/"]];
1054 exit(2);
1055 }
1056 }
1058 -(IBAction) autoLaunchPrefButtonWasClicked: (id) sender
1059 {
1060 if([sender state]) {
1061 [self saveAutoLaunchCheckboxState:TRUE];
1062 } else {
1063 [self saveAutoLaunchCheckboxState:FALSE];
1064 }
1065 }
1067 -(IBAction) nameserverPrefButtonWasClicked: (id) sender
1068 {
1069 if([sender state]) {
1070 [self saveUseNameserverCheckboxState:TRUE];
1071 } else {
1072 [self saveUseNameserverCheckboxState:FALSE];
1073 }
1074 }
1077 @end