From 1979b7c94813e8278b4b7616aeafecd5a406f7a1 Mon Sep 17 00:00:00 2001 From: rowanbeentje Date: Thu, 28 May 2009 01:14:26 +0000 Subject: Add support for SSH tunnels, improve password security, and tweaks: - Implementation of a new SPSSHTunnel class, designed to closely integrate SSH tunnels within Sequel Pro. - Integration of SPSSHTunnel - new connection methods using callbacks, and CMMCPConnection integration - Keychain class upgrade to include the new SPSSHTunnel keychain password helper on the trusted access list for new passwords - Keychain passwords are now held in memory/UI for only as long as necessary, increasing password security - Updated interface to enable/add SSH tunnel functionality - Remove old SSHTunnel class - Addition of new target for the SSH Tunnel password assistant, addition as a dependency of the main target, and addition to build script to copy into resources directory - Fix a keychain password deletion crash --- Source/SPSSHTunnel.m | 359 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 Source/SPSSHTunnel.m (limited to 'Source/SPSSHTunnel.m') diff --git a/Source/SPSSHTunnel.m b/Source/SPSSHTunnel.m new file mode 100644 index 00000000..619657c3 --- /dev/null +++ b/Source/SPSSHTunnel.m @@ -0,0 +1,359 @@ +// +// SPSSHTunnel.m +// sequel-pro +// +// Created by Rowan Beentje on April 26, 2009. Inspired by code by +// Yann Bizuel for SSH Tunnel Manager 2. +// +// This program 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 2 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, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at + +#import "SPSSHTunnel.h" +#import + + +@implementation SPSSHTunnel + +/* + * Initialise with the supplied connection details. Host, login and port should all be provided. + * The password can either be set later via setPassword:, which stores the password locally and is + * therefore not recommended, or via setPasswordKeychainName:, which will use the keychain on-demand + * and is therefore preferred. + */ +- (id) initToHost:(NSString *) theHost port:(int) thePort login:(NSString *) theLogin tunnellingToPort:(int) targetPort onHost:(NSString *) targetHost +{ + if (!theHost || !thePort || !theLogin || !targetPort || !targetHost) return nil; + + self = [super init]; + + // Store the connection settings as appropriate + sshHost = [[NSString alloc] initWithString:theHost]; + sshLogin = [[NSString alloc] initWithString:theLogin]; + sshPort = thePort; + if ([theHost isEqualToString:targetHost]) { + remoteHost = [[NSString alloc] initWithString:@"127.0.0.1"]; + } else { + remoteHost = [[NSString alloc] initWithString:targetHost]; + } + remotePort = targetPort; + delegate = nil; + stateChangeSelector = nil; + lastError = nil; + + passwordConnection = nil; + password = nil; + keychainName = nil; + keychainAccount = nil; + passwordInKeychain = NO; + task = nil; + localPort = 0; + connectionState = SPSSH_STATE_IDLE; + + return self; +} + +/* + * Sets the connection callback selector; a function to be called whenever the tunnel state changes. + * The callback function will be called and passed this SSH Tunnel object.. + */ +- (BOOL) setConnectionStateChangeSelector:(SEL)theStateChangeSelector delegate:(id)theDelegate +{ + delegate = theDelegate; + stateChangeSelector = theStateChangeSelector; + + return true; +} + +/* + * Sets the password to be stored (and returned to the tunnel authenticator) locally. + * Providing a keychain name is much more secure. + */ +- (BOOL) setPassword:(NSString *)thePassword +{ + if (passwordInKeychain) return NO; + password = [[NSString alloc] initWithString:thePassword]; + passwordConnection = [[NSConnection defaultConnection] retain]; + [passwordConnection runInNewThread]; + [passwordConnection removeRunLoop:[NSRunLoop currentRunLoop]]; + [passwordConnection setRootObject:self]; + passwordConnectionName = [NSString stringWithFormat:@"SequelPro-%f", [[NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSince1970]] hash]]; + passwordConnectionVerifyHash = [NSString stringWithFormat:@"%f", [[NSString stringWithFormat:@"%f%i", [[NSDate date] timeIntervalSince1970]] hash]]; + if ([passwordConnection registerName:passwordConnectionName] == NO) { + [password release], password = nil; + return NO; + } + + return YES; +} + +/* + * Sets the keychain name to use to retrieve the password. This is the recommended and + * secure way of supplying a password to the SSH tunnel. + */ +- (BOOL) setPasswordKeychainName:(NSString *)theName account:(NSString *)theAccount +{ + if (passwordConnection) [passwordConnection release], passwordConnection = nil; + if (password) [password release], password = nil; + + passwordInKeychain = YES; + keychainName = [[NSString alloc] initWithString:theName]; + keychainAccount = [[NSString alloc] initWithString:theAccount]; + + return YES; +} + +/* + * Get the state of the connection. + */ +- (int) state +{ + return connectionState; +} + +/* + * Returns the last error string, if any. + */ +- (NSString *) lastError +{ + return [NSString stringWithString:lastError]; +} + +/* + * Initiate the SSH tunnel connection, launching the task in a background thread. + */ +- (void) connect +{ + localPort = 0; + if (connectionState != SPSSH_STATE_IDLE || (!passwordInKeychain && !password)) return; + [NSThread detachNewThreadSelector:@selector(launchTask:) toTarget: self withObject: nil ]; +} + +/* + * Launch the NSTask which wraps the SSH process, and use it to initiate the + * tunnel to the remote server. + * Sets up and tears down as appropriate for usage in a background thread. + */ +- (void) launchTask:(id) dummy +{ + if (connectionState != SPSSH_STATE_IDLE || task) return; + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + NSMutableArray *taskArguments; + NSMutableDictionary *taskEnvironment; + NSString *authenticationAppPath; + + connectionState = SPSSH_STATE_CONNECTING; + if (delegate) [delegate performSelectorOnMainThread:stateChangeSelector withObject:self waitUntilDone:NO]; + + int connectionTimeout = [[[NSUserDefaults standardUserDefaults] objectForKey:@"ConnectionTimeout"] intValue]; + if (!connectionTimeout) connectionTimeout = 10; + BOOL useKeepAlive = [[[NSUserDefaults standardUserDefaults] objectForKey:@"UseKeepAlive"] doubleValue]; + double keepAliveInterval = [[[NSUserDefaults standardUserDefaults] objectForKey:@"KeepAliveInterval"] doubleValue]; + if (!keepAliveInterval) keepAliveInterval = 0; + + // If no local port has yet been chosen, choose one + if (!localPort) { + int tempSocket; + struct sockaddr_in tempSocketAddress; + int addressLength = sizeof(tempSocketAddress); + if((tempSocket = socket(AF_INET, SOCK_STREAM, 0)) > 0) { + memset(&tempSocketAddress, 0, sizeof(tempSocketAddress)); + tempSocketAddress.sin_family = AF_INET; + tempSocketAddress.sin_addr.s_addr = htonl(INADDR_ANY); + tempSocketAddress.sin_port = 0; + if (bind(tempSocket, (struct sockaddr *)&tempSocketAddress, addressLength) >= 0) { + if (getsockname(tempSocket, (struct sockaddr *)&tempSocketAddress, (uint32_t *)&addressLength) >= 0) { + localPort = ntohs(tempSocketAddress.sin_port); + } + } + close(tempSocket); + } + + // Abort if no local free port could be allocated + if (!localPort) { + connectionState = SPSSH_STATE_IDLE; + if (delegate) [delegate performSelectorOnMainThread:stateChangeSelector withObject:self waitUntilDone:NO]; + if (lastError) [lastError release]; + lastError = [[NSString alloc] initWithString:NSLocalizedString(@"No local port could be allocated for the SSH Tunnel.", @"SSH tunnel could not be created because no local port could be allocated")]; + [pool release]; + return; + } + } + + // Set up the NSTask + task = [[NSTask alloc] init]; + [task setLaunchPath: @"/usr/bin/ssh"]; + + // Set up the arguments for the task + taskArguments = [ NSMutableArray array ]; + [taskArguments addObject:@"-N"]; // Tunnel only + [taskArguments addObject:@"-v"]; // Verbose mode for messages +// [taskArguments addObject:@"-C"]; // TODO: compression? + [taskArguments addObject:@"-o ExitOnForwardFailure=yes"]; + [taskArguments addObject:[NSString stringWithFormat:@"-o ConnectTimeout=%i", connectionTimeout]]; + if (useKeepAlive && keepAliveInterval) { + [taskArguments addObject:@"-o TCPKeepAlive=no"]; + [taskArguments addObject:[NSString stringWithFormat:@"-o ServerAliveInterval=%i", ceil(keepAliveInterval)]]; + [taskArguments addObject:@"-o ServerAliveCountMax=1"]; + } + [taskArguments addObject:[NSString stringWithFormat:@"-p %i", sshPort]]; + [taskArguments addObject:[NSString stringWithFormat:@"%@@%@", sshLogin, sshHost]]; + [taskArguments addObject:[NSString stringWithFormat:@"-L %i/%@/%i", localPort, remoteHost, remotePort]]; + [task setArguments:taskArguments]; + + // Set up the environment for the task + authenticationAppPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"TunnelPassphraseRequester"]; + taskEnvironment = [NSMutableDictionary dictionaryWithDictionary:[[NSProcessInfo processInfo] environment]]; + [taskEnvironment removeObjectForKey: @"SSH_AGENT_PID"]; + [taskEnvironment removeObjectForKey: @"SSH_AUTH_SOCK"]; + [taskEnvironment setObject:authenticationAppPath forKey:@"SSH_ASKPASS"]; + [taskEnvironment setObject:@":0" forKey:@"DISPLAY"]; + if (passwordInKeychain) { + [taskEnvironment setObject:[[NSNumber numberWithInt:SPSSH_PASSWORD_USES_KEYCHAIN] stringValue] forKey:@"SP_PASSWORD_METHOD"]; + [taskEnvironment setObject:keychainName forKey:@"SP_KEYCHAIN_ITEM_NAME"]; + [taskEnvironment setObject:keychainAccount forKey:@"SP_KEYCHAIN_ITEM_ACCOUNT"]; + } else { + [taskEnvironment setObject:[[NSNumber numberWithInt:SPSSH_PASSWORD_ASKS_UI] stringValue] forKey:@"SP_PASSWORD_METHOD"]; + [taskEnvironment setObject:passwordConnectionName forKey:@"SP_CONNECTION_NAME"]; + [taskEnvironment setObject:passwordConnectionVerifyHash forKey:@"SP_CONNECTION_VERIFY_HASH"]; + } + [task setEnvironment:taskEnvironment]; + + // Set up the standard error pipe + standardError = [[NSPipe alloc] init]; + [task setStandardError:standardError]; + [[ NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(standardErrorHandler:) + name:@"NSFileHandleDataAvailableNotification" + object:[standardError fileHandleForReading]]; + [[standardError fileHandleForReading] waitForDataInBackgroundAndNotify]; + + // Launch and run the tunnel + [task launch]; + + // TODO: The below code doesn't actually appear to work. We will probably have to switch to system()/exec() for grouped children... + // Apply the process group to the child task to ensure it quits with the parent process. + // Note that if run from within Xcode, Xcode is the parent process! +/* pid_t group = setsid(); + if (group == -1) group = getpgrp(); + if(setpgid([task processIdentifier], group) == -1) { + connectionState = SPSSH_STATE_IDLE; + [task terminate]; + if (lastError) [lastError release]; + lastError = [[NSString alloc] initWithFormat:NSLocalizedString(@"The SSH Tunnel could not safely be marked as belonging to Sequel Pro, and so has been shut down for security reasons. Please try again.\n\n(Error %i)", @"SSH tunnel could not be security marked by Sequel Pro"), errno]; + if (delegate) [delegate performSelectorOnMainThread:stateChangeSelector withObject:self waitUntilDone:NO]; + }*/ + + // Listen for output + [task waitUntilExit]; + if (connectionState != SPSSH_STATE_IDLE) { + connectionState = SPSSH_STATE_IDLE; + lastError = [[NSString alloc] initWithString:NSLocalizedString(@"The SSH Tunnel has unexpectedly closed.", @"SSH tunnel unexpectedly closed")]; + if (delegate) [delegate performSelectorOnMainThread:stateChangeSelector withObject:self waitUntilDone:NO]; + } + + // On tunnel close, clean up + [[NSNotificationCenter defaultCenter] removeObserver:self + name:@"NSFileHandleDataAvailableNotification" + object:[standardError fileHandleForReading]]; + [task release], task = nil; + [standardError release], standardError = nil; + + [pool release]; +} + +/* + * Disconnects the tunnel + */ +- (void)disconnect +{ + if (connectionState == SPSSH_STATE_IDLE) return; + [task terminate]; + connectionState = SPSSH_STATE_IDLE; + if (delegate) [delegate performSelectorOnMainThread:stateChangeSelector withObject:self waitUntilDone:NO]; +} + +/* + * Processes messages recieved from the SSH task + */ +- (void)standardErrorHandler:(NSNotification*)aNotification +{ + NSString *notificationText; + NSEnumerator *enumerator; + NSArray *messages; + NSString *message; + + notificationText = [[NSString alloc] initWithData:[[aNotification object] availableData] encoding:NSASCIIStringEncoding]; + + if ([notificationText length]) { + messages = [notificationText componentsSeparatedByString:@"\n"]; + enumerator = [messages objectEnumerator]; + while (message = [enumerator nextObject]) { + + if ([message rangeOfString:@"Entering interactive session."].location != NSNotFound) { + connectionState = SPSSH_STATE_CONNECTED; + if (delegate) [delegate performSelectorOnMainThread:stateChangeSelector withObject:self waitUntilDone:NO]; + } + + if ([message rangeOfString:@"Connection established"].location != NSNotFound) { + connectionState = SPSSH_STATE_WAITING_FOR_AUTH; + if (delegate) [delegate performSelectorOnMainThread:stateChangeSelector withObject:self waitUntilDone:NO]; + } + + if ([message rangeOfString:@"closed by remote host." ].location != NSNotFound) { + connectionState = SPSSH_STATE_IDLE; + [task terminate]; + if (lastError) [lastError release]; + lastError = [[NSString alloc] initWithString:NSLocalizedString(@"The SSH Tunnel was closed 'by the remote host'. This may indicate a networking issue or a network timeout.", @"SSH tunnel was closed by remote host message")]; + if (delegate) [delegate performSelectorOnMainThread:stateChangeSelector withObject:self waitUntilDone:NO]; + } + if ([message rangeOfString:@"Operation timed out" ].location != NSNotFound) { + connectionState = SPSSH_STATE_IDLE; + [task terminate]; + if (lastError) [lastError release]; + lastError = [[NSString alloc] initWithFormat:NSLocalizedString(@"The SSH Tunnel was unable to connect to host %@, or the request timed out.\n\nBe sure that the address is correct and that you have the necessary privileges, or try increasing the connection timeout (currently %i seconds).", @"SSH tunnel failed or timed out message"), sshHost, [[[NSUserDefaults standardUserDefaults] objectForKey:@"ConnectionTimeoutValue"] intValue]]; + if (delegate) [delegate performSelectorOnMainThread:stateChangeSelector withObject:self waitUntilDone:NO]; + } + } + } + + if (connectionState != SPSSH_STATE_IDLE) { + [[standardError fileHandleForReading] waitForDataInBackgroundAndNotify]; + } + + [notificationText release]; +} + +/* + * Returns the local port assigned for use by the tunnel + */ +- (int) localPort +{ + return localPort; +} + +/* + * Method to request the password for the current connection, as used by TunnelPassphraseRequester; + * called with a verification hash to check against the stored hash, to provide basic security. Note + * that this is easily bypassed, but if bypassed the password can already easily be retrieved in the same way. + */ +- (NSString *)getPasswordWithVerificationHash:(NSString *)theHash +{ + if (passwordInKeychain) return nil; + if (theHash != passwordConnectionVerifyHash) return nil; + return password; +} + +@end -- cgit v1.2.3