//
// $Id$
//
// SPSSHTunnel.m
// sequel-pro
//
// Created by Rowan Beentje on April 26, 2009.
// Copyright (c) 2009 Rowan Beentje. All rights reserved.
//
// Inspired by code by Yann Bizuel for SSH Tunnel Manager 2.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
// More info at
#import "SPSSHTunnel.h"
#import "RegexKitLite.h"
#import "SPKeychain.h"
#import "SPAlertSheets.h"
#import
#import
@implementation SPSSHTunnel
@synthesize passwordPromptCancelled;
/*
* 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:(NSInteger)thePort login:(NSString *)theLogin tunnellingToPort:(NSInteger)targetPort onHost:(NSString *)targetHost
{
if (!theHost || !targetPort || !targetHost) return nil;
if ((self = [super init])) {
// Store the connection settings as appropriate
sshHost = [[NSString alloc] initWithString:theHost];
sshLogin = [[NSString alloc] initWithString:(theLogin?theLogin:@"")];
sshPort = thePort;
useHostFallback = [theHost isEqualToString:targetHost];
remoteHost = [[NSString alloc] initWithString:targetHost];
remotePort = targetPort;
delegate = nil;
stateChangeSelector = nil;
lastError = nil;
debugMessages = [[NSMutableArray alloc] init];
debugMessagesLock = [[NSLock alloc] init];
answerAvailableLock = [[NSLock alloc] init];
// Set up a connection for use by the tunnel process
tunnelConnectionName = [[NSString alloc] initWithFormat:@"SequelPro-%lu", (unsigned long)[[NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSince1970]] hash]];
tunnelConnectionVerifyHash = [[NSString alloc] initWithFormat:@"%lu", (unsigned long)[[NSString stringWithFormat:@"%f-seeded", [[NSDate date] timeIntervalSince1970]] hash]];
tunnelConnection = [NSConnection new];
[tunnelConnection runInNewThread];
[tunnelConnection removeRunLoop:[NSRunLoop currentRunLoop]];
[tunnelConnection setRootObject:self];
if (![tunnelConnection registerName:tunnelConnectionName]) return nil;
parentWindow = nil;
identityFilePath = nil;
sshQuestionDialog = nil;
sshPasswordDialog = nil;
password = nil;
keychainName = nil;
keychainAccount = nil;
requestedPassphrase = nil;
task = nil;
localPort = 0;
connectionState = SPMySQLProxyIdle;
requestedResponse = NO;
passwordInKeychain = NO;
passwordPromptCancelled = NO;
}
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;
}
/*
* Set the parent window of the connection for use with dialogs.
*/
- (void)setParentWindow:(NSWindow *)theWindow
{
// As this object is not a NSWindowController, use manual top-level nib item management
if (sshQuestionDialog) [sshQuestionDialog release], sshQuestionDialog = nil;
if (sshPasswordDialog) [sshPasswordDialog release], sshPasswordDialog = nil;
parentWindow = theWindow;
if (![NSBundle loadNibNamed:@"SSHQuestionDialog" owner:self]) {
NSLog(@"SSH query dialog could not be loaded; SSH tunnels will not function correctly.");
parentWindow = nil;
}
}
/*
* 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];
return YES;
}
/**
* Sets the path of an identity file, or public key file, to use when connecting.
*/
- (BOOL)setKeyFilePath:(NSString *)thePath
{
NSString *expandedPath = [thePath stringByExpandingTildeInPath];
if (![[NSFileManager defaultManager] fileExistsAtPath:expandedPath]) return NO;
if (identityFilePath) [identityFilePath release];
identityFilePath = [[NSString alloc] initWithString:expandedPath];
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 (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.
*/
- (SPMySQLConnectionProxyState)state
{
// See if an auth dialog is up
if (![answerAvailableLock tryLock]) {
return SPMySQLProxyWaitingForAuth;
}
[answerAvailableLock unlock];
// Return the currently recorded state
return connectionState;
}
/*
* Returns the last error string, if any.
*/
- (NSString *)lastError
{
if (!lastError) return nil;
return [NSString stringWithString:lastError];
}
/*
* Returns all the debug text for this tunnel as a string, separated
* by line endings.
*/
- (NSString *)debugMessages {
[debugMessagesLock lock];
NSString *debugMessagesString = [debugMessages componentsJoinedByString:@"\n"];
[debugMessagesLock unlock];
return debugMessagesString;
}
/*
* Initiate the SSH tunnel connection, launching the task in a background thread.
*/
- (void)connect
{
localPort = 0;
if (connectionState != SPMySQLProxyIdle) return;
[debugMessagesLock lock];
[debugMessages removeAllObjects];
[debugMessagesLock unlock];
[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 != SPMySQLProxyIdle || task) return;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSMutableArray *taskArguments;
NSMutableDictionary *taskEnvironment;
NSString *authenticationAppPath;
connectionState = SPMySQLProxyConnecting;
if (delegate) [delegate performSelectorOnMainThread:stateChangeSelector withObject:self waitUntilDone:NO];
// Enforce a parent window being present for dialogs
if (!parentWindow) {
connectionState = SPMySQLProxyIdle;
if (delegate) [delegate performSelectorOnMainThread:stateChangeSelector withObject:self waitUntilDone:NO];
if (lastError) [lastError release];
lastError = [[NSString alloc] initWithString:@"SSH Tunnel started without a parent window. A parent window must be present."];
[pool release];
return;
}
NSInteger connectionTimeout = [[[NSUserDefaults standardUserDefaults] objectForKey:SPConnectionTimeoutValue] integerValue];
if (!connectionTimeout) connectionTimeout = 10;
BOOL useKeepAlive = [[[NSUserDefaults standardUserDefaults] objectForKey:SPUseKeepAlive] doubleValue];
double keepAliveInterval = [[[NSUserDefaults standardUserDefaults] objectForKey:SPKeepAliveInterval] doubleValue];
if (!keepAliveInterval) keepAliveInterval = 0;
// If no local port has yet been chosen, choose one
if (!localPort) {
int tempSocket;
struct sockaddr_in tempSocketAddress;
size_t 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, (socklen_t)addressLength) >= 0) {
if (getsockname(tempSocket, (struct sockaddr *)&tempSocketAddress, (uint32_t *)&addressLength) >= 0) {
localPort = ntohs(tempSocketAddress.sin_port);
}
}
close(tempSocket);
}
if (useHostFallback) {
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, (socklen_t)addressLength) >= 0) {
if (getsockname(tempSocket, (struct sockaddr *)&tempSocketAddress, (uint32_t *)&addressLength) >= 0) {
localPortFallback = ntohs(tempSocketAddress.sin_port);
}
}
close(tempSocket);
}
}
// Abort if no local free port could be allocated
if (!localPort || (useHostFallback && !localPortFallback)) {
connectionState = SPMySQLProxyIdle;
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"];
// Prepare to set up the arguments for the task
taskArguments = [[NSMutableArray alloc] init];
// Enable verbose mode for message parsing
[taskArguments addObject:@"-v"];
// Ensure that the muxed connection can be used for only tunnels, not interactive
[taskArguments addObject:@"-N"];
// Enable automatic connection muxing/sharing, for faster connections
[taskArguments addObject:@"-o ControlMaster=auto"];
// Set a custom control path to isolate connection sharing to Sequel Pro, to prevent picking up
// existing masters without forwarding enabled and to isolate from interactive sessions. Use a short
// hashed path to aid length limit issues.
unsigned char hashedPathResult[16];
NSString *pathString = [NSString stringWithFormat:@"%@@%@:%ld", sshLogin?sshLogin:@"", sshHost, sshPort?sshPort:0];
CC_MD5([pathString UTF8String], (unsigned int)strlen([pathString UTF8String]), hashedPathResult);
[taskArguments addObject:[NSString stringWithFormat:@"-o ControlPath=%@/SPSSH-%@", [NSFileManager temporaryDirectory], [[[NSData dataWithBytes:hashedPathResult length:16] dataToHexString] substringToIndex:8]]];
// If the port forwarding fails, exit - as this is the primary use case for the instance
[taskArguments addObject:@"-o ExitOnForwardFailure=yes"];
// Specify a connection timeout based on the preferences value
[taskArguments addObject:[NSString stringWithFormat:@"-o ConnectTimeout=%ld", (long)connectionTimeout]];
// Allow three password prompts
[taskArguments addObject:@"-o NumberOfPasswordPrompts=3"];
// Specify an identity file if available
if (identityFilePath) {
[taskArguments addObject:@"-i"];
[taskArguments addObject:identityFilePath];
}
// If keepalive is set in the preferences, use the same value for the SSH tunnel
if (useKeepAlive && keepAliveInterval) {
[taskArguments addObject:@"-o TCPKeepAlive=no"];
[taskArguments addObject:[NSString stringWithFormat:@"-o ServerAliveInterval=%ld", (long)ceil(keepAliveInterval)]];
[taskArguments addObject:@"-o ServerAliveCountMax=1"];
}
// Specify the port, host, and authentication details
if (sshPort) {
[taskArguments addObject:[NSString stringWithFormat:@"-p %ld", (long)sshPort]];
}
if ([sshLogin length]) {
[taskArguments addObject:[NSString stringWithFormat:@"%@@%@", sshLogin, sshHost]];
} else {
[taskArguments addObject:sshHost];
}
if (useHostFallback) {
[taskArguments addObject:[NSString stringWithFormat:@"-L %ld/127.0.0.1/%ld", (long)localPort, (long)remotePort]];
[taskArguments addObject:[NSString stringWithFormat:@"-L %ld/%@/%ld", (long)localPortFallback, remoteHost, (long)remotePort]];
} else {
[taskArguments addObject:[NSString stringWithFormat:@"-L %ld/%@/%ld", (long)localPort, remoteHost, (long)remotePort]];
}
[task setArguments:taskArguments];
// Set up the environment for the task
authenticationAppPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"SequelProTunnelAssistant"];
taskEnvironment = [[NSMutableDictionary alloc] initWithDictionary:[[NSProcessInfo processInfo] environment]];
[taskEnvironment setObject:authenticationAppPath forKey:@"SSH_ASKPASS"];
[taskEnvironment setObject:@":0" forKey:@"DISPLAY"];
[taskEnvironment setObject:tunnelConnectionName forKey:@"SP_CONNECTION_NAME"];
[taskEnvironment setObject:tunnelConnectionVerifyHash forKey:@"SP_CONNECTION_VERIFY_HASH"];
if (passwordInKeychain) {
[taskEnvironment setObject:[[NSNumber numberWithInteger:SPSSHPasswordUsesKeychain] stringValue] forKey:@"SP_PASSWORD_METHOD"];
[taskEnvironment setObject:[keychainName stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding] forKey:@"SP_KEYCHAIN_ITEM_NAME"];
[taskEnvironment setObject:[keychainAccount stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding] forKey:@"SP_KEYCHAIN_ITEM_ACCOUNT"];
} else if (password) {
[taskEnvironment setObject:[[NSNumber numberWithInteger:SPSSHPasswordAsksUI] stringValue] forKey:@"SP_PASSWORD_METHOD"];
} else {
[taskEnvironment setObject:[[NSNumber numberWithInteger:SPSSHPasswordNone] stringValue] forKey:@"SP_PASSWORD_METHOD"];
}
[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];
// Listen for output
[task waitUntilExit];
// On tunnel close, clean up, ready for re-use if the delegate reconnects.
[task release], task = nil;
[standardError release], standardError = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self
name:@"NSFileHandleDataAvailableNotification"
object:nil];
// If the task closed unexpectedly, alert appropriately
if (connectionState != SPMySQLProxyIdle) {
connectionState = SPMySQLProxyIdle;
if (lastError) [lastError release];
lastError = [[NSString alloc] initWithString:NSLocalizedString(@"The SSH Tunnel has unexpectedly closed.", @"SSH tunnel unexpectedly closed")];
if (delegate) [delegate performSelectorOnMainThread:stateChangeSelector withObject:self waitUntilDone:NO];
}
// Run the run loop for a short time to ensure all task/pipe callbacks are dealt with
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
[taskEnvironment release], taskEnvironment = nil;
[taskArguments release], taskArguments = nil;
[pool release];
}
/*
* Disconnects the tunnel
*/
- (void)disconnect
{
if (connectionState == SPMySQLProxyIdle) return;
// If there's a delegate set, clear it to prevent unexpected state change messaging
if (delegate) {
delegate = nil;
stateChangeSelector = NULL;
}
// Before terminating the tunnel, check that it's actually running. This is to accommodate tunnels which
// suddenly disappear as a result of network disconnections.
if ([task isRunning]) [task terminate];
}
/*
* Processes messages recieved from the SSH task. These may be received singly
* or several stuck together.
*/
- (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] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]])) {
if (![message length]) continue;
[debugMessagesLock lock];
[debugMessages addObject:[NSString stringWithString:message]];
[debugMessagesLock unlock];
if ([message rangeOfString:@"Entering interactive session."].location != NSNotFound
|| [message rangeOfString:@"mux_client_request_session: master session id: "].location != NSNotFound)
{
connectionState = SPMySQLProxyConnected;
if (delegate) [delegate performSelectorOnMainThread:stateChangeSelector withObject:self waitUntilDone:NO];
}
if ([message rangeOfString:@"Connection established"].location != NSNotFound) {
connectionState = SPMySQLProxyWaitingForAuth;
if (delegate) [delegate performSelectorOnMainThread:stateChangeSelector withObject:self waitUntilDone:NO];
}
if ([message rangeOfString:@"bind: Address already in use"].location != NSNotFound) {
connectionState = SPMySQLProxyIdle;
[task terminate];
if (lastError) [lastError release];
lastError = [[NSString alloc] initWithString:NSLocalizedString(@"The SSH Tunnel was unable to bind to the local port. This error may occur if you already have an SSH connection to the same server and are using a 'LocalForward' setting in your SSH configuration.\n\nWould you like to fall back to a standard connection to localhost in order to use the existing tunnel?", @"SSH tunnel unable to bind to local port message")];
if (delegate) [delegate performSelectorOnMainThread:stateChangeSelector withObject:self waitUntilDone:NO];
}
if ([message rangeOfString:@"closed by remote host." ].location != NSNotFound) {
connectionState = SPMySQLProxyIdle;
[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:@"Permission denied (" ].location != NSNotFound || [message rangeOfString:@"No more authentication methods to try" ].location != NSNotFound) {
connectionState = SPMySQLProxyIdle;
[task terminate];
if (lastError) [lastError release];
lastError = [[NSString alloc] initWithString:NSLocalizedString(@"The SSH Tunnel could not authenticate with the remote host. Please check your password and ensure you still have access.", @"SSH tunnel authentication failed message")];
if (delegate) [delegate performSelectorOnMainThread:stateChangeSelector withObject:self waitUntilDone:NO];
}
if ([message rangeOfString:@"connect failed: Connection refused" ].location != NSNotFound) {
connectionState = SPMySQLProxyForwardingFailed;
if (lastError) [lastError release];
lastError = [[NSString alloc] initWithString:NSLocalizedString(@"The SSH Tunnel was established successfully, but could not forward data to the remote port as the remote port refused the connection.", @"SSH tunnel forwarding port connection refused message")];
}
if ([message rangeOfString:@"Operation timed out" ].location != NSNotFound) {
connectionState = SPMySQLProxyIdle;
[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 %ld seconds).", @"SSH tunnel failed or timed out message"), sshHost, (long)[[[NSUserDefaults standardUserDefaults] objectForKey:SPConnectionTimeoutValue] integerValue]];
if (delegate) [delegate performSelectorOnMainThread:stateChangeSelector withObject:self waitUntilDone:NO];
}
}
}
if (connectionState != SPMySQLProxyIdle) {
[[standardError fileHandleForReading] waitForDataInBackgroundAndNotify];
}
[notificationText release];
}
/*
* Returns the local port assigned for use by the tunnel
*/
- (NSUInteger)localPort
{
return localPort;
}
/*
* Returns the local port assigned for fallback use by the tunnel, if any
*/
- (NSUInteger)localPortFallback
{
if (!useHostFallback) return 0;
return localPortFallback;
}
/*
* Method to request the password for the current connection, as used by SequelProTunnelAssistant;
* 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 isEqualToString:tunnelConnectionVerifyHash]) return nil;
return password;
}
/*
* Method to allow an SSH tunnel to request the response to a question, returning the response as
* a boolean. This is used by the SSH_ASKPASS environment setting to deal with situations like
* host key mismatches.
*/
- (BOOL)getResponseForQuestion:(NSString *)theQuestion
{
// Lock the answer available lock
[[answerAvailableLock onMainThread] lock];
// Request an answer on the main thread (UI stuff must be done on main thread)
[self performSelectorOnMainThread:@selector(workerGetResponseForQuestion:) withObject:theQuestion waitUntilDone:YES];
// Wait for closeSSHQuestionSheet: to unlock the lock, indicating an answer is available
while (![answerAvailableLock tryLock]) usleep(25000);
// Save the answer
BOOL response = requestedResponse;
// Unlock the lock again
[answerAvailableLock unlock];
// Return the answer
return response;
}
- (void)workerGetResponseForQuestion:(NSString *)theQuestion
{
NSSize questionTextSize;
NSRect windowFrameRect;
// set up the question window
[sshQuestionText setStringValue:theQuestion];
questionTextSize = [[sshQuestionText cell] cellSizeForBounds:NSMakeRect(0, 0, [sshQuestionText bounds].size.width, 500)];
windowFrameRect = [sshQuestionDialog frame];
windowFrameRect.size.height = ((questionTextSize.height < 100)?100:questionTextSize.height) + 70 + ([sshPasswordDialog isSheet]?0:22);
[sshQuestionDialog setFrame:windowFrameRect display:NO];
//show the question window
[NSApp beginSheet:sshQuestionDialog modalForWindow:parentWindow modalDelegate:self didEndSelector:nil contextInfo:nil];
[parentWindow makeKeyAndOrderFront:self];
}
/*
* Ends an existing modal session
*/
- (IBAction)closeSSHQuestionSheet:(id)sender
{
requestedResponse = [sender tag]==1 ? YES : NO;
[NSApp endSheet:sshQuestionDialog];
[sshQuestionDialog orderOut:nil];
[[answerAvailableLock onMainThread] unlock];
}
/*
* Method to allow an SSH tunnel to request a password. This is used by the program set by the
* SSH_ASKPASS environment setting to request passphrases for SSH keys.
*/
- (NSString *)getPasswordForQuery:(NSString *)theQuery verificationHash:(NSString *)theHash
{
if (![theHash isEqualToString:tunnelConnectionVerifyHash]) return nil;
if (passwordPromptCancelled) return nil;
// Lock the answer available lock
[[answerAvailableLock onMainThread] lock];
// Request password on the main thread (UI stuff must be done on main thread)
[self performSelectorOnMainThread:@selector(workerGetPasswordForQuery:) withObject:theQuery waitUntilDone:YES];
// Wait for closeSSHPasswordSheet: to unlock the lock, indicating an answer is available
while (![answerAvailableLock tryLock]) usleep(25000);
// Save the answer
NSString *thePassword = nil;
if (requestedPassphrase) {
thePassword = [NSString stringWithString:requestedPassphrase];
[requestedPassphrase release], requestedPassphrase = nil;
}
// Unlock the lock again
[answerAvailableLock unlock];
// Return the answer
return thePassword;
}
- (void)workerGetPasswordForQuery:(NSString *)theQuery
{
NSSize queryTextSize;
NSRect windowFrameRect;
// Work out whether a passphrase is being requested, extracting the key name
NSString *keyName = [theQuery stringByMatching:@"^\\s*Enter passphrase for key \\'(.*)\\':\\s*$" capture:1L];
if (keyName) {
[sshPasswordText setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Enter your password for the SSH key\n\"%@\"", @"SSH key password prompt"), keyName]];
[sshPasswordKeychainCheckbox setHidden:NO];
currentKeyName = [keyName retain];
}
else {
[sshPasswordText setStringValue:theQuery];
[sshPasswordKeychainCheckbox setHidden:YES];
currentKeyName = nil;
}
// Request the password, sizing the window appropriately to fit the query
queryTextSize = [[sshPasswordText cell] cellSizeForBounds:NSMakeRect(0, 0, [sshPasswordText bounds].size.width, 500)];
windowFrameRect = [sshPasswordDialog frame];
windowFrameRect.size.height = ((queryTextSize.height < 40)?40:queryTextSize.height) + 140 + ([sshPasswordDialog isSheet]?0:22);
[sshPasswordDialog setFrame:windowFrameRect display:NO];
[NSApp beginSheet:sshPasswordDialog modalForWindow:parentWindow modalDelegate:self didEndSelector:nil contextInfo:nil];
[parentWindow makeKeyAndOrderFront:self];
}
/*
* Ends an existing modal session
*/
- (IBAction)closeSSHPasswordSheet:(id)sender
{
requestedResponse = [sender tag]==1 ? YES : NO;
[NSApp endSheet:sshPasswordDialog];
[sshPasswordDialog orderOut:nil];
if (requestedResponse) {
NSString *thePassword = [NSString stringWithString:[sshPasswordField stringValue]];
[sshPasswordField setStringValue:@""];
if ([delegate respondsToSelector:@selector(undoManager)] && [delegate undoManager]) {
[[delegate undoManager] removeAllActionsWithTarget:sshPasswordField];
} else if ([[parentWindow windowController] document] && [[[parentWindow windowController] document] undoManager]) {
[[[[parentWindow windowController] document] undoManager] removeAllActionsWithTarget:sshPasswordField];
}
requestedPassphrase = [[NSString alloc] initWithString:thePassword];
// Add to keychain if appropriate
if (currentKeyName && [sshPasswordKeychainCheckbox state] == NSOnState) {
SPKeychain *keychain = [[SPKeychain alloc] init];
[keychain addPassword:thePassword forName:@"SSH" account:currentKeyName withLabel:[NSString stringWithFormat:@"SSH: %@", currentKeyName]];
[keychain release];
[currentKeyName release];
currentKeyName = nil;
}
}
if (!requestedPassphrase) passwordPromptCancelled = YES;
[[answerAvailableLock onMainThread] unlock];
}
#pragma mark -
- (void)dealloc
{
delegate = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self];
if (connectionState != SPMySQLProxyIdle) [self disconnect];
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[sshHost release];
[sshLogin release];
[remoteHost release];
[tunnelConnectionName release];
[tunnelConnectionVerifyHash release];
[tunnelConnection invalidate];
[tunnelConnection release];
[debugMessages release];
[debugMessagesLock release];
[answerAvailableLock tryLock];
[answerAvailableLock unlock];
[answerAvailableLock release];
if (password) [password release];
if (keychainName) [keychainName release];
if (keychainAccount) [keychainAccount release];
if (identityFilePath) [identityFilePath release];
// As this object is not a NSWindowController, use manual top-level nib item management
if (sshQuestionDialog) [sshQuestionDialog release], sshQuestionDialog = nil;
if (sshPasswordDialog) [sshPasswordDialog release], sshPasswordDialog = nil;
[super dealloc];
}
@end